From 5e6a008fba9a85c9a5319d93c1e52e183211a342 Mon Sep 17 00:00:00 2001 From: zeripath Date: Mon, 28 Oct 2019 18:31:55 +0000 Subject: [PATCH] Add basic repository lfs management (#7199) This PR adds basic repository LFS management UI including the ability to find all possible pointers within the repository. Locks are not managed at present but would be addable through some simple additions. * Add basic repository lfs management * add auto-associate function * Add functionality to find commits with this lfs file * Add link to find commits on the lfs file view * Adjust commit view to state the likely branch causing the commit * Only read Oid from database --- models/lfs.go | 82 ++- models/repo_list.go | 48 +- modules/git/pipeline/catfile.go | 94 ++++ modules/git/pipeline/namerev.go | 28 ++ modules/git/pipeline/revlist.go | 75 +++ modules/git/repo.go | 5 + modules/lfs/server.go | 2 +- modules/repofiles/update.go | 2 +- modules/repofiles/upload.go | 2 +- options/locale/locale_en-US.ini | 15 + public/css/index.css | 1 + public/less/_base.less | 10 + routers/repo/lfs.go | 551 +++++++++++++++++++++ routers/routes/routes.go | 10 + services/pull/lfs.go | 116 +---- templates/repo/settings/lfs.tmpl | 62 +++ templates/repo/settings/lfs_file.tmpl | 57 +++ templates/repo/settings/lfs_file_find.tmpl | 52 ++ templates/repo/settings/lfs_pointers.tmpl | 71 +++ templates/repo/settings/navbar.tmpl | 5 + 20 files changed, 1151 insertions(+), 137 deletions(-) create mode 100644 modules/git/pipeline/catfile.go create mode 100644 modules/git/pipeline/namerev.go create mode 100644 modules/git/pipeline/revlist.go create mode 100644 routers/repo/lfs.go create mode 100644 templates/repo/settings/lfs.tmpl create mode 100644 templates/repo/settings/lfs_file.tmpl create mode 100644 templates/repo/settings/lfs_file_find.tmpl create mode 100644 templates/repo/settings/lfs_pointers.tmpl diff --git a/models/lfs.go b/models/lfs.go index 9b2064277..5f5fe2ccf 100644 --- a/models/lfs.go +++ b/models/lfs.go @@ -8,6 +8,8 @@ import ( "io" "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" ) // LFSMetaObject stores metadata for LFS tracked files. @@ -106,19 +108,91 @@ func (repo *Repository) GetLFSMetaObjectByOid(oid string) (*LFSMetaObject, error // RemoveLFSMetaObjectByOid removes a LFSMetaObject entry from database by its OID. // It may return ErrLFSObjectNotExist or a database error. -func (repo *Repository) RemoveLFSMetaObjectByOid(oid string) error { +func (repo *Repository) RemoveLFSMetaObjectByOid(oid string) (int64, error) { if len(oid) == 0 { - return ErrLFSObjectNotExist + return 0, ErrLFSObjectNotExist } + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return -1, err + } + + m := &LFSMetaObject{Oid: oid, RepositoryID: repo.ID} + if _, err := sess.Delete(m); err != nil { + return -1, err + } + + count, err := sess.Count(&LFSMetaObject{Oid: oid}) + if err != nil { + return count, err + } + + return count, sess.Commit() +} + +// GetLFSMetaObjects returns all LFSMetaObjects associated with a repository +func (repo *Repository) GetLFSMetaObjects(page, pageSize int) ([]*LFSMetaObject, 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) + } + lfsObjects := make([]*LFSMetaObject, 0, pageSize) + return lfsObjects, sess.Find(&lfsObjects, &LFSMetaObject{RepositoryID: repo.ID}) +} + +// CountLFSMetaObjects returns a count of all LFSMetaObjects associated with a repository +func (repo *Repository) CountLFSMetaObjects() (int64, error) { + return x.Count(&LFSMetaObject{RepositoryID: repo.ID}) +} + +// LFSObjectAccessible checks if a provided Oid is accessible to the user +func LFSObjectAccessible(user *User, oid string) (bool, error) { + if user.IsAdmin { + count, err := x.Count(&LFSMetaObject{Oid: oid}) + return (count > 0), err + } + cond := accessibleRepositoryCondition(user.ID) + count, err := x.Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Oid: oid}) + return (count > 0), err +} + +// LFSAutoAssociate auto associates accessible LFSMetaObjects +func LFSAutoAssociate(metas []*LFSMetaObject, user *User, repoID int64) error { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err } - m := &LFSMetaObject{Oid: oid, RepositoryID: repo.ID} - if _, err := sess.Delete(m); err != nil { + oids := make([]interface{}, len(metas)) + oidMap := make(map[string]*LFSMetaObject, len(metas)) + for i, meta := range metas { + oids[i] = meta.Oid + oidMap[meta.Oid] = meta + } + + cond := builder.NewCond() + if !user.IsAdmin { + cond = builder.In("`lfs_meta_object`.repository_id", + builder.Select("`repository`.id").From("repository").Where(accessibleRepositoryCondition(user.ID))) + } + newMetas := make([]*LFSMetaObject, 0, len(metas)) + if err := sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil { + return err + } + for i := range newMetas { + newMetas[i].Size = oidMap[newMetas[i].Oid].Size + newMetas[i].RepositoryID = repoID + } + if _, err := sess.InsertMulti(newMetas); err != nil { return err } diff --git a/models/repo_list.go b/models/repo_list.go index 692d4d002..c823647eb 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -176,28 +176,7 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { if opts.Private { if !opts.UserIsAdmin && opts.UserID != 0 && opts.UserID != opts.OwnerID { // OK we're in the context of a User - // We should be Either - cond = cond.And(builder.Or( - // 1. Be able to see all non-private repositories that either: - cond.And( - builder.Eq{"is_private": false}, - builder.Or( - // A. Aren't in organisations __OR__ - builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"type": UserTypeOrganization})), - // B. Isn't a private organisation. (Limited is OK because we're logged in) - builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePrivate}))), - ), - // 2. Be able to see all repositories that we have access to - builder.In("id", builder.Select("repo_id"). - From("`access`"). - Where(builder.And( - builder.Eq{"user_id": opts.UserID}, - builder.Gt{"mode": int(AccessModeNone)}))), - // 3. Be able to see all repositories that we are in a team - builder.In("id", builder.Select("`team_repo`.repo_id"). - From("team_repo"). - Where(builder.Eq{"`team_user`.uid": opts.UserID}). - Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")))) + cond = cond.And(accessibleRepositoryCondition(opts.UserID)) } } else { // Not looking at private organisations @@ -316,6 +295,31 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { return repos, count, nil } +// accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible +func accessibleRepositoryCondition(userID int64) builder.Cond { + return builder.Or( + // 1. Be able to see all non-private repositories that either: + builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.Or( + // A. Aren't in organisations __OR__ + builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"type": UserTypeOrganization})), + // B. Isn't a private organisation. (Limited is OK because we're logged in) + builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePrivate}))), + ), + // 2. Be able to see all repositories that we have access to + builder.In("`repository`.id", builder.Select("repo_id"). + From("`access`"). + Where(builder.And( + builder.Eq{"user_id": userID}, + builder.Gt{"mode": int(AccessModeNone)}))), + // 3. Be able to see all repositories that we are in a team + builder.In("`repository`.id", builder.Select("`team_repo`.repo_id"). + From("team_repo"). + Where(builder.Eq{"`team_user`.uid": userID}). + Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id"))) +} + // SearchRepositoryByName takes keyword and part of repository name to search, // it returns results in given range and number of total results. func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) { diff --git a/modules/git/pipeline/catfile.go b/modules/git/pipeline/catfile.go new file mode 100644 index 000000000..7293cf9d7 --- /dev/null +++ b/modules/git/pipeline/catfile.go @@ -0,0 +1,94 @@ +// 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 pipeline + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + "strings" + "sync" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// CatFileBatchCheck runs cat-file with --batch-check +func CatFileBatchCheck(shasToCheckReader *io.PipeReader, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { + defer wg.Done() + defer shasToCheckReader.Close() + defer catFileCheckWriter.Close() + + stderr := new(bytes.Buffer) + var errbuf strings.Builder + cmd := git.NewCommand("cat-file", "--batch-check") + if err := cmd.RunInDirFullPipeline(tmpBasePath, catFileCheckWriter, stderr, shasToCheckReader); err != nil { + _ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %v - %s", tmpBasePath, err, errbuf.String())) + } +} + +// CatFileBatchCheckAllObjects runs cat-file with --batch-check --batch-all +func CatFileBatchCheckAllObjects(catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string, errChan chan<- error) { + defer wg.Done() + defer catFileCheckWriter.Close() + + stderr := new(bytes.Buffer) + var errbuf strings.Builder + cmd := git.NewCommand("cat-file", "--batch-check", "--batch-all-objects") + if err := cmd.RunInDirPipeline(tmpBasePath, catFileCheckWriter, stderr); err != nil { + log.Error("git cat-file --batch-check --batch-all-object [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + err = fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + _ = catFileCheckWriter.CloseWithError(err) + errChan <- err + } +} + +// CatFileBatch runs cat-file --batch +func CatFileBatch(shasToBatchReader *io.PipeReader, catFileBatchWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { + defer wg.Done() + defer shasToBatchReader.Close() + defer catFileBatchWriter.Close() + + stderr := new(bytes.Buffer) + var errbuf strings.Builder + if err := git.NewCommand("cat-file", "--batch").RunInDirFullPipeline(tmpBasePath, catFileBatchWriter, stderr, shasToBatchReader); err != nil { + _ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String())) + } +} + +// BlobsLessThan1024FromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <1024 in size +func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) { + defer wg.Done() + defer catFileCheckReader.Close() + scanner := bufio.NewScanner(catFileCheckReader) + defer func() { + _ = shasToBatchWriter.CloseWithError(scanner.Err()) + }() + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + fields := strings.Split(line, " ") + if len(fields) < 3 || fields[1] != "blob" { + continue + } + size, _ := strconv.Atoi(fields[2]) + if size > 1024 { + continue + } + toWrite := []byte(fields[0] + "\n") + for len(toWrite) > 0 { + n, err := shasToBatchWriter.Write(toWrite) + if err != nil { + _ = catFileCheckReader.CloseWithError(err) + break + } + toWrite = toWrite[n:] + } + } +} diff --git a/modules/git/pipeline/namerev.go b/modules/git/pipeline/namerev.go new file mode 100644 index 000000000..eebb53b0c --- /dev/null +++ b/modules/git/pipeline/namerev.go @@ -0,0 +1,28 @@ +// 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 pipeline + +import ( + "bytes" + "fmt" + "io" + "strings" + "sync" + + "code.gitea.io/gitea/modules/git" +) + +// NameRevStdin runs name-rev --stdin +func NameRevStdin(shasToNameReader *io.PipeReader, nameRevStdinWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { + defer wg.Done() + defer shasToNameReader.Close() + defer nameRevStdinWriter.Close() + + stderr := new(bytes.Buffer) + var errbuf strings.Builder + if err := git.NewCommand("name-rev", "--stdin", "--name-only", "--always").RunInDirFullPipeline(tmpBasePath, nameRevStdinWriter, stderr, shasToNameReader); err != nil { + _ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %v - %s", tmpBasePath, err, errbuf.String())) + } +} diff --git a/modules/git/pipeline/revlist.go b/modules/git/pipeline/revlist.go new file mode 100644 index 000000000..4e13e1944 --- /dev/null +++ b/modules/git/pipeline/revlist.go @@ -0,0 +1,75 @@ +// 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 pipeline + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" + "sync" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// RevListAllObjects runs rev-list --objects --all and writes to a pipewriter +func RevListAllObjects(revListWriter *io.PipeWriter, wg *sync.WaitGroup, basePath string, errChan chan<- error) { + defer wg.Done() + defer revListWriter.Close() + + stderr := new(bytes.Buffer) + var errbuf strings.Builder + cmd := git.NewCommand("rev-list", "--objects", "--all") + if err := cmd.RunInDirPipeline(basePath, revListWriter, stderr); err != nil { + log.Error("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String()) + err = fmt.Errorf("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String()) + _ = revListWriter.CloseWithError(err) + errChan <- err + } +} + +// RevListObjects run rev-list --objects from headSHA to baseSHA +func RevListObjects(revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) { + defer wg.Done() + defer revListWriter.Close() + stderr := new(bytes.Buffer) + var errbuf strings.Builder + cmd := git.NewCommand("rev-list", "--objects", headSHA, "--not", baseSHA) + if err := cmd.RunInDirPipeline(tmpBasePath, revListWriter, stderr); err != nil { + log.Error("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + errChan <- fmt.Errorf("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + } +} + +// BlobsFromRevListObjects reads a RevListAllObjects and only selects blobs +func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io.PipeWriter, wg *sync.WaitGroup) { + defer wg.Done() + defer revListReader.Close() + scanner := bufio.NewScanner(revListReader) + defer func() { + _ = shasToCheckWriter.CloseWithError(scanner.Err()) + }() + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + fields := strings.Split(line, " ") + if len(fields) < 2 || len(fields[1]) == 0 { + continue + } + toWrite := []byte(fields[0] + "\n") + for len(toWrite) > 0 { + n, err := shasToCheckWriter.Write(toWrite) + if err != nil { + _ = revListReader.CloseWithError(err) + break + } + toWrite = toWrite[n:] + } + } +} diff --git a/modules/git/repo.go b/modules/git/repo.go index dd886f3a2..e1d75ca4a 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -117,6 +117,11 @@ func OpenRepository(repoPath string) (*Repository, error) { }, nil } +// GoGitRepo gets the go-git repo representation +func (repo *Repository) GoGitRepo() *gogit.Repository { + return repo.gogitRepo +} + // IsEmpty Check if repository is empty. func (repo *Repository) IsEmpty() (bool, error) { var errbuf strings.Builder diff --git a/modules/lfs/server.go b/modules/lfs/server.go index 6fa97a289..dc498a86c 100644 --- a/modules/lfs/server.go +++ b/modules/lfs/server.go @@ -332,7 +332,7 @@ func PutHandler(ctx *context.Context) { if err := contentStore.Put(meta, bodyReader); err != nil { ctx.Resp.WriteHeader(500) fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err) - if err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil { + if _, err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil { log.Error("RemoveLFSMetaObjectByOid: %v", err) } return diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 8a1e51730..8e057700a 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -385,7 +385,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} if !contentStore.Exists(lfsMetaObject) { if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil { - if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { + if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) } return nil, err diff --git a/modules/repofiles/upload.go b/modules/repofiles/upload.go index 202e66b89..a2e7cc927 100644 --- a/modules/repofiles/upload.go +++ b/modules/repofiles/upload.go @@ -36,7 +36,7 @@ func cleanUpAfterFailure(infos *[]uploadInfo, t *TemporaryUploadRepository, orig continue } if !info.lfsMetaObject.Existing { - if err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil { + if _, err := t.repo.RemoveLFSMetaObjectByOid(info.lfsMetaObject.Oid); err != nil { original = fmt.Errorf("%v, %v", original, err) } } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f8e25a85f..4210ed121 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1378,6 +1378,21 @@ settings.unarchive.text = Un-Archiving the repo will restore its ability to rece settings.unarchive.success = The repo was successfully un-archived. settings.unarchive.error = An error occurred while trying to un-archive the repo. See the log for more details. settings.update_avatar_success = The repository avatar has been updated. +settings.lfs=LFS +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_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_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 +settings.lfs_pointers.inRepo=In Repo +settings.lfs_pointers.exists=Exists in store +settings.lfs_pointers.accessible=Accessible to User +settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs diff.browse_source = Browse Source diff.parent = parent diff --git a/public/css/index.css b/public/css/index.css index 68339cf0b..d0fe896a0 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -126,6 +126,7 @@ a{cursor:pointer} .ui .form .fake{display:none!important} .ui .form .sub.field{margin-left:25px} .ui .sha.label{font-family:'SF Mono',Consolas,Menlo,'Liberation Mono',Monaco,'Lucida Console',monospace;font-size:13px;padding:6px 10px 4px 10px;font-weight:400;margin:0 6px} +.ui .button.truncate{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:top;white-space:nowrap;margin-right:6px} .ui.status.buttons .octicon{margin-right:4px} .ui.inline.delete-button{padding:8px 15px;font-weight:400} .ui .background.red{background-color:#d95c5c!important} diff --git a/public/less/_base.less b/public/less/_base.less index a993bbed3..7fcfaf82e 100644 --- a/public/less/_base.less +++ b/public/less/_base.less @@ -539,6 +539,16 @@ code, margin: 0 6px; } + .button.truncate { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + margin-right: 6px; + } + &.status.buttons { .octicon { margin-right: 4px; diff --git a/routers/repo/lfs.go b/routers/repo/lfs.go new file mode 100644 index 000000000..de5020c94 --- /dev/null +++ b/routers/repo/lfs.go @@ -0,0 +1,551 @@ +// 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 repo + +import ( + "bufio" + "bytes" + "fmt" + gotemplate "html/template" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/pipeline" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/mcuadros/go-version" + "github.com/unknwon/com" + gogit "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" +) + +const ( + tplSettingsLFS base.TplName = "repo/settings/lfs" + tplSettingsLFSFile base.TplName = "repo/settings/lfs_file" + tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find" + tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers" +) + +// LFSFiles shows a repository's LFS files +func LFSFiles(ctx *context.Context) { + if !setting.LFS.StartServer { + ctx.NotFound("LFSFiles", nil) + return + } + page := ctx.QueryInt("page") + if page <= 1 { + page = 1 + } + total, err := ctx.Repo.Repository.CountLFSMetaObjects() + if err != nil { + ctx.ServerError("LFSFiles", err) + return + } + + pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5) + ctx.Data["Title"] = ctx.Tr("repo.settings.lfs") + ctx.Data["PageIsSettingsLFS"] = true + lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum) + if err != nil { + ctx.ServerError("LFSFiles", err) + return + } + ctx.Data["LFSFiles"] = lfsMetaObjects + ctx.Data["Page"] = pager + ctx.HTML(200, tplSettingsLFS) +} + +// LFSFileGet serves a single LFS file +func LFSFileGet(ctx *context.Context) { + if !setting.LFS.StartServer { + ctx.NotFound("LFSFileGet", nil) + return + } + ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" + oid := ctx.Params("oid") + ctx.Data["Title"] = oid + ctx.Data["PageIsSettingsLFS"] = true + meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid) + if err != nil { + if err == models.ErrLFSObjectNotExist { + ctx.NotFound("LFSFileGet", nil) + return + } + ctx.ServerError("LFSFileGet", err) + return + } + ctx.Data["LFSFile"] = meta + dataRc, err := lfs.ReadMetaObject(meta) + if err != nil { + ctx.ServerError("LFSFileGet", err) + return + } + defer dataRc.Close() + buf := make([]byte, 1024) + n, err := dataRc.Read(buf) + if err != nil { + ctx.ServerError("Data", err) + return + } + buf = buf[:n] + + isTextFile := base.IsTextFile(buf) + ctx.Data["IsTextFile"] = isTextFile + + fileSize := meta.Size + ctx.Data["FileSize"] = meta.Size + ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct") + switch { + case isTextFile: + if fileSize >= setting.UI.MaxDisplayFileSize { + ctx.Data["IsFileTooLarge"] = true + break + } + + d, _ := ioutil.ReadAll(dataRc) + buf = charset.ToUTF8WithFallback(append(buf, d...)) + + // Building code view blocks with line number on server side. + var fileContent string + if content, err := charset.ToUTF8WithErr(buf); err != nil { + log.Error("ToUTF8WithErr: %v", err) + fileContent = string(buf) + } else { + fileContent = content + } + + var output bytes.Buffer + lines := strings.Split(fileContent, "\n") + //Remove blank line at the end of file + if len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + for index, line := range lines { + line = gotemplate.HTMLEscapeString(line) + if index != len(lines)-1 { + line += "\n" + } + output.WriteString(fmt.Sprintf(`
  • %s
  • `, index+1, index+1, line)) + } + ctx.Data["FileContent"] = gotemplate.HTML(output.String()) + + output.Reset() + for i := 0; i < len(lines); i++ { + output.WriteString(fmt.Sprintf(`%d`, i+1, i+1)) + } + ctx.Data["LineNums"] = gotemplate.HTML(output.String()) + + case base.IsPDFFile(buf): + ctx.Data["IsPDFFile"] = true + case base.IsVideoFile(buf): + ctx.Data["IsVideoFile"] = true + case base.IsAudioFile(buf): + ctx.Data["IsAudioFile"] = true + case base.IsImageFile(buf): + ctx.Data["IsImageFile"] = true + } + ctx.HTML(200, tplSettingsLFSFile) +} + +// LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it +func LFSDelete(ctx *context.Context) { + if !setting.LFS.StartServer { + ctx.NotFound("LFSDelete", nil) + return + } + oid := ctx.Params("oid") + count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(oid) + if err != nil { + ctx.ServerError("LFSDelete", err) + return + } + // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here + // Please note a similar condition happens in models/repo.go DeleteRepository + if count == 0 { + oidPath := filepath.Join(oid[0:2], oid[2:4], oid[4:]) + err = os.Remove(filepath.Join(setting.LFS.ContentPath, oidPath)) + if err != nil { + ctx.ServerError("LFSDelete", err) + return + } + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs") +} + +type lfsResult struct { + Name string + SHA string + Summary string + When time.Time + ParentHashes []plumbing.Hash + BranchName string + FullCommitName string +} + +type lfsResultSlice []*lfsResult + +func (a lfsResultSlice) Len() int { return len(a) } +func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) } + +// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha +func LFSFileFind(ctx *context.Context) { + if !setting.LFS.StartServer { + ctx.NotFound("LFSFind", nil) + return + } + oid := ctx.Query("oid") + size := ctx.QueryInt64("size") + if len(oid) == 0 || size == 0 { + ctx.NotFound("LFSFind", nil) + return + } + sha := ctx.Query("sha") + ctx.Data["Title"] = oid + ctx.Data["PageIsSettingsLFS"] = true + var hash plumbing.Hash + if len(sha) == 0 { + meta := models.LFSMetaObject{Oid: oid, Size: size} + pointer := meta.Pointer() + hash = plumbing.ComputeHash(plumbing.BlobObject, []byte(pointer)) + sha = hash.String() + } else { + hash = plumbing.NewHash(sha) + } + ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" + ctx.Data["Oid"] = oid + ctx.Data["Size"] = size + ctx.Data["SHA"] = sha + + resultsMap := map[string]*lfsResult{} + results := make([]*lfsResult, 0) + + basePath := ctx.Repo.Repository.RepoPath() + gogitRepo := ctx.Repo.GitRepo.GoGitRepo() + + commitsIter, err := gogitRepo.Log(&gogit.LogOptions{ + Order: gogit.LogOrderCommitterTime, + All: true, + }) + if err != nil { + log.Error("Failed to get GoGit CommitsIter: %v", err) + ctx.ServerError("LFSFind: Iterate Commits", err) + return + } + + err = commitsIter.ForEach(func(gitCommit *object.Commit) error { + tree, err := gitCommit.Tree() + if err != nil { + return err + } + treeWalker := object.NewTreeWalker(tree, true, nil) + defer treeWalker.Close() + for { + name, entry, err := treeWalker.Next() + if err == io.EOF { + break + } + if entry.Hash == hash { + result := lfsResult{ + Name: name, + SHA: gitCommit.Hash.String(), + Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0], + When: gitCommit.Author.When, + ParentHashes: gitCommit.ParentHashes, + } + resultsMap[gitCommit.Hash.String()+":"+name] = &result + } + } + return nil + }) + if err != nil && err != io.EOF { + log.Error("Failure in CommitIter.ForEach: %v", err) + ctx.ServerError("LFSFind: IterateCommits ForEach", err) + return + } + + for _, result := range resultsMap { + hasParent := false + for _, parentHash := range result.ParentHashes { + if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent { + break + } + } + if !hasParent { + results = append(results, result) + } + } + + sort.Sort(lfsResultSlice(results)) + + // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple + shasToNameReader, shasToNameWriter := io.Pipe() + nameRevStdinReader, nameRevStdinWriter := io.Pipe() + errChan := make(chan error, 1) + wg := sync.WaitGroup{} + wg.Add(3) + + go func() { + defer wg.Done() + scanner := bufio.NewScanner(nameRevStdinReader) + i := 0 + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + result := results[i] + result.FullCommitName = line + result.BranchName = strings.Split(line, "~")[0] + i++ + } + }() + go pipeline.NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath) + go func() { + defer wg.Done() + defer shasToNameWriter.Close() + for _, result := range results { + i := 0 + if i < len(result.SHA) { + n, err := shasToNameWriter.Write([]byte(result.SHA)[i:]) + if err != nil { + errChan <- err + break + } + i += n + } + n := 0 + for n < 1 { + n, err = shasToNameWriter.Write([]byte{'\n'}) + if err != nil { + errChan <- err + break + } + + } + + } + }() + + wg.Wait() + + select { + case err, has := <-errChan: + if has { + ctx.ServerError("LFSPointerFiles", err) + } + default: + } + + ctx.Data["Results"] = results + ctx.HTML(200, tplSettingsLFSFileFind) +} + +// LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store +func LFSPointerFiles(ctx *context.Context) { + if !setting.LFS.StartServer { + ctx.NotFound("LFSFileGet", nil) + return + } + ctx.Data["PageIsSettingsLFS"] = true + binVersion, err := git.BinVersion() + if err != nil { + log.Fatal("Error retrieving git version: %v", err) + } + ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" + + basePath := ctx.Repo.Repository.RepoPath() + + pointerChan := make(chan pointerResult) + + catFileCheckReader, catFileCheckWriter := io.Pipe() + shasToBatchReader, shasToBatchWriter := io.Pipe() + catFileBatchReader, catFileBatchWriter := io.Pipe() + errChan := make(chan error, 1) + wg := sync.WaitGroup{} + wg.Add(5) + + var numPointers, numAssociated, numNoExist, numAssociatable int + + go func() { + defer wg.Done() + pointers := make([]pointerResult, 0, 50) + for pointer := range pointerChan { + pointers = append(pointers, pointer) + if pointer.InRepo { + numAssociated++ + } + if !pointer.Exists { + numNoExist++ + } + if !pointer.InRepo && pointer.Accessible { + numAssociatable++ + } + } + numPointers = len(pointers) + ctx.Data["Pointers"] = pointers + ctx.Data["NumPointers"] = numPointers + ctx.Data["NumAssociated"] = numAssociated + ctx.Data["NumAssociatable"] = numAssociatable + ctx.Data["NumNoExist"] = numNoExist + ctx.Data["NumNotAssociated"] = numPointers - numAssociated + }() + go createPointerResultsFromCatFileBatch(catFileBatchReader, &wg, pointerChan, ctx.Repo.Repository, ctx.User) + go pipeline.CatFileBatch(shasToBatchReader, catFileBatchWriter, &wg, basePath) + go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) + if !version.Compare(binVersion, "2.6.0", ">=") { + revListReader, revListWriter := io.Pipe() + shasToCheckReader, shasToCheckWriter := io.Pipe() + wg.Add(2) + go pipeline.CatFileBatchCheck(shasToCheckReader, catFileCheckWriter, &wg, basePath) + go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg) + go pipeline.RevListAllObjects(revListWriter, &wg, basePath, errChan) + } else { + go pipeline.CatFileBatchCheckAllObjects(catFileCheckWriter, &wg, basePath, errChan) + } + wg.Wait() + + select { + case err, has := <-errChan: + if has { + ctx.ServerError("LFSPointerFiles", err) + } + default: + } + ctx.HTML(200, tplSettingsLFSPointers) +} + +type pointerResult struct { + SHA string + Oid string + Size int64 + InRepo bool + Exists bool + Accessible bool +} + +func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- pointerResult, repo *models.Repository, user *models.User) { + defer wg.Done() + defer catFileBatchReader.Close() + contentStore := lfs.ContentStore{BasePath: setting.LFS.ContentPath} + + bufferedReader := bufio.NewReader(catFileBatchReader) + buf := make([]byte, 1025) + for { + // File descriptor line: sha + sha, err := bufferedReader.ReadString(' ') + if err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } + // Throw away the blob + if _, err := bufferedReader.ReadString(' '); err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } + sizeStr, err := bufferedReader.ReadString('\n') + if err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } + size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1]) + if err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } + pointerBuf := buf[:size+1] + if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } + pointerBuf = pointerBuf[:size] + // Now we need to check if the pointerBuf is an LFS pointer + pointer := lfs.IsPointerFile(&pointerBuf) + if pointer == nil { + continue + } + + result := pointerResult{ + SHA: strings.TrimSpace(sha), + Oid: pointer.Oid, + Size: pointer.Size, + } + + // Then we need to check that this pointer is in the db + if _, err := repo.GetLFSMetaObjectByOid(pointer.Oid); err != nil { + if err != models.ErrLFSObjectNotExist { + _ = catFileBatchReader.CloseWithError(err) + break + } + } else { + result.InRepo = true + } + + result.Exists = contentStore.Exists(pointer) + + if result.Exists { + if !result.InRepo { + // Can we fix? + // OK well that's "simple" + // - we need to check whether current user has access to a repo that has access to the file + result.Accessible, err = models.LFSObjectAccessible(user, result.Oid) + if err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } + } else { + result.Accessible = true + } + } + pointerChan <- result + } + close(pointerChan) +} + +// LFSAutoAssociate auto associates accessible lfs files +func LFSAutoAssociate(ctx *context.Context) { + if !setting.LFS.StartServer { + ctx.NotFound("LFSAutoAssociate", nil) + return + } + oids := ctx.QueryStrings("oid") + metas := make([]*models.LFSMetaObject, len(oids)) + for i, oid := range oids { + idx := strings.IndexRune(oid, ' ') + if idx < 0 || idx+1 > len(oid) { + ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s", oid)) + return + } + var err error + metas[i] = &models.LFSMetaObject{} + metas[i].Size, err = com.StrTo(oid[idx+1:]).Int64() + if err != nil { + ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s %v", oid, err)) + return + } + metas[i].Oid = oid[:idx] + //metas[i].RepositoryID = ctx.Repo.Repository.ID + } + if err := models.LFSAutoAssociate(metas, ctx.User, ctx.Repo.Repository.ID); err != nil { + ctx.ServerError("LFSAutoAssociate", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs") +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 9572ea803..13a5bb270 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -677,8 +677,18 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/delete", repo.DeleteDeployKey) }) + m.Group("/lfs", func() { + m.Get("", repo.LFSFiles) + m.Get("/show/:oid", repo.LFSFileGet) + m.Post("/delete/:oid", repo.LFSDelete) + m.Get("/pointers", repo.LFSPointerFiles) + m.Post("/pointers/associate", repo.LFSAutoAssociate) + m.Get("/find", repo.LFSFileFind) + }) + }, func(ctx *context.Context) { ctx.Data["PageIsSettings"] = true + ctx.Data["LFSStartServer"] = setting.LFS.StartServer }) }, reqSignIn, context.RepoAssignment(), context.UnitTypes(), reqRepoAdmin, context.RepoRef()) diff --git a/services/pull/lfs.go b/services/pull/lfs.go index 2706d3a20..a1981b825 100644 --- a/services/pull/lfs.go +++ b/services/pull/lfs.go @@ -7,15 +7,12 @@ package pull import ( "bufio" - "bytes" - "fmt" "io" "strconv" - "strings" "sync" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/pipeline" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" ) @@ -41,22 +38,22 @@ func LFSPush(tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *models.PullRequ // 6. Take the output of cat-file --batch and check if each file in turn // to see if they're pointers to files in the LFS store associated with // the head repo and add them to the base repo if so - go readCatFileBatch(catFileBatchReader, &wg, pr) + go createLFSMetaObjectsFromCatFileBatch(catFileBatchReader, &wg, pr) // 5. Take the shas of the blobs and batch read them - go doCatFileBatch(shasToBatchReader, catFileBatchWriter, &wg, tmpBasePath) + go pipeline.CatFileBatch(shasToBatchReader, catFileBatchWriter, &wg, tmpBasePath) // 4. From the provided objects restrict to blobs <=1k - go readCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) + go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) // 3. Run batch-check on the objects retrieved from rev-list - go doCatFileBatchCheck(shasToCheckReader, catFileCheckWriter, &wg, tmpBasePath) + go pipeline.CatFileBatchCheck(shasToCheckReader, catFileCheckWriter, &wg, tmpBasePath) // 2. Check each object retrieved rejecting those without names as they will be commits or trees - go readRevListObjects(revListReader, shasToCheckWriter, &wg) + go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg) // 1. Run rev-list objects from mergeHead to mergeBase - go doRevListObjects(revListWriter, &wg, tmpBasePath, mergeHeadSHA, mergeBaseSHA, errChan) + go pipeline.RevListObjects(revListWriter, &wg, tmpBasePath, mergeHeadSHA, mergeBaseSHA, errChan) wg.Wait() select { @@ -69,104 +66,7 @@ func LFSPush(tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *models.PullRequ return nil } -func doRevListObjects(revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) { - defer wg.Done() - defer revListWriter.Close() - stderr := new(bytes.Buffer) - var errbuf strings.Builder - cmd := git.NewCommand("rev-list", "--objects", headSHA, "--not", baseSHA) - if err := cmd.RunInDirPipeline(tmpBasePath, revListWriter, stderr); err != nil { - log.Error("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - errChan <- fmt.Errorf("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - } -} - -func readRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io.PipeWriter, wg *sync.WaitGroup) { - defer wg.Done() - defer revListReader.Close() - defer shasToCheckWriter.Close() - scanner := bufio.NewScanner(revListReader) - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 { - continue - } - fields := strings.Split(line, " ") - if len(fields) < 2 || len(fields[1]) == 0 { - continue - } - toWrite := []byte(fields[0] + "\n") - for len(toWrite) > 0 { - n, err := shasToCheckWriter.Write(toWrite) - if err != nil { - _ = revListReader.CloseWithError(err) - break - } - toWrite = toWrite[n:] - } - } - _ = shasToCheckWriter.CloseWithError(scanner.Err()) -} - -func doCatFileBatchCheck(shasToCheckReader *io.PipeReader, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { - defer wg.Done() - defer shasToCheckReader.Close() - defer catFileCheckWriter.Close() - - stderr := new(bytes.Buffer) - var errbuf strings.Builder - cmd := git.NewCommand("cat-file", "--batch-check") - if err := cmd.RunInDirFullPipeline(tmpBasePath, catFileCheckWriter, stderr, shasToCheckReader); err != nil { - _ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %v - %s", tmpBasePath, err, errbuf.String())) - } -} - -func readCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) { - defer wg.Done() - defer catFileCheckReader.Close() - - scanner := bufio.NewScanner(catFileCheckReader) - defer func() { - _ = shasToBatchWriter.CloseWithError(scanner.Err()) - }() - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 { - continue - } - fields := strings.Split(line, " ") - if len(fields) < 3 || fields[1] != "blob" { - continue - } - size, _ := strconv.Atoi(fields[2]) - if size > 1024 { - continue - } - toWrite := []byte(fields[0] + "\n") - for len(toWrite) > 0 { - n, err := shasToBatchWriter.Write(toWrite) - if err != nil { - _ = catFileCheckReader.CloseWithError(err) - break - } - toWrite = toWrite[n:] - } - } -} - -func doCatFileBatch(shasToBatchReader *io.PipeReader, catFileBatchWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { - defer wg.Done() - defer shasToBatchReader.Close() - defer catFileBatchWriter.Close() - - stderr := new(bytes.Buffer) - var errbuf strings.Builder - if err := git.NewCommand("cat-file", "--batch").RunInDirFullPipeline(tmpBasePath, catFileBatchWriter, stderr, shasToBatchReader); err != nil { - _ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String())) - } -} - -func readCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *models.PullRequest) { +func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *models.PullRequest) { defer wg.Done() defer catFileBatchReader.Close() diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl new file mode 100644 index 000000000..e4480a8b9 --- /dev/null +++ b/templates/repo/settings/lfs.tmpl @@ -0,0 +1,62 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} +

    + {{.i18n.Tr "repo.settings.lfs_filelist"}} + +

    + + + {{range .LFSFiles}} + + + + + + + {{else}} + + + + {{end}} + +
    + + + {{ShortSha .Oid}} + + + {{FileSize .Size}}{{TimeSince .CreatedUnix.AsTime $.Lang}} + {{$.i18n.Tr "repo.settings.lfs_findcommits"}} + +
    {{.i18n.Tr "repo.settings.lfs_no_lfs_files"}}
    + {{template "base/paginate" .}} + {{range .LFSFiles}} + + {{end}} +
    +
    +{{template "base/footer" .}} diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl new file mode 100644 index 000000000..6283548ea --- /dev/null +++ b/templates/repo/settings/lfs_file.tmpl @@ -0,0 +1,57 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} +
    +

    + {{.i18n.Tr "repo.settings.lfs"}} / {{.LFSFile.Oid}} + +

    +
    +
    + {{if .IsMarkup}} + {{if .FileContent}}{{.FileContent | Safe}}{{end}} + {{else if .IsRenderedHTML}} +
    {{if .FileContent}}{{.FileContent | Str2html}}{{end}}
    + {{else if not .IsTextFile}} +
    + {{if .IsImageFile}} + + {{else if .IsVideoFile}} + + {{else if .IsAudioFile}} + + {{else if .IsPDFFile}} + + {{else}} + {{.i18n.Tr "repo.file_view_raw"}} + {{end}} +
    + {{else if .FileSize}} + + + + {{if .IsFileTooLarge}} + + {{else}} + + + {{end}} + + +
    {{.i18n.Tr "repo.file_too_large"}}{{.LineNums}}
      {{.FileContent}}
    + {{end}} +
    +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/repo/settings/lfs_file_find.tmpl b/templates/repo/settings/lfs_file_find.tmpl new file mode 100644 index 000000000..18db0215a --- /dev/null +++ b/templates/repo/settings/lfs_file_find.tmpl @@ -0,0 +1,52 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} +
    +

    + {{.i18n.Tr "repo.settings.lfs"}} / {{.Oid}} +

    + + + {{range .Results}} + + + + + + + + {{else}} + + + + {{end}} + +
    + + {{.Name}} + + + + {{.Summary}} + + + + {{.BranchName}} + + {{if .ParentHashes}} + {{$.i18n.Tr "repo.diff.parent"}} + {{range .ParentHashes}} + {{ShortSha .String}} + {{end}} + {{end}} +
    + {{$.i18n.Tr "repo.diff.commit"}} + {{ShortSha .SHA}} +
    {{TimeSince .When $.Lang}}
    {{.i18n.Tr "repo.settings.lfs_lfs_file_no_commits"}}
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl new file mode 100644 index 000000000..1bd48de15 --- /dev/null +++ b/templates/repo/settings/lfs_pointers.tmpl @@ -0,0 +1,71 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} +

    + {{.i18n.Tr "repo.settings.lfs_pointers.found" .NumPointers .NumAssociated .NumNotAssociated .NumNoExist }} + {{if gt .NumAssociatable 0}} +
    +
    + {{.CsrfTokenHtml}} + {{range .Pointers}} + {{if and (not .InRepo) .Exists .Accessible}} + + {{end}} + {{end}} + +
    +
    + {{end}} +

    +
    + + + + + + + + + + + + + {{range .Pointers}} + + + + + + + + + {{end}} + +
    {{.i18n.Tr "repo.settings.lfs_pointers.sha"}}{{.i18n.Tr "repo.settings.lfs_pointers.oid"}}{{.i18n.Tr "repo.settings.lfs_pointers.inRepo"}}{{.i18n.Tr "repo.settings.lfs_pointers.exists"}}{{.i18n.Tr "repo.settings.lfs_pointers.accessible"}}
    + + + {{ShortSha .SHA}} + + + + + {{if and .Exists .InRepo}} + + {{ShortSha .Oid}} + + {{else}} + + {{ShortSha .Oid}} + + {{end}} + + + {{$.i18n.Tr "repo.settings.lfs_findcommits"}} +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 24082000e..abd6e285d 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -21,4 +21,9 @@ {{.i18n.Tr "repo.settings.deploy_keys"}} + {{if .LFSStartServer}} + + {{.i18n.Tr "repo.settings.lfs"}} + + {{end}}