summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/lfs.go80
-rw-r--r--models/repo_list.go48
-rw-r--r--modules/git/pipeline/catfile.go94
-rw-r--r--modules/git/pipeline/namerev.go28
-rw-r--r--modules/git/pipeline/revlist.go75
-rw-r--r--modules/git/repo.go5
-rw-r--r--modules/lfs/server.go2
-rw-r--r--modules/repofiles/update.go2
-rw-r--r--modules/repofiles/upload.go2
-rw-r--r--options/locale/locale_en-US.ini15
-rw-r--r--public/css/index.css1
-rw-r--r--public/less/_base.less10
-rw-r--r--routers/repo/lfs.go551
-rw-r--r--routers/routes/routes.go10
-rw-r--r--services/pull/lfs.go116
-rw-r--r--templates/repo/settings/lfs.tmpl62
-rw-r--r--templates/repo/settings/lfs_file.tmpl57
-rw-r--r--templates/repo/settings/lfs_file_find.tmpl52
-rw-r--r--templates/repo/settings/lfs_pointers.tmpl71
-rw-r--r--templates/repo/settings/navbar.tmpl5
20 files changed, 1150 insertions, 136 deletions
diff --git a/models/lfs.go b/models/lfs.go
index 9b20642777..5f5fe2ccf4 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 err
+ 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
+ }
+
+ 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 692d4d002f..c823647eba 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 0000000000..7293cf9d7f
--- /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 0000000000..eebb53b0ca
--- /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 0000000000..4e13e19444
--- /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 dd886f3a2e..e1d75ca4aa 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 6fa97a2894..dc498a86c8 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 8a1e51730b..8e057700ab 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 202e66b89a..a2e7cc927c 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 f8e25a85f9..4210ed1212 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 68339cf0b9..d0fe896a06 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 a993bbed32..7fcfaf82ea 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 0000000000..de5020c944
--- /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(`<li class="L%d" rel="L%d">%s</li>`, 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(`<span id="L%d">%d</span>`, 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 9572ea8039..13a5bb2708 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 2706d3a200..a1981b8253 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 0000000000..e4480a8b97
--- /dev/null
+++ b/templates/repo/settings/lfs.tmpl
@@ -0,0 +1,62 @@
+{{template "base/head" .}}
+<div class="repository settings lfs">
+ {{template "repo/header" .}}
+ {{template "repo/settings/navbar" .}}
+ <div class="ui container">
+ {{template "base/alert" .}}
+ <h4 class="ui top attached header">
+ {{.i18n.Tr "repo.settings.lfs_filelist"}}
+ <div class="ui right">
+ <a class="ui blue tiny show-panel button" href="{{.Link}}/pointers">{{.i18n.Tr "repo.settings.lfs_findpointerfiles"}}</a>
+ </div>
+ </h4>
+ <table id="lfs-files-table" class="ui attached segment single line table">
+ <tbody>
+ {{range .LFSFiles}}
+ <tr>
+ <td>
+ <span class="text sha label">
+ <a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui detail icon button brown truncate">
+ {{ShortSha .Oid}}
+ </a>
+ </span>
+ </td>
+ <td>{{FileSize .Size}}</td>
+ <td>{{TimeSince .CreatedUnix.AsTime $.Lang}}</td>
+ <td class="right aligned">
+ <a class="ui blue show-panel button" href="{{$.Link}}/find?oid={{.Oid}}&size={{.Size}}">{{$.i18n.Tr "repo.settings.lfs_findcommits"}}</a>
+ <button class="ui basic show-modal icon button" data-modal="#delete-{{.Oid}}">
+ <i class="octicon octicon-trashcan btn-octicon btn-octicon-danger poping up" data-content="{{$.i18n.Tr "repo.editor.delete_this_file"}}" data-position="bottom center" data-variation="tiny inverted"></i>
+ </button>
+ </td>
+ </tr>
+ {{else}}
+ <tr>
+ <td colspan="4">{{.i18n.Tr "repo.settings.lfs_no_lfs_files"}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{template "base/paginate" .}}
+ {{range .LFSFiles}}
+ <div class="ui basic modal" id="delete-{{.Oid}}">
+ <div class="ui icon header">
+ {{$.i18n.Tr "repo.settings.lfs_delete" .Oid}}
+ </div>
+ <div class="content center">
+ <p>
+ {{$.i18n.Tr "repo.settings.lfs_delete_warning"}}
+ </p>
+ <form class="ui form" action="{{$.Link}}/delete/{{.Oid}}" method="post">
+ {{$.CsrfTokenHtml}}
+ <div class="center actions">
+ <div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
+ <button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
+ </div>
+ </form>
+ </div>
+ </div>
+ {{end}}
+ </div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
new file mode 100644
index 0000000000..6283548eaa
--- /dev/null
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -0,0 +1,57 @@
+{{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> / <span class="truncate sha">{{.LFSFile.Oid}}</span>
+ <div class="ui right">
+ <a class="ui blue show-panel button" href="{{.LFSFilesLink}}/find?oid={{.LFSFile.Oid}}&size={{.LFSFile.Size}}">{{$.i18n.Tr "repo.settings.lfs_findcommits"}}</a>
+ </div>
+ </h4>
+ <div class="ui attached table unstackable segment">
+ <div class="file-view {{if .IsMarkup}}markdown{{else if .IsRenderedHTML}}plain-text{{else if .IsTextFile}}code-view{{end}} has-emoji">
+ {{if .IsMarkup}}
+ {{if .FileContent}}{{.FileContent | Safe}}{{end}}
+ {{else if .IsRenderedHTML}}
+ <pre>{{if .FileContent}}{{.FileContent | Str2html}}{{end}}</pre>
+ {{else if not .IsTextFile}}
+ <div class="view-raw ui center">
+ {{if .IsImageFile}}
+ <img src="{{EscapePound $.RawFileLink}}">
+ {{else if .IsVideoFile}}
+ <video controls src="{{EscapePound $.RawFileLink}}">
+ <strong>{{.i18n.Tr "repo.video_not_supported_in_browser"}}</strong>
+ </video>
+ {{else if .IsAudioFile}}
+ <audio controls src="{{EscapePound $.RawFileLink}}">
+ <strong>{{.i18n.Tr "repo.audio_not_supported_in_browser"}}</strong>
+ </audio>
+ {{else if .IsPDFFile}}
+ <iframe width="100%" height="600px" src="{{AppSubUrl}}/vendor/plugins/pdfjs/web/viewer.html?file={{EscapePound $.RawFileLink}}"></iframe>
+ {{else}}
+ <a href="{{EscapePound $.RawFileLink}}" rel="nofollow" class="btn btn-gray btn-radius">{{.i18n.Tr "repo.file_view_raw"}}</a>
+ {{end}}
+ </div>
+ {{else if .FileSize}}
+ <table>
+ <tbody>
+ <tr>
+ {{if .IsFileTooLarge}}
+ <td><strong>{{.i18n.Tr "repo.file_too_large"}}</strong></td>
+ {{else}}
+ <td class="lines-num">{{.LineNums}}</td>
+ <td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol class="linenums">{{.FileContent}}</ol></code></pre></td>
+ {{end}}
+ </tr>
+ </tbody>
+ </table>
+ {{end}}
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+{{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 0000000000..18db0215a5
--- /dev/null
+++ b/templates/repo/settings/lfs_file_find.tmpl
@@ -0,0 +1,52 @@
+{{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> / <span class="truncate sha">{{.Oid}}</span>
+ </h4>
+ <table id="lfs-files-find-table" class="ui attached segment single line table">
+ <tbody>
+ {{range .Results}}
+ <tr>
+ <td>
+ <span class="octicon octicon-file-text"></span>
+ <a href="{{EscapePound $.RepoLink}}/src/commit/{{.SHA}}/{{EscapePound .Name}}" title="{{.Name}}">{{.Name}}</a>
+ </td>
+ <td class="message has-emoji">
+ <span class="truncate">
+ <a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary}}">
+ {{.Summary}}
+ </a>
+ </span>
+ </td>
+ <td>
+ <span class="text grey"><i class="octicon octicon-git-branch"></i>{{.BranchName}}</span>
+ </td>
+ <td>
+ {{if .ParentHashes}}
+ {{$.i18n.Tr "repo.diff.parent"}}
+ {{range .ParentHashes}}
+ <a class="ui blue sha label" href="{{$.RepoLink}}/commit/{{.String}}">{{ShortSha .String}}</a>
+ {{end}}
+ {{end}}
+ <div class="mobile-only"></div>
+ {{$.i18n.Tr "repo.diff.commit"}}
+ <a class="ui blue sha label" href="{{$.RepoLink}}/commit/{{.SHA}}">{{ShortSha .SHA}}</a>
+ </td>
+ <td>{{TimeSince .When $.Lang}}</td>
+ </tr>
+ {{else}}
+ <tr>
+ <td colspan="5">{{.i18n.Tr "repo.settings.lfs_lfs_file_no_commits"}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl
new file mode 100644
index 0000000000..1bd48de157
--- /dev/null
+++ b/templates/repo/settings/lfs_pointers.tmpl
@@ -0,0 +1,71 @@
+{{template "base/head" .}}
+<div class="repository settings lfs">
+ {{template "repo/header" .}}
+ {{template "repo/settings/navbar" .}}
+ <div class="ui container">
+ {{template "base/alert" .}}
+ <h4 class="ui top attached header">
+ {{.i18n.Tr "repo.settings.lfs_pointers.found" .NumPointers .NumAssociated .NumNotAssociated .NumNoExist }}
+ {{if gt .NumAssociatable 0}}
+ <div class="ui right">
+ <form class="ui form" method="post" action="{{$.Link}}/associate">
+ {{.CsrfTokenHtml}}
+ {{range .Pointers}}
+ {{if and (not .InRepo) .Exists .Accessible}}
+ <input type="hidden" name="oid" value="{{.Oid}} {{.Size}}"/>
+ {{end}}
+ {{end}}
+ <button class="ui green button">{{$.i18n.Tr "repo.settings.lfs_pointers.associateAccessible" $.NumAssociatable}}</button>
+ </form>
+ </div>
+ {{end}}
+ </h4>
+ <div class="ui attached segment">
+ <table id="lfs-files-table" class="ui fixed single line table">
+ <thead>
+ <tr>
+ <th class="three wide">{{.i18n.Tr "repo.settings.lfs_pointers.sha"}}</th>
+ <th class="four wide">{{.i18n.Tr "repo.settings.lfs_pointers.oid"}}</th>
+ <th class="three wide"></th>
+ <th class="two wide">{{.i18n.Tr "repo.settings.lfs_pointers.inRepo"}}</th>
+ <th class="two wide">{{.i18n.Tr "repo.settings.lfs_pointers.exists"}}</th>
+ <th class="two wide">{{.i18n.Tr "repo.settings.lfs_pointers.accessible"}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .Pointers}}
+ <tr>
+ <td>
+ <span class="text sha label" title="{{.SHA}}">
+ <a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" class="ui detail icon button truncate">
+ {{ShortSha .SHA}}
+ </a>
+ </span>
+ </td>
+ <td>
+ <span class="text sha label" title="{{.Oid}}">
+ {{if and .Exists .InRepo}}
+ <a href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank" class="ui text detail icon button brown truncate">
+ {{ShortSha .Oid}}
+ </a>
+ {{else}}
+ <span class="ui detail icon button brown disabled truncate">
+ {{ShortSha .Oid}}
+ </span>
+ {{end}}
+ </span>
+ </td>
+ <td>
+ <a class="ui blue show-panel button" href="{{$.LFSFilesLink}}/find?oid={{.Oid}}&size={{.Size}}&sha={{.SHA}}">{{$.i18n.Tr "repo.settings.lfs_findcommits"}}</a>
+ </td>
+ <td><i class="fa fa{{if .InRepo}}-check{{end}}-square-o"></i></td>
+ <td><i class="fa fa{{if .Exists}}-check{{end}}-square-o"></i></td>
+ <td><i class="fa fa{{if .Accessible}}-check{{end}}-square-o"></i></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index 24082000e2..abd6e285dc 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -21,4 +21,9 @@
<a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{.RepoLink}}/settings/keys">
{{.i18n.Tr "repo.settings.deploy_keys"}}
</a>
+ {{if .LFSStartServer}}
+ <a class="{{if .PageIsSettingsLFS}}active{{end}} item" href="{{.RepoLink}}/settings/lfs">
+ {{.i18n.Tr "repo.settings.lfs"}}
+ </a>
+ {{end}}
</div>