aboutsummaryrefslogtreecommitdiffstats
path: root/services/repository/files
diff options
context:
space:
mode:
Diffstat (limited to 'services/repository/files')
-rw-r--r--services/repository/files/cherry_pick.go36
-rw-r--r--services/repository/files/commit.go4
-rw-r--r--services/repository/files/content.go292
-rw-r--r--services/repository/files/content_test.go235
-rw-r--r--services/repository/files/diff.go12
-rw-r--r--services/repository/files/diff_test.go9
-rw-r--r--services/repository/files/file.go108
-rw-r--r--services/repository/files/file_test.go119
-rw-r--r--services/repository/files/patch.go56
-rw-r--r--services/repository/files/temp_repo.go172
-rw-r--r--services/repository/files/tree.go119
-rw-r--r--services/repository/files/tree_test.go69
-rw-r--r--services/repository/files/update.go372
-rw-r--r--services/repository/files/upload.go228
14 files changed, 876 insertions, 955 deletions
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
index 10545e9e03..6818bb343d 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -5,6 +5,7 @@ package files
import (
"context"
+ "errors"
"fmt"
"strings"
@@ -32,27 +33,25 @@ func (err ErrCommitIDDoesNotMatch) Error() string {
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
}
-// CherryPick cherrypicks or reverts a commit to the given repository
+// CherryPick cherry-picks or reverts a commit to the given repository
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
if err := opts.Validate(ctx, repo, doer); err != nil {
return nil, err
}
message := strings.TrimSpace(opts.Message)
- author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
-
- t, err := NewTemporaryUploadRepository(ctx, repo)
+ t, err := NewTemporaryUploadRepository(repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
- if err := t.Clone(opts.OldBranch, false); err != nil {
+ if err := t.Clone(ctx, opts.OldBranch, false); err != nil {
return nil, err
}
- if err := t.SetDefaultIndex(); err != nil {
+ if err := t.SetDefaultIndex(ctx); err != nil {
return nil, err
}
- if err := t.RefreshIndex(); err != nil {
+ if err := t.RefreshIndex(ctx); err != nil {
return nil, err
}
@@ -102,28 +101,37 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
}
if conflict {
- return nil, fmt.Errorf("failed to merge due to conflicts")
+ return nil, errors.New("failed to merge due to conflicts")
}
- treeHash, err := t.WriteTree()
+ treeHash, err := t.WriteTree(ctx)
if err != nil {
// likely non-sensical tree due to merge conflicts...
return nil, err
}
// Now commit the tree
- var commitHash string
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: "HEAD",
+ TreeHash: treeHash,
+ CommitMessage: message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ AuthorIdentity: opts.Author,
+ AuthorTime: nil,
+ CommitterIdentity: opts.Committer,
+ CommitterTime: nil,
+ }
if opts.Dates != nil {
- commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
- } else {
- commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
+ commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
+ commitHash, err := t.CommitTree(ctx, commitOpts)
if err != nil {
return nil, err
}
// Then push this tree to NewBranch
- if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
+ if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
return nil, err
}
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index e0dad29273..3cc326d065 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -6,10 +6,10 @@ package files
import (
"context"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/structs"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
)
// CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch
@@ -24,7 +24,7 @@ func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, bra
// GetPayloadCommitVerification returns the verification information of a commit
func GetPayloadCommitVerification(ctx context.Context, commit *git.Commit) *structs.PayloadCommitVerification {
verification := &structs.PayloadCommitVerification{}
- commitVerification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ commitVerification := asymkey_service.ParseCommitWithSignature(ctx, commit)
if commit.Signature != nil {
verification.Signature = commit.Signature.Signature
verification.Payload = commit.Signature.Payload
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index 0ab7422ce2..2c1e88bb59 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -5,17 +5,18 @@ package files
import (
"context"
- "fmt"
+ "io"
"net/url"
"path"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/v1/utils"
)
// ContentType repo content type
@@ -23,14 +24,10 @@ type ContentType string
// The string representations of different content types
const (
- // ContentTypeRegular regular content type (file)
- ContentTypeRegular ContentType = "file"
- // ContentTypeDir dir content type (dir)
- ContentTypeDir ContentType = "dir"
- // ContentLink link content type (symlink)
- ContentTypeLink ContentType = "symlink"
- // ContentTag submodule content type (submodule)
- ContentTypeSubmodule ContentType = "submodule"
+ ContentTypeRegular ContentType = "file" // regular content type (file)
+ ContentTypeDir ContentType = "dir" // dir content type (dir)
+ ContentTypeLink ContentType = "symlink" // link content type (symlink)
+ ContentTypeSubmodule ContentType = "submodule" // submodule content type (submodule)
)
// String gets the string of ContentType
@@ -38,67 +35,52 @@ func (ct *ContentType) String() string {
return string(*ct)
}
-// GetContentsOrList gets the meta data of a file's contents (*ContentsResponse) if treePath not a tree
-// directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
-func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, treePath, ref string) (any, error) {
- if repo.IsEmpty {
- return make([]any, 0), nil
- }
- if ref == "" {
- ref = repo.DefaultBranch
- }
- origRef := ref
-
- // Check that the path given in opts.treePath is valid (not a git path)
- cleanTreePath := CleanUploadFileName(treePath)
- if cleanTreePath == "" && treePath != "" {
- return nil, ErrFilenameInvalid{
- Path: treePath,
- }
- }
- treePath = cleanTreePath
-
- gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
- if err != nil {
- return nil, err
- }
- defer closer.Close()
+type GetContentsOrListOptions struct {
+ TreePath string
+ IncludeSingleFileContent bool // include the file's content when the tree path is a file
+ IncludeLfsMetadata bool
+ IncludeCommitMetadata bool
+ IncludeCommitMessage bool
+}
- // Get the commit object for the ref
- commit, err := gitRepo.GetCommit(ref)
- if err != nil {
- return nil, err
+// GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
+// directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
+func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) {
+ entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath)
+ if repo.IsEmpty && opts.TreePath == "" {
+ return api.ContentsExtResponse{DirContents: make([]*api.ContentsResponse, 0)}, nil
}
-
- entry, err := commit.GetTreeEntryByPath(treePath)
if err != nil {
- return nil, err
+ return ret, err
}
+ // get file contents
if entry.Type() != "tree" {
- return GetContents(ctx, repo, treePath, origRef, false)
+ ret.FileContents, err = getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
+ return ret, err
}
- // We are in a directory, so we return a list of FileContentResponse objects
- var fileList []*api.ContentsResponse
-
- gitTree, err := commit.SubTree(treePath)
+ // list directory contents
+ gitTree, err := refCommit.Commit.SubTree(opts.TreePath)
if err != nil {
- return nil, err
+ return ret, err
}
entries, err := gitTree.ListEntries()
if err != nil {
- return nil, err
+ return ret, err
}
+ ret.DirContents = make([]*api.ContentsResponse, 0, len(entries))
for _, e := range entries {
- subTreePath := path.Join(treePath, e.Name())
- fileContentResponse, err := GetContents(ctx, repo, subTreePath, origRef, true)
+ subOpts := opts
+ subOpts.TreePath = path.Join(opts.TreePath, e.Name())
+ subOpts.IncludeSingleFileContent = false // never include file content when listing a directory
+ fileContentResponse, err := GetFileContents(ctx, repo, gitRepo, refCommit, subOpts)
if err != nil {
- return nil, err
+ return ret, err
}
- fileList = append(fileList, fileContentResponse)
+ ret.DirContents = append(ret.DirContents, fileContentResponse)
}
- return fileList, nil
+ return ret, nil
}
// GetObjectTypeFromTreeEntry check what content is behind it
@@ -117,86 +99,96 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
}
}
-// GetContents gets the meta data on a file's contents. Ref can be a branch, commit or tag
-func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref string, forList bool) (*api.ContentsResponse, error) {
- if ref == "" {
- ref = repo.DefaultBranch
- }
- origRef := ref
-
+func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) {
// Check that the path given in opts.treePath is valid (not a git path)
- cleanTreePath := CleanUploadFileName(treePath)
- if cleanTreePath == "" && treePath != "" {
- return nil, ErrFilenameInvalid{
- Path: treePath,
- }
+ cleanTreePath := CleanGitTreePath(*treePath)
+ if cleanTreePath == "" && *treePath != "" {
+ return nil, ErrFilenameInvalid{Path: *treePath}
}
- treePath = cleanTreePath
+ *treePath = cleanTreePath
- gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
- if err != nil {
- return nil, err
+ // Only allow safe ref types
+ refType := refCommit.RefName.RefType()
+ if refType != git.RefTypeBranch && refType != git.RefTypeTag && refType != git.RefTypeCommit {
+ return nil, util.NewNotExistErrorf("no commit found for the ref [ref: %s]", refCommit.RefName)
}
- defer closer.Close()
- // Get the commit object for the ref
- commit, err := gitRepo.GetCommit(ref)
- if err != nil {
- return nil, err
- }
- commitID := commit.ID.String()
- if len(ref) >= 4 && strings.HasPrefix(commitID, ref) {
- ref = commit.ID.String()
- }
+ return refCommit.Commit.GetTreeEntryByPath(*treePath)
+}
- entry, err := commit.GetTreeEntryByPath(treePath)
+// GetFileContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
+func GetFileContents(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
+ entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath)
if err != nil {
return nil, err
}
+ return getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
+}
- refType := gitRepo.GetRefType(ref)
- if refType == "invalid" {
- return nil, fmt.Errorf("no commit found for the ref [ref: %s]", ref)
- }
-
- selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(origRef))
+func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
+ refType := refCommit.RefName.RefType()
+ commit := refCommit.Commit
+ selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(opts.TreePath) + "?ref=" + url.QueryEscape(refCommit.InputRef))
if err != nil {
return nil, err
}
selfURLString := selfURL.String()
- err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(ref, refType != git.ObjectCommit), repo.FullName(), commitID)
- if err != nil {
- return nil, err
- }
-
- lastCommit, err := commit.GetCommitByPath(treePath)
- if err != nil {
- return nil, err
- }
-
// All content types have these fields in populated
contentsResponse := &api.ContentsResponse{
- Name: entry.Name(),
- Path: treePath,
- SHA: entry.ID.String(),
- LastCommitSHA: lastCommit.ID.String(),
- Size: entry.Size(),
- URL: &selfURLString,
+ Name: entry.Name(),
+ Path: opts.TreePath,
+ SHA: entry.ID.String(),
+ Size: entry.Size(),
+ URL: &selfURLString,
Links: &api.FileLinksResponse{
Self: &selfURLString,
},
}
- // Now populate the rest of the ContentsResponse based on entry type
+ if opts.IncludeCommitMetadata || opts.IncludeCommitMessage {
+ err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID)
+ if err != nil {
+ return nil, err
+ }
+
+ lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath)
+ if err != nil {
+ return nil, err
+ }
+
+ if opts.IncludeCommitMetadata {
+ contentsResponse.LastCommitSHA = util.ToPointer(lastCommit.ID.String())
+ // GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
+ // https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
+ if lastCommit.Committer != nil {
+ contentsResponse.LastCommitterDate = util.ToPointer(lastCommit.Committer.When)
+ }
+ if lastCommit.Author != nil {
+ contentsResponse.LastAuthorDate = util.ToPointer(lastCommit.Author.When)
+ }
+ }
+ if opts.IncludeCommitMessage {
+ contentsResponse.LastCommitMessage = util.ToPointer(lastCommit.Message())
+ }
+ }
+
+ // Now populate the rest of the ContentsResponse based on the entry type
if entry.IsRegular() || entry.IsExecutable() {
contentsResponse.Type = string(ContentTypeRegular)
- if blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String()); err != nil {
- return nil, err
- } else if !forList {
- // We don't show the content if we are getting a list of FileContentResponses
- contentsResponse.Encoding = &blobResponse.Encoding
- contentsResponse.Content = &blobResponse.Content
+ // if it is listing the repo root dir, don't waste system resources on reading content
+ if opts.IncludeSingleFileContent {
+ blobResponse, err := GetBlobBySHA(repo, gitRepo, entry.ID.String())
+ if err != nil {
+ return nil, err
+ }
+ contentsResponse.Encoding, contentsResponse.Content = blobResponse.Encoding, blobResponse.Content
+ contentsResponse.LfsOid, contentsResponse.LfsSize = blobResponse.LfsOid, blobResponse.LfsSize
+ } else if opts.IncludeLfsMetadata {
+ contentsResponse.LfsOid, contentsResponse.LfsSize, err = parsePossibleLfsPointerBlob(gitRepo, entry.ID.String())
+ if err != nil {
+ return nil, err
+ }
}
} else if entry.IsDir() {
contentsResponse.Type = string(ContentTypeDir)
@@ -210,7 +202,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
contentsResponse.Target = &targetFromContent
} else if entry.IsSubModule() {
contentsResponse.Type = string(ContentTypeSubmodule)
- submodule, err := commit.GetSubModule(treePath)
+ submodule, err := commit.GetSubModule(opts.TreePath)
if err != nil {
return nil, err
}
@@ -220,7 +212,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
}
// Handle links
if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
- downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
+ downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
if err != nil {
return nil, err
}
@@ -228,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
contentsResponse.DownloadURL = &downloadURLString
}
if !entry.IsSubModule() {
- htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
+ htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
if err != nil {
return nil, err
}
@@ -248,49 +240,59 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
return contentsResponse, nil
}
-// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
-func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
+func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
gitBlob, err := gitRepo.GetBlob(sha)
if err != nil {
return nil, err
}
- content := ""
- if gitBlob.Size() <= setting.API.DefaultMaxBlobSize {
- content, err = gitBlob.GetBlobContentBase64()
- if err != nil {
- return nil, err
- }
+ ret := &api.GitBlobResponse{
+ SHA: gitBlob.ID.String(),
+ URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
+ Size: gitBlob.Size(),
}
- return &api.GitBlobResponse{
- SHA: gitBlob.ID.String(),
- URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
- Size: gitBlob.Size(),
- Encoding: "base64",
- Content: content,
- }, nil
-}
-// TryGetContentLanguage tries to get the (linguist) language of the file content
-func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) {
- indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID)
- if err != nil {
- return "", err
+ blobSize := gitBlob.Size()
+ if blobSize > setting.API.DefaultMaxBlobSize {
+ return ret, nil
}
- defer deleteTemporaryFile()
+ var originContent *strings.Builder
+ if 0 < blobSize && blobSize < lfs.MetaFileMaxSize {
+ originContent = &strings.Builder{}
+ }
- filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
- CachedOnly: true,
- Attributes: []string{git.AttributeLinguistLanguage, git.AttributeGitlabLanguage},
- Filenames: []string{treePath},
- IndexFile: indexFilename,
- WorkTree: worktree,
- })
+ content, err := gitBlob.GetBlobContentBase64(originContent)
if err != nil {
- return "", err
+ return nil, err
+ }
+
+ ret.Encoding, ret.Content = util.ToPointer("base64"), &content
+ if originContent != nil {
+ ret.LfsOid, ret.LfsSize = parsePossibleLfsPointerBuffer(strings.NewReader(originContent.String()))
}
+ return ret, nil
+}
- language := git.TryReadLanguageAttribute(filename2attribute2info[treePath])
+func parsePossibleLfsPointerBuffer(r io.Reader) (*string, *int64) {
+ p, _ := lfs.ReadPointer(r)
+ if p.IsValid() {
+ return &p.Oid, &p.Size
+ }
+ return nil, nil
+}
- return language.Value(), nil
+func parsePossibleLfsPointerBlob(gitRepo *git.Repository, sha string) (*string, *int64, error) {
+ gitBlob, err := gitRepo.GetBlob(sha)
+ if err != nil {
+ return nil, nil, err
+ }
+ if gitBlob.Size() > lfs.MetaFileMaxSize {
+ return nil, nil, nil // not a LFS pointer
+ }
+ buf, err := gitBlob.GetBlobContent(lfs.MetaFileMaxSize)
+ if err != nil {
+ return nil, nil, err
+ }
+ oid, size := parsePossibleLfsPointerBuffer(strings.NewReader(buf))
+ return oid, size, nil
}
diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go
index 7cb46c0bb6..d72f918074 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -7,8 +7,8 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/contexttest"
_ "code.gitea.io/gitea/models/actions"
@@ -20,36 +20,6 @@ func TestMain(m *testing.M) {
unittest.MainTest(m)
}
-func getExpectedReadmeContentsResponse() *api.ContentsResponse {
- treePath := "README.md"
- sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
- encoding := "base64"
- content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
- selfURL := "https://try.gitea.io/api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
- htmlURL := "https://try.gitea.io/user2/repo1/src/branch/master/" + treePath
- gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha
- downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath
- return &api.ContentsResponse{
- Name: treePath,
- Path: treePath,
- SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
- LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
- Type: "file",
- Size: 30,
- Encoding: &encoding,
- Content: &content,
- URL: &selfURL,
- HTMLURL: &htmlURL,
- GitURL: &gitURL,
- DownloadURL: &downloadURL,
- Links: &api.FileLinksResponse{
- Self: &selfURL,
- GitURL: &gitURL,
- HTMLURL: &htmlURL,
- },
- }
-}
-
func TestGetContents(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1")
@@ -58,195 +28,22 @@ func TestGetContents(t *testing.T) {
contexttest.LoadRepoCommit(t, ctx)
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- treePath := "README.md"
- ref := ctx.Repo.Repository.DefaultBranch
-
- expectedContentsResponse := getExpectedReadmeContentsResponse()
-
- t.Run("Get README.md contents with GetContents(ctx, )", func(t *testing.T) {
- fileContentResponse, err := GetContents(ctx, ctx.Repo.Repository, treePath, ref, false)
- assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
- assert.NoError(t, err)
- })
-
- t.Run("Get README.md contents with ref as empty string (should then use the repo's default branch) with GetContents(ctx, )", func(t *testing.T) {
- fileContentResponse, err := GetContents(ctx, ctx.Repo.Repository, treePath, "", false)
- assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
- assert.NoError(t, err)
- })
-}
-
-func TestGetContentsOrListForDir(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- treePath := "" // root dir
- ref := ctx.Repo.Repository.DefaultBranch
-
- readmeContentsResponse := getExpectedReadmeContentsResponse()
- // because will be in a list, doesn't have encoding and content
- readmeContentsResponse.Encoding = nil
- readmeContentsResponse.Content = nil
-
- expectedContentsListResponse := []*api.ContentsResponse{
- readmeContentsResponse,
- }
-
- t.Run("Get root dir contents with GetContentsOrList(ctx, )", func(t *testing.T) {
- fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref)
- assert.EqualValues(t, expectedContentsListResponse, fileContentResponse)
- assert.NoError(t, err)
- })
-
- t.Run("Get root dir contents with ref as empty string (should then use the repo's default branch) with GetContentsOrList(ctx, )", func(t *testing.T) {
- fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, treePath, "")
- assert.EqualValues(t, expectedContentsListResponse, fileContentResponse)
- assert.NoError(t, err)
- })
-}
-
-func TestGetContentsOrListForFile(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- treePath := "README.md"
- ref := ctx.Repo.Repository.DefaultBranch
-
- expectedContentsResponse := getExpectedReadmeContentsResponse()
-
- t.Run("Get README.md contents with GetContentsOrList(ctx, )", func(t *testing.T) {
- fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref)
- assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
- assert.NoError(t, err)
- })
-
- t.Run("Get README.md contents with ref as empty string (should then use the repo's default branch) with GetContentsOrList(ctx, )", func(t *testing.T) {
- fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, treePath, "")
- assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
- assert.NoError(t, err)
- })
-}
-
-func TestGetContentsErrors(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- treePath := "README.md"
- ref := repo.DefaultBranch
-
- t.Run("bad treePath", func(t *testing.T) {
- badTreePath := "bad/tree.md"
- fileContentResponse, err := GetContents(ctx, repo, badTreePath, ref, false)
- assert.Error(t, err)
- assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
- assert.Nil(t, fileContentResponse)
- })
-
- t.Run("bad ref", func(t *testing.T) {
- badRef := "bad_ref"
- fileContentResponse, err := GetContents(ctx, repo, treePath, badRef, false)
- assert.Error(t, err)
- assert.EqualError(t, err, "object does not exist [id: "+badRef+", rel_path: ]")
- assert.Nil(t, fileContentResponse)
- })
-}
-
-func TestGetContentsOrListErrors(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
- repo := ctx.Repo.Repository
- treePath := "README.md"
- ref := repo.DefaultBranch
-
- t.Run("bad treePath", func(t *testing.T) {
- badTreePath := "bad/tree.md"
- fileContentResponse, err := GetContentsOrList(ctx, repo, badTreePath, ref)
- assert.Error(t, err)
- assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
- assert.Nil(t, fileContentResponse)
- })
-
- t.Run("bad ref", func(t *testing.T) {
- badRef := "bad_ref"
- fileContentResponse, err := GetContentsOrList(ctx, repo, treePath, badRef)
- assert.Error(t, err)
- assert.EqualError(t, err, "object does not exist [id: "+badRef+", rel_path: ]")
- assert.Nil(t, fileContentResponse)
- })
-}
-
-func TestGetContentsOrListOfEmptyRepos(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user30/empty")
- ctx.SetPathParam("id", "52")
- contexttest.LoadRepo(t, ctx, 52)
- contexttest.LoadUser(t, ctx, 30)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
-
- t.Run("empty repo", func(t *testing.T) {
- contents, err := GetContentsOrList(ctx, repo, "", "")
+ // GetContentsOrList's behavior is fully tested in integration tests, so we don't need to test it here.
+
+ t.Run("GetBlobBySHA", func(t *testing.T) {
+ sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
+ ctx.SetPathParam("id", "1")
+ ctx.SetPathParam("sha", sha)
+ gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha"))
+ expectedGBR := &api.GitBlobResponse{
+ Content: util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"),
+ Encoding: util.ToPointer("base64"),
+ URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ Size: 180,
+ }
assert.NoError(t, err)
- assert.Empty(t, contents)
+ assert.Equal(t, expectedGBR, gbr)
})
}
-
-func TestGetBlobBySHA(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
- ctx.SetPathParam("id", "1")
- ctx.SetPathParam("sha", sha)
-
- gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
- if err != nil {
- t.Fail()
- }
-
- gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam("sha"))
- expectedGBR := &api.GitBlobResponse{
- Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK",
- Encoding: "base64",
- URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d",
- SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
- Size: 180,
- }
- assert.NoError(t, err)
- assert.Equal(t, expectedGBR, gbr)
-}
diff --git a/services/repository/files/diff.go b/services/repository/files/diff.go
index bf8b938e21..50d01f9d7c 100644
--- a/services/repository/files/diff.go
+++ b/services/repository/files/diff.go
@@ -16,27 +16,27 @@ func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, tr
if branch == "" {
branch = repo.DefaultBranch
}
- t, err := NewTemporaryUploadRepository(ctx, repo)
+ t, err := NewTemporaryUploadRepository(repo)
if err != nil {
return nil, err
}
defer t.Close()
- if err := t.Clone(branch, true); err != nil {
+ if err := t.Clone(ctx, branch, true); err != nil {
return nil, err
}
- if err := t.SetDefaultIndex(); err != nil {
+ if err := t.SetDefaultIndex(ctx); err != nil {
return nil, err
}
// Add the object to the database
- objectHash, err := t.HashObject(strings.NewReader(content))
+ objectHash, err := t.HashObjectAndWrite(ctx, strings.NewReader(content))
if err != nil {
return nil, err
}
// Add the object to the index
- if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
+ if err := t.AddObjectToIndex(ctx, "100644", objectHash, treePath); err != nil {
return nil, err
}
- return t.DiffIndex()
+ return t.DiffIndex(ctx)
}
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index b7bdcd8ecf..ae702e4189 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -30,14 +30,11 @@ func TestGetDiffPreview(t *testing.T) {
content := "# repo1\n\nDescription for repo1\nthis is a new line"
expectedDiff := &gitdiff.Diff{
- TotalAddition: 2,
- TotalDeletion: 1,
Files: []*gitdiff.DiffFile{
{
Name: "README.md",
OldName: "README.md",
NameHash: "8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d",
- Index: 1,
Addition: 2,
Deletion: 1,
Type: 2,
@@ -50,7 +47,6 @@ func TestGetDiffPreview(t *testing.T) {
Sections: []*gitdiff.DiffSection{
{
FileName: "README.md",
- Name: "",
Lines: []*gitdiff.DiffLine{
{
LeftIdx: 0,
@@ -114,7 +110,6 @@ func TestGetDiffPreview(t *testing.T) {
},
IsIncomplete: false,
}
- expectedDiff.NumFiles = len(expectedDiff.Files)
t.Run("with given branch", func(t *testing.T) {
diff, err := GetDiffPreview(ctx, ctx.Repo.Repository, branch, treePath, content)
@@ -123,7 +118,7 @@ func TestGetDiffPreview(t *testing.T) {
assert.NoError(t, err)
bs, err := json.Marshal(diff)
assert.NoError(t, err)
- assert.EqualValues(t, string(expectedBs), string(bs))
+ assert.Equal(t, string(expectedBs), string(bs))
})
t.Run("empty branch, same results", func(t *testing.T) {
@@ -133,7 +128,7 @@ func TestGetDiffPreview(t *testing.T) {
assert.NoError(t, err)
bs, err := json.Marshal(diff)
assert.NoError(t, err)
- assert.EqualValues(t, expectedBs, bs)
+ assert.Equal(t, expectedBs, bs)
})
}
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index d7ca8e79e5..13d171d139 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -5,26 +5,48 @@ package files
import (
"context"
+ "errors"
"fmt"
"net/url"
"strings"
"time"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/v1/utils"
)
-func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
- files := []*api.ContentsResponse{}
- for _, file := range treeNames {
- fileContents, _ := GetContents(ctx, repo, file, branch, false) // ok if fails, then will be nil
+func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
+ var size int64
+ for _, treePath := range treePaths {
+ // ok if fails, then will be nil
+ fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{
+ TreePath: treePath,
+ IncludeSingleFileContent: true,
+ IncludeCommitMetadata: true,
+ })
+ if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
+ // if content isn't empty (e.g., due to the single blob being too large), add file size to response size
+ size += int64(len(*fileContents.Content))
+ }
+ if size > setting.API.DefaultMaxResponseSize {
+ break // stop if max response size would be exceeded
+ }
files = append(files, fileContents)
+ if len(files) == setting.API.DefaultPagingNum {
+ break // stop if paging num reached
+ }
}
- fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
- verification := GetPayloadCommitVerification(ctx, commit)
+ return files
+}
+
+func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) {
+ files := GetContentsListFromTreePaths(ctx, repo, gitRepo, refCommit, treeNames)
+ fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil
+ verification := GetPayloadCommitVerification(ctx, refCommit.Commit)
filesResponse := &api.FilesResponse{
Files: files,
Commit: fileCommitResponse,
@@ -33,19 +55,6 @@ func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository
return filesResponse, nil
}
-// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
-func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
- fileContents, _ := GetContents(ctx, repo, treeName, branch, false) // ok if fails, then will be nil
- fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
- verification := GetPayloadCommitVerification(ctx, commit)
- fileResponse := &api.FileResponse{
- Content: fileContents,
- Commit: fileCommitResponse,
- Verification: verification,
- }
- return fileResponse, nil
-}
-
// constructs a FileResponse with the file at the index from FilesResponse
func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index int) *api.FileResponse {
content := &api.ContentsResponse{}
@@ -63,10 +72,10 @@ func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index in
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
if repo == nil {
- return nil, fmt.Errorf("repo cannot be nil")
+ return nil, errors.New("repo cannot be nil")
}
if commit == nil {
- return nil, fmt.Errorf("commit cannot be nil")
+ return nil, errors.New("commit cannot be nil")
}
commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()))
commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.Tree.ID.String()))
@@ -111,51 +120,6 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap
return fileCommit, nil
}
-// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions
-func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_model.User) (authorUser, committerUser *user_model.User) {
- // Committer and author are optional. If they are not the doer (not same email address)
- // then we use bogus User objects for them to store their FullName and Email.
- // If only one of the two are provided, we set both of them to it.
- // If neither are provided, both are the doer.
- if committer != nil && committer.Email != "" {
- if doer != nil && strings.EqualFold(doer.Email, committer.Email) {
- committerUser = doer // the committer is the doer, so will use their user object
- if committer.Name != "" {
- committerUser.FullName = committer.Name
- }
- } else {
- committerUser = &user_model.User{
- FullName: committer.Name,
- Email: committer.Email,
- }
- }
- }
- if author != nil && author.Email != "" {
- if doer != nil && strings.EqualFold(doer.Email, author.Email) {
- authorUser = doer // the author is the doer, so will use their user object
- if authorUser.Name != "" {
- authorUser.FullName = author.Name
- }
- } else {
- authorUser = &user_model.User{
- FullName: author.Name,
- Email: author.Email,
- }
- }
- }
- if authorUser == nil {
- if committerUser != nil {
- authorUser = committerUser // No valid author was given so use the committer
- } else if doer != nil {
- authorUser = doer // No valid author was given and no valid committer so use the doer
- }
- }
- if committerUser == nil {
- committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above)
- }
- return authorUser, committerUser
-}
-
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
type ErrFilenameInvalid struct {
Path string
@@ -175,15 +139,17 @@ func (err ErrFilenameInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
-// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
-func CleanUploadFileName(name string) string {
- // Rebase the filename
+// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part)
+func CleanGitTreePath(name string) string {
name = util.PathJoinRel(name)
// Git disallows any filenames to have a .git directory in them.
- for _, part := range strings.Split(name, "/") {
+ for part := range strings.SplitSeq(name, "/") {
if strings.ToLower(part) == ".git" {
return ""
}
}
+ if name == "." {
+ name = ""
+ }
return name
}
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index 52c0574883..cdb6a266ff 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -6,115 +6,22 @@ package files
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/contexttest"
-
"github.com/stretchr/testify/assert"
)
func TestCleanUploadFileName(t *testing.T) {
- t.Run("Clean regular file", func(t *testing.T) {
- name := "this/is/test"
- cleanName := CleanUploadFileName(name)
- expectedCleanName := name
- assert.EqualValues(t, expectedCleanName, cleanName)
- })
-
- t.Run("Clean a .git path", func(t *testing.T) {
- name := "this/is/test/.git"
- cleanName := CleanUploadFileName(name)
- expectedCleanName := ""
- assert.EqualValues(t, expectedCleanName, cleanName)
- })
-}
-
-func getExpectedFileResponse() *api.FileResponse {
- treePath := "README.md"
- sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
- encoding := "base64"
- content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
- selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
- htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath
- gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha
- downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
- return &api.FileResponse{
- Content: &api.ContentsResponse{
- Name: treePath,
- Path: treePath,
- SHA: sha,
- LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
- Type: "file",
- Size: 30,
- Encoding: &encoding,
- Content: &content,
- URL: &selfURL,
- HTMLURL: &htmlURL,
- GitURL: &gitURL,
- DownloadURL: &downloadURL,
- Links: &api.FileLinksResponse{
- Self: &selfURL,
- GitURL: &gitURL,
- HTMLURL: &htmlURL,
- },
- },
- Commit: &api.FileCommitResponse{
- CommitMeta: api.CommitMeta{
- URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
- SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
- },
- HTMLURL: "https://try.gitea.io/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
- Author: &api.CommitUser{
- Identity: api.Identity{
- Name: "user1",
- Email: "address1@example.com",
- },
- Date: "2017-03-19T20:47:59Z",
- },
- Committer: &api.CommitUser{
- Identity: api.Identity{
- Name: "Ethan Koenig",
- Email: "ethantkoenig@gmail.com",
- },
- Date: "2017-03-19T20:47:59Z",
- },
- Parents: []*api.CommitMeta{},
- Message: "Initial commit\n",
- Tree: &api.CommitMeta{
- URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/2a2f1d4670728a2e10049e345bd7a276468beab6",
- SHA: "2a2f1d4670728a2e10049e345bd7a276468beab6",
- },
- },
- Verification: &api.PayloadCommitVerification{
- Verified: false,
- Reason: "gpg.error.not_signed_commit",
- Signature: "",
- Payload: "",
- },
+ cases := []struct {
+ input, expected string
+ }{
+ {"", ""},
+ {".", ""},
+ {"a/./b", "a/b"},
+ {"a.git", "a.git"},
+ {".git/b", ""},
+ {"a/.git", ""},
+ {"/a/../../b", "b"},
+ }
+ for _, c := range cases {
+ assert.Equal(t, c.expected, CleanGitTreePath(c.input), "input: %q", c.input)
}
-}
-
-func TestGetFileResponseFromCommit(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- branch := repo.DefaultBranch
- treePath := "README.md"
- gitRepo, _ := gitrepo.OpenRepository(ctx, repo)
- defer gitRepo.Close()
- commit, _ := gitRepo.GetBranchCommit(branch)
- expectedFileResponse := getExpectedFileResponse()
-
- fileResponse, err := GetFileResponseFromCommit(ctx, repo, commit, branch, treePath)
- assert.NoError(t, err)
- assert.EqualValues(t, expectedFileResponse, fileResponse)
}
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index 38c17b4073..11a8744b7f 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -12,7 +12,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -45,7 +44,6 @@ type ApplyDiffPatchOptions struct {
NewBranch string
Message string
Content string
- SHA string
Author *IdentityOptions
Committer *IdentityOptions
Dates *CommitDateOptions
@@ -62,29 +60,26 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
opts.NewBranch = opts.OldBranch
}
- gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
- if err != nil {
- return err
- }
- defer closer.Close()
-
// oldBranch must exist for this operation
- if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil {
+ if exist, err := git_model.IsBranchExist(ctx, repo.ID, opts.OldBranch); err != nil {
return err
+ } else if !exist {
+ return git_model.ErrBranchNotExist{
+ BranchName: opts.OldBranch,
+ }
}
// A NewBranch can be specified for the patch to be applied to.
// Check to make sure the branch does not already exist, otherwise we can't proceed.
// If we aren't branching to a new branch, make sure user can commit to the given branch
if opts.NewBranch != opts.OldBranch {
- existingBranch, err := gitRepo.GetBranch(opts.NewBranch)
- if existingBranch != nil {
+ exist, err := git_model.IsBranchExist(ctx, repo.ID, opts.NewBranch)
+ if err != nil {
+ return err
+ } else if exist {
return git_model.ErrBranchAlreadyExists{
BranchName: opts.NewBranch,
}
}
- if err != nil && !git.IsErrBranchNotExist(err) {
- return err
- }
} else {
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, opts.OldBranch)
if err != nil {
@@ -126,17 +121,15 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
message := strings.TrimSpace(opts.Message)
- author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
-
- t, err := NewTemporaryUploadRepository(ctx, repo)
+ t, err := NewTemporaryUploadRepository(repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
- if err := t.Clone(opts.OldBranch, true); err != nil {
+ if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
return nil, err
}
- if err := t.SetDefaultIndex(); err != nil {
+ if err := t.SetDefaultIndex(ctx); err != nil {
return nil, err
}
@@ -166,12 +159,12 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
stdout := &strings.Builder{}
stderr := &strings.Builder{}
- cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary")
+ cmdApply := git.NewCommand("apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary")
if git.DefaultFeatures().CheckVersionAtLeast("2.32") {
cmdApply.AddArguments("-3")
}
- if err := cmdApply.Run(&git.RunOpts{
+ if err := cmdApply.Run(ctx, &git.RunOpts{
Dir: t.basePath,
Stdout: stdout,
Stderr: stderr,
@@ -181,24 +174,33 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
}
// Now write the tree
- treeHash, err := t.WriteTree()
+ treeHash, err := t.WriteTree(ctx)
if err != nil {
return nil, err
}
// Now commit the tree
- var commitHash string
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: "HEAD",
+ TreeHash: treeHash,
+ CommitMessage: message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ AuthorIdentity: opts.Author,
+ AuthorTime: nil,
+ CommitterIdentity: opts.Committer,
+ CommitterTime: nil,
+ }
if opts.Dates != nil {
- commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
- } else {
- commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
+ commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
+ commitHash, err := t.CommitTree(ctx, commitOpts)
if err != nil {
return nil, err
}
// Then push this tree to NewBranch
- if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
+ if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
return nil, err
}
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 138af991f9..c2f61c8223 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -6,6 +6,7 @@ package files
import (
"bytes"
"context"
+ "errors"
"fmt"
"io"
"os"
@@ -19,44 +20,45 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
)
// TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone
type TemporaryUploadRepository struct {
- ctx context.Context
repo *repo_model.Repository
gitRepo *git.Repository
basePath string
+ cleanup func()
}
// NewTemporaryUploadRepository creates a new temporary upload repository
-func NewTemporaryUploadRepository(ctx context.Context, repo *repo_model.Repository) (*TemporaryUploadRepository, error) {
- basePath, err := repo_module.CreateTemporaryPath("upload")
+func NewTemporaryUploadRepository(repo *repo_model.Repository) (*TemporaryUploadRepository, error) {
+ basePath, cleanup, err := repo_module.CreateTemporaryPath("upload")
if err != nil {
return nil, err
}
- t := &TemporaryUploadRepository{ctx: ctx, repo: repo, basePath: basePath}
+ t := &TemporaryUploadRepository{repo: repo, basePath: basePath, cleanup: cleanup}
return t, nil
}
// Close the repository cleaning up all files
func (t *TemporaryUploadRepository) Close() {
defer t.gitRepo.Close()
- if err := repo_module.RemoveTemporaryPath(t.basePath); err != nil {
- log.Error("Failed to remove temporary path %s: %v", t.basePath, err)
+ if t.cleanup != nil {
+ t.cleanup()
}
}
// Clone the base repository to our path and set branch as the HEAD
-func (t *TemporaryUploadRepository) Clone(branch string, bare bool) error {
- cmd := git.NewCommand(t.ctx, "clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath)
+func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, bare bool) error {
+ cmd := git.NewCommand("clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath)
if bare {
cmd.AddArguments("--bare")
}
- if _, _, err := cmd.RunStdString(nil); err != nil {
+ if _, _, err := cmd.RunStdString(ctx, nil); err != nil {
stderr := err.Error()
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
return git.ErrBranchNotExist{
@@ -72,7 +74,7 @@ func (t *TemporaryUploadRepository) Clone(branch string, bare bool) error {
}
return fmt.Errorf("Clone: %w %s", err, stderr)
}
- gitRepo, err := git.OpenRepository(t.ctx, t.basePath)
+ gitRepo, err := git.OpenRepository(ctx, t.basePath)
if err != nil {
return err
}
@@ -81,11 +83,11 @@ func (t *TemporaryUploadRepository) Clone(branch string, bare bool) error {
}
// Init the repository
-func (t *TemporaryUploadRepository) Init(objectFormatName string) error {
- if err := git.InitRepository(t.ctx, t.basePath, false, objectFormatName); err != nil {
+func (t *TemporaryUploadRepository) Init(ctx context.Context, objectFormatName string) error {
+ if err := git.InitRepository(ctx, t.basePath, false, objectFormatName); err != nil {
return err
}
- gitRepo, err := git.OpenRepository(t.ctx, t.basePath)
+ gitRepo, err := git.OpenRepository(ctx, t.basePath)
if err != nil {
return err
}
@@ -94,28 +96,28 @@ func (t *TemporaryUploadRepository) Init(objectFormatName string) error {
}
// SetDefaultIndex sets the git index to our HEAD
-func (t *TemporaryUploadRepository) SetDefaultIndex() error {
- if _, _, err := git.NewCommand(t.ctx, "read-tree", "HEAD").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
+func (t *TemporaryUploadRepository) SetDefaultIndex(ctx context.Context) error {
+ if _, _, err := git.NewCommand("read-tree", "HEAD").RunStdString(ctx, &git.RunOpts{Dir: t.basePath}); err != nil {
return fmt.Errorf("SetDefaultIndex: %w", err)
}
return nil
}
// RefreshIndex looks at the current index and checks to see if merges or updates are needed by checking stat() information.
-func (t *TemporaryUploadRepository) RefreshIndex() error {
- if _, _, err := git.NewCommand(t.ctx, "update-index", "--refresh").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
+func (t *TemporaryUploadRepository) RefreshIndex(ctx context.Context) error {
+ if _, _, err := git.NewCommand("update-index", "--refresh").RunStdString(ctx, &git.RunOpts{Dir: t.basePath}); err != nil {
return fmt.Errorf("RefreshIndex: %w", err)
}
return nil
}
// LsFiles checks if the given filename arguments are in the index
-func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) {
+func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...string) ([]string, error) {
stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
- if err := git.NewCommand(t.ctx, "ls-files", "-z").AddDashesAndList(filenames...).
- Run(&git.RunOpts{
+ if err := git.NewCommand("ls-files", "-z").AddDashesAndList(filenames...).
+ Run(ctx, &git.RunOpts{
Dir: t.basePath,
Stdout: stdOut,
Stderr: stdErr,
@@ -126,7 +128,7 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro
}
fileList := make([]string, 0, len(filenames))
- for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
+ for line := range bytes.SplitSeq(stdOut.Bytes(), []byte{'\000'}) {
fileList = append(fileList, string(line))
}
@@ -134,7 +136,7 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro
}
// RemoveFilesFromIndex removes the given files from the index
-func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) error {
+func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error {
objFmt, err := t.gitRepo.GetObjectFormat()
if err != nil {
return fmt.Errorf("unable to get object format for temporary repo: %q, error: %w", t.repo.FullName(), err)
@@ -150,8 +152,8 @@ func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) er
}
}
- if err := git.NewCommand(t.ctx, "update-index", "--remove", "-z", "--index-info").
- Run(&git.RunOpts{
+ if err := git.NewCommand("update-index", "--remove", "-z", "--index-info").
+ Run(ctx, &git.RunOpts{
Dir: t.basePath,
Stdin: stdIn,
Stdout: stdOut,
@@ -162,13 +164,13 @@ func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) er
return nil
}
-// HashObject writes the provided content to the object db and returns its hash
-func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error) {
+// HashObjectAndWrite writes the provided content to the object db and returns its hash
+func (t *TemporaryUploadRepository) HashObjectAndWrite(ctx context.Context, content io.Reader) (string, error) {
stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
- if err := git.NewCommand(t.ctx, "hash-object", "-w", "--stdin").
- Run(&git.RunOpts{
+ if err := git.NewCommand("hash-object", "-w", "--stdin").
+ Run(ctx, &git.RunOpts{
Dir: t.basePath,
Stdin: content,
Stdout: stdOut,
@@ -182,8 +184,8 @@ func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error
}
// AddObjectToIndex adds the provided object hash to the index with the provided mode and path
-func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPath string) error {
- if _, _, err := git.NewCommand(t.ctx, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments(mode, objectHash, objectPath).RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
+func (t *TemporaryUploadRepository) AddObjectToIndex(ctx context.Context, mode, objectHash, objectPath string) error {
+ if _, _, err := git.NewCommand("update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments(mode, objectHash, objectPath).RunStdString(ctx, &git.RunOpts{Dir: t.basePath}); err != nil {
stderr := err.Error()
if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched {
return ErrFilePathInvalid{
@@ -198,8 +200,8 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat
}
// WriteTree writes the current index as a tree to the object db and returns its hash
-func (t *TemporaryUploadRepository) WriteTree() (string, error) {
- stdout, _, err := git.NewCommand(t.ctx, "write-tree").RunStdString(&git.RunOpts{Dir: t.basePath})
+func (t *TemporaryUploadRepository) WriteTree(ctx context.Context) (string, error) {
+ stdout, _, err := git.NewCommand("write-tree").RunStdString(ctx, &git.RunOpts{Dir: t.basePath})
if err != nil {
log.Error("Unable to write tree in temporary repo: %s(%s): Error: %v", t.repo.FullName(), t.basePath, err)
return "", fmt.Errorf("Unable to write-tree in temporary repo for: %s Error: %w", t.repo.FullName(), err)
@@ -208,16 +210,16 @@ func (t *TemporaryUploadRepository) WriteTree() (string, error) {
}
// GetLastCommit gets the last commit ID SHA of the repo
-func (t *TemporaryUploadRepository) GetLastCommit() (string, error) {
- return t.GetLastCommitByRef("HEAD")
+func (t *TemporaryUploadRepository) GetLastCommit(ctx context.Context) (string, error) {
+ return t.GetLastCommitByRef(ctx, "HEAD")
}
// GetLastCommitByRef gets the last commit ID SHA of the repo by ref
-func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, error) {
+func (t *TemporaryUploadRepository) GetLastCommitByRef(ctx context.Context, ref string) (string, error) {
if ref == "" {
ref = "HEAD"
}
- stdout, _, err := git.NewCommand(t.ctx, "rev-parse").AddDynamicArguments(ref).RunStdString(&git.RunOpts{Dir: t.basePath})
+ stdout, _, err := git.NewCommand("rev-parse").AddDynamicArguments(ref).RunStdString(ctx, &git.RunOpts{Dir: t.basePath})
if err != nil {
log.Error("Unable to get last ref for %s in temporary repo: %s(%s): Error: %v", ref, t.repo.FullName(), t.basePath, err)
return "", fmt.Errorf("Unable to rev-parse %s in temporary repo for: %s Error: %w", ref, t.repo.FullName(), err)
@@ -225,15 +227,53 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro
return strings.TrimSpace(stdout), nil
}
-// CommitTree creates a commit from a given tree for the user with provided message
-func (t *TemporaryUploadRepository) CommitTree(parent string, author, committer *user_model.User, treeHash, message string, signoff bool) (string, error) {
- return t.CommitTreeWithDate(parent, author, committer, treeHash, message, signoff, time.Now(), time.Now())
+type CommitTreeUserOptions struct {
+ ParentCommitID string
+ TreeHash string
+ CommitMessage string
+ SignOff bool
+
+ DoerUser *user_model.User
+
+ AuthorIdentity *IdentityOptions // if nil, use doer
+ AuthorTime *time.Time // if nil, use now
+ CommitterIdentity *IdentityOptions
+ CommitterTime *time.Time
+}
+
+func makeGitUserSignature(doer *user_model.User, identity, other *IdentityOptions) *git.Signature {
+ gitSig := &git.Signature{}
+ if identity != nil {
+ gitSig.Name, gitSig.Email = identity.GitUserName, identity.GitUserEmail
+ }
+ if other != nil {
+ gitSig.Name = util.IfZero(gitSig.Name, other.GitUserName)
+ gitSig.Email = util.IfZero(gitSig.Email, other.GitUserEmail)
+ }
+ if gitSig.Name == "" {
+ gitSig.Name = doer.GitName()
+ }
+ if gitSig.Email == "" {
+ gitSig.Email = doer.GetEmail()
+ }
+ return gitSig
}
-// CommitTreeWithDate creates a commit from a given tree for the user with provided message
-func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, committer *user_model.User, treeHash, message string, signoff bool, authorDate, committerDate time.Time) (string, error) {
- authorSig := author.NewGitSig()
- committerSig := committer.NewGitSig()
+// CommitTree creates a commit from a given tree for the user with provided message
+func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *CommitTreeUserOptions) (string, error) {
+ authorSig := makeGitUserSignature(opts.DoerUser, opts.AuthorIdentity, opts.CommitterIdentity)
+ committerSig := makeGitUserSignature(opts.DoerUser, opts.CommitterIdentity, opts.AuthorIdentity)
+
+ authorDate := opts.AuthorTime
+ committerDate := opts.CommitterTime
+ if authorDate == nil && committerDate == nil {
+ authorDate = util.ToPointer(time.Now())
+ committerDate = authorDate
+ } else if authorDate == nil {
+ authorDate = committerDate
+ } else if committerDate == nil {
+ committerDate = authorDate
+ }
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
@@ -244,24 +284,27 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
)
messageBytes := new(bytes.Buffer)
- _, _ = messageBytes.WriteString(message)
+ _, _ = messageBytes.WriteString(opts.CommitMessage)
_, _ = messageBytes.WriteString("\n")
- cmdCommitTree := git.NewCommand(t.ctx, "commit-tree").AddDynamicArguments(treeHash)
- if parent != "" {
- cmdCommitTree.AddOptionValues("-p", parent)
+ cmdCommitTree := git.NewCommand("commit-tree").AddDynamicArguments(opts.TreeHash)
+ if opts.ParentCommitID != "" {
+ cmdCommitTree.AddOptionValues("-p", opts.ParentCommitID)
}
var sign bool
- var keyID string
+ var key *git.SigningKey
var signer *git.Signature
- if parent != "" {
- sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent)
+ if opts.ParentCommitID != "" {
+ sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID)
} else {
- sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author)
+ sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser)
}
if sign {
- cmdCommitTree.AddOptionFormat("-S%s", keyID)
+ if key.Format != "" {
+ cmdCommitTree.AddConfig("gpg.format", key.Format)
+ }
+ cmdCommitTree.AddOptionFormat("-S%s", key.KeyID)
if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email {
// Add trailers
@@ -279,7 +322,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
cmdCommitTree.AddArguments("--no-gpg-sign")
}
- if signoff {
+ if opts.SignOff {
// Signed-off-by
_, _ = messageBytes.WriteString("\n")
_, _ = messageBytes.WriteString("Signed-off-by: ")
@@ -294,7 +337,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
if err := cmdCommitTree.
- Run(&git.RunOpts{
+ Run(ctx, &git.RunOpts{
Env: env,
Dir: t.basePath,
Stdin: messageBytes,
@@ -310,10 +353,10 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
}
// Push the provided commitHash to the repository branch by the provided user
-func (t *TemporaryUploadRepository) Push(doer *user_model.User, commitHash, branch string) error {
+func (t *TemporaryUploadRepository) Push(ctx context.Context, doer *user_model.User, commitHash, branch string) error {
// Because calls hooks we need to pass in the environment
env := repo_module.PushingEnvironment(doer, t.repo)
- if err := git.Push(t.ctx, t.basePath, git.PushOptions{
+ if err := git.Push(ctx, t.basePath, git.PushOptions{
Remote: t.repo.RepoPath(),
Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
Env: env,
@@ -335,7 +378,7 @@ func (t *TemporaryUploadRepository) Push(doer *user_model.User, commitHash, bran
}
// DiffIndex returns a Diff of the current index to the head
-func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
+func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context) (*gitdiff.Diff, error) {
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("unable to open stdout pipe: %w", err)
@@ -346,8 +389,8 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
}()
stderr := new(bytes.Buffer)
var diff *gitdiff.Diff
- err = git.NewCommand(t.ctx, "diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD").
- Run(&git.RunOpts{
+ err = git.NewCommand("diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD").
+ Run(ctx, &git.RunOpts{
Timeout: 30 * time.Second,
Dir: t.basePath,
Stdout: stdoutWriter,
@@ -356,7 +399,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
_ = stdoutWriter.Close()
defer cancel()
var diffErr error
- diff, diffErr = gitdiff.ParsePatch(t.ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "")
+ diff, diffErr = gitdiff.ParsePatch(ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "")
_ = stdoutReader.Close()
if diffErr != nil {
// if the diffErr is not nil, it will be returned as the error of "Run()"
@@ -370,18 +413,13 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
return nil, fmt.Errorf("unable to run diff-index pipeline in temporary repo: %w", err)
}
- diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(t.ctx, t.basePath, git.TrustedCmdArgs{"--cached"}, "HEAD")
- if err != nil {
- return nil, err
- }
-
return diff, nil
}
// GetBranchCommit Gets the commit object of the given branch
func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) {
if t.gitRepo == nil {
- return nil, fmt.Errorf("repository has not been cloned")
+ return nil, errors.New("repository has not been cloned")
}
return t.gitRepo.GetBranchCommit(branch)
}
@@ -389,7 +427,7 @@ func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit,
// GetCommit Gets the commit object of the given commit ID
func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) {
if t.gitRepo == nil {
- return nil, fmt.Errorf("repository has not been cloned")
+ return nil, errors.New("repository has not been cloned")
}
return t.gitRepo.GetCommit(commitID)
}
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index 6775186afd..f2cbacbf1c 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -6,10 +6,16 @@ package files
import (
"context"
"fmt"
+ "html/template"
"net/url"
+ "path"
+ "sort"
+ "strings"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -88,11 +94,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if len(entries) > perPage {
tree.Truncated = true
}
- if rangeStart+perPage < len(entries) {
- rangeEnd = rangeStart + perPage
- } else {
- rangeEnd = len(entries)
- }
+ rangeEnd = min(rangeStart+perPage, len(entries))
tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart)
for e := rangeStart; e < rangeEnd; e++ {
i := e - rangeStart
@@ -118,3 +120,110 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
}
return tree, nil
}
+
+func entryModeString(entryMode git.EntryMode) string {
+ switch entryMode {
+ case git.EntryModeBlob:
+ return "blob"
+ case git.EntryModeExec:
+ return "exec"
+ case git.EntryModeSymlink:
+ return "symlink"
+ case git.EntryModeCommit:
+ return "commit" // submodule
+ case git.EntryModeTree:
+ return "tree"
+ }
+ return "unknown"
+}
+
+type TreeViewNode struct {
+ EntryName string `json:"entryName"`
+ EntryMode string `json:"entryMode"`
+ EntryIcon template.HTML `json:"entryIcon"`
+ EntryIconOpen template.HTML `json:"entryIconOpen,omitempty"`
+
+ SymLinkedToMode string `json:"symLinkedToMode,omitempty"` // TODO: for the EntryMode="symlink"
+
+ FullPath string `json:"fullPath"`
+ SubmoduleURL string `json:"submoduleUrl,omitempty"`
+ Children []*TreeViewNode `json:"children,omitempty"`
+}
+
+func (node *TreeViewNode) sortLevel() int {
+ return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
+}
+
+func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
+ node := &TreeViewNode{
+ EntryName: entry.Name(),
+ EntryMode: entryModeString(entry.Mode()),
+ FullPath: path.Join(parentDir, entry.Name()),
+ }
+
+ entryInfo := fileicon.EntryInfoFromGitTreeEntry(commit, node.FullPath, entry)
+ node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
+ if entryInfo.EntryMode.IsDir() {
+ entryInfo.IsOpen = true
+ node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
+ }
+
+ if node.EntryMode == "commit" {
+ if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
+ log.Error("GetSubModule: %v", err)
+ } else if subModule != nil {
+ submoduleFile := git.NewCommitSubmoduleFile(subModule.URL, entry.ID.String())
+ webLink := submoduleFile.SubmoduleWebLink(ctx)
+ node.SubmoduleURL = webLink.CommitWebLink
+ }
+ }
+
+ return node
+}
+
+// sortTreeViewNodes list directory first and with alpha sequence
+func sortTreeViewNodes(nodes []*TreeViewNode) {
+ sort.Slice(nodes, func(i, j int) bool {
+ a, b := nodes[i].sortLevel(), nodes[j].sortLevel()
+ if a != b {
+ return a < b
+ }
+ return nodes[i].EntryName < nodes[j].EntryName
+ })
+}
+
+func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
+ entries, err := tree.ListEntries()
+ if err != nil {
+ return nil, err
+ }
+
+ subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
+ nodes := make([]*TreeViewNode, 0, len(entries))
+ for _, entry := range entries {
+ node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry)
+ nodes = append(nodes, node)
+ if entry.IsDir() && subPathDirName == entry.Name() {
+ subTreePath := treePath + "/" + node.EntryName
+ if subTreePath[0] == '/' {
+ subTreePath = subTreePath[1:]
+ }
+ subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
+ if err != nil {
+ log.Error("listTreeNodes: %v", err)
+ } else {
+ node.Children = subNodes
+ }
+ }
+ }
+ sortTreeViewNodes(nodes)
+ return nodes, nil
+}
+
+func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
+ entry, err := commit.GetTreeEntryByPath(treePath)
+ if err != nil {
+ return nil, err
+ }
+ return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath)
+}
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index 0c60fddf7b..a53f342d40 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -4,9 +4,12 @@
package files
import (
+ "html/template"
"testing"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/fileicon"
+ "code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/contexttest"
@@ -48,5 +51,69 @@ func TestGetTreeBySHA(t *testing.T) {
TotalCount: 1,
}
- assert.EqualValues(t, expectedTree, tree)
+ assert.Equal(t, expectedTree, tree)
+}
+
+func TestGetTreeViewNodes(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ ctx, _ := contexttest.MockContext(t, "user2/repo1")
+ ctx.Repo.RefFullName = git.RefNameFromBranch("sub-home-md-img-check")
+ contexttest.LoadRepo(t, ctx, 1)
+ contexttest.LoadRepoCommit(t, ctx)
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ mockIconForFile := func(id string) template.HTML {
+ return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
+ }
+ mockIconForFolder := func(id string) template.HTML {
+ return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
+ }
+ mockOpenIconForFolder := func(id string) template.HTML {
+ return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
+ }
+ treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
+ assert.NoError(t, err)
+ assert.Equal(t, []*TreeViewNode{
+ {
+ EntryName: "docs",
+ EntryMode: "tree",
+ FullPath: "docs",
+ EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
+ EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
+ },
+ }, treeNodes)
+
+ treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
+ assert.NoError(t, err)
+ assert.Equal(t, []*TreeViewNode{
+ {
+ EntryName: "docs",
+ EntryMode: "tree",
+ FullPath: "docs",
+ EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
+ EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
+ Children: []*TreeViewNode{
+ {
+ EntryName: "README.md",
+ EntryMode: "blob",
+ FullPath: "docs/README.md",
+ EntryIcon: mockIconForFile(`svg-mfi-readme`),
+ },
+ },
+ },
+ }, treeNodes)
+
+ treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
+ assert.NoError(t, err)
+ assert.Equal(t, []*TreeViewNode{
+ {
+ EntryName: "README.md",
+ EntryMode: "blob",
+ FullPath: "docs/README.md",
+ EntryIcon: mockIconForFile(`svg-mfi-readme`),
+ },
+ }, treeNodes)
}
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index a2763105b0..e871f777e5 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"path"
+ "slices"
"strings"
"time"
@@ -15,20 +16,22 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
pull_service "code.gitea.io/gitea/services/pull"
)
// IdentityOptions for a person's identity like an author or committer
type IdentityOptions struct {
- Name string
- Email string
+ GitUserName string // to match "git config user.name"
+ GitUserEmail string // to match "git config user.email"
}
// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
@@ -85,14 +88,32 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error {
return util.ErrNotExist
}
+type LazyReadSeeker interface {
+ io.ReadSeeker
+ io.Closer
+ OpenLazyReader() error
+}
+
// ChangeRepoFiles adds, updates or removes multiple files in the given repository
-func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) {
+func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (_ *structs.FilesResponse, errRet error) {
+ var addedLfsPointers []lfs.Pointer
+ defer func() {
+ if errRet != nil {
+ for _, lfsPointer := range addedLfsPointers {
+ _, err := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsPointer.Oid)
+ if err != nil {
+ log.Error("ChangeRepoFiles: RemoveLFSMetaObjectByOid failed: %v", err)
+ }
+ }
+ }
+ }()
+
err := repo.MustNotBeArchived()
if err != nil {
return nil, err
}
- // If no branch name is set, assume default branch
+ // If no branch name is set, assume the default branch
if opts.OldBranch == "" {
opts.OldBranch = repo.DefaultBranch
}
@@ -107,8 +128,13 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
defer closer.Close()
// oldBranch must exist for this operation
- if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil && !repo.IsEmpty {
+ if exist, err := git_model.IsBranchExist(ctx, repo.ID, opts.OldBranch); err != nil {
return nil, err
+ } else if !exist && !repo.IsEmpty {
+ return nil, git_model.ErrBranchNotExist{
+ RepoID: repo.ID,
+ BranchName: opts.OldBranch,
+ }
}
var treePaths []string
@@ -119,14 +145,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// Check that the path given in opts.treePath is valid (not a git path)
- treePath := CleanUploadFileName(file.TreePath)
+ treePath := CleanGitTreePath(file.TreePath)
if treePath == "" {
return nil, ErrFilenameInvalid{
Path: file.TreePath,
}
}
// If there is a fromTreePath (we are copying it), also clean it up
- fromTreePath := CleanUploadFileName(file.FromTreePath)
+ fromTreePath := CleanGitTreePath(file.FromTreePath)
if fromTreePath == "" && file.FromTreePath != "" {
return nil, ErrFilenameInvalid{
Path: file.FromTreePath,
@@ -145,30 +171,28 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
// Check to make sure the branch does not already exist, otherwise we can't proceed.
// If we aren't branching to a new branch, make sure user can commit to the given branch
if opts.NewBranch != opts.OldBranch {
- existingBranch, err := gitRepo.GetBranch(opts.NewBranch)
- if existingBranch != nil {
+ exist, err := git_model.IsBranchExist(ctx, repo.ID, opts.NewBranch)
+ if err != nil {
+ return nil, err
+ }
+ if exist {
return nil, git_model.ErrBranchAlreadyExists{
BranchName: opts.NewBranch,
}
}
- if err != nil && !git.IsErrBranchNotExist(err) {
- return nil, err
- }
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil {
return nil, err
}
message := strings.TrimSpace(opts.Message)
- author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
-
- t, err := NewTemporaryUploadRepository(ctx, repo)
+ t, err := NewTemporaryUploadRepository(repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
hasOldBranch := true
- if err := t.Clone(opts.OldBranch, true); err != nil {
+ if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
for _, file := range opts.Files {
if file.Operation == "delete" {
return nil, err
@@ -177,14 +201,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
return nil, err
}
- if err := t.Init(repo.ObjectFormatName); err != nil {
+ if err := t.Init(ctx, repo.ObjectFormatName); err != nil {
return nil, err
}
hasOldBranch = false
opts.LastCommitID = ""
}
if hasOldBranch {
- if err := t.SetDefaultIndex(); err != nil {
+ if err := t.SetDefaultIndex(ctx); err != nil {
return nil, err
}
}
@@ -192,19 +216,13 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
for _, file := range opts.Files {
if file.Operation == "delete" {
// Get the files in the index
- filesInIndex, err := t.LsFiles(file.TreePath)
+ filesInIndex, err := t.LsFiles(ctx, file.TreePath)
if err != nil {
return nil, fmt.Errorf("DeleteRepoFile: %w", err)
}
// Find the file we want to delete in the index
- inFilelist := false
- for _, indexFile := range filesInIndex {
- if indexFile == file.TreePath {
- inFilelist = true
- break
- }
- }
+ inFilelist := slices.Contains(filesInIndex, file.TreePath)
if !inFilelist {
return nil, ErrRepoFileDoesNotExist{
Path: file.TreePath,
@@ -220,7 +238,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil, err // Couldn't get a commit for the branch
}
- // Assigned LastCommitID in opts if it hasn't been set
+ // Assigned LastCommitID in "opts" if it hasn't been set
if opts.LastCommitID == "" {
opts.LastCommitID = commit.ID.String()
} else {
@@ -232,22 +250,25 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
for _, file := range opts.Files {
- if err := handleCheckErrors(file, commit, opts); err != nil {
+ if err = handleCheckErrors(file, commit, opts); err != nil {
return nil, err
}
}
}
- contentStore := lfs.NewContentStore()
+ lfsContentStore := lfs.NewContentStore()
for _, file := range opts.Files {
switch file.Operation {
- case "create", "update":
- if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil {
+ case "create", "update", "rename", "upload":
+ addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID)
+ if err != nil {
return nil, err
}
+ if addedLfsPointer != nil {
+ addedLfsPointers = append(addedLfsPointers, *addedLfsPointer)
+ }
case "delete":
- // Remove the file from the index
- if err := t.RemoveFilesFromIndex(file.TreePath); err != nil {
+ if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil {
return nil, err
}
default:
@@ -256,24 +277,33 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// Now write the tree
- treeHash, err := t.WriteTree()
+ treeHash, err := t.WriteTree(ctx)
if err != nil {
return nil, err
}
// Now commit the tree
- var commitHash string
+ commitOpts := &CommitTreeUserOptions{
+ ParentCommitID: opts.LastCommitID,
+ TreeHash: treeHash,
+ CommitMessage: message,
+ SignOff: opts.Signoff,
+ DoerUser: doer,
+ AuthorIdentity: opts.Author,
+ AuthorTime: nil,
+ CommitterIdentity: opts.Committer,
+ CommitterTime: nil,
+ }
if opts.Dates != nil {
- commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
- } else {
- commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff)
+ commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer
}
+ commitHash, err := t.CommitTree(ctx, commitOpts)
if err != nil {
return nil, err
}
// Then push this tree to NewBranch
- if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
+ if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil {
log.Error("%T %v", err, err)
return nil, err
}
@@ -283,14 +313,16 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil, err
}
- filesResponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths)
+ // FIXME: this call seems not right, why it needs to read the file content again
+ // FIXME: why it uses the NewBranch as "ref", it should use the commit ID because the response is only for this commit
+ filesResponse, err := GetFilesResponseFromCommit(ctx, repo, gitRepo, utils.NewRefCommit(git.RefNameFromBranch(opts.NewBranch), commit), treePaths)
if err != nil {
return nil, err
}
if repo.IsEmpty {
if isEmpty, err := gitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch")
+ _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch")
}
}
@@ -356,22 +388,33 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string {
// handles the check for various issues for ChangeRepoFiles
func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
- if file.Operation == "update" || file.Operation == "delete" {
- fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
- if err != nil {
- return err
+ // check old entry (fromTreePath/fromEntry)
+ if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" {
+ var fromEntryIDString string
+ {
+ fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
+ if file.Operation == "upload" && git.IsErrNotExist(err) {
+ fromEntry = nil
+ } else if err != nil {
+ return err
+ }
+ if fromEntry != nil {
+ fromEntryIDString = fromEntry.ID.String()
+ file.Options.executable = fromEntry.IsExecutable() // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function
+ }
}
+
if file.SHA != "" {
- // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
- if file.SHA != fromEntry.ID.String() {
+ // If the SHA given doesn't match the SHA of the fromTreePath, throw error
+ if file.SHA != fromEntryIDString {
return pull_service.ErrSHADoesNotMatch{
Path: file.Options.treePath,
GivenSHA: file.SHA,
- CurrentSHA: fromEntry.ID.String(),
+ CurrentSHA: fromEntryIDString,
}
}
} else if opts.LastCommitID != "" {
- // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
+ // If a lastCommitID given doesn't match the branch head's commitID throw
// an error, but only if we aren't creating a new branch.
if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil {
@@ -389,13 +432,13 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
// haven't been made. We throw an error if one wasn't provided.
return ErrSHAOrCommitIDNotProvided{}
}
- file.Options.executable = fromEntry.IsExecutable()
}
- if file.Operation == "create" || file.Operation == "update" {
- // For the path where this file will be created/updated, we need to make
- // sure no parts of the path are existing files or links except for the last
- // item in the path which is the file name, and that shouldn't exist IF it is
- // a new file OR is being moved to a new path.
+
+ // check new entry (treePath/treeEntry)
+ if file.Operation == "create" || file.Operation == "update" || file.Operation == "upload" || file.Operation == "rename" {
+ // For operation's target path, we need to make sure no parts of the path are existing files or links
+ // except for the last item in the path (which is the file name).
+ // And that shouldn't exist IF it is a new file OR is being moved to a new path.
treePathParts := strings.Split(file.Options.treePath, "/")
subTreePath := ""
for index, part := range treePathParts {
@@ -432,7 +475,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
Type: git.EntryModeTree,
}
} else if file.Options.fromTreePath != file.Options.treePath || file.Operation == "create" {
- // The entry shouldn't exist if we are creating new file or moving to a new path
+ // The entry shouldn't exist if we are creating the new file or moving to a new path
return ErrRepoFileAlreadyExists{
Path: file.Options.treePath,
}
@@ -443,21 +486,23 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
return nil
}
-// CreateOrUpdateFile handles creating or updating a file for ChangeRepoFiles
-func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error {
+func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64) (addedLfsPointer *lfs.Pointer, _ error) {
+ if rd, ok := file.ContentReader.(LazyReadSeeker); ok {
+ if err := rd.OpenLazyReader(); err != nil {
+ return nil, fmt.Errorf("OpenLazyReader: %w", err)
+ }
+ defer rd.Close()
+ }
+
// Get the two paths (might be the same if not moving) from the index if they exist
- filesInIndex, err := t.LsFiles(file.TreePath, file.FromTreePath)
+ filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath)
if err != nil {
- return fmt.Errorf("UpdateRepoFile: %w", err)
+ return nil, fmt.Errorf("LsFiles: %w", err)
}
// If is a new file (not updating) then the given path shouldn't exist
if file.Operation == "create" {
- for _, indexFile := range filesInIndex {
- if indexFile == file.TreePath {
- return ErrRepoFileAlreadyExists{
- Path: file.TreePath,
- }
- }
+ if slices.Contains(filesInIndex, file.TreePath) {
+ return nil, ErrRepoFileAlreadyExists{Path: file.TreePath}
}
}
@@ -465,79 +510,178 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
if file.Options.fromTreePath != file.Options.treePath && len(filesInIndex) > 0 {
for _, indexFile := range filesInIndex {
if indexFile == file.Options.fromTreePath {
- if err := t.RemoveFilesFromIndex(file.FromTreePath); err != nil {
- return err
+ if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil {
+ return nil, err
}
}
}
}
- treeObjectContentReader := file.ContentReader
- var lfsMetaObject *git_model.LFSMetaObject
- if setting.LFS.StartServer && hasOldBranch {
- // Check there is no way this can return multiple infos
- filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{
- Attributes: []string{"filter"},
- Filenames: []string{file.Options.treePath},
- CachedOnly: true,
- })
+ var writeObjectRet *writeRepoObjectRet
+ switch file.Operation {
+ case "create", "update", "upload":
+ writeObjectRet, err = writeRepoObjectForModify(ctx, t, file)
+ case "rename":
+ writeObjectRet, err = writeRepoObjectForRename(ctx, t, file)
+ default:
+ return nil, util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ // Add the object to the index, the "file.Options.executable" is set in handleCheckErrors by the caller (legacy hacky approach)
+ if err = t.AddObjectToIndex(ctx, util.Iif(file.Options.executable, "100755", "100644"), writeObjectRet.ObjectHash, file.Options.treePath); err != nil {
+ return nil, err
+ }
+
+ if writeObjectRet.LfsContent == nil {
+ return nil, nil // No LFS pointer, so nothing to do
+ }
+ defer writeObjectRet.LfsContent.Close()
+
+ // Now we must store the content into an LFS object
+ lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer)
+ if err != nil {
+ return nil, err
+ }
+ exist, err := contentStore.Exists(lfsMetaObject.Pointer)
+ if err != nil {
+ return nil, err
+ }
+ if !exist {
+ err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent)
if err != nil {
- return err
+ if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil {
+ return nil, fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err)
+ }
+ return nil, err
}
+ }
+ return &lfsMetaObject.Pointer, nil
+}
- if filename2attribute2info[file.Options.treePath] != nil && filename2attribute2info[file.Options.treePath]["filter"] == "lfs" {
- // OK so we are supposed to LFS this data!
- pointer, err := lfs.GeneratePointer(treeObjectContentReader)
+func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) {
+ attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{
+ Attributes: []string{attribute.Filter},
+ Filenames: paths,
+ })
+ if err != nil {
+ return nil, err
+ }
+ for _, p := range paths {
+ isLFSFile := attributesMap[p] != nil && attributesMap[p].Get(attribute.Filter).ToString().Value() == "lfs"
+ ret = append(ret, isLFSFile)
+ }
+ return ret, nil
+}
+
+type writeRepoObjectRet struct {
+ ObjectHash string
+ LfsContent io.ReadCloser // if not nil, then the caller should store its content in LfsPointer, then close it
+ LfsPointer lfs.Pointer
+}
+
+// writeRepoObjectForModify hashes the git object for create or update operations
+func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
+ ret = &writeRepoObjectRet{}
+ treeObjectContentReader := file.ContentReader
+ if setting.LFS.StartServer {
+ checkIsLfsFiles, err := checkIsLfsFileInGitAttributes(ctx, t, []string{file.Options.treePath})
+ if err != nil {
+ return nil, err
+ }
+ if checkIsLfsFiles[0] {
+ // OK, so we are supposed to LFS this data!
+ ret.LfsPointer, err = lfs.GeneratePointer(file.ContentReader)
if err != nil {
- return err
+ return nil, err
}
- lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID}
- treeObjectContentReader = strings.NewReader(pointer.StringContent())
+ if _, err = file.ContentReader.Seek(0, io.SeekStart); err != nil {
+ return nil, err
+ }
+ ret.LfsContent = io.NopCloser(file.ContentReader)
+ treeObjectContentReader = strings.NewReader(ret.LfsPointer.StringContent())
}
}
- // Add the object to the database
- objectHash, err := t.HashObject(treeObjectContentReader)
+ ret.ObjectHash, err = t.HashObjectAndWrite(ctx, treeObjectContentReader)
if err != nil {
- return err
+ return nil, err
}
+ return ret, nil
+}
- // Add the object to the index
- if file.Options.executable {
- if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil {
- return err
- }
- } else {
- if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil {
- return err
+// writeRepoObjectForRename the same as writeRepoObjectForModify buf for "rename"
+func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) {
+ lastCommitID, err := t.GetLastCommit(ctx)
+ if err != nil {
+ return nil, err
+ }
+ commit, err := t.GetCommit(lastCommitID)
+ if err != nil {
+ return nil, err
+ }
+ oldEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
+ if err != nil {
+ return nil, err
+ }
+
+ ret = &writeRepoObjectRet{ObjectHash: oldEntry.ID.String()}
+ if !setting.LFS.StartServer {
+ return ret, nil
+ }
+
+ checkIsLfsFiles, err := checkIsLfsFileInGitAttributes(ctx, t, []string{file.Options.fromTreePath, file.Options.treePath})
+ if err != nil {
+ return nil, err
+ }
+ oldIsLfs, newIsLfs := checkIsLfsFiles[0], checkIsLfsFiles[1]
+
+ // If the old and new paths are both in lfs or both not in lfs, the object hash of the old file can be used directly
+ // as the object doesn't change
+ if oldIsLfs == newIsLfs {
+ return ret, nil
+ }
+
+ oldEntryBlobPointerBy := func(f func(r io.Reader) (lfs.Pointer, error)) (lfsPointer lfs.Pointer, err error) {
+ r, err := oldEntry.Blob().DataAsync()
+ if err != nil {
+ return lfsPointer, err
}
+ defer r.Close()
+ return f(r)
}
- if lfsMetaObject != nil {
- // We have an LFS object - create it
- lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject.RepositoryID, lfsMetaObject.Pointer)
+ var treeObjectContentReader io.ReadCloser
+ if oldIsLfs {
+ // If the old is in lfs but the new isn't, read the content from lfs and add it as a normal git object
+ pointer, err := oldEntryBlobPointerBy(lfs.ReadPointer)
if err != nil {
- return err
+ return nil, err
}
- exist, err := contentStore.Exists(lfsMetaObject.Pointer)
+ treeObjectContentReader, err = lfs.ReadMetaObject(pointer)
if err != nil {
- return err
+ return nil, err
}
- if !exist {
- _, err := file.ContentReader.Seek(0, io.SeekStart)
- if err != nil {
- return err
- }
- if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil {
- if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
- return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
- }
- return err
- }
+ defer treeObjectContentReader.Close()
+ } else {
+ // If the new is in lfs but the old isn't, read the content from the git object and generate a lfs pointer of it
+ ret.LfsPointer, err = oldEntryBlobPointerBy(lfs.GeneratePointer)
+ if err != nil {
+ return nil, err
+ }
+ ret.LfsContent, err = oldEntry.Blob().DataAsync()
+ if err != nil {
+ return nil, err
}
+ treeObjectContentReader = io.NopCloser(strings.NewReader(ret.LfsPointer.StringContent()))
}
-
- return nil
+ ret.ObjectHash, err = t.HashObjectAndWrite(ctx, treeObjectContentReader)
+ if err != nil {
+ return nil, err
+ }
+ return ret, nil
}
// VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch
diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go
index cbfaf49d13..b783cbd01d 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -8,14 +8,11 @@ import (
"fmt"
"os"
"path"
- "strings"
+ "sync"
- git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/log"
)
// UploadRepoFileOptions contains the uploaded repository file options
@@ -27,199 +24,88 @@ type UploadRepoFileOptions struct {
Message string
Files []string // In UUID format.
Signoff bool
+ Author *IdentityOptions
+ Committer *IdentityOptions
}
-type uploadInfo struct {
- upload *repo_model.Upload
- lfsMetaObject *git_model.LFSMetaObject
+type lazyLocalFileReader struct {
+ *os.File
+ localFilename string
+ counter int
+ mu sync.Mutex
}
-func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
- for _, info := range *infos {
- if info.lfsMetaObject == nil {
- continue
- }
- if !info.lfsMetaObject.Existing {
- if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
- original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
- }
- }
- }
- return original
-}
-
-// UploadRepoFiles uploads files to the given repository
-func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error {
- if len(opts.Files) == 0 {
- return nil
- }
+var _ LazyReadSeeker = (*lazyLocalFileReader)(nil)
- uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files)
- if err != nil {
- return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
- }
+func (l *lazyLocalFileReader) Close() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
- names := make([]string, len(uploads))
- infos := make([]uploadInfo, len(uploads))
- for i, upload := range uploads {
- // Check file is not lfs locked, will return nil if lock setting not enabled
- filepath := path.Join(opts.TreePath, upload.Name)
- lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
- if err != nil {
- return err
- }
- if lfsLock != nil && lfsLock.OwnerID != doer.ID {
- u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
- if err != nil {
- return err
+ if l.counter > 0 {
+ l.counter--
+ if l.counter == 0 {
+ if err := l.File.Close(); err != nil {
+ return fmt.Errorf("close file %s: %w", l.localFilename, err)
}
- return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
- }
-
- names[i] = upload.Name
- infos[i] = uploadInfo{upload: upload}
- }
-
- t, err := NewTemporaryUploadRepository(ctx, repo)
- if err != nil {
- return err
- }
- defer t.Close()
-
- hasOldBranch := true
- if err = t.Clone(opts.OldBranch, true); err != nil {
- if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
- return err
- }
- if err = t.Init(repo.ObjectFormatName); err != nil {
- return err
- }
- hasOldBranch = false
- opts.LastCommitID = ""
- }
- if hasOldBranch {
- if err = t.SetDefaultIndex(); err != nil {
- return err
- }
- }
-
- var filename2attribute2info map[string]map[string]string
- if setting.LFS.StartServer {
- filename2attribute2info, err = t.gitRepo.CheckAttribute(git.CheckAttributeOpts{
- Attributes: []string{"filter"},
- Filenames: names,
- CachedOnly: true,
- })
- if err != nil {
- return err
+ l.File = nil
}
+ return nil
}
+ return fmt.Errorf("file %s already closed", l.localFilename)
+}
- // Copy uploaded files into repository.
- for i := range infos {
- if err := copyUploadedLFSFileIntoRepository(&infos[i], filename2attribute2info, t, opts.TreePath); err != nil {
- return err
- }
- }
+func (l *lazyLocalFileReader) OpenLazyReader() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
- // Now write the tree
- treeHash, err := t.WriteTree()
- if err != nil {
- return err
+ if l.File != nil {
+ l.counter++
+ return nil
}
- // make author and committer the doer
- author := doer
- committer := doer
-
- // Now commit the tree
- commitHash, err := t.CommitTree(opts.LastCommitID, author, committer, treeHash, opts.Message, opts.Signoff)
+ file, err := os.Open(l.localFilename)
if err != nil {
return err
}
+ l.File = file
+ l.counter = 1
+ return nil
+}
- // Now deal with LFS objects
- for i := range infos {
- if infos[i].lfsMetaObject == nil {
- continue
- }
- infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer)
- if err != nil {
- // OK Now we need to cleanup
- return cleanUpAfterFailure(ctx, &infos, t, err)
- }
- // Don't move the files yet - we need to ensure that
- // everything can be inserted first
- }
-
- // OK now we can insert the data into the store - there's no way to clean up the store
- // once it's in there, it's in there.
- contentStore := lfs.NewContentStore()
- for _, info := range infos {
- if err := uploadToLFSContentStore(info, contentStore); err != nil {
- return cleanUpAfterFailure(ctx, &infos, t, err)
- }
- }
-
- // Then push this tree to NewBranch
- if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
- return err
+// UploadRepoFiles uploads files to the given repository
+func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error {
+ if len(opts.Files) == 0 {
+ return nil
}
- return repo_model.DeleteUploads(ctx, uploads...)
-}
-
-func copyUploadedLFSFileIntoRepository(info *uploadInfo, filename2attribute2info map[string]map[string]string, t *TemporaryUploadRepository, treePath string) error {
- file, err := os.Open(info.upload.LocalPath())
+ uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files)
if err != nil {
- return err
+ return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
}
- defer file.Close()
- var objectHash string
- if setting.LFS.StartServer && filename2attribute2info[info.upload.Name] != nil && filename2attribute2info[info.upload.Name]["filter"] == "lfs" {
- // Handle LFS
- // FIXME: Inefficient! this should probably happen in models.Upload
- pointer, err := lfs.GeneratePointer(file)
- if err != nil {
- return err
- }
-
- info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
-
- if objectHash, err = t.HashObject(strings.NewReader(pointer.StringContent())); err != nil {
- return err
- }
- } else if objectHash, err = t.HashObject(file); err != nil {
- return err
+ changeOpts := &ChangeRepoFilesOptions{
+ LastCommitID: opts.LastCommitID,
+ OldBranch: opts.OldBranch,
+ NewBranch: opts.NewBranch,
+ Message: opts.Message,
+ Signoff: opts.Signoff,
+ Author: opts.Author,
+ Committer: opts.Committer,
+ }
+ for _, upload := range uploads {
+ changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{
+ Operation: "upload",
+ TreePath: path.Join(opts.TreePath, upload.Name),
+ ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()},
+ })
}
- // Add the object to the index
- return t.AddObjectToIndex("100644", objectHash, path.Join(treePath, info.upload.Name))
-}
-
-func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error {
- if info.lfsMetaObject == nil {
- return nil
- }
- exist, err := contentStore.Exists(info.lfsMetaObject.Pointer)
+ _, err = ChangeRepoFiles(ctx, repo, doer, changeOpts)
if err != nil {
return err
}
- if !exist {
- file, err := os.Open(info.upload.LocalPath())
- if err != nil {
- return err
- }
-
- defer file.Close()
- // FIXME: Put regenerates the hash and copies the file over.
- // I guess this strictly ensures the soundness of the store but this is inefficient.
- if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil {
- // OK Now we need to cleanup
- // Can't clean up the store, once uploaded there they're there.
- return err
- }
+ if err := repo_model.DeleteUploads(ctx, uploads...); err != nil {
+ log.Error("DeleteUploads: %v", err)
}
return nil
}