summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenys Konovalov <privat@denyskon.de>2023-05-29 11:41:35 +0200
committerGitHub <noreply@github.com>2023-05-29 17:41:35 +0800
commit275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2 (patch)
tree4283f97bce56c7783e6c77c380cbe4ce06277720
parent245f2c08db34e535576633748fc143bb09097ca8 (diff)
downloadgitea-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
-rw-r--r--modules/structs/repo_file.go36
-rw-r--r--routers/api/v1/api.go1
-rw-r--r--routers/api/v1/repo/file.go195
-rw-r--r--routers/api/v1/swagger/options.go3
-rw-r--r--routers/api/v1/swagger/repo.go7
-rw-r--r--routers/web/repo/editor.go36
-rw-r--r--services/repository/files/delete.go204
-rw-r--r--services/repository/files/file.go30
-rw-r--r--services/repository/files/update.go413
-rw-r--r--templates/swagger/v1_json.tmpl161
-rw-r--r--tests/integration/api_repo_file_helpers.go18
-rw-r--r--tests/integration/api_repo_files_change_test.go309
-rw-r--r--tests/integration/pull_merge_test.go24
-rw-r--r--tests/integration/pull_update_test.go24
-rw-r--r--tests/integration/repofiles_change_test.go (renamed from tests/integration/repofiles_update_test.go)283
-rw-r--r--tests/integration/repofiles_delete_test.go201
16 files changed, 1238 insertions, 707 deletions
diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go
index 328d7e47c8..6ca0e1c101 100644
--- a/modules/structs/repo_file.go
+++ b/modules/structs/repo_file.go
@@ -64,6 +64,35 @@ func (o *UpdateFileOptions) Branch() string {
return o.FileOptions.BranchName
}
+// ChangeFileOperation for creating, updating or deleting a file
+type ChangeFileOperation struct {
+ // indicates what to do with the file
+ // required: true
+ // enum: create,update,delete
+ Operation string `json:"operation" binding:"Required"`
+ // path to the existing or new file
+ Path string `json:"path" binding:"MaxSize(500)"`
+ // content must be base64 encoded
+ // required: true
+ Content string `json:"content"`
+ // sha is the SHA for the file that already exists, required for update, delete
+ SHA string `json:"sha"`
+ // old path of the file to move
+ FromPath string `json:"from_path"`
+}
+
+// ChangeFilesOptions options for creating, updating or deleting multiple files
+// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
+type ChangeFilesOptions struct {
+ FileOptions
+ Files []*ChangeFileOperation `json:"files"`
+}
+
+// Branch returns branch name
+func (o *ChangeFilesOptions) Branch() string {
+ return o.FileOptions.BranchName
+}
+
// FileOptionInterface provides a unified interface for the different file options
type FileOptionInterface interface {
Branch() string
@@ -126,6 +155,13 @@ type FileResponse struct {
Verification *PayloadCommitVerification `json:"verification"`
}
+// FilesResponse contains information about multiple files from a repo
+type FilesResponse struct {
+ Files []*ContentsResponse `json:"files"`
+ Commit *FileCommitResponse `json:"commit"`
+ Verification *PayloadCommitVerification `json:"verification"`
+}
+
// FileDeleteResponse contains information about a repo's file that was deleted
type FileDeleteResponse struct {
Content interface{} `json:"content"` // to be set to nil
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index fccfc5792c..45e36e84fe 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1173,6 +1173,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
m.Group("/contents", func() {
m.Get("", repo.GetContentsList)
+ m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
m.Get("/*", repo.GetContents)
m.Group("/*", func() {
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 786407827c..ae0d31c2a6 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -12,6 +12,7 @@ import (
"io"
"net/http"
"path"
+ "strings"
"time"
"code.gitea.io/gitea/models"
@@ -407,6 +408,96 @@ func canReadFiles(r *context.Repository) bool {
return r.Permission.CanRead(unit.TypeCode)
}
+// ChangeFiles handles API call for creating or updating multiple files
+func ChangeFiles(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
+ // ---
+ // summary: Create or update multiple files in a repository
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // required: true
+ // schema:
+ // "$ref": "#/definitions/ChangeFilesOptions"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/FilesResponse"
+ // "403":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/error"
+
+ apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
+
+ if apiOpts.BranchName == "" {
+ apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
+ }
+
+ files := []*files_service.ChangeRepoFile{}
+ for _, file := range apiOpts.Files {
+ changeRepoFile := &files_service.ChangeRepoFile{
+ Operation: file.Operation,
+ TreePath: file.Path,
+ FromTreePath: file.FromPath,
+ Content: file.Content,
+ SHA: file.SHA,
+ }
+ files = append(files, changeRepoFile)
+ }
+
+ opts := &files_service.ChangeRepoFilesOptions{
+ Files: files,
+ Message: apiOpts.Message,
+ OldBranch: apiOpts.BranchName,
+ NewBranch: apiOpts.NewBranchName,
+ Committer: &files_service.IdentityOptions{
+ Name: apiOpts.Committer.Name,
+ Email: apiOpts.Committer.Email,
+ },
+ Author: &files_service.IdentityOptions{
+ Name: apiOpts.Author.Name,
+ Email: apiOpts.Author.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: apiOpts.Dates.Author,
+ Committer: apiOpts.Dates.Committer,
+ },
+ Signoff: apiOpts.Signoff,
+ }
+ if opts.Dates.Author.IsZero() {
+ opts.Dates.Author = time.Now()
+ }
+ if opts.Dates.Committer.IsZero() {
+ opts.Dates.Committer = time.Now()
+ }
+
+ if opts.Message == "" {
+ opts.Message = changeFilesCommitMessage(ctx, files)
+ }
+
+ if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
+ handleCreateOrUpdateFileError(ctx, err)
+ } else {
+ ctx.JSON(http.StatusCreated, filesResponse)
+ }
+}
+
// CreateFile handles API call for creating a file
func CreateFile(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
@@ -453,11 +544,15 @@ func CreateFile(ctx *context.APIContext) {
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
}
- opts := &files_service.UpdateRepoFileOptions{
- Content: apiOpts.Content,
- IsNewFile: true,
+ opts := &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ctx.Params("*"),
+ Content: apiOpts.Content,
+ },
+ },
Message: apiOpts.Message,
- TreePath: ctx.Params("*"),
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
@@ -482,12 +577,13 @@ func CreateFile(ctx *context.APIContext) {
}
if opts.Message == "" {
- opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
+ opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
- if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
+ if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
handleCreateOrUpdateFileError(ctx, err)
} else {
+ fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusCreated, fileResponse)
}
}
@@ -540,15 +636,19 @@ func UpdateFile(ctx *context.APIContext) {
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
}
- opts := &files_service.UpdateRepoFileOptions{
- Content: apiOpts.Content,
- SHA: apiOpts.SHA,
- IsNewFile: false,
- Message: apiOpts.Message,
- FromTreePath: apiOpts.FromPath,
- TreePath: ctx.Params("*"),
- OldBranch: apiOpts.BranchName,
- NewBranch: apiOpts.NewBranchName,
+ opts := &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "update",
+ Content: apiOpts.Content,
+ SHA: apiOpts.SHA,
+ FromTreePath: apiOpts.FromPath,
+ TreePath: ctx.Params("*"),
+ },
+ },
+ Message: apiOpts.Message,
+ OldBranch: apiOpts.BranchName,
+ NewBranch: apiOpts.NewBranchName,
Committer: &files_service.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
@@ -571,12 +671,13 @@ func UpdateFile(ctx *context.APIContext) {
}
if opts.Message == "" {
- opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
+ opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
- if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
+ if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
handleCreateOrUpdateFileError(ctx, err)
} else {
+ fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusOK, fileResponse)
}
}
@@ -600,7 +701,7 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
}
// Called from both CreateFile or UpdateFile to handle both
-func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
+func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) {
if !canWriteFiles(ctx, opts.OldBranch) {
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.Doer.ID,
@@ -608,13 +709,45 @@ func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoF
}
}
- content, err := base64.StdEncoding.DecodeString(opts.Content)
- if err != nil {
- return nil, err
+ for _, file := range opts.Files {
+ content, err := base64.StdEncoding.DecodeString(file.Content)
+ if err != nil {
+ return nil, err
+ }
+ file.Content = string(content)
}
- opts.Content = string(content)
- return files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts)
+ return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts)
+}
+
+// format commit message if empty
+func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string {
+ var (
+ createFiles []string
+ updateFiles []string
+ deleteFiles []string
+ )
+ for _, file := range files {
+ switch file.Operation {
+ case "create":
+ createFiles = append(createFiles, file.TreePath)
+ case "update":
+ updateFiles = append(updateFiles, file.TreePath)
+ case "delete":
+ deleteFiles = append(deleteFiles, file.TreePath)
+ }
+ }
+ message := ""
+ if len(createFiles) != 0 {
+ message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
+ }
+ if len(updateFiles) != 0 {
+ message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
+ }
+ if len(deleteFiles) != 0 {
+ message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
+ }
+ return strings.Trim(message, "\n")
}
// DeleteFile Delete a file in a repository
@@ -670,12 +803,17 @@ func DeleteFile(ctx *context.APIContext) {
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
}
- opts := &files_service.DeleteRepoFileOptions{
+ opts := &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "delete",
+ SHA: apiOpts.SHA,
+ TreePath: ctx.Params("*"),
+ },
+ },
Message: apiOpts.Message,
OldBranch: apiOpts.BranchName,
NewBranch: apiOpts.NewBranchName,
- SHA: apiOpts.SHA,
- TreePath: ctx.Params("*"),
Committer: &files_service.IdentityOptions{
Name: apiOpts.Committer.Name,
Email: apiOpts.Committer.Email,
@@ -698,10 +836,10 @@ func DeleteFile(ctx *context.APIContext) {
}
if opts.Message == "" {
- opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
+ opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
- if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
+ if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteFile", err)
return
@@ -718,6 +856,7 @@ func DeleteFile(ctx *context.APIContext) {
}
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
} else {
+ fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
}
}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 09bb1d18f3..353d32e214 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -117,6 +117,9 @@ type swaggerParameterBodies struct {
EditAttachmentOptions api.EditAttachmentOptions
// in:body
+ ChangeFilesOptions api.ChangeFilesOptions
+
+ // in:body
CreateFileOptions api.CreateFileOptions
// in:body
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 10056ac8cb..3e23aa4d5a 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -296,6 +296,13 @@ type swaggerFileResponse struct {
Body api.FileResponse `json:"body"`
}
+// FilesResponse
+// swagger:response FilesResponse
+type swaggerFilesResponse struct {
+ // in: body
+ Body api.FilesResponse `json:"body"`
+}
+
// ContentsResponse
// swagger:response ContentsResponse
type swaggerContentsResponse struct {
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index b94aa1b7ba..7433a0a56b 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -272,18 +272,27 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message += "\n\n" + form.CommitMessage
}
- if _, err := files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UpdateRepoFileOptions{
+ operation := "update"
+ if isNewFile {
+ operation = "create"
+ }
+
+ if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: branchName,
- FromTreePath: ctx.Repo.TreePath,
- TreePath: form.TreePath,
Message: message,
- Content: strings.ReplaceAll(form.Content, "\r", ""),
- IsNewFile: isNewFile,
- Signoff: form.Signoff,
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: operation,
+ FromTreePath: ctx.Repo.TreePath,
+ TreePath: form.TreePath,
+ Content: strings.ReplaceAll(form.Content, "\r", ""),
+ },
+ },
+ Signoff: form.Signoff,
}); err != nil {
- // This is where we handle all the errors thrown by files_service.CreateOrUpdateRepoFile
+ // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
if git.IsErrNotExist(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
} else if git_model.IsErrLFSFileLocked(err) {
@@ -478,13 +487,18 @@ func DeleteFilePost(ctx *context.Context) {
message += "\n\n" + form.CommitMessage
}
- if _, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.DeleteRepoFileOptions{
+ if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: form.LastCommit,
OldBranch: ctx.Repo.BranchName,
NewBranch: branchName,
- TreePath: ctx.Repo.TreePath,
- Message: message,
- Signoff: form.Signoff,
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "delete",
+ TreePath: ctx.Repo.TreePath,
+ },
+ },
+ Message: message,
+ Signoff: form.Signoff,
}); err != nil {
// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
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
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 15043e465f..75492ab631 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -4063,6 +4063,57 @@
"$ref": "#/responses/notFound"
}
}
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Create or update multiple files in a repository",
+ "operationId": "repoChangeFiles",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/ChangeFilesOptions"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/FilesResponse"
+ },
+ "403": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/error"
+ }
+ }
}
},
"/repos/{owner}/{repo}/contents/{filepath}": {
@@ -15891,6 +15942,90 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "ChangeFileOperation": {
+ "description": "ChangeFileOperation for creating, updating or deleting a file",
+ "type": "object",
+ "required": [
+ "operation",
+ "content"
+ ],
+ "properties": {
+ "content": {
+ "description": "content must be base64 encoded",
+ "type": "string",
+ "x-go-name": "Content"
+ },
+ "from_path": {
+ "description": "old path of the file to move",
+ "type": "string",
+ "x-go-name": "FromPath"
+ },
+ "operation": {
+ "description": "indicates what to do with the file",
+ "type": "string",
+ "enum": [
+ "create",
+ "update",
+ "delete"
+ ],
+ "x-go-name": "Operation"
+ },
+ "path": {
+ "description": "path to the existing or new file",
+ "type": "string",
+ "x-go-name": "Path"
+ },
+ "sha": {
+ "description": "sha is the SHA for the file that already exists, required for update, delete",
+ "type": "string",
+ "x-go-name": "SHA"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
+ "ChangeFilesOptions": {
+ "description": "ChangeFilesOptions options for creating, updating or deleting multiple files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)",
+ "type": "object",
+ "properties": {
+ "author": {
+ "$ref": "#/definitions/Identity"
+ },
+ "branch": {
+ "description": "branch (optional) to base this file from. if not given, the default branch is used",
+ "type": "string",
+ "x-go-name": "BranchName"
+ },
+ "committer": {
+ "$ref": "#/definitions/Identity"
+ },
+ "dates": {
+ "$ref": "#/definitions/CommitDateOptions"
+ },
+ "files": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ChangeFileOperation"
+ },
+ "x-go-name": "Files"
+ },
+ "message": {
+ "description": "message (optional) for the commit of this file. if not supplied, a default message will be used",
+ "type": "string",
+ "x-go-name": "Message"
+ },
+ "new_branch": {
+ "description": "new_branch (optional) will make a new branch from `branch` before creating the file",
+ "type": "string",
+ "x-go-name": "NewBranchName"
+ },
+ "signoff": {
+ "description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.",
+ "type": "boolean",
+ "x-go-name": "Signoff"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"ChangedFile": {
"description": "ChangedFile store information about files affected by the pull request",
"type": "object",
@@ -18326,6 +18461,26 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "FilesResponse": {
+ "description": "FilesResponse contains information about multiple files from a repo",
+ "type": "object",
+ "properties": {
+ "commit": {
+ "$ref": "#/definitions/FileCommitResponse"
+ },
+ "files": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ContentsResponse"
+ },
+ "x-go-name": "Files"
+ },
+ "verification": {
+ "$ref": "#/definitions/PayloadCommitVerification"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"GPGKey": {
"description": "GPGKey a user GPG key to sign commit and tag in repository",
"type": "object",
@@ -21996,6 +22151,12 @@
"$ref": "#/definitions/FileResponse"
}
},
+ "FilesResponse": {
+ "description": "FilesResponse",
+ "schema": {
+ "$ref": "#/definitions/FilesResponse"
+ }
+ },
"GPGKey": {
"description": "GPGKey",
"schema": {
diff --git a/tests/integration/api_repo_file_helpers.go b/tests/integration/api_repo_file_helpers.go
index d773bcd629..8e9b2bfecc 100644
--- a/tests/integration/api_repo_file_helpers.go
+++ b/tests/integration/api_repo_file_helpers.go
@@ -11,18 +11,22 @@ import (
files_service "code.gitea.io/gitea/services/repository/files"
)
-func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FileResponse, error) {
- opts := &files_service.UpdateRepoFileOptions{
+func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FilesResponse, error) {
+ opts := &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: treePath,
+ Content: content,
+ },
+ },
OldBranch: branchName,
- TreePath: treePath,
- Content: content,
- IsNewFile: true,
Author: nil,
Committer: nil,
}
- return files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, user, opts)
+ return files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts)
}
-func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FileResponse, error) {
+func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FilesResponse, error) {
return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file")
}
diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go
new file mode 100644
index 0000000000..38187ec5b9
--- /dev/null
+++ b/tests/integration/api_repo_files_change_test.go
@@ -0,0 +1,309 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ stdCtx "context"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func getChangeFilesOptions() *api.ChangeFilesOptions {
+ newContent := "This is new text"
+ updateContent := "This is updated text"
+ newContentEncoded := base64.StdEncoding.EncodeToString([]byte(newContent))
+ updateContentEncoded := base64.StdEncoding.EncodeToString([]byte(updateContent))
+ return &api.ChangeFilesOptions{
+ FileOptions: api.FileOptions{
+ BranchName: "master",
+ NewBranchName: "master",
+ Message: "My update of new/file.txt",
+ Author: api.Identity{
+ Name: "Anne Doe",
+ Email: "annedoe@example.com",
+ },
+ Committer: api.Identity{
+ Name: "John Doe",
+ Email: "johndoe@example.com",
+ },
+ },
+ Files: []*api.ChangeFileOperation{
+ {
+ Operation: "create",
+ Content: newContentEncoded,
+ },
+ {
+ Operation: "update",
+ Content: updateContentEncoded,
+ SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
+ },
+ {
+ Operation: "delete",
+ SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
+ },
+ },
+ }
+}
+
+func TestAPIChangeFiles(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
+ user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
+ repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
+ fileID := 0
+
+ // Get user2's token
+ session := loginUser(t, user2.Name)
+ token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+ // Get user4's token
+ session = loginUser(t, user4.Name)
+ token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+
+ // Test changing files in repo1 which user2 owns, try both with branch and empty branch
+ for _, branch := range [...]string{
+ "master", // Branch
+ "", // Empty branch
+ } {
+ fileID++
+ createTreePath := fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath := fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, updateTreePath)
+ createFile(user2, repo1, deleteTreePath)
+ changeFilesOptions := getChangeFilesOptions()
+ changeFilesOptions.BranchName = branch
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token2)
+ req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ resp := MakeRequest(t, req, http.StatusCreated)
+ gitRepo, _ := git.OpenRepository(stdCtx.Background(), repo1.RepoPath())
+ commitID, _ := gitRepo.GetBranchCommitID(changeFilesOptions.NewBranchName)
+ createLasCommit, _ := gitRepo.GetCommitByPath(createTreePath)
+ updateLastCommit, _ := gitRepo.GetCommitByPath(updateTreePath)
+ expectedCreateFileResponse := getExpectedFileResponseForCreate(fmt.Sprintf("%v/%v", user2.Name, repo1.Name), commitID, createTreePath, createLasCommit.ID.String())
+ expectedUpdateFileResponse := getExpectedFileResponseForUpdate(commitID, updateTreePath, updateLastCommit.ID.String())
+ var filesResponse api.FilesResponse
+ DecodeJSON(t, resp, &filesResponse)
+
+ // check create file
+ assert.EqualValues(t, expectedCreateFileResponse.Content, filesResponse.Files[0])
+
+ // check update file
+ assert.EqualValues(t, expectedUpdateFileResponse.Content, filesResponse.Files[1])
+
+ // test commit info
+ assert.EqualValues(t, expectedCreateFileResponse.Commit.SHA, filesResponse.Commit.SHA)
+ assert.EqualValues(t, expectedCreateFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedCreateFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedCreateFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
+ assert.EqualValues(t, expectedCreateFileResponse.Commit.Committer.Email, filesResponse.Commit.Committer.Email)
+ assert.EqualValues(t, expectedCreateFileResponse.Commit.Committer.Name, filesResponse.Commit.Committer.Name)
+
+ // test delete file
+ assert.Nil(t, filesResponse.Files[2])
+
+ gitRepo.Close()
+ }
+
+ // Test changing files in a new branch
+ changeFilesOptions := getChangeFilesOptions()
+ changeFilesOptions.BranchName = repo1.DefaultBranch
+ changeFilesOptions.NewBranchName = "new_branch"
+ fileID++
+ createTreePath := fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath := fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID)
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ createFile(user2, repo1, updateTreePath)
+ createFile(user2, repo1, deleteTreePath)
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token2)
+ req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ resp := MakeRequest(t, req, http.StatusCreated)
+ var filesResponse api.FilesResponse
+ DecodeJSON(t, resp, &filesResponse)
+ expectedCreateSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
+ expectedCreateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
+ expectedCreateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
+ expectedUpdateSHA := "08bd14b2e2852529157324de9c226b3364e76136"
+ expectedUpdateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID)
+ expectedUpdateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
+ assert.EqualValues(t, expectedCreateSHA, filesResponse.Files[0].SHA)
+ assert.EqualValues(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
+ assert.EqualValues(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
+ assert.EqualValues(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
+ assert.EqualValues(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
+ assert.EqualValues(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
+ assert.Nil(t, filesResponse.Files[2])
+
+ assert.EqualValues(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
+
+ // Test updating a file and renaming it
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.BranchName = repo1.DefaultBranch
+ fileID++
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, updateTreePath)
+ changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]}
+ changeFilesOptions.Files[0].FromPath = updateTreePath
+ changeFilesOptions.Files[0].Path = "rename/" + updateTreePath
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ resp = MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, &filesResponse)
+ expectedUpdateSHA = "08bd14b2e2852529157324de9c226b3364e76136"
+ expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID)
+ expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
+ assert.EqualValues(t, expectedUpdateSHA, filesResponse.Files[0].SHA)
+ assert.EqualValues(t, expectedUpdateHTMLURL, *filesResponse.Files[0].HTMLURL)
+ assert.EqualValues(t, expectedUpdateDownloadURL, *filesResponse.Files[0].DownloadURL)
+
+ // Test updating a file without a message
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Message = ""
+ changeFilesOptions.BranchName = repo1.DefaultBranch
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ createFile(user2, repo1, updateTreePath)
+ createFile(user2, repo1, deleteTreePath)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ resp = MakeRequest(t, req, http.StatusCreated)
+ DecodeJSON(t, resp, &filesResponse)
+ expectedMessage := fmt.Sprintf("Add %v\nUpdate %v\nDelete %v\n", createTreePath, updateTreePath, deleteTreePath)
+ assert.EqualValues(t, expectedMessage, filesResponse.Commit.Message)
+
+ // Test updating a file with the wrong SHA
+ fileID++
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ createFile(user2, repo1, updateTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]}
+ changeFilesOptions.Files[0].Path = updateTreePath
+ correctSHA := changeFilesOptions.Files[0].SHA
+ changeFilesOptions.Files[0].SHA = "badsha"
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
+ expectedAPIError := context.APIError{
+ Message: "sha does not match [given: " + changeFilesOptions.Files[0].SHA + ", expected: " + correctSHA + "]",
+ URL: setting.API.SwaggerURL,
+ }
+ var apiError context.APIError
+ DecodeJSON(t, resp, &apiError)
+ assert.Equal(t, expectedAPIError, apiError)
+
+ // Test creating a file in repo1 by user4 who does not have write access
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo16, updateTreePath)
+ createFile(user2, repo16, deleteTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo16.Name, token4)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ // Tests a repo with no token given so will fail
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo16, updateTreePath)
+ createFile(user2, repo16, deleteTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using access token for a private repo that the user of the token owns
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo16, updateTreePath)
+ createFile(user2, repo16, deleteTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo16.Name, token2)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // Test using org repo "user3/repo3" where user2 is a collaborator
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user3, repo3, updateTreePath)
+ createFile(user3, repo3, deleteTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user3.Name, repo3.Name, token2)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // Test using org repo "user3/repo3" with no user token
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user3, repo3, updateTreePath)
+ createFile(user3, repo3, deleteTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user3.Name, repo3.Name)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ // Test using repo "user2/repo1" where user4 is a NOT collaborator
+ fileID++
+ createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
+ updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
+ deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
+ createFile(user2, repo1, updateTreePath)
+ createFile(user2, repo1, deleteTreePath)
+ changeFilesOptions = getChangeFilesOptions()
+ changeFilesOptions.Files[0].Path = createTreePath
+ changeFilesOptions.Files[1].Path = updateTreePath
+ changeFilesOptions.Files[2].Path = deleteTreePath
+ url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token4)
+ req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions)
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+}
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index 9271f25e5f..f6a36f60af 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -367,22 +367,30 @@ func TestConflictChecking(t *testing.T) {
assert.NotEmpty(t, baseRepo)
// create a commit on new branch.
- _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
- TreePath: "important_file",
+ _, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "important_file",
+ Content: "Just a non-important file",
+ },
+ },
Message: "Add a important file",
- Content: "Just a non-important file",
- IsNewFile: true,
OldBranch: "main",
NewBranch: "important-secrets",
})
assert.NoError(t, err)
// create a commit on main branch.
- _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
- TreePath: "important_file",
+ _, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "important_file",
+ Content: "Not the same content :P",
+ },
+ },
Message: "Add a important file",
- Content: "Not the same content :P",
- IsNewFile: true,
OldBranch: "main",
NewBranch: "main",
})
diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go
index 1b66656518..b94731002f 100644
--- a/tests/integration/pull_update_test.go
+++ b/tests/integration/pull_update_test.go
@@ -101,11 +101,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
assert.NotEmpty(t, headRepo)
// create a commit on base Repo
- _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, actor, &files_service.UpdateRepoFileOptions{
- TreePath: "File_A",
+ _, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, actor, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "File_A",
+ Content: "File A",
+ },
+ },
Message: "Add File A",
- Content: "File A",
- IsNewFile: true,
OldBranch: "master",
NewBranch: "master",
Author: &files_service.IdentityOptions{
@@ -124,11 +128,15 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod
assert.NoError(t, err)
// create a commit on head Repo
- _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, headRepo, actor, &files_service.UpdateRepoFileOptions{
- TreePath: "File_B",
+ _, err = files_service.ChangeRepoFiles(git.DefaultContext, headRepo, actor, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "File_B",
+ Content: "File B",
+ },
+ },
Message: "Add File on PR branch",
- Content: "File B",
- IsNewFile: true,
OldBranch: "master",
NewBranch: "newBranch",
Author: &files_service.IdentityOptions{
diff --git a/tests/integration/repofiles_update_test.go b/tests/integration/repofiles_change_test.go
index 47b61c1eeb..a257b95a84 100644
--- a/tests/integration/repofiles_update_test.go
+++ b/tests/integration/repofiles_change_test.go
@@ -10,6 +10,7 @@ import (
"time"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -19,33 +20,90 @@ import (
"github.com/stretchr/testify/assert"
)
-func getCreateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
- return &files_service.UpdateRepoFileOptions{
+func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
+ return &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "new/file.txt",
+ Content: "This is a NEW file",
+ },
+ },
OldBranch: repo.DefaultBranch,
NewBranch: repo.DefaultBranch,
- TreePath: "new/file.txt",
Message: "Creates new/file.txt",
- Content: "This is a NEW file",
- IsNewFile: true,
Author: nil,
Committer: nil,
}
}
-func getUpdateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions {
- return &files_service.UpdateRepoFileOptions{
+func getUpdateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
+ return &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "update",
+ TreePath: "README.md",
+ SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+ Content: "This is UPDATED content for the README file",
+ },
+ },
OldBranch: repo.DefaultBranch,
NewBranch: repo.DefaultBranch,
- TreePath: "README.md",
Message: "Updates README.md",
- SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
- Content: "This is UPDATED content for the README file",
- IsNewFile: false,
Author: nil,
Committer: nil,
}
}
+func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions {
+ return &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "delete",
+ TreePath: "README.md",
+ SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+ },
+ },
+ LastCommitID: "",
+ OldBranch: repo.DefaultBranch,
+ NewBranch: repo.DefaultBranch,
+ Message: "Deletes README.md",
+ Author: &files_service.IdentityOptions{
+ Name: "Bob Smith",
+ Email: "bob@smith.com",
+ },
+ Committer: nil,
+ }
+}
+
+func getExpectedFileResponseForRepofilesDelete(u *url.URL) *api.FileResponse {
+ // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined
+ return &api.FileResponse{
+ Content: nil,
+ Commit: &api.FileCommitResponse{
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Bob Smith",
+ Email: "bob@smith.com",
+ },
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: "Bob Smith",
+ Email: "bob@smith.com",
+ },
+ },
+ Message: "Deletes README.md\n",
+ },
+ Verification: &api.PayloadCommitVerification{
+ Verified: false,
+ Reason: "gpg.error.not_signed_commit",
+ Signature: "",
+ Payload: "",
+ },
+ }
+}
+
func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse {
treePath := "new/file.txt"
encoding := "base64"
@@ -183,7 +241,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA
}
}
-func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
+func TestChangeRepoFilesForCreate(t *testing.T) {
// setup
onGiteaRun(t, func(t *testing.T, u *url.URL) {
ctx := test.MockContext(t, "user2/repo1")
@@ -196,10 +254,10 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
repo := ctx.Repo.Repository
doer := ctx.Doer
- opts := getCreateRepoFileOptions(repo)
+ opts := getCreateRepoFilesOptions(repo)
// test
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
// asserts
assert.NoError(t, err)
@@ -211,16 +269,16 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String())
assert.NotNil(t, expectedFileResponse)
if expectedFileResponse != nil {
- assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
- assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
- assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
- assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
- assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
}
})
}
-func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
+func TestChangeRepoFilesForUpdate(t *testing.T) {
// setup
onGiteaRun(t, func(t *testing.T, u *url.URL) {
ctx := test.MockContext(t, "user2/repo1")
@@ -233,10 +291,10 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
repo := ctx.Repo.Repository
doer := ctx.Doer
- opts := getUpdateRepoFileOptions(repo)
+ opts := getUpdateRepoFilesOptions(repo)
// test
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
// asserts
assert.NoError(t, err)
@@ -244,17 +302,17 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
defer gitRepo.Close()
commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
- lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
- expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
- assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
- assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
- assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
- assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
- assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
+ lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
+ expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
+ assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
})
}
-func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
+func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) {
// setup
onGiteaRun(t, func(t *testing.T, u *url.URL) {
ctx := test.MockContext(t, "user2/repo1")
@@ -267,12 +325,12 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
repo := ctx.Repo.Repository
doer := ctx.Doer
- opts := getUpdateRepoFileOptions(repo)
- opts.FromTreePath = "README.md"
- opts.TreePath = "README_new.md" // new file name, README_new.md
+ opts := getUpdateRepoFilesOptions(repo)
+ opts.Files[0].FromTreePath = "README.md"
+ opts.Files[0].TreePath = "README_new.md" // new file name, README_new.md
// test
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
// asserts
assert.NoError(t, err)
@@ -280,32 +338,32 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
defer gitRepo.Close()
commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
- lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
- expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
+ lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
+ expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
// assert that the old file no longer exists in the last commit of the branch
- fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath)
+ fromEntry, err := commit.GetTreeEntryByPath(opts.Files[0].FromTreePath)
switch err.(type) {
case git.ErrNotExist:
// correct, continue
default:
t.Fatalf("expected git.ErrNotExist, got:%v", err)
}
- toEntry, err := commit.GetTreeEntryByPath(opts.TreePath)
+ toEntry, err := commit.GetTreeEntryByPath(opts.Files[0].TreePath)
assert.NoError(t, err)
assert.Nil(t, fromEntry) // Should no longer exist here
assert.NotNil(t, toEntry) // Should exist here
// assert SHA has remained the same but paths use the new file name
- assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA)
- assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name)
- assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path)
- assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL)
- assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
- assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
+ assert.EqualValues(t, expectedFileResponse.Content.SHA, filesResponse.Files[0].SHA)
+ assert.EqualValues(t, expectedFileResponse.Content.Name, filesResponse.Files[0].Name)
+ assert.EqualValues(t, expectedFileResponse.Content.Path, filesResponse.Files[0].Path)
+ assert.EqualValues(t, expectedFileResponse.Content.URL, filesResponse.Files[0].URL)
+ assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA)
+ assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
})
}
// Test opts with branch names removed, should get same results as above test
-func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
+func TestChangeRepoFilesWithoutBranchNames(t *testing.T) {
// setup
onGiteaRun(t, func(t *testing.T, u *url.URL) {
ctx := test.MockContext(t, "user2/repo1")
@@ -318,12 +376,12 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
repo := ctx.Repo.Repository
doer := ctx.Doer
- opts := getUpdateRepoFileOptions(repo)
+ opts := getUpdateRepoFilesOptions(repo)
opts.OldBranch = ""
opts.NewBranch = ""
// test
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
// asserts
assert.NoError(t, err)
@@ -331,13 +389,86 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
defer gitRepo.Close()
commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch)
- lastCommit, _ := commit.GetCommitByPath(opts.TreePath)
- expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String())
- assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
+ lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath)
+ expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String())
+ assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0])
+ })
+}
+
+func TestChangeRepoFilesForDelete(t *testing.T) {
+ onGiteaRun(t, testDeleteRepoFiles)
+}
+
+func testDeleteRepoFiles(t *testing.T, u *url.URL) {
+ // setup
+ unittest.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getDeleteRepoFilesOptions(repo)
+
+ t.Run("Delete README.md file", func(t *testing.T) {
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.NoError(t, err)
+ expectedFileResponse := getExpectedFileResponseForRepofilesDelete(u)
+ assert.NotNil(t, filesResponse)
+ assert.Nil(t, filesResponse.Files[0])
+ assert.EqualValues(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity)
+ assert.EqualValues(t, expectedFileResponse.Verification, filesResponse.Verification)
+ })
+
+ t.Run("Verify README.md has been deleted", func(t *testing.T) {
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, filesResponse)
+ expectedError := "repository file does not exist [path: " + opts.Files[0].TreePath + "]"
+ assert.EqualError(t, err, expectedError)
})
}
-func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
+// Test opts with branch names removed, same results
+func TestChangeRepoFilesForDeleteWithoutBranchNames(t *testing.T) {
+ onGiteaRun(t, testDeleteRepoFilesWithoutBranchNames)
+}
+
+func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) {
+ // setup
+ unittest.PrepareTestEnv(t)
+ ctx := test.MockContext(t, "user2/repo1")
+ ctx.SetParams(":id", "1")
+ test.LoadRepo(t, ctx, 1)
+ test.LoadRepoCommit(t, ctx)
+ test.LoadUser(t, ctx, 2)
+ test.LoadGitRepo(t, ctx)
+ defer ctx.Repo.GitRepo.Close()
+
+ repo := ctx.Repo.Repository
+ doer := ctx.Doer
+ opts := getDeleteRepoFilesOptions(repo)
+ opts.OldBranch = ""
+ opts.NewBranch = ""
+
+ t.Run("Delete README.md without Branch Name", func(t *testing.T) {
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.NoError(t, err)
+ expectedFileResponse := getExpectedFileResponseForRepofilesDelete(u)
+ assert.NotNil(t, filesResponse)
+ assert.Nil(t, filesResponse.Files[0])
+ assert.EqualValues(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message)
+ assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity)
+ assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity)
+ assert.EqualValues(t, expectedFileResponse.Verification, filesResponse.Verification)
+ })
+}
+
+func TestChangeRepoFilesErrors(t *testing.T) {
// setup
onGiteaRun(t, func(t *testing.T, u *url.URL) {
ctx := test.MockContext(t, "user2/repo1")
@@ -352,63 +483,63 @@ func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
doer := ctx.Doer
t.Run("bad branch", func(t *testing.T) {
- opts := getUpdateRepoFileOptions(repo)
+ opts := getUpdateRepoFilesOptions(repo)
opts.OldBranch = "bad_branch"
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
assert.Error(t, err)
- assert.Nil(t, fileResponse)
+ assert.Nil(t, filesResponse)
expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
assert.EqualError(t, err, expectedError)
})
t.Run("bad SHA", func(t *testing.T) {
- opts := getUpdateRepoFileOptions(repo)
- origSHA := opts.SHA
- opts.SHA = "bad_sha"
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
+ opts := getUpdateRepoFilesOptions(repo)
+ origSHA := opts.Files[0].SHA
+ opts.Files[0].SHA = "bad_sha"
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, filesResponse)
assert.Error(t, err)
- expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
+ expectedError := "sha does not match [given: " + opts.Files[0].SHA + ", expected: " + origSHA + "]"
assert.EqualError(t, err, expectedError)
})
t.Run("new branch already exists", func(t *testing.T) {
- opts := getUpdateRepoFileOptions(repo)
+ opts := getUpdateRepoFilesOptions(repo)
opts.NewBranch = "develop"
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, filesResponse)
assert.Error(t, err)
expectedError := "branch already exists [name: " + opts.NewBranch + "]"
assert.EqualError(t, err, expectedError)
})
t.Run("treePath is empty:", func(t *testing.T) {
- opts := getUpdateRepoFileOptions(repo)
- opts.TreePath = ""
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
+ opts := getUpdateRepoFilesOptions(repo)
+ opts.Files[0].TreePath = ""
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, filesResponse)
assert.Error(t, err)
expectedError := "path contains a malformed path component [path: ]"
assert.EqualError(t, err, expectedError)
})
t.Run("treePath is a git directory:", func(t *testing.T) {
- opts := getUpdateRepoFileOptions(repo)
- opts.TreePath = ".git"
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
+ opts := getUpdateRepoFilesOptions(repo)
+ opts.Files[0].TreePath = ".git"
+ filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
+ assert.Nil(t, filesResponse)
assert.Error(t, err)
- expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
+ expectedError := "path contains a malformed path component [path: " + opts.Files[0].TreePath + "]"
assert.EqualError(t, err, expectedError)
})
t.Run("create file that already exists", func(t *testing.T) {
- opts := getCreateRepoFileOptions(repo)
- opts.TreePath = "README.md" // already exists
- fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts)
+ opts := getCreateRepoFilesOptions(repo)
+ opts.Files[0].TreePath = "README.md" // already exists
+ fileResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts)
assert.Nil(t, fileResponse)
assert.Error(t, err)
- expectedError := "repository file already exists [path: " + opts.TreePath + "]"
+ expectedError := "repository file already exists [path: " + opts.Files[0].TreePath + "]"
assert.EqualError(t, err, expectedError)
})
})
diff --git a/tests/integration/repofiles_delete_test.go b/tests/integration/repofiles_delete_test.go
deleted file mode 100644
index 6698b280bd..0000000000
--- a/tests/integration/repofiles_delete_test.go
+++ /dev/null
@@ -1,201 +0,0 @@
-// Copyright 2019 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package integration
-
-import (
- "net/url"
- "testing"
-
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/test"
- files_service "code.gitea.io/gitea/services/repository/files"
-
- "github.com/stretchr/testify/assert"
-)
-
-func getDeleteRepoFileOptions(repo *repo_model.Repository) *files_service.DeleteRepoFileOptions {
- return &files_service.DeleteRepoFileOptions{
- LastCommitID: "",
- OldBranch: repo.DefaultBranch,
- NewBranch: repo.DefaultBranch,
- TreePath: "README.md",
- Message: "Deletes README.md",
- SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
- Author: &files_service.IdentityOptions{
- Name: "Bob Smith",
- Email: "bob@smith.com",
- },
- Committer: nil,
- }
-}
-
-func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
- // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined
- return &api.FileResponse{
- Content: nil,
- Commit: &api.FileCommitResponse{
- Author: &api.CommitUser{
- Identity: api.Identity{
- Name: "Bob Smith",
- Email: "bob@smith.com",
- },
- },
- Committer: &api.CommitUser{
- Identity: api.Identity{
- Name: "Bob Smith",
- Email: "bob@smith.com",
- },
- },
- Message: "Deletes README.md\n",
- },
- Verification: &api.PayloadCommitVerification{
- Verified: false,
- Reason: "gpg.error.not_signed_commit",
- Signature: "",
- Payload: "",
- },
- }
-}
-
-func TestDeleteRepoFile(t *testing.T) {
- onGiteaRun(t, testDeleteRepoFile)
-}
-
-func testDeleteRepoFile(t *testing.T, u *url.URL) {
- // setup
- unittest.PrepareTestEnv(t)
- ctx := test.MockContext(t, "user2/repo1")
- ctx.SetParams(":id", "1")
- test.LoadRepo(t, ctx, 1)
- test.LoadRepoCommit(t, ctx)
- test.LoadUser(t, ctx, 2)
- test.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
- repo := ctx.Repo.Repository
- doer := ctx.Doer
- opts := getDeleteRepoFileOptions(repo)
-
- t.Run("Delete README.md file", func(t *testing.T) {
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.NoError(t, err)
- expectedFileResponse := getExpectedDeleteFileResponse(u)
- assert.NotNil(t, fileResponse)
- assert.Nil(t, fileResponse.Content)
- assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
- assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
- assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
- assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
- })
-
- t.Run("Verify README.md has been deleted", func(t *testing.T) {
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
- expectedError := "repository file does not exist [path: " + opts.TreePath + "]"
- assert.EqualError(t, err, expectedError)
- })
-}
-
-// Test opts with branch names removed, same results
-func TestDeleteRepoFileWithoutBranchNames(t *testing.T) {
- onGiteaRun(t, testDeleteRepoFileWithoutBranchNames)
-}
-
-func testDeleteRepoFileWithoutBranchNames(t *testing.T, u *url.URL) {
- // setup
- unittest.PrepareTestEnv(t)
- ctx := test.MockContext(t, "user2/repo1")
- ctx.SetParams(":id", "1")
- test.LoadRepo(t, ctx, 1)
- test.LoadRepoCommit(t, ctx)
- test.LoadUser(t, ctx, 2)
- test.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- doer := ctx.Doer
- opts := getDeleteRepoFileOptions(repo)
- opts.OldBranch = ""
- opts.NewBranch = ""
-
- t.Run("Delete README.md without Branch Name", func(t *testing.T) {
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.NoError(t, err)
- expectedFileResponse := getExpectedDeleteFileResponse(u)
- assert.NotNil(t, fileResponse)
- assert.Nil(t, fileResponse.Content)
- assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message)
- assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity)
- assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity)
- assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification)
- })
-}
-
-func TestDeleteRepoFileErrors(t *testing.T) {
- // setup
- unittest.PrepareTestEnv(t)
- ctx := test.MockContext(t, "user2/repo1")
- ctx.SetParams(":id", "1")
- test.LoadRepo(t, ctx, 1)
- test.LoadRepoCommit(t, ctx)
- test.LoadUser(t, ctx, 2)
- test.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- doer := ctx.Doer
-
- t.Run("Bad branch", func(t *testing.T) {
- opts := getDeleteRepoFileOptions(repo)
- opts.OldBranch = "bad_branch"
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Error(t, err)
- assert.Nil(t, fileResponse)
- expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
- assert.EqualError(t, err, expectedError)
- })
-
- t.Run("Bad SHA", func(t *testing.T) {
- opts := getDeleteRepoFileOptions(repo)
- origSHA := opts.SHA
- opts.SHA = "bad_sha"
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
- assert.Error(t, err)
- expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
- assert.EqualError(t, err, expectedError)
- })
-
- t.Run("New branch already exists", func(t *testing.T) {
- opts := getDeleteRepoFileOptions(repo)
- opts.NewBranch = "develop"
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
- assert.Error(t, err)
- expectedError := "branch already exists [name: " + opts.NewBranch + "]"
- assert.EqualError(t, err, expectedError)
- })
-
- t.Run("TreePath is empty:", func(t *testing.T) {
- opts := getDeleteRepoFileOptions(repo)
- opts.TreePath = ""
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
- assert.Error(t, err)
- expectedError := "path contains a malformed path component [path: ]"
- assert.EqualError(t, err, expectedError)
- })
-
- t.Run("TreePath is a git directory:", func(t *testing.T) {
- opts := getDeleteRepoFileOptions(repo)
- opts.TreePath = ".git"
- fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts)
- assert.Nil(t, fileResponse)
- assert.Error(t, err)
- expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
- assert.EqualError(t, err, expectedError)
- })
-}