diff options
author | Denys Konovalov <privat@denyskon.de> | 2023-05-29 11:41:35 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-29 17:41:35 +0800 |
commit | 275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2 (patch) | |
tree | 4283f97bce56c7783e6c77c380cbe4ce06277720 /services | |
parent | 245f2c08db34e535576633748fc143bb09097ca8 (diff) | |
download | gitea-275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2.tar.gz gitea-275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2.zip |
API endpoint for changing/creating/deleting multiple files (#24887)
This PR creates an API endpoint for creating/updating/deleting multiple
files in one API call similar to the solution provided by
[GitLab](https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions).
To archive this, the CreateOrUpdateRepoFile and DeleteRepoFIle functions
in files service are unified into one function supporting multiple files
and actions.
Resolves #14619
Diffstat (limited to 'services')
-rw-r--r-- | services/repository/files/delete.go | 204 | ||||
-rw-r--r-- | services/repository/files/file.go | 30 | ||||
-rw-r--r-- | services/repository/files/update.go | 413 |
3 files changed, 279 insertions, 368 deletions
diff --git a/services/repository/files/delete.go b/services/repository/files/delete.go deleted file mode 100644 index faa60bb3ba..0000000000 --- a/services/repository/files/delete.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package files - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models" - repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - api "code.gitea.io/gitea/modules/structs" -) - -// DeleteRepoFileOptions holds the repository delete file options -type DeleteRepoFileOptions struct { - LastCommitID string - OldBranch string - NewBranch string - TreePath string - Message string - SHA string - Author *IdentityOptions - Committer *IdentityOptions - Dates *CommitDateOptions - Signoff bool -} - -// DeleteRepoFile deletes a file in the given repository -func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *DeleteRepoFileOptions) (*api.FileResponse, error) { - // If no branch name is set, assume the repo's default branch - if opts.OldBranch == "" { - opts.OldBranch = repo.DefaultBranch - } - if opts.NewBranch == "" { - opts.NewBranch = opts.OldBranch - } - - gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) - if err != nil { - return nil, err - } - defer closer.Close() - - // oldBranch must exist for this operation - if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil { - return nil, err - } - - // A NewBranch can be specified for the file to be created/updated in a new branch. - // 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 { - newBranch, err := gitRepo.GetBranch(opts.NewBranch) - if err != nil && !git.IsErrBranchNotExist(err) { - return nil, err - } - if newBranch != nil { - return nil, models.ErrBranchAlreadyExists{ - BranchName: opts.NewBranch, - } - } - } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, opts.TreePath); err != nil { - return nil, err - } - - // Check that the path given in opts.treeName is valid (not a git path) - treePath := CleanUploadFileName(opts.TreePath) - if treePath == "" { - return nil, models.ErrFilenameInvalid{ - Path: opts.TreePath, - } - } - - message := strings.TrimSpace(opts.Message) - - author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) - - t, err := NewTemporaryUploadRepository(ctx, repo) - if err != nil { - return nil, err - } - defer t.Close() - if err := t.Clone(opts.OldBranch); err != nil { - return nil, err - } - if err := t.SetDefaultIndex(); err != nil { - return nil, err - } - - // Get the commit of the original branch - commit, err := t.GetBranchCommit(opts.OldBranch) - if err != nil { - return nil, err // Couldn't get a commit for the branch - } - - // Assigned LastCommitID in opts if it hasn't been set - if opts.LastCommitID == "" { - opts.LastCommitID = commit.ID.String() - } else { - lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) - if err != nil { - return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %w", err) - } - opts.LastCommitID = lastCommitID.String() - } - - // Get the files in the index - filesInIndex, err := t.LsFiles(opts.TreePath) - if err != nil { - return nil, fmt.Errorf("DeleteRepoFile: %w", err) - } - - // Find the file we want to delete in the index - inFilelist := false - for _, file := range filesInIndex { - if file == opts.TreePath { - inFilelist = true - break - } - } - if !inFilelist { - return nil, models.ErrRepoFileDoesNotExist{ - Path: opts.TreePath, - } - } - - // Get the entry of treePath and check if the SHA given is the same as the file - entry, err := commit.GetTreeEntryByPath(treePath) - if err != nil { - return nil, err - } - if opts.SHA != "" { - // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error - if opts.SHA != entry.ID.String() { - return nil, models.ErrSHADoesNotMatch{ - Path: treePath, - GivenSHA: opts.SHA, - CurrentSHA: entry.ID.String(), - } - } - } else if opts.LastCommitID != "" { - // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw - // an error, but only if we aren't creating a new branch. - if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { - // CommitIDs don't match, but we don't want to throw a ErrCommitIDDoesNotMatch unless - // this specific file has been edited since opts.LastCommitID - if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil { - return nil, err - } else if changed { - return nil, models.ErrCommitIDDoesNotMatch{ - GivenCommitID: opts.LastCommitID, - CurrentCommitID: opts.LastCommitID, - } - } - // The file wasn't modified, so we are good to delete it - } - } else { - // When deleting a file, a lastCommitID or SHA needs to be given to make sure other commits haven't been - // made. We throw an error if one wasn't provided. - return nil, models.ErrSHAOrCommitIDNotProvided{} - } - - // Remove the file from the index - if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil { - return nil, err - } - - // Now write the tree - treeHash, err := t.WriteTree() - if err != nil { - return nil, err - } - - // Now commit the tree - var commitHash string - 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) - } - if err != nil { - return nil, err - } - - // Then push this tree to NewBranch - if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { - return nil, err - } - - commit, err = t.GetCommit(commitHash) - if err != nil { - return nil, err - } - - file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath) - if err != nil { - return nil, err - } - return file, nil -} diff --git a/services/repository/files/file.go b/services/repository/files/file.go index dc1e547dcd..16783f5b5f 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -17,6 +17,22 @@ import ( "code.gitea.io/gitea/modules/util" ) +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 + files = append(files, fileContents) + } + fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil + verification := GetPayloadCommitVerification(ctx, commit) + filesResponse := &api.FilesResponse{ + Files: files, + Commit: fileCommitResponse, + Verification: verification, + } + 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 @@ -30,6 +46,20 @@ func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, 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{} + if len(filesResponse.Files) > index { + content = filesResponse.Files[index] + } + fileResponse := &api.FileResponse{ + Content: content, + Commit: filesResponse.Commit, + Verification: filesResponse.Verification, + } + return fileResponse +} + // GetFileCommitResponse Constructs a FileCommitResponse from a Commit object func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) { if repo == nil { diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 25014f4418..81d5e32773 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -41,23 +41,36 @@ type CommitDateOptions struct { Committer time.Time } -// UpdateRepoFileOptions holds the repository file update options -type UpdateRepoFileOptions struct { - LastCommitID string - OldBranch string - NewBranch string +type ChangeRepoFile struct { + Operation string TreePath string FromTreePath string - Message string Content string SHA string - IsNewFile bool + Options *RepoFileOptions +} + +// UpdateRepoFilesOptions holds the repository files update options +type ChangeRepoFilesOptions struct { + LastCommitID string + OldBranch string + NewBranch string + Message string + Files []*ChangeRepoFile Author *IdentityOptions Committer *IdentityOptions Dates *CommitDateOptions Signoff bool } +type RepoFileOptions struct { + treePath string + fromTreePath string + encoding string + bom bool + executable bool +} + func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (string, bool) { reader, err := entry.Blob().DataAsync() if err != nil { @@ -125,8 +138,8 @@ func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (st return encoding, false } -// CreateOrUpdateRepoFile adds or updates a file in the given repository -func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UpdateRepoFileOptions) (*structs.FileResponse, 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) { // If no branch name is set, assume default branch if opts.OldBranch == "" { opts.OldBranch = repo.DefaultBranch @@ -146,6 +159,38 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do return nil, err } + treePaths := []string{} + for _, file := range opts.Files { + // If FromTreePath is not set, set it to the opts.TreePath + if file.TreePath != "" && file.FromTreePath == "" { + file.FromTreePath = file.TreePath + } + + // Check that the path given in opts.treePath is valid (not a git path) + treePath := CleanUploadFileName(file.TreePath) + if treePath == "" { + return nil, models.ErrFilenameInvalid{ + Path: file.TreePath, + } + } + // If there is a fromTreePath (we are copying it), also clean it up + fromTreePath := CleanUploadFileName(file.FromTreePath) + if fromTreePath == "" && file.FromTreePath != "" { + return nil, models.ErrFilenameInvalid{ + Path: file.FromTreePath, + } + } + + file.Options = &RepoFileOptions{ + treePath: treePath, + fromTreePath: fromTreePath, + encoding: "UTF-8", + bom: false, + executable: false, + } + treePaths = append(treePaths, treePath) + } + // A NewBranch can be specified for the file to be created/updated in a new branch. // 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 @@ -159,30 +204,10 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do if err != nil && !git.IsErrBranchNotExist(err) { return nil, err } - } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, opts.TreePath); err != nil { + } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil { return nil, err } - // If FromTreePath is not set, set it to the opts.TreePath - if opts.TreePath != "" && opts.FromTreePath == "" { - opts.FromTreePath = opts.TreePath - } - - // Check that the path given in opts.treePath is valid (not a git path) - treePath := CleanUploadFileName(opts.TreePath) - if treePath == "" { - return nil, models.ErrFilenameInvalid{ - Path: opts.TreePath, - } - } - // If there is a fromTreePath (we are copying it), also clean it up - fromTreePath := CleanUploadFileName(opts.FromTreePath) - if fromTreePath == "" && opts.FromTreePath != "" { - return nil, models.ErrFilenameInvalid{ - Path: opts.FromTreePath, - } - } - message := strings.TrimSpace(opts.Message) author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) @@ -194,6 +219,11 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do defer t.Close() hasOldBranch := true if err := t.Clone(opts.OldBranch); err != nil { + for _, file := range opts.Files { + if file.Operation == "delete" { + return nil, err + } + } if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { return nil, err } @@ -209,9 +239,29 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do } } - encoding := "UTF-8" - bom := false - executable := false + for _, file := range opts.Files { + if file.Operation == "delete" { + // Get the files in the index + filesInIndex, err := t.LsFiles(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 + } + } + if !inFilelist { + return nil, models.ErrRepoFileDoesNotExist{ + Path: file.TreePath, + } + } + } + } if hasOldBranch { // Get the commit of the original branch @@ -232,48 +282,114 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do } - if !opts.IsNewFile { - fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) - if err != nil { + for _, file := range opts.Files { + if err := handleCheckErrors(file, commit, opts, repo); err != nil { return nil, err } - if opts.SHA != "" { - // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error - if opts.SHA != fromEntry.ID.String() { - return nil, models.ErrSHADoesNotMatch{ - Path: treePath, - GivenSHA: opts.SHA, - CurrentSHA: fromEntry.ID.String(), - } + } + } + + contentStore := 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 { + return nil, err + } + case "delete": + // Remove the file from the index + if err := t.RemoveFilesFromIndex(file.TreePath); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("Invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath) + } + } + + // Now write the tree + treeHash, err := t.WriteTree() + if err != nil { + return nil, err + } + + // Now commit the tree + var commitHash string + 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) + } + if err != nil { + return nil, err + } + + // Then push this tree to NewBranch + if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { + log.Error("%T %v", err, err) + return nil, err + } + + commit, err := t.GetCommit(commitHash) + if err != nil { + return nil, err + } + + filesReponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths) + if err != nil { + return nil, err + } + + if repo.IsEmpty { + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty") + } + + return filesReponse, nil +} + +// handles the check for various issues for ChangeRepoFiles +func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions, repo *repo_model.Repository) error { + if file.Operation == "update" || file.Operation == "delete" { + fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + if err != nil { + return err + } + 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() { + return models.ErrSHADoesNotMatch{ + Path: file.Options.treePath, + GivenSHA: file.SHA, + CurrentSHA: fromEntry.ID.String(), } - } else if opts.LastCommitID != "" { - // If a lastCommitID was given and it doesn't match the commitID of the head of the branch 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(treePath, opts.LastCommitID); err != nil { - return nil, err - } else if changed { - return nil, models.ErrCommitIDDoesNotMatch{ - GivenCommitID: opts.LastCommitID, - CurrentCommitID: opts.LastCommitID, - } + } + } else if opts.LastCommitID != "" { + // If a lastCommitID was given and it doesn't match the commitID of the head of the branch 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 { + return err + } else if changed { + return models.ErrCommitIDDoesNotMatch{ + GivenCommitID: opts.LastCommitID, + CurrentCommitID: opts.LastCommitID, } - // The file wasn't modified, so we are good to delete it } - } else { - // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits - // haven't been made. We throw an error if one wasn't provided. - return nil, models.ErrSHAOrCommitIDNotProvided{} + // The file wasn't modified, so we are good to delete it } - encoding, bom = detectEncodingAndBOM(fromEntry, repo) - executable = fromEntry.IsExecutable() + } else { + // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits + // haven't been made. We throw an error if one wasn't provided. + return models.ErrSHAOrCommitIDNotProvided{} } - + file.Options.encoding, file.Options.bom = detectEncodingAndBOM(fromEntry, repo) + 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. - treePathParts := strings.Split(treePath, "/") + treePathParts := strings.Split(file.Options.treePath, "/") subTreePath := "" for index, part := range treePathParts { subTreePath = path.Join(subTreePath, part) @@ -283,11 +399,11 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do // Means there is no item with that name, so we're good break } - return nil, err + return err } if index < len(treePathParts)-1 { if !entry.IsDir() { - return nil, models.ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), Path: subTreePath, Name: part, @@ -295,193 +411,170 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do } } } else if entry.IsLink() { - return nil, models.ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), Path: subTreePath, Name: part, Type: git.EntryModeSymlink, } } else if entry.IsDir() { - return nil, models.ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath), Path: subTreePath, Name: part, Type: git.EntryModeTree, } - } else if fromTreePath != treePath || opts.IsNewFile { + } 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 - return nil, models.ErrRepoFileAlreadyExists{ - Path: treePath, + return models.ErrRepoFileAlreadyExists{ + Path: file.Options.treePath, } } } } + return nil +} + +// handle creating or updating a file for ChangeRepoFiles +func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error { // Get the two paths (might be the same if not moving) from the index if they exist - filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath) + filesInIndex, err := t.LsFiles(file.TreePath, file.FromTreePath) if err != nil { - return nil, fmt.Errorf("UpdateRepoFile: %w", err) + return fmt.Errorf("UpdateRepoFile: %w", err) } // If is a new file (not updating) then the given path shouldn't exist - if opts.IsNewFile { - for _, file := range filesInIndex { - if file == opts.TreePath { - return nil, models.ErrRepoFileAlreadyExists{ - Path: opts.TreePath, + if file.Operation == "create" { + for _, indexFile := range filesInIndex { + if indexFile == file.TreePath { + return models.ErrRepoFileAlreadyExists{ + Path: file.TreePath, } } } } // Remove the old path from the tree - if fromTreePath != treePath && len(filesInIndex) > 0 { - for _, file := range filesInIndex { - if file == fromTreePath { - if err := t.RemoveFilesFromIndex(opts.FromTreePath); err != nil { - return nil, err + 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 } } } } - content := opts.Content - if bom { + content := file.Content + if file.Options.bom { content = string(charset.UTF8BOM) + content } - if encoding != "UTF-8" { - charsetEncoding, _ := stdcharset.Lookup(encoding) + if file.Options.encoding != "UTF-8" { + charsetEncoding, _ := stdcharset.Lookup(file.Options.encoding) if charsetEncoding != nil { result, _, err := transform.String(charsetEncoding.NewEncoder(), content) if err != nil { // Look if we can't encode back in to the original we should just stick with utf-8 - log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", opts.TreePath, opts.FromTreePath, encoding, err) + log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", file.TreePath, file.FromTreePath, file.Options.encoding, err) result = content } content = result } else { - log.Error("Unknown encoding: %s", encoding) + log.Error("Unknown encoding: %s", file.Options.encoding) } } // Reset the opts.Content to our adjusted content to ensure that LFS gets the correct content - opts.Content = content + file.Content = content 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{treePath}, + Filenames: []string{file.Options.treePath}, CachedOnly: true, }) if err != nil { - return nil, err + return err } - if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { + 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(strings.NewReader(opts.Content)) + pointer, err := lfs.GeneratePointer(strings.NewReader(file.Content)) if err != nil { - return nil, err + return err } - lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} + lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID} content = pointer.StringContent() } } + // Add the object to the database objectHash, err := t.HashObject(strings.NewReader(content)) if err != nil { - return nil, err + return err } // Add the object to the index - if executable { - if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil { - return nil, err + if file.Options.executable { + if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil { + return err } } else { - if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil { - return nil, err + if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil { + return err } } - // Now write the tree - treeHash, err := t.WriteTree() - if err != nil { - return nil, err - } - - // Now commit the tree - var commitHash string - 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) - } - if err != nil { - return nil, err - } - if lfsMetaObject != nil { // We have an LFS object - create it lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject) if err != nil { - return nil, err + return err } - contentStore := lfs.NewContentStore() exist, err := contentStore.Exists(lfsMetaObject.Pointer) if err != nil { - return nil, err + return err } if !exist { - if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(opts.Content)); err != nil { - if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsMetaObject.Oid); err2 != nil { - return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) + if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(file.Content)); err != nil { + if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil { + return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) } - return nil, err + return err } } } - // Then push this tree to NewBranch - if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { - log.Error("%T %v", err, err) - return nil, err - } - - commit, err := t.GetCommit(commitHash) - if err != nil { - return nil, err - } - - file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath) - if err != nil { - return nil, err - } - - if repo.IsEmpty { - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty") - } - - return file, nil + return nil } // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch -func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, treePath string) error { +func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error { protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) if err != nil { return err } if protectedBranch != nil { protectedBranch.Repo = repo - isUnprotectedFile := false - glob := protectedBranch.GetUnprotectedFilePatterns() - if len(glob) != 0 { - isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath) - } - if !protectedBranch.CanUserPush(ctx, doer) && !isUnprotectedFile { - return models.ErrUserCannotCommit{ - UserName: doer.LowerName, + globUnprotected := protectedBranch.GetUnprotectedFilePatterns() + globProtected := protectedBranch.GetProtectedFilePatterns() + canUserPush := protectedBranch.CanUserPush(ctx, doer) + for _, treePath := range treePaths { + isUnprotectedFile := false + if len(globUnprotected) != 0 { + isUnprotectedFile = protectedBranch.IsUnprotectedFile(globUnprotected, treePath) + } + if !canUserPush && !isUnprotectedFile { + return models.ErrUserCannotCommit{ + UserName: doer.LowerName, + } + } + if protectedBranch.IsProtectedFile(globProtected, treePath) { + return models.ErrFilePathProtected{ + Path: treePath, + } } } if protectedBranch.RequireSignedCommits { @@ -495,14 +588,6 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do } } } - patterns := protectedBranch.GetProtectedFilePatterns() - for _, pat := range patterns { - if pat.Match(strings.ToLower(treePath)) { - return models.ErrFilePathProtected{ - Path: treePath, - } - } - } } return nil } |