Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make repository management section handle lfs locks #8726

Merged
merged 12 commits into from
Dec 12, 2019
23 changes: 20 additions & 3 deletions models/lfs_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func CreateLFSLock(lock *LFSLock) (*LFSLock, error) {
return nil, err
}

lock.Path = cleanPath(lock.Path)

l, err := GetLFSLock(lock.Repo, lock.Path)
if err == nil {
return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path}
Expand Down Expand Up @@ -110,9 +112,24 @@ func GetLFSLockByID(id int64) (*LFSLock, error) {
}

// GetLFSLockByRepoID returns a list of locks of repository.
func GetLFSLockByRepoID(repoID int64) (locks []*LFSLock, err error) {
err = x.Where("repo_id = ?", repoID).Find(&locks)
return
func GetLFSLockByRepoID(repoID int64, page, pageSize int) ([]*LFSLock, error) {
sess := x.NewSession()
defer sess.Close()

if page >= 0 && pageSize > 0 {
start := 0
if page > 0 {
start = (page - 1) * pageSize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so page=0 and page=1 shall give same data?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup that's standard in Gitea

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't recognize to code becuase the usual syntax is if page == 0 { page = 1 }, but nevermind.

}
sess.Limit(pageSize, start)
}
lfsLocks := make([]*LFSLock, 0, pageSize)
return lfsLocks, sess.Find(&lfsLocks, &LFSLock{RepoID: repoID})
}

// CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository.
func CountLFSLockByRepoID(repoID int64) (int64, error) {
return x.Count(&LFSLock{RepoID: repoID})
}

// DeleteLFSLockByID deletes a lock by given ID.
Expand Down
2 changes: 1 addition & 1 deletion models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2881,7 +2881,7 @@ func (repo *Repository) GetOriginalURLHostname() string {
// GetTreePathLock returns LSF lock for the treePath
func (repo *Repository) GetTreePathLock(treePath string) (*LFSLock, error) {
if setting.LFS.StartServer {
locks, err := GetLFSLockByRepoID(repo.ID)
locks, err := GetLFSLockByRepoID(repo.ID, 0, 0)
if err != nil {
return nil, err
}
Expand Down
84 changes: 84 additions & 0 deletions modules/git/repo_attribute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package git

import (
"bytes"
"fmt"

"github.com/mcuadros/go-version"
)

// CheckAttributeOpts represents the possible options to CheckAttribute
type CheckAttributeOpts struct {
CachedOnly bool
AllAttributes bool
Attributes []string
Filenames []string
}

// CheckAttribute return the Blame object of file
func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
binVersion, err := BinVersion()
if err != nil {
return nil, fmt.Errorf("Git version missing: %v", err)
}

stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)

cmdArgs := []string{"check-attr", "-z"}

if opts.AllAttributes {
cmdArgs = append(cmdArgs, "-a")
} else {
for _, attribute := range opts.Attributes {
if attribute != "" {
cmdArgs = append(cmdArgs, attribute)
}
}
}

// git check-attr --cached first appears in git 1.7.8
if opts.CachedOnly && version.Compare(binVersion, "1.7.8", ">=") {
cmdArgs = append(cmdArgs, "--cached")
}

cmdArgs = append(cmdArgs, "--")

for _, arg := range opts.Filenames {
if arg != "" {
cmdArgs = append(cmdArgs, arg)
}
}

cmd := NewCommand(cmdArgs...)

if err := cmd.RunInDirPipeline(repo.Path, stdOut, stdErr); err != nil {
return nil, fmt.Errorf("Failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String())
}

fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})

if len(fields)%3 != 1 {
return nil, fmt.Errorf("Wrong number of fields in return from check-attr")
}

var name2attribute2info = make(map[string]map[string]string)

for i := 0; i < (len(fields) / 3); i++ {
filename := string(fields[3*i])
attribute := string(fields[3*i+1])
info := string(fields[3*i+2])
attribute2info := name2attribute2info[filename]
if attribute2info == nil {
attribute2info = make(map[string]string)
}
attribute2info[attribute] = info
name2attribute2info[filename] = attribute2info
}

return name2attribute2info, nil
}
4 changes: 2 additions & 2 deletions modules/lfs/locks.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func GetListLockHandler(ctx *context.Context) {
}

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

//TODO handle body json cursor and limit
lockList, err := models.GetLFSLockByRepoID(repository.ID)
lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
if err != nil {
ctx.JSON(500, api.LFSLockError{
Message: "unable to list locks : " + err.Error(),
Expand Down
10 changes: 10 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1432,9 +1432,19 @@ settings.lfs_filelist=LFS files stored in this repository
settings.lfs_no_lfs_files=No LFS files stored in this repository
settings.lfs_findcommits=Find commits
settings.lfs_lfs_file_no_commits=No Commits found for this LFS file
settings.lfs_noattribute=This path does not have the lockable attribute in the default branch
settings.lfs_delete=Delete LFS file with OID %s
settings.lfs_delete_warning=Deleting an LFS file may cause 'object does not exist' errors on checkout. Are you sure?
settings.lfs_findpointerfiles=Find pointer files
settings.lfs_locks=Locks
settings.lfs_invalid_locking_path=Invalid path: %s
settings.lfs_invalid_lock_directory=Cannot lock directory: %s
settings.lfs_lock_already_exists=Lock already exists: %s
settings.lfs_lock=Lock
settings.lfs_lock_path=Filepath to lock...
settings.lfs_locks_no_locks=No Locks
settings.lfs_lock_file_no_exist=Locked file does not exist in default branch
settings.lfs_force_unlock=Force Unlock
settings.lfs_pointers.found=Found %d blob pointer(s) - %d associated, %d unassociated (%d missing from store)
settings.lfs_pointers.sha=Blob SHA
settings.lfs_pointers.oid=OID
Expand Down
1 change: 1 addition & 0 deletions public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ i.icon.centerlock{top:1.5em}
.code-view :not(.fa):not(.octicon):not(.icon){font-size:12px;font-family:'SF Mono',Consolas,Menlo,'Liberation Mono',Monaco,'Lucida Console',monospace;line-height:20px}
.code-view table{width:100%}
.code-view .active{background:#fff866}
.octicon-tiny{font-size:.85714286rem}
.markdown:not(code){overflow:hidden;font-size:16px;line-height:1.6!important;word-wrap:break-word}
.markdown:not(code).ui.segment{padding:3em}
.markdown:not(code).file-view{padding:2em 2em 2em!important}
Expand Down
179 changes: 179 additions & 0 deletions routers/repo/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strconv"
Expand All @@ -38,6 +39,7 @@ import (

const (
tplSettingsLFS base.TplName = "repo/settings/lfs"
tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks"
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
Expand All @@ -58,6 +60,7 @@ func LFSFiles(ctx *context.Context) {
ctx.ServerError("LFSFiles", err)
return
}
ctx.Data["Total"] = total

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

// LFSLocks shows a repository's LFS locks
func LFSLocks(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSLocks", nil)
return
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"

page := ctx.QueryInt("page")
if page <= 1 {
page = 1
}
total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSLocks", err)
return
}
ctx.Data["Total"] = total

pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
ctx.Data["PageIsSettingsLFS"] = true
lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
if err != nil {
ctx.ServerError("LFSLocks", err)
return
}
ctx.Data["LFSLocks"] = lfsLocks

if len(lfsLocks) == 0 {
ctx.Data["Page"] = pager
ctx.HTML(200, tplSettingsLFSLocks)
return
}

// Clone base repo.
tmpBasePath, err := models.CreateTemporaryPath("locks")
if err != nil {
log.Error("Failed to create temporary path: %v", err)
ctx.ServerError("LFSLocks", err)
return
}
defer func() {
if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
}
}()

if err := git.Clone(ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
}); err != nil {
log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
ctx.ServerError("LFSLocks", fmt.Errorf("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
}

gitRepo, err := git.OpenRepository(tmpBasePath)
if err != nil {
log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", fmt.Errorf("Failed to open new temporary repository in: %s %v", tmpBasePath, err))
}

filenames := make([]string, len(lfsLocks))

for i, lock := range lfsLocks {
filenames[i] = lock.Path
}

if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
ctx.ServerError("LFSLocks", fmt.Errorf("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
}

name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
Attributes: []string{"lockable"},
Filenames: filenames,
CachedOnly: true,
})
if err != nil {
log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", err)
}

lockables := make([]bool, len(lfsLocks))
for i, lock := range lfsLocks {
attribute2info, has := name2attribute2info[lock.Path]
if !has {
continue
}
if attribute2info["lockable"] != "set" {
continue
}
lockables[i] = true
}
ctx.Data["Lockables"] = lockables

filelist, err := gitRepo.LsFiles(filenames...)
if err != nil {
log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you set ctx.ServerError do you have to log the error with log.Error too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No not necessarily but, if there's more context you can give - like here then sometimes it makes sense to log and then let server error log its smaller error.

ctx.ServerError("LFSLocks", err)
}

filemap := make(map[string]bool, len(filelist))
for _, name := range filelist {
filemap[name] = true
}

linkable := make([]bool, len(lfsLocks))
for i, lock := range lfsLocks {
linkable[i] = filemap[lock.Path]
}
ctx.Data["Linkable"] = linkable

ctx.Data["Page"] = pager
ctx.HTML(200, tplSettingsLFSLocks)
}

// LFSLockFile locks a file
func LFSLockFile(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSLocks", nil)
return
}
originalPath := ctx.Query("path")
lockPath := originalPath
if len(lockPath) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
if lockPath[len(lockPath)-1] == '/' {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
lockPath = path.Clean(lockPath)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
if lockPath[0] == '/' {
lockPath = lockPath[1:]
}
if len(lockPath) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is paths like . and .. covered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path.Cleanshould sort those

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm to do that it would need to have an initial "/" added. Two secs...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although to be honest you can't do anything with a pathological lock like that because it shouldn't work.


_, err := models.CreateLFSLock(&models.LFSLock{
Repo: ctx.Repo.Repository,
Path: lockPath,
Owner: ctx.User,
})
if err != nil {
if models.IsErrLFSLockAlreadyExist(err) {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
ctx.ServerError("LFSLockFile", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
}

// LFSUnlock forcibly unlocks an LFS lock
func LFSUnlock(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.NotFound("LFSUnlock", nil)
return
}
_, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, true)
if err != nil {
ctx.ServerError("LFSUnlock", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
}

// LFSFileGet serves a single LFS file
func LFSFileGet(ctx *context.Context) {
if !setting.LFS.StartServer {
Expand Down
5 changes: 5 additions & 0 deletions routers/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/pointers", repo.LFSPointerFiles)
m.Post("/pointers/associate", repo.LFSAutoAssociate)
m.Get("/find", repo.LFSFileFind)
m.Group("/locks", func() {
m.Get("/", repo.LFSLocks)
m.Post("/", repo.LFSLockFile)
m.Post("/:lid/unlock", repo.LFSUnlock)
})
})

}, func(ctx *context.Context) {
Expand Down
Loading