Skip to content

Commit 5821f3c

Browse files
committed
add multiple file api
1 parent 1ef0fda commit 5821f3c

File tree

13 files changed

+852
-606
lines changed

13 files changed

+852
-606
lines changed

modules/structs/repo_file.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ func (o *UpdateFileOptions) Branch() string {
6464
return o.FileOptions.BranchName
6565
}
6666

67+
// ChangeFileOperation for creating, updating or deleting a file
68+
type ChangeFileOperation struct {
69+
// required: true
70+
// enum: create,update,delete
71+
Operation string `json:"operation" binding:"Required"`
72+
// path to the existing or new file
73+
Path string `json:"path" binding:"MaxSize(500)"`
74+
// content must be base64 encoded
75+
// required: true
76+
Content string `json:"content"`
77+
// sha is the SHA for the file that already exists
78+
SHA string `json:"sha"`
79+
// old path of the file to move
80+
FromPath string `json:"from_path"`
81+
}
82+
83+
// ChangeFilesOptions options for creating, updating or deleting multiple files
84+
// 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)
85+
type ChangeFilesOptions struct {
86+
FileOptions
87+
Files []*ChangeFileOperation `json:"files"`
88+
}
89+
6790
// FileOptionInterface provides a unified interface for the different file options
6891
type FileOptionInterface interface {
6992
Branch() string

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,7 @@ func Routes(ctx gocontext.Context) *web.Route {
11731173
m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
11741174
m.Group("/contents", func() {
11751175
m.Get("", repo.GetContentsList)
1176+
m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
11761177
m.Get("/*", repo.GetContents)
11771178
m.Group("/*", func() {
11781179
m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)

routers/api/v1/repo/file.go

Lines changed: 136 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,96 @@ func canReadFiles(r *context.Repository) bool {
407407
return r.Permission.CanRead(unit.TypeCode)
408408
}
409409

410+
// ChangeFiles handles API call for creating or updating multiple files
411+
func ChangeFiles(ctx *context.APIContext) {
412+
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
413+
// ---
414+
// summary: Create or update multiple files in a repository
415+
// consumes:
416+
// - application/json
417+
// produces:
418+
// - application/json
419+
// parameters:
420+
// - name: owner
421+
// in: path
422+
// description: owner of the repo
423+
// type: string
424+
// required: true
425+
// - name: repo
426+
// in: path
427+
// description: name of the repo
428+
// type: string
429+
// required: true
430+
// - name: body
431+
// in: body
432+
// required: true
433+
// schema:
434+
// "$ref": "#/definitions/ChangeFilesOptions"
435+
// responses:
436+
// "201":
437+
// "$ref": "#/responses/FileListResponse"
438+
// "403":
439+
// "$ref": "#/responses/error"
440+
// "404":
441+
// "$ref": "#/responses/notFound"
442+
// "422":
443+
// "$ref": "#/responses/error"
444+
445+
apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
446+
447+
if apiOpts.BranchName == "" {
448+
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
449+
}
450+
451+
files := []*files_service.ChangeRepoFile{}
452+
for _, file := range apiOpts.Files {
453+
changeRepoFile := &files_service.ChangeRepoFile{
454+
Operation: file.Operation,
455+
TreePath: file.Path,
456+
FromTreePath: file.FromPath,
457+
Content: file.Content,
458+
SHA: file.SHA,
459+
}
460+
files = append(files, changeRepoFile)
461+
}
462+
463+
opts := &files_service.ChangeRepoFilesOptions{
464+
Files: files,
465+
Message: apiOpts.Message,
466+
OldBranch: apiOpts.BranchName,
467+
NewBranch: apiOpts.NewBranchName,
468+
Committer: &files_service.IdentityOptions{
469+
Name: apiOpts.Committer.Name,
470+
Email: apiOpts.Committer.Email,
471+
},
472+
Author: &files_service.IdentityOptions{
473+
Name: apiOpts.Author.Name,
474+
Email: apiOpts.Author.Email,
475+
},
476+
Dates: &files_service.CommitDateOptions{
477+
Author: apiOpts.Dates.Author,
478+
Committer: apiOpts.Dates.Committer,
479+
},
480+
Signoff: apiOpts.Signoff,
481+
}
482+
if opts.Dates.Author.IsZero() {
483+
opts.Dates.Author = time.Now()
484+
}
485+
if opts.Dates.Committer.IsZero() {
486+
opts.Dates.Committer = time.Now()
487+
}
488+
489+
if opts.Message == "" {
490+
opts.Message = "Upload files over API"
491+
}
492+
493+
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
494+
handleCreateOrUpdateFileError(ctx, err)
495+
} else {
496+
ctx.JSON(http.StatusCreated, filesResponse)
497+
}
498+
}
499+
410500
// CreateFile handles API call for creating a file
411501
func CreateFile(ctx *context.APIContext) {
412502
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
@@ -453,11 +543,15 @@ func CreateFile(ctx *context.APIContext) {
453543
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
454544
}
455545

456-
opts := &files_service.UpdateRepoFileOptions{
457-
Content: apiOpts.Content,
458-
IsNewFile: true,
546+
opts := &files_service.ChangeRepoFilesOptions{
547+
Files: []*files_service.ChangeRepoFile{
548+
{
549+
Operation: "create",
550+
TreePath: ctx.Params("*"),
551+
Content: apiOpts.Content,
552+
},
553+
},
459554
Message: apiOpts.Message,
460-
TreePath: ctx.Params("*"),
461555
OldBranch: apiOpts.BranchName,
462556
NewBranch: apiOpts.NewBranchName,
463557
Committer: &files_service.IdentityOptions{
@@ -482,13 +576,13 @@ func CreateFile(ctx *context.APIContext) {
482576
}
483577

484578
if opts.Message == "" {
485-
opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
579+
opts.Message = ctx.Tr("repo.editor.add", opts.Files[0].TreePath)
486580
}
487581

488-
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
582+
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
489583
handleCreateOrUpdateFileError(ctx, err)
490584
} else {
491-
ctx.JSON(http.StatusCreated, fileResponse)
585+
ctx.JSON(http.StatusCreated, filesResponse[0])
492586
}
493587
}
494588

@@ -540,15 +634,19 @@ func UpdateFile(ctx *context.APIContext) {
540634
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
541635
}
542636

543-
opts := &files_service.UpdateRepoFileOptions{
544-
Content: apiOpts.Content,
545-
SHA: apiOpts.SHA,
546-
IsNewFile: false,
547-
Message: apiOpts.Message,
548-
FromTreePath: apiOpts.FromPath,
549-
TreePath: ctx.Params("*"),
550-
OldBranch: apiOpts.BranchName,
551-
NewBranch: apiOpts.NewBranchName,
637+
opts := &files_service.ChangeRepoFilesOptions{
638+
Files: []*files_service.ChangeRepoFile{
639+
{
640+
Operation: "update",
641+
Content: apiOpts.Content,
642+
SHA: apiOpts.SHA,
643+
FromTreePath: apiOpts.FromPath,
644+
TreePath: ctx.Params("*"),
645+
},
646+
},
647+
Message: apiOpts.Message,
648+
OldBranch: apiOpts.BranchName,
649+
NewBranch: apiOpts.NewBranchName,
552650
Committer: &files_service.IdentityOptions{
553651
Name: apiOpts.Committer.Name,
554652
Email: apiOpts.Committer.Email,
@@ -571,13 +669,13 @@ func UpdateFile(ctx *context.APIContext) {
571669
}
572670

573671
if opts.Message == "" {
574-
opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
672+
opts.Message = ctx.Tr("repo.editor.update", opts.Files[0].TreePath)
575673
}
576674

577-
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
675+
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
578676
handleCreateOrUpdateFileError(ctx, err)
579677
} else {
580-
ctx.JSON(http.StatusOK, fileResponse)
678+
ctx.JSON(http.StatusOK, filesResponse[0])
581679
}
582680
}
583681

@@ -600,21 +698,23 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
600698
}
601699

602700
// Called from both CreateFile or UpdateFile to handle both
603-
func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
701+
func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) ([]*api.FileResponse, error) {
604702
if !canWriteFiles(ctx, opts.OldBranch) {
605703
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
606704
UserID: ctx.Doer.ID,
607705
RepoName: ctx.Repo.Repository.LowerName,
608706
}
609707
}
610708

611-
content, err := base64.StdEncoding.DecodeString(opts.Content)
612-
if err != nil {
613-
return nil, err
709+
for _, file := range opts.Files {
710+
content, err := base64.StdEncoding.DecodeString(file.Content)
711+
if err != nil {
712+
return nil, err
713+
}
714+
file.Content = string(content)
614715
}
615-
opts.Content = string(content)
616716

617-
return files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts)
717+
return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts)
618718
}
619719

620720
// DeleteFile Delete a file in a repository
@@ -670,12 +770,17 @@ func DeleteFile(ctx *context.APIContext) {
670770
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
671771
}
672772

673-
opts := &files_service.DeleteRepoFileOptions{
773+
opts := &files_service.ChangeRepoFilesOptions{
774+
Files: []*files_service.ChangeRepoFile{
775+
{
776+
Operation: "delete",
777+
SHA: apiOpts.SHA,
778+
TreePath: ctx.Params("*"),
779+
},
780+
},
674781
Message: apiOpts.Message,
675782
OldBranch: apiOpts.BranchName,
676783
NewBranch: apiOpts.NewBranchName,
677-
SHA: apiOpts.SHA,
678-
TreePath: ctx.Params("*"),
679784
Committer: &files_service.IdentityOptions{
680785
Name: apiOpts.Committer.Name,
681786
Email: apiOpts.Committer.Email,
@@ -698,10 +803,10 @@ func DeleteFile(ctx *context.APIContext) {
698803
}
699804

700805
if opts.Message == "" {
701-
opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
806+
opts.Message = ctx.Tr("repo.editor.delete", opts.Files[0].TreePath)
702807
}
703808

704-
if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
809+
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
705810
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
706811
ctx.Error(http.StatusNotFound, "DeleteFile", err)
707812
return
@@ -718,7 +823,7 @@ func DeleteFile(ctx *context.APIContext) {
718823
}
719824
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
720825
} else {
721-
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
826+
ctx.JSON(http.StatusOK, filesResponse[0]) // FIXME on APIv2: return http.StatusNoContent
722827
}
723828
}
724829

routers/api/v1/swagger/repo.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ type swaggerFileResponse struct {
296296
Body api.FileResponse `json:"body"`
297297
}
298298

299+
// FileListResponse
300+
// swagger:response FileListResponse
301+
type swaggerFileListResponse struct {
302+
Body []api.FileResponse `json:"body"`
303+
}
304+
299305
// ContentsResponse
300306
// swagger:response ContentsResponse
301307
type swaggerContentsResponse struct {

routers/web/repo/editor.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -272,18 +272,27 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
272272
message += "\n\n" + form.CommitMessage
273273
}
274274

275-
if _, err := files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UpdateRepoFileOptions{
275+
operation := "update"
276+
if isNewFile {
277+
operation = "create"
278+
}
279+
280+
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
276281
LastCommitID: form.LastCommit,
277282
OldBranch: ctx.Repo.BranchName,
278283
NewBranch: branchName,
279-
FromTreePath: ctx.Repo.TreePath,
280-
TreePath: form.TreePath,
281284
Message: message,
282-
Content: strings.ReplaceAll(form.Content, "\r", ""),
283-
IsNewFile: isNewFile,
284-
Signoff: form.Signoff,
285+
Files: []*files_service.ChangeRepoFile{
286+
{
287+
Operation: operation,
288+
FromTreePath: ctx.Repo.TreePath,
289+
TreePath: form.TreePath,
290+
Content: strings.ReplaceAll(form.Content, "\r", ""),
291+
},
292+
},
293+
Signoff: form.Signoff,
285294
}); err != nil {
286-
// This is where we handle all the errors thrown by files_service.CreateOrUpdateRepoFile
295+
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
287296
if git.IsErrNotExist(err) {
288297
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
289298
} else if git_model.IsErrLFSFileLocked(err) {
@@ -478,13 +487,18 @@ func DeleteFilePost(ctx *context.Context) {
478487
message += "\n\n" + form.CommitMessage
479488
}
480489

481-
if _, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.DeleteRepoFileOptions{
490+
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
482491
LastCommitID: form.LastCommit,
483492
OldBranch: ctx.Repo.BranchName,
484493
NewBranch: branchName,
485-
TreePath: ctx.Repo.TreePath,
486-
Message: message,
487-
Signoff: form.Signoff,
494+
Files: []*files_service.ChangeRepoFile{
495+
{
496+
Operation: "delete",
497+
TreePath: ctx.Repo.TreePath,
498+
},
499+
},
500+
Message: message,
501+
Signoff: form.Signoff,
488502
}); err != nil {
489503
// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
490504
if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {

0 commit comments

Comments
 (0)