diff options
author | Denys Konovalov <privat@denyskon.de> | 2023-05-29 11:41:35 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-29 17:41:35 +0800 |
commit | 275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2 (patch) | |
tree | 4283f97bce56c7783e6c77c380cbe4ce06277720 /routers/api/v1 | |
parent | 245f2c08db34e535576633748fc143bb09097ca8 (diff) | |
download | gitea-275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2.tar.gz gitea-275d4b7e3f4595206e5c4b1657d4f6d6969d9ce2.zip |
API endpoint for changing/creating/deleting multiple files (#24887)
This PR creates an API endpoint for creating/updating/deleting multiple
files in one API call similar to the solution provided by
[GitLab](https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions).
To archive this, the CreateOrUpdateRepoFile and DeleteRepoFIle functions
in files service are unified into one function supporting multiple files
and actions.
Resolves #14619
Diffstat (limited to 'routers/api/v1')
-rw-r--r-- | routers/api/v1/api.go | 1 | ||||
-rw-r--r-- | routers/api/v1/repo/file.go | 195 | ||||
-rw-r--r-- | routers/api/v1/swagger/options.go | 3 | ||||
-rw-r--r-- | routers/api/v1/swagger/repo.go | 7 |
4 files changed, 178 insertions, 28 deletions
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 { |