summaryrefslogtreecommitdiffstats
path: root/routers/api/v1
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 /routers/api/v1
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
Diffstat (limited to 'routers/api/v1')
-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
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 {