mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-22 13:25:36 +03:00
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
This commit is contained in:
parent
751cfb805d
commit
dc2fe9801f
10 changed files with 367 additions and 9 deletions
|
@ -49,7 +49,7 @@ func (l *LFSLock) AfterLoad(session *xorm.Session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanPath(p string) string {
|
func cleanPath(p string) string {
|
||||||
return path.Clean(p)
|
return path.Clean("/" + p)[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIFormat convert a Release to lfs.LFSLock
|
// APIFormat convert a Release to lfs.LFSLock
|
||||||
|
@ -71,6 +71,8 @@ func CreateLFSLock(lock *LFSLock) (*LFSLock, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock.Path = cleanPath(lock.Path)
|
||||||
|
|
||||||
l, err := GetLFSLock(lock.Repo, lock.Path)
|
l, err := GetLFSLock(lock.Repo, lock.Path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path}
|
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.
|
// GetLFSLockByRepoID returns a list of locks of repository.
|
||||||
func GetLFSLockByRepoID(repoID int64) (locks []*LFSLock, err error) {
|
func GetLFSLockByRepoID(repoID int64, page, pageSize int) ([]*LFSLock, error) {
|
||||||
err = x.Where("repo_id = ?", repoID).Find(&locks)
|
sess := x.NewSession()
|
||||||
return
|
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.
|
// DeleteLFSLockByID deletes a lock by given ID.
|
||||||
|
|
|
@ -2913,7 +2913,7 @@ func (repo *Repository) GetOriginalURLHostname() string {
|
||||||
// GetTreePathLock returns LSF lock for the treePath
|
// GetTreePathLock returns LSF lock for the treePath
|
||||||
func (repo *Repository) GetTreePathLock(treePath string) (*LFSLock, error) {
|
func (repo *Repository) GetTreePathLock(treePath string) (*LFSLock, error) {
|
||||||
if setting.LFS.StartServer {
|
if setting.LFS.StartServer {
|
||||||
locks, err := GetLFSLockByRepoID(repo.ID)
|
locks, err := GetLFSLockByRepoID(repo.ID, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
84
modules/git/repo_attribute.go
Normal file
84
modules/git/repo_attribute.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -110,7 +110,7 @@ func GetListLockHandler(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//If no query params path or id
|
//If no query params path or id
|
||||||
lockList, err := models.GetLFSLockByRepoID(repository.ID)
|
lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(500, api.LFSLockError{
|
ctx.JSON(500, api.LFSLockError{
|
||||||
Message: "unable to list locks : " + err.Error(),
|
Message: "unable to list locks : " + err.Error(),
|
||||||
|
@ -220,7 +220,7 @@ func VerifyLockHandler(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO handle body json cursor and limit
|
//TODO handle body json cursor and limit
|
||||||
lockList, err := models.GetLFSLockByRepoID(repository.ID)
|
lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(500, api.LFSLockError{
|
ctx.JSON(500, api.LFSLockError{
|
||||||
Message: "unable to list locks : " + err.Error(),
|
Message: "unable to list locks : " + err.Error(),
|
||||||
|
|
|
@ -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_no_lfs_files=No LFS files stored in this repository
|
||||||
settings.lfs_findcommits=Find commits
|
settings.lfs_findcommits=Find commits
|
||||||
settings.lfs_lfs_file_no_commits=No Commits found for this LFS file
|
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=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_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_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.found=Found %d blob pointer(s) - %d associated, %d unassociated (%d missing from store)
|
||||||
settings.lfs_pointers.sha=Blob SHA
|
settings.lfs_pointers.sha=Blob SHA
|
||||||
settings.lfs_pointers.oid=OID
|
settings.lfs_pointers.oid=OID
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -38,6 +39,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tplSettingsLFS base.TplName = "repo/settings/lfs"
|
tplSettingsLFS base.TplName = "repo/settings/lfs"
|
||||||
|
tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks"
|
||||||
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
|
tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
|
||||||
tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
|
tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
|
||||||
tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
|
tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
|
||||||
|
@ -58,6 +60,7 @@ func LFSFiles(ctx *context.Context) {
|
||||||
ctx.ServerError("LFSFiles", err)
|
ctx.ServerError("LFSFiles", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.Data["Total"] = total
|
||||||
|
|
||||||
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
|
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
|
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
|
||||||
|
@ -72,6 +75,179 @@ func LFSFiles(ctx *context.Context) {
|
||||||
ctx.HTML(200, tplSettingsLFS)
|
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
|
// LFSFileGet serves a single LFS file
|
||||||
func LFSFileGet(ctx *context.Context) {
|
func LFSFileGet(ctx *context.Context) {
|
||||||
if !setting.LFS.StartServer {
|
if !setting.LFS.StartServer {
|
||||||
|
|
|
@ -685,6 +685,11 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Get("/pointers", repo.LFSPointerFiles)
|
m.Get("/pointers", repo.LFSPointerFiles)
|
||||||
m.Post("/pointers/associate", repo.LFSAutoAssociate)
|
m.Post("/pointers/associate", repo.LFSAutoAssociate)
|
||||||
m.Get("/find", repo.LFSFileFind)
|
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) {
|
}, func(ctx *context.Context) {
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<h4 class="ui top attached header">
|
<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">
|
<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> {{.i18n.Tr "repo.settings.lfs_findpointerfiles"}}</a>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
<table id="lfs-files-table" class="ui attached segment single line table">
|
<table id="lfs-files-table" class="ui attached segment single line table">
|
||||||
|
|
61
templates/repo/settings/lfs_locks.tmpl
Normal file
61
templates/repo/settings/lfs_locks.tmpl
Normal file
|
@ -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" .}}
|
|
@ -1112,3 +1112,7 @@ i.icon.centerlock {
|
||||||
background: #fff866;
|
background: #fff866;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.octicon-tiny {
|
||||||
|
font-size: 0.85714286rem;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue