Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API endpoint for changing/creating/deleting multiple files #24887

Merged
merged 16 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions modules/structs/repo_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ func (o *UpdateFileOptions) Branch() string {
return o.FileOptions.BranchName
}

// ChangeFileOperation for creating, updating or deleting a file
type ChangeFileOperation struct {
// 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
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
Expand Down
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
198 changes: 167 additions & 31 deletions routers/api/v1/repo/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io"
"net/http"
"path"
"strings"
"time"

"code.gitea.io/gitea/models"
Expand Down Expand Up @@ -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/FileListResponse"
// "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
Expand Down Expand Up @@ -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{
Expand All @@ -482,13 +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 {
ctx.JSON(http.StatusCreated, fileResponse)
ctx.JSON(http.StatusCreated, filesResponse[0])
}
}

Expand Down Expand Up @@ -540,15 +635,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,
Expand All @@ -571,13 +670,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 {
ctx.JSON(http.StatusOK, fileResponse)
ctx.JSON(http.StatusOK, filesResponse[0])
}
}

Expand All @@ -600,21 +699,53 @@ 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.FileResponse, error) {
if !canWriteFiles(ctx, opts.OldBranch) {
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.Doer.ID,
RepoName: ctx.Repo.Repository.LowerName,
}
}

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, ", ")+"\n")
}
return message
lunny marked this conversation as resolved.
Show resolved Hide resolved
}

// DeleteFile Delete a file in a repository
Expand Down Expand Up @@ -670,12 +801,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,
Expand All @@ -698,10 +834,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
Expand All @@ -718,7 +854,7 @@ func DeleteFile(ctx *context.APIContext) {
}
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
} else {
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
ctx.JSON(http.StatusOK, filesResponse[0]) // FIXME on APIv2: return http.StatusNoContent
}
}

Expand Down
3 changes: 3 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ type swaggerParameterBodies struct {
// in:body
EditAttachmentOptions api.EditAttachmentOptions

// in:body
ChangeFilesOptions api.ChangeFilesOptions

// in:body
CreateFileOptions api.CreateFileOptions

Expand Down
7 changes: 7 additions & 0 deletions routers/api/v1/swagger/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ type swaggerFileResponse struct {
Body api.FileResponse `json:"body"`
}

// FileListResponse
// swagger:response FileListResponse
type swaggerFileListResponse struct {
// in: body
Body []api.FileResponse `json:"body"`
}

// ContentsResponse
// swagger:response ContentsResponse
type swaggerContentsResponse struct {
Expand Down
Loading