Skip to content

Commit dc2fe98

Browse files
zeripathlunny
authored andcommitted
Make repository management section handle lfs locks (#8726)
* Make repository maangement section handle lfs locks * Add check attribute handling and handle locking paths better * More cleanly check-attributes * handle error * Check if file exists in default branch before linking to it. * fixup * Properly cleanPath * Use cleanPath * Sigh
1 parent 751cfb8 commit dc2fe98

File tree

10 files changed

+367
-9
lines changed

10 files changed

+367
-9
lines changed

models/lfs_lock.go

+21-4
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (l *LFSLock) AfterLoad(session *xorm.Session) {
4949
}
5050

5151
func cleanPath(p string) string {
52-
return path.Clean(p)
52+
return path.Clean("/" + p)[1:]
5353
}
5454

5555
// APIFormat convert a Release to lfs.LFSLock
@@ -71,6 +71,8 @@ func CreateLFSLock(lock *LFSLock) (*LFSLock, error) {
7171
return nil, err
7272
}
7373

74+
lock.Path = cleanPath(lock.Path)
75+
7476
l, err := GetLFSLock(lock.Repo, lock.Path)
7577
if err == nil {
7678
return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path}
@@ -110,9 +112,24 @@ func GetLFSLockByID(id int64) (*LFSLock, error) {
110112
}
111113

112114
// GetLFSLockByRepoID returns a list of locks of repository.
113-
func GetLFSLockByRepoID(repoID int64) (locks []*LFSLock, err error) {
114-
err = x.Where("repo_id = ?", repoID).Find(&locks)
115-
return
115+
func GetLFSLockByRepoID(repoID int64, page, pageSize int) ([]*LFSLock, error) {
116+
sess := x.NewSession()
117+
defer sess.Close()
118+
119+
if page >= 0 && pageSize > 0 {
120+
start := 0
121+
if page > 0 {
122+
start = (page - 1) * pageSize
123+
}
124+
sess.Limit(pageSize, start)
125+
}
126+
lfsLocks := make([]*LFSLock, 0, pageSize)
127+
return lfsLocks, sess.Find(&lfsLocks, &LFSLock{RepoID: repoID})
128+
}
129+
130+
// CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository.
131+
func CountLFSLockByRepoID(repoID int64) (int64, error) {
132+
return x.Count(&LFSLock{RepoID: repoID})
116133
}
117134

118135
// DeleteLFSLockByID deletes a lock by given ID.

models/repo.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -2913,7 +2913,7 @@ func (repo *Repository) GetOriginalURLHostname() string {
29132913
// GetTreePathLock returns LSF lock for the treePath
29142914
func (repo *Repository) GetTreePathLock(treePath string) (*LFSLock, error) {
29152915
if setting.LFS.StartServer {
2916-
locks, err := GetLFSLockByRepoID(repo.ID)
2916+
locks, err := GetLFSLockByRepoID(repo.ID, 0, 0)
29172917
if err != nil {
29182918
return nil, err
29192919
}

modules/git/repo_attribute.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package git
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
11+
"github.com/mcuadros/go-version"
12+
)
13+
14+
// CheckAttributeOpts represents the possible options to CheckAttribute
15+
type CheckAttributeOpts struct {
16+
CachedOnly bool
17+
AllAttributes bool
18+
Attributes []string
19+
Filenames []string
20+
}
21+
22+
// CheckAttribute return the Blame object of file
23+
func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
24+
binVersion, err := BinVersion()
25+
if err != nil {
26+
return nil, fmt.Errorf("Git version missing: %v", err)
27+
}
28+
29+
stdOut := new(bytes.Buffer)
30+
stdErr := new(bytes.Buffer)
31+
32+
cmdArgs := []string{"check-attr", "-z"}
33+
34+
if opts.AllAttributes {
35+
cmdArgs = append(cmdArgs, "-a")
36+
} else {
37+
for _, attribute := range opts.Attributes {
38+
if attribute != "" {
39+
cmdArgs = append(cmdArgs, attribute)
40+
}
41+
}
42+
}
43+
44+
// git check-attr --cached first appears in git 1.7.8
45+
if opts.CachedOnly && version.Compare(binVersion, "1.7.8", ">=") {
46+
cmdArgs = append(cmdArgs, "--cached")
47+
}
48+
49+
cmdArgs = append(cmdArgs, "--")
50+
51+
for _, arg := range opts.Filenames {
52+
if arg != "" {
53+
cmdArgs = append(cmdArgs, arg)
54+
}
55+
}
56+
57+
cmd := NewCommand(cmdArgs...)
58+
59+
if err := cmd.RunInDirPipeline(repo.Path, stdOut, stdErr); err != nil {
60+
return nil, fmt.Errorf("Failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String())
61+
}
62+
63+
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
64+
65+
if len(fields)%3 != 1 {
66+
return nil, fmt.Errorf("Wrong number of fields in return from check-attr")
67+
}
68+
69+
var name2attribute2info = make(map[string]map[string]string)
70+
71+
for i := 0; i < (len(fields) / 3); i++ {
72+
filename := string(fields[3*i])
73+
attribute := string(fields[3*i+1])
74+
info := string(fields[3*i+2])
75+
attribute2info := name2attribute2info[filename]
76+
if attribute2info == nil {
77+
attribute2info = make(map[string]string)
78+
}
79+
attribute2info[attribute] = info
80+
name2attribute2info[filename] = attribute2info
81+
}
82+
83+
return name2attribute2info, nil
84+
}

modules/lfs/locks.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func GetListLockHandler(ctx *context.Context) {
110110
}
111111

112112
//If no query params path or id
113-
lockList, err := models.GetLFSLockByRepoID(repository.ID)
113+
lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
114114
if err != nil {
115115
ctx.JSON(500, api.LFSLockError{
116116
Message: "unable to list locks : " + err.Error(),
@@ -220,7 +220,7 @@ func VerifyLockHandler(ctx *context.Context) {
220220
}
221221

222222
//TODO handle body json cursor and limit
223-
lockList, err := models.GetLFSLockByRepoID(repository.ID)
223+
lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
224224
if err != nil {
225225
ctx.JSON(500, api.LFSLockError{
226226
Message: "unable to list locks : " + err.Error(),

options/locale/locale_en-US.ini

+10
Original file line numberDiff line numberDiff line change
@@ -1438,9 +1438,19 @@ settings.lfs_filelist=LFS files stored in this repository
14381438
settings.lfs_no_lfs_files=No LFS files stored in this repository
14391439
settings.lfs_findcommits=Find commits
14401440
settings.lfs_lfs_file_no_commits=No Commits found for this LFS file
1441+
settings.lfs_noattribute=This path does not have the lockable attribute in the default branch
14411442
settings.lfs_delete=Delete LFS file with OID %s
14421443
settings.lfs_delete_warning=Deleting an LFS file may cause 'object does not exist' errors on checkout. Are you sure?
14431444
settings.lfs_findpointerfiles=Find pointer files
1445+
settings.lfs_locks=Locks
1446+
settings.lfs_invalid_locking_path=Invalid path: %s
1447+
settings.lfs_invalid_lock_directory=Cannot lock directory: %s
1448+
settings.lfs_lock_already_exists=Lock already exists: %s
1449+
settings.lfs_lock=Lock
1450+
settings.lfs_lock_path=Filepath to lock...
1451+
settings.lfs_locks_no_locks=No Locks
1452+
settings.lfs_lock_file_no_exist=Locked file does not exist in default branch
1453+
settings.lfs_force_unlock=Force Unlock
14441454
settings.lfs_pointers.found=Found %d blob pointer(s) - %d associated, %d unassociated (%d missing from store)
14451455
settings.lfs_pointers.sha=Blob SHA
14461456
settings.lfs_pointers.oid=OID

routers/repo/lfs.go

+176
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io"
1313
"io/ioutil"
1414
"os"
15+
"path"
1516
"path/filepath"
1617
"sort"
1718
"strconv"
@@ -38,6 +39,7 @@ import (
3839

3940
const (
4041
tplSettingsLFS base.TplName = "repo/settings/lfs"
42+
tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks"
4143
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
4244
tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
4345
tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
@@ -58,6 +60,7 @@ func LFSFiles(ctx *context.Context) {
5860
ctx.ServerError("LFSFiles", err)
5961
return
6062
}
63+
ctx.Data["Total"] = total
6164

6265
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
6366
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
@@ -72,6 +75,179 @@ func LFSFiles(ctx *context.Context) {
7275
ctx.HTML(200, tplSettingsLFS)
7376
}
7477

78+
// LFSLocks shows a repository's LFS locks
79+
func LFSLocks(ctx *context.Context) {
80+
if !setting.LFS.StartServer {
81+
ctx.NotFound("LFSLocks", nil)
82+
return
83+
}
84+
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
85+
86+
page := ctx.QueryInt("page")
87+
if page <= 1 {
88+
page = 1
89+
}
90+
total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
91+
if err != nil {
92+
ctx.ServerError("LFSLocks", err)
93+
return
94+
}
95+
ctx.Data["Total"] = total
96+
97+
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
98+
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
99+
ctx.Data["PageIsSettingsLFS"] = true
100+
lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
101+
if err != nil {
102+
ctx.ServerError("LFSLocks", err)
103+
return
104+
}
105+
ctx.Data["LFSLocks"] = lfsLocks
106+
107+
if len(lfsLocks) == 0 {
108+
ctx.Data["Page"] = pager
109+
ctx.HTML(200, tplSettingsLFSLocks)
110+
return
111+
}
112+
113+
// Clone base repo.
114+
tmpBasePath, err := models.CreateTemporaryPath("locks")
115+
if err != nil {
116+
log.Error("Failed to create temporary path: %v", err)
117+
ctx.ServerError("LFSLocks", err)
118+
return
119+
}
120+
defer func() {
121+
if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
122+
log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
123+
}
124+
}()
125+
126+
if err := git.Clone(ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
127+
Bare: true,
128+
Shared: true,
129+
}); err != nil {
130+
log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
131+
ctx.ServerError("LFSLocks", fmt.Errorf("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
132+
}
133+
134+
gitRepo, err := git.OpenRepository(tmpBasePath)
135+
if err != nil {
136+
log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
137+
ctx.ServerError("LFSLocks", fmt.Errorf("Failed to open new temporary repository in: %s %v", tmpBasePath, err))
138+
}
139+
140+
filenames := make([]string, len(lfsLocks))
141+
142+
for i, lock := range lfsLocks {
143+
filenames[i] = lock.Path
144+
}
145+
146+
if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
147+
log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
148+
ctx.ServerError("LFSLocks", fmt.Errorf("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
149+
}
150+
151+
name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
152+
Attributes: []string{"lockable"},
153+
Filenames: filenames,
154+
CachedOnly: true,
155+
})
156+
if err != nil {
157+
log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
158+
ctx.ServerError("LFSLocks", err)
159+
}
160+
161+
lockables := make([]bool, len(lfsLocks))
162+
for i, lock := range lfsLocks {
163+
attribute2info, has := name2attribute2info[lock.Path]
164+
if !has {
165+
continue
166+
}
167+
if attribute2info["lockable"] != "set" {
168+
continue
169+
}
170+
lockables[i] = true
171+
}
172+
ctx.Data["Lockables"] = lockables
173+
174+
filelist, err := gitRepo.LsFiles(filenames...)
175+
if err != nil {
176+
log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
177+
ctx.ServerError("LFSLocks", err)
178+
}
179+
180+
filemap := make(map[string]bool, len(filelist))
181+
for _, name := range filelist {
182+
filemap[name] = true
183+
}
184+
185+
linkable := make([]bool, len(lfsLocks))
186+
for i, lock := range lfsLocks {
187+
linkable[i] = filemap[lock.Path]
188+
}
189+
ctx.Data["Linkable"] = linkable
190+
191+
ctx.Data["Page"] = pager
192+
ctx.HTML(200, tplSettingsLFSLocks)
193+
}
194+
195+
// LFSLockFile locks a file
196+
func LFSLockFile(ctx *context.Context) {
197+
if !setting.LFS.StartServer {
198+
ctx.NotFound("LFSLocks", nil)
199+
return
200+
}
201+
originalPath := ctx.Query("path")
202+
lockPath := originalPath
203+
if len(lockPath) == 0 {
204+
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
205+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
206+
return
207+
}
208+
if lockPath[len(lockPath)-1] == '/' {
209+
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
210+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
211+
return
212+
}
213+
lockPath = path.Clean("/" + lockPath)[1:]
214+
if len(lockPath) == 0 {
215+
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
216+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
217+
return
218+
}
219+
220+
_, err := models.CreateLFSLock(&models.LFSLock{
221+
Repo: ctx.Repo.Repository,
222+
Path: lockPath,
223+
Owner: ctx.User,
224+
})
225+
if err != nil {
226+
if models.IsErrLFSLockAlreadyExist(err) {
227+
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
228+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
229+
return
230+
}
231+
ctx.ServerError("LFSLockFile", err)
232+
return
233+
}
234+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
235+
}
236+
237+
// LFSUnlock forcibly unlocks an LFS lock
238+
func LFSUnlock(ctx *context.Context) {
239+
if !setting.LFS.StartServer {
240+
ctx.NotFound("LFSUnlock", nil)
241+
return
242+
}
243+
_, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, true)
244+
if err != nil {
245+
ctx.ServerError("LFSUnlock", err)
246+
return
247+
}
248+
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
249+
}
250+
75251
// LFSFileGet serves a single LFS file
76252
func LFSFileGet(ctx *context.Context) {
77253
if !setting.LFS.StartServer {

routers/routes/routes.go

+5
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,11 @@ func RegisterRoutes(m *macaron.Macaron) {
685685
m.Get("/pointers", repo.LFSPointerFiles)
686686
m.Post("/pointers/associate", repo.LFSAutoAssociate)
687687
m.Get("/find", repo.LFSFileFind)
688+
m.Group("/locks", func() {
689+
m.Get("/", repo.LFSLocks)
690+
m.Post("/", repo.LFSLockFile)
691+
m.Post("/:lid/unlock", repo.LFSUnlock)
692+
})
688693
})
689694

690695
}, func(ctx *context.Context) {

0 commit comments

Comments
 (0)