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
This commit is contained in:
zeripath
2019-10-28 18:31:55 +00:00
committed by GitHub
parent af8957bc4c
commit 5e6a008fba
20 changed files with 1151 additions and 137 deletions

View File

@ -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
}

View File

@ -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) {

View File

@ -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:]
}
}
}

View File

@ -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()))
}
}

View File

@ -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:]
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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}

View File

@ -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;

551
routers/repo/lfs.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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())

View File

@ -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()

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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>