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