* 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 * Sightags/v1.21.12.1
| @@ -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) { | |||||
| 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. | // 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 | ||||
| } | } | ||||
| @@ -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"> | ||||
| @@ -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; | |||||
| } | |||||