summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/lfs_lock.go25
-rw-r--r--models/repo.go2
-rw-r--r--modules/git/repo_attribute.go84
-rw-r--r--modules/lfs/locks.go4
-rw-r--r--options/locale/locale_en-US.ini10
-rw-r--r--routers/repo/lfs.go176
-rw-r--r--routers/routes/routes.go5
-rw-r--r--templates/repo/settings/lfs.tmpl5
-rw-r--r--templates/repo/settings/lfs_locks.tmpl61
-rw-r--r--web_src/less/_base.less4
10 files changed, 367 insertions, 9 deletions
diff --git a/models/lfs_lock.go b/models/lfs_lock.go
index ba1a452815..3e56a7960b 100644
--- a/models/lfs_lock.go
+++ b/models/lfs_lock.go
@@ -49,7 +49,7 @@ func (l *LFSLock) AfterLoad(session *xorm.Session) {
}
func cleanPath(p string) string {
- return path.Clean(p)
+ return path.Clean("/" + p)[1:]
}
// APIFormat convert a Release to lfs.LFSLock
@@ -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}
@@ -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
+ }
+ 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.
diff --git a/models/repo.go b/models/repo.go
index d5ea29c501..c904449bbd 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -2913,7 +2913,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
}
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
new file mode 100644
index 0000000000..c10c96f558
--- /dev/null
+++ b/modules/git/repo_attribute.go
@@ -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
+}
diff --git a/modules/lfs/locks.go b/modules/lfs/locks.go
index 9ffe6b9d59..b077cd2d0b 100644
--- a/modules/lfs/locks.go
+++ b/modules/lfs/locks.go
@@ -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(),
@@ -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(),
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 98133cdab3..c6fd3b863f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1438,9 +1438,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
diff --git a/routers/repo/lfs.go b/routers/repo/lfs.go
index de5020c944..c3266844b4 100644
--- a/routers/repo/lfs.go
+++ b/routers/repo/lfs.go
@@ -12,6 +12,7 @@ import (
"io"
"io/ioutil"
"os"
+ "path"
"path/filepath"
"sort"
"strconv"
@@ -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"
@@ -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")
@@ -72,6 +75,179 @@ 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)
+ 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)[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
+ }
+
+ _, 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 {
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index cdbbfaee04..cfd4a60974 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -685,6 +685,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) {
diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl
index e4480a8b97..f43f9479a2 100644
--- a/templates/repo/settings/lfs.tmpl
+++ b/templates/repo/settings/lfs.tmpl
@@ -5,9 +5,10 @@
<div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header">
- {{.i18n.Tr "repo.settings.lfs_filelist"}}
+ {{.i18n.Tr "repo.settings.lfs_filelist"}} ({{.i18n.Tr "admin.total" .Total}})
<div class="ui right">
- <a class="ui blue tiny show-panel button" href="{{.Link}}/pointers">{{.i18n.Tr "repo.settings.lfs_findpointerfiles"}}</a>
+ <a class="ui black tiny show-panel button" href="{{.Link}}/locks"><i class="octicon octicon-lock octicon-tiny"></i>{{.i18n.Tr "repo.settings.lfs_locks"}}</a>
+ <a class="ui blue tiny show-panel button" href="{{.Link}}/pointers"><i class="octicon octicon-search octicon-tiny"></i>&nbsp;{{.i18n.Tr "repo.settings.lfs_findpointerfiles"}}</a>
</div>
</h4>
<table id="lfs-files-table" class="ui attached segment single line table">
diff --git a/templates/repo/settings/lfs_locks.tmpl b/templates/repo/settings/lfs_locks.tmpl
new file mode 100644
index 0000000000..8a5f6e1658
--- /dev/null
+++ b/templates/repo/settings/lfs_locks.tmpl
@@ -0,0 +1,61 @@
+{{template "base/head" .}}
+<div class="repository settings lfs">
+ {{template "repo/header" .}}
+ {{template "repo/settings/navbar" .}}
+ <div class="ui container repository file list">
+ {{template "base/alert" .}}
+ <div class="tab-size-8 non-diff-file-content">
+ <h4 class="ui top attached header">
+ <a href="{{.LFSFilesLink}}">{{.i18n.Tr "repo.settings.lfs"}}</a> / {{.i18n.Tr "repo.settings.lfs_locks"}} ({{.i18n.Tr "admin.total" .Total}})
+ </h4>
+ <div class="ui attached segment">
+ <form class="ui form ignore-dirty" method="POST">
+ {{$.CsrfTokenHtml}}
+ <div class="ui fluid action input">
+ <input name="path" value="" placeholder="{{.i18n.Tr "repo.settings.lfs_lock_path"}}" autofocus>
+ <button class="ui blue button">{{.i18n.Tr "repo.settings.lfs_lock"}}</button>
+ </div>
+ </form>
+ </div>
+ <table id="lfs-files-locks-table" class="ui attached segment single line table">
+ <tbody>
+ {{range $index, $lock := .LFSLocks}}
+ <tr>
+ <td>
+ {{if index $.Linkable $index}}
+ <span class="octicon octicon-file-text"></span>
+ <a href="{{EscapePound $.RepoLink}}/src/branch/{{EscapePound $lock.Repo.DefaultBranch}}/{{EscapePound $lock.Path}}" title="{{$lock.Path}}">{{$lock.Path}}</a>
+ {{else}}
+ <span class="octicon octicon-diff"></span>
+ <span class="poping up" title="{{$.i18n.Tr "repo.settings.lfs_lock_file_no_exist"}}">{{$lock.Path}}</span>
+ {{end}}
+ {{if not (index $.Lockables $index)}}
+ <i class="octicon octicon-alert poping up" title="{{$.i18n.Tr "repo.settings.lfs_noattribute"}}"></i>
+ {{end}}
+ </td>
+ <td>
+ <a href="{{$.AppSubUrl}}/{{$lock.Owner.Name}}">
+ <img class="ui avatar image" src="{{$lock.Owner.RelAvatarLink}}">
+ {{$lock.Owner.DisplayName}}
+ </a>
+ </td>
+ <td>{{TimeSince .Created $.Lang}}</td>
+ <td class="right aligned">
+ <form action="{{$.LFSFilesLink}}/locks/{{$lock.ID}}/unlock" method="POST">
+ {{$.CsrfTokenHtml}}
+ <button class="ui blue button"><i class="octicon octicon-lock btn-octicon"></i>{{$.i18n.Tr "repo.settings.lfs_force_unlock"}}</button>
+ </form>
+ </td>
+ </tr>
+ {{else}}
+ <tr>
+ <td colspan="4">{{.i18n.Tr "repo.settings.lfs_locks_no_locks"}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{template "base/paginate" .}}
+ </div>
+ </div>
+</div>
+{{template "base/footer" .}}
diff --git a/web_src/less/_base.less b/web_src/less/_base.less
index 0fb12878ff..34a647f9a9 100644
--- a/web_src/less/_base.less
+++ b/web_src/less/_base.less
@@ -1112,3 +1112,7 @@ i.icon.centerlock {
background: #fff866;
}
}
+
+.octicon-tiny {
+ font-size: 0.85714286rem;
+}