loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

Denys Konovalov and committed by
GitHub
275d4b7e 245f2c08

+1577 -1046
+36
modules/structs/repo_file.go
··· 64 64 return o.FileOptions.BranchName 65 65 } 66 66 67 + // ChangeFileOperation for creating, updating or deleting a file 68 + type ChangeFileOperation struct { 69 + // indicates what to do with the file 70 + // required: true 71 + // enum: create,update,delete 72 + Operation string `json:"operation" binding:"Required"` 73 + // path to the existing or new file 74 + Path string `json:"path" binding:"MaxSize(500)"` 75 + // content must be base64 encoded 76 + // required: true 77 + Content string `json:"content"` 78 + // sha is the SHA for the file that already exists, required for update, delete 79 + SHA string `json:"sha"` 80 + // old path of the file to move 81 + FromPath string `json:"from_path"` 82 + } 83 + 84 + // ChangeFilesOptions options for creating, updating or deleting multiple files 85 + // 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) 86 + type ChangeFilesOptions struct { 87 + FileOptions 88 + Files []*ChangeFileOperation `json:"files"` 89 + } 90 + 91 + // Branch returns branch name 92 + func (o *ChangeFilesOptions) Branch() string { 93 + return o.FileOptions.BranchName 94 + } 95 + 67 96 // FileOptionInterface provides a unified interface for the different file options 68 97 type FileOptionInterface interface { 69 98 Branch() string ··· 122 151 // FileResponse contains information about a repo's file 123 152 type FileResponse struct { 124 153 Content *ContentsResponse `json:"content"` 154 + Commit *FileCommitResponse `json:"commit"` 155 + Verification *PayloadCommitVerification `json:"verification"` 156 + } 157 + 158 + // FilesResponse contains information about multiple files from a repo 159 + type FilesResponse struct { 160 + Files []*ContentsResponse `json:"files"` 125 161 Commit *FileCommitResponse `json:"commit"` 126 162 Verification *PayloadCommitVerification `json:"verification"` 127 163 }
+1
routers/api/v1/api.go
··· 1173 1173 m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch) 1174 1174 m.Group("/contents", func() { 1175 1175 m.Get("", repo.GetContentsList) 1176 + m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles) 1176 1177 m.Get("/*", repo.GetContents) 1177 1178 m.Group("/*", func() { 1178 1179 m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
+167 -28
routers/api/v1/repo/file.go
··· 12 12 "io" 13 13 "net/http" 14 14 "path" 15 + "strings" 15 16 "time" 16 17 17 18 "code.gitea.io/gitea/models" ··· 407 408 return r.Permission.CanRead(unit.TypeCode) 408 409 } 409 410 411 + // ChangeFiles handles API call for creating or updating multiple files 412 + func ChangeFiles(ctx *context.APIContext) { 413 + // swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles 414 + // --- 415 + // summary: Create or update multiple files in a repository 416 + // consumes: 417 + // - application/json 418 + // produces: 419 + // - application/json 420 + // parameters: 421 + // - name: owner 422 + // in: path 423 + // description: owner of the repo 424 + // type: string 425 + // required: true 426 + // - name: repo 427 + // in: path 428 + // description: name of the repo 429 + // type: string 430 + // required: true 431 + // - name: body 432 + // in: body 433 + // required: true 434 + // schema: 435 + // "$ref": "#/definitions/ChangeFilesOptions" 436 + // responses: 437 + // "201": 438 + // "$ref": "#/responses/FilesResponse" 439 + // "403": 440 + // "$ref": "#/responses/error" 441 + // "404": 442 + // "$ref": "#/responses/notFound" 443 + // "422": 444 + // "$ref": "#/responses/error" 445 + 446 + apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions) 447 + 448 + if apiOpts.BranchName == "" { 449 + apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch 450 + } 451 + 452 + files := []*files_service.ChangeRepoFile{} 453 + for _, file := range apiOpts.Files { 454 + changeRepoFile := &files_service.ChangeRepoFile{ 455 + Operation: file.Operation, 456 + TreePath: file.Path, 457 + FromTreePath: file.FromPath, 458 + Content: file.Content, 459 + SHA: file.SHA, 460 + } 461 + files = append(files, changeRepoFile) 462 + } 463 + 464 + opts := &files_service.ChangeRepoFilesOptions{ 465 + Files: files, 466 + Message: apiOpts.Message, 467 + OldBranch: apiOpts.BranchName, 468 + NewBranch: apiOpts.NewBranchName, 469 + Committer: &files_service.IdentityOptions{ 470 + Name: apiOpts.Committer.Name, 471 + Email: apiOpts.Committer.Email, 472 + }, 473 + Author: &files_service.IdentityOptions{ 474 + Name: apiOpts.Author.Name, 475 + Email: apiOpts.Author.Email, 476 + }, 477 + Dates: &files_service.CommitDateOptions{ 478 + Author: apiOpts.Dates.Author, 479 + Committer: apiOpts.Dates.Committer, 480 + }, 481 + Signoff: apiOpts.Signoff, 482 + } 483 + if opts.Dates.Author.IsZero() { 484 + opts.Dates.Author = time.Now() 485 + } 486 + if opts.Dates.Committer.IsZero() { 487 + opts.Dates.Committer = time.Now() 488 + } 489 + 490 + if opts.Message == "" { 491 + opts.Message = changeFilesCommitMessage(ctx, files) 492 + } 493 + 494 + if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { 495 + handleCreateOrUpdateFileError(ctx, err) 496 + } else { 497 + ctx.JSON(http.StatusCreated, filesResponse) 498 + } 499 + } 500 + 410 501 // CreateFile handles API call for creating a file 411 502 func CreateFile(ctx *context.APIContext) { 412 503 // swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile ··· 453 544 apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch 454 545 } 455 546 456 - opts := &files_service.UpdateRepoFileOptions{ 457 - Content: apiOpts.Content, 458 - IsNewFile: true, 547 + opts := &files_service.ChangeRepoFilesOptions{ 548 + Files: []*files_service.ChangeRepoFile{ 549 + { 550 + Operation: "create", 551 + TreePath: ctx.Params("*"), 552 + Content: apiOpts.Content, 553 + }, 554 + }, 459 555 Message: apiOpts.Message, 460 - TreePath: ctx.Params("*"), 461 556 OldBranch: apiOpts.BranchName, 462 557 NewBranch: apiOpts.NewBranchName, 463 558 Committer: &files_service.IdentityOptions{ ··· 482 577 } 483 578 484 579 if opts.Message == "" { 485 - opts.Message = ctx.Tr("repo.editor.add", opts.TreePath) 580 + opts.Message = changeFilesCommitMessage(ctx, opts.Files) 486 581 } 487 582 488 - if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil { 583 + if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { 489 584 handleCreateOrUpdateFileError(ctx, err) 490 585 } else { 586 + fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) 491 587 ctx.JSON(http.StatusCreated, fileResponse) 492 588 } 493 589 } ··· 540 636 apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch 541 637 } 542 638 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, 639 + opts := &files_service.ChangeRepoFilesOptions{ 640 + Files: []*files_service.ChangeRepoFile{ 641 + { 642 + Operation: "update", 643 + Content: apiOpts.Content, 644 + SHA: apiOpts.SHA, 645 + FromTreePath: apiOpts.FromPath, 646 + TreePath: ctx.Params("*"), 647 + }, 648 + }, 649 + Message: apiOpts.Message, 650 + OldBranch: apiOpts.BranchName, 651 + NewBranch: apiOpts.NewBranchName, 552 652 Committer: &files_service.IdentityOptions{ 553 653 Name: apiOpts.Committer.Name, 554 654 Email: apiOpts.Committer.Email, ··· 571 671 } 572 672 573 673 if opts.Message == "" { 574 - opts.Message = ctx.Tr("repo.editor.update", opts.TreePath) 674 + opts.Message = changeFilesCommitMessage(ctx, opts.Files) 575 675 } 576 676 577 - if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil { 677 + if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { 578 678 handleCreateOrUpdateFileError(ctx, err) 579 679 } else { 680 + fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) 580 681 ctx.JSON(http.StatusOK, fileResponse) 581 682 } 582 683 } ··· 600 701 } 601 702 602 703 // Called from both CreateFile or UpdateFile to handle both 603 - func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) { 704 + func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) { 604 705 if !canWriteFiles(ctx, opts.OldBranch) { 605 706 return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{ 606 707 UserID: ctx.Doer.ID, ··· 608 709 } 609 710 } 610 711 611 - content, err := base64.StdEncoding.DecodeString(opts.Content) 612 - if err != nil { 613 - return nil, err 712 + for _, file := range opts.Files { 713 + content, err := base64.StdEncoding.DecodeString(file.Content) 714 + if err != nil { 715 + return nil, err 716 + } 717 + file.Content = string(content) 614 718 } 615 - opts.Content = string(content) 719 + 720 + return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts) 721 + } 616 722 617 - return files_service.CreateOrUpdateRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts) 723 + // format commit message if empty 724 + func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string { 725 + var ( 726 + createFiles []string 727 + updateFiles []string 728 + deleteFiles []string 729 + ) 730 + for _, file := range files { 731 + switch file.Operation { 732 + case "create": 733 + createFiles = append(createFiles, file.TreePath) 734 + case "update": 735 + updateFiles = append(updateFiles, file.TreePath) 736 + case "delete": 737 + deleteFiles = append(deleteFiles, file.TreePath) 738 + } 739 + } 740 + message := "" 741 + if len(createFiles) != 0 { 742 + message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n") 743 + } 744 + if len(updateFiles) != 0 { 745 + message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n") 746 + } 747 + if len(deleteFiles) != 0 { 748 + message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", ")) 749 + } 750 + return strings.Trim(message, "\n") 618 751 } 619 752 620 753 // DeleteFile Delete a file in a repository ··· 670 803 apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch 671 804 } 672 805 673 - opts := &files_service.DeleteRepoFileOptions{ 806 + opts := &files_service.ChangeRepoFilesOptions{ 807 + Files: []*files_service.ChangeRepoFile{ 808 + { 809 + Operation: "delete", 810 + SHA: apiOpts.SHA, 811 + TreePath: ctx.Params("*"), 812 + }, 813 + }, 674 814 Message: apiOpts.Message, 675 815 OldBranch: apiOpts.BranchName, 676 816 NewBranch: apiOpts.NewBranchName, 677 - SHA: apiOpts.SHA, 678 - TreePath: ctx.Params("*"), 679 817 Committer: &files_service.IdentityOptions{ 680 818 Name: apiOpts.Committer.Name, 681 819 Email: apiOpts.Committer.Email, ··· 698 836 } 699 837 700 838 if opts.Message == "" { 701 - opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath) 839 + opts.Message = changeFilesCommitMessage(ctx, opts.Files) 702 840 } 703 841 704 - if fileResponse, err := files_service.DeleteRepoFile(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { 842 + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { 705 843 if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { 706 844 ctx.Error(http.StatusNotFound, "DeleteFile", err) 707 845 return ··· 718 856 } 719 857 ctx.Error(http.StatusInternalServerError, "DeleteFile", err) 720 858 } else { 859 + fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) 721 860 ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent 722 861 } 723 862 }
+3
routers/api/v1/swagger/options.go
··· 117 117 EditAttachmentOptions api.EditAttachmentOptions 118 118 119 119 // in:body 120 + ChangeFilesOptions api.ChangeFilesOptions 121 + 122 + // in:body 120 123 CreateFileOptions api.CreateFileOptions 121 124 122 125 // in:body
+7
routers/api/v1/swagger/repo.go
··· 296 296 Body api.FileResponse `json:"body"` 297 297 } 298 298 299 + // FilesResponse 300 + // swagger:response FilesResponse 301 + type swaggerFilesResponse struct { 302 + // in: body 303 + Body api.FilesResponse `json:"body"` 304 + } 305 + 299 306 // ContentsResponse 300 307 // swagger:response ContentsResponse 301 308 type swaggerContentsResponse struct {
+25 -11
routers/web/repo/editor.go
··· 272 272 message += "\n\n" + form.CommitMessage 273 273 } 274 274 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{ 276 281 LastCommitID: form.LastCommit, 277 282 OldBranch: ctx.Repo.BranchName, 278 283 NewBranch: branchName, 279 - FromTreePath: ctx.Repo.TreePath, 280 - TreePath: form.TreePath, 281 284 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, 285 294 }); 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 287 296 if git.IsErrNotExist(err) { 288 297 ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form) 289 298 } else if git_model.IsErrLFSFileLocked(err) { ··· 478 487 message += "\n\n" + form.CommitMessage 479 488 } 480 489 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{ 482 491 LastCommitID: form.LastCommit, 483 492 OldBranch: ctx.Repo.BranchName, 484 493 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, 488 502 }); err != nil { 489 503 // This is where we handle all the errors thrown by repofiles.DeleteRepoFile 490 504 if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
-204
services/repository/files/delete.go
··· 1 - // Copyright 2019 The Gitea Authors. All rights reserved. 2 - // SPDX-License-Identifier: MIT 3 - 4 - package files 5 - 6 - import ( 7 - "context" 8 - "fmt" 9 - "strings" 10 - 11 - "code.gitea.io/gitea/models" 12 - repo_model "code.gitea.io/gitea/models/repo" 13 - user_model "code.gitea.io/gitea/models/user" 14 - "code.gitea.io/gitea/modules/git" 15 - api "code.gitea.io/gitea/modules/structs" 16 - ) 17 - 18 - // DeleteRepoFileOptions holds the repository delete file options 19 - type DeleteRepoFileOptions struct { 20 - LastCommitID string 21 - OldBranch string 22 - NewBranch string 23 - TreePath string 24 - Message string 25 - SHA string 26 - Author *IdentityOptions 27 - Committer *IdentityOptions 28 - Dates *CommitDateOptions 29 - Signoff bool 30 - } 31 - 32 - // DeleteRepoFile deletes a file in the given repository 33 - func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *DeleteRepoFileOptions) (*api.FileResponse, error) { 34 - // If no branch name is set, assume the repo's default branch 35 - if opts.OldBranch == "" { 36 - opts.OldBranch = repo.DefaultBranch 37 - } 38 - if opts.NewBranch == "" { 39 - opts.NewBranch = opts.OldBranch 40 - } 41 - 42 - gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) 43 - if err != nil { 44 - return nil, err 45 - } 46 - defer closer.Close() 47 - 48 - // oldBranch must exist for this operation 49 - if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil { 50 - return nil, err 51 - } 52 - 53 - // A NewBranch can be specified for the file to be created/updated in a new branch. 54 - // Check to make sure the branch does not already exist, otherwise we can't proceed. 55 - // If we aren't branching to a new branch, make sure user can commit to the given branch 56 - if opts.NewBranch != opts.OldBranch { 57 - newBranch, err := gitRepo.GetBranch(opts.NewBranch) 58 - if err != nil && !git.IsErrBranchNotExist(err) { 59 - return nil, err 60 - } 61 - if newBranch != nil { 62 - return nil, models.ErrBranchAlreadyExists{ 63 - BranchName: opts.NewBranch, 64 - } 65 - } 66 - } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, opts.TreePath); err != nil { 67 - return nil, err 68 - } 69 - 70 - // Check that the path given in opts.treeName is valid (not a git path) 71 - treePath := CleanUploadFileName(opts.TreePath) 72 - if treePath == "" { 73 - return nil, models.ErrFilenameInvalid{ 74 - Path: opts.TreePath, 75 - } 76 - } 77 - 78 - message := strings.TrimSpace(opts.Message) 79 - 80 - author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) 81 - 82 - t, err := NewTemporaryUploadRepository(ctx, repo) 83 - if err != nil { 84 - return nil, err 85 - } 86 - defer t.Close() 87 - if err := t.Clone(opts.OldBranch); err != nil { 88 - return nil, err 89 - } 90 - if err := t.SetDefaultIndex(); err != nil { 91 - return nil, err 92 - } 93 - 94 - // Get the commit of the original branch 95 - commit, err := t.GetBranchCommit(opts.OldBranch) 96 - if err != nil { 97 - return nil, err // Couldn't get a commit for the branch 98 - } 99 - 100 - // Assigned LastCommitID in opts if it hasn't been set 101 - if opts.LastCommitID == "" { 102 - opts.LastCommitID = commit.ID.String() 103 - } else { 104 - lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) 105 - if err != nil { 106 - return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %w", err) 107 - } 108 - opts.LastCommitID = lastCommitID.String() 109 - } 110 - 111 - // Get the files in the index 112 - filesInIndex, err := t.LsFiles(opts.TreePath) 113 - if err != nil { 114 - return nil, fmt.Errorf("DeleteRepoFile: %w", err) 115 - } 116 - 117 - // Find the file we want to delete in the index 118 - inFilelist := false 119 - for _, file := range filesInIndex { 120 - if file == opts.TreePath { 121 - inFilelist = true 122 - break 123 - } 124 - } 125 - if !inFilelist { 126 - return nil, models.ErrRepoFileDoesNotExist{ 127 - Path: opts.TreePath, 128 - } 129 - } 130 - 131 - // Get the entry of treePath and check if the SHA given is the same as the file 132 - entry, err := commit.GetTreeEntryByPath(treePath) 133 - if err != nil { 134 - return nil, err 135 - } 136 - if opts.SHA != "" { 137 - // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error 138 - if opts.SHA != entry.ID.String() { 139 - return nil, models.ErrSHADoesNotMatch{ 140 - Path: treePath, 141 - GivenSHA: opts.SHA, 142 - CurrentSHA: entry.ID.String(), 143 - } 144 - } 145 - } else if opts.LastCommitID != "" { 146 - // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw 147 - // an error, but only if we aren't creating a new branch. 148 - if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { 149 - // CommitIDs don't match, but we don't want to throw a ErrCommitIDDoesNotMatch unless 150 - // this specific file has been edited since opts.LastCommitID 151 - if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil { 152 - return nil, err 153 - } else if changed { 154 - return nil, models.ErrCommitIDDoesNotMatch{ 155 - GivenCommitID: opts.LastCommitID, 156 - CurrentCommitID: opts.LastCommitID, 157 - } 158 - } 159 - // The file wasn't modified, so we are good to delete it 160 - } 161 - } else { 162 - // When deleting a file, a lastCommitID or SHA needs to be given to make sure other commits haven't been 163 - // made. We throw an error if one wasn't provided. 164 - return nil, models.ErrSHAOrCommitIDNotProvided{} 165 - } 166 - 167 - // Remove the file from the index 168 - if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil { 169 - return nil, err 170 - } 171 - 172 - // Now write the tree 173 - treeHash, err := t.WriteTree() 174 - if err != nil { 175 - return nil, err 176 - } 177 - 178 - // Now commit the tree 179 - var commitHash string 180 - if opts.Dates != nil { 181 - commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) 182 - } else { 183 - commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) 184 - } 185 - if err != nil { 186 - return nil, err 187 - } 188 - 189 - // Then push this tree to NewBranch 190 - if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { 191 - return nil, err 192 - } 193 - 194 - commit, err = t.GetCommit(commitHash) 195 - if err != nil { 196 - return nil, err 197 - } 198 - 199 - file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath) 200 - if err != nil { 201 - return nil, err 202 - } 203 - return file, nil 204 - }
+30
services/repository/files/file.go
··· 17 17 "code.gitea.io/gitea/modules/util" 18 18 ) 19 19 20 + func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) { 21 + files := []*api.ContentsResponse{} 22 + for _, file := range treeNames { 23 + fileContents, _ := GetContents(ctx, repo, file, branch, false) // ok if fails, then will be nil 24 + files = append(files, fileContents) 25 + } 26 + fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil 27 + verification := GetPayloadCommitVerification(ctx, commit) 28 + filesResponse := &api.FilesResponse{ 29 + Files: files, 30 + Commit: fileCommitResponse, 31 + Verification: verification, 32 + } 33 + return filesResponse, nil 34 + } 35 + 20 36 // GetFileResponseFromCommit Constructs a FileResponse from a Commit object 21 37 func GetFileResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) { 22 38 fileContents, _ := GetContents(ctx, repo, treeName, branch, false) // ok if fails, then will be nil ··· 28 44 Verification: verification, 29 45 } 30 46 return fileResponse, nil 47 + } 48 + 49 + // constructs a FileResponse with the file at the index from FilesResponse 50 + func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index int) *api.FileResponse { 51 + content := &api.ContentsResponse{} 52 + if len(filesResponse.Files) > index { 53 + content = filesResponse.Files[index] 54 + } 55 + fileResponse := &api.FileResponse{ 56 + Content: content, 57 + Commit: filesResponse.Commit, 58 + Verification: filesResponse.Verification, 59 + } 60 + return fileResponse 31 61 } 32 62 33 63 // GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
+249 -164
services/repository/files/update.go
··· 41 41 Committer time.Time 42 42 } 43 43 44 - // UpdateRepoFileOptions holds the repository file update options 45 - type UpdateRepoFileOptions struct { 46 - LastCommitID string 47 - OldBranch string 48 - NewBranch string 44 + type ChangeRepoFile struct { 45 + Operation string 49 46 TreePath string 50 47 FromTreePath string 51 - Message string 52 48 Content string 53 49 SHA string 54 - IsNewFile bool 50 + Options *RepoFileOptions 51 + } 52 + 53 + // UpdateRepoFilesOptions holds the repository files update options 54 + type ChangeRepoFilesOptions struct { 55 + LastCommitID string 56 + OldBranch string 57 + NewBranch string 58 + Message string 59 + Files []*ChangeRepoFile 55 60 Author *IdentityOptions 56 61 Committer *IdentityOptions 57 62 Dates *CommitDateOptions 58 63 Signoff bool 59 64 } 60 65 66 + type RepoFileOptions struct { 67 + treePath string 68 + fromTreePath string 69 + encoding string 70 + bom bool 71 + executable bool 72 + } 73 + 61 74 func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (string, bool) { 62 75 reader, err := entry.Blob().DataAsync() 63 76 if err != nil { ··· 125 138 return encoding, false 126 139 } 127 140 128 - // CreateOrUpdateRepoFile adds or updates a file in the given repository 129 - func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UpdateRepoFileOptions) (*structs.FileResponse, error) { 141 + // ChangeRepoFiles adds, updates or removes multiple files in the given repository 142 + func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) { 130 143 // If no branch name is set, assume default branch 131 144 if opts.OldBranch == "" { 132 145 opts.OldBranch = repo.DefaultBranch ··· 146 159 return nil, err 147 160 } 148 161 162 + treePaths := []string{} 163 + for _, file := range opts.Files { 164 + // If FromTreePath is not set, set it to the opts.TreePath 165 + if file.TreePath != "" && file.FromTreePath == "" { 166 + file.FromTreePath = file.TreePath 167 + } 168 + 169 + // Check that the path given in opts.treePath is valid (not a git path) 170 + treePath := CleanUploadFileName(file.TreePath) 171 + if treePath == "" { 172 + return nil, models.ErrFilenameInvalid{ 173 + Path: file.TreePath, 174 + } 175 + } 176 + // If there is a fromTreePath (we are copying it), also clean it up 177 + fromTreePath := CleanUploadFileName(file.FromTreePath) 178 + if fromTreePath == "" && file.FromTreePath != "" { 179 + return nil, models.ErrFilenameInvalid{ 180 + Path: file.FromTreePath, 181 + } 182 + } 183 + 184 + file.Options = &RepoFileOptions{ 185 + treePath: treePath, 186 + fromTreePath: fromTreePath, 187 + encoding: "UTF-8", 188 + bom: false, 189 + executable: false, 190 + } 191 + treePaths = append(treePaths, treePath) 192 + } 193 + 149 194 // A NewBranch can be specified for the file to be created/updated in a new branch. 150 195 // Check to make sure the branch does not already exist, otherwise we can't proceed. 151 196 // If we aren't branching to a new branch, make sure user can commit to the given branch ··· 159 204 if err != nil && !git.IsErrBranchNotExist(err) { 160 205 return nil, err 161 206 } 162 - } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, opts.TreePath); err != nil { 207 + } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil { 163 208 return nil, err 164 209 } 165 210 166 - // If FromTreePath is not set, set it to the opts.TreePath 167 - if opts.TreePath != "" && opts.FromTreePath == "" { 168 - opts.FromTreePath = opts.TreePath 169 - } 170 - 171 - // Check that the path given in opts.treePath is valid (not a git path) 172 - treePath := CleanUploadFileName(opts.TreePath) 173 - if treePath == "" { 174 - return nil, models.ErrFilenameInvalid{ 175 - Path: opts.TreePath, 176 - } 177 - } 178 - // If there is a fromTreePath (we are copying it), also clean it up 179 - fromTreePath := CleanUploadFileName(opts.FromTreePath) 180 - if fromTreePath == "" && opts.FromTreePath != "" { 181 - return nil, models.ErrFilenameInvalid{ 182 - Path: opts.FromTreePath, 183 - } 184 - } 185 - 186 211 message := strings.TrimSpace(opts.Message) 187 212 188 213 author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) ··· 194 219 defer t.Close() 195 220 hasOldBranch := true 196 221 if err := t.Clone(opts.OldBranch); err != nil { 222 + for _, file := range opts.Files { 223 + if file.Operation == "delete" { 224 + return nil, err 225 + } 226 + } 197 227 if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { 198 228 return nil, err 199 229 } ··· 209 239 } 210 240 } 211 241 212 - encoding := "UTF-8" 213 - bom := false 214 - executable := false 242 + for _, file := range opts.Files { 243 + if file.Operation == "delete" { 244 + // Get the files in the index 245 + filesInIndex, err := t.LsFiles(file.TreePath) 246 + if err != nil { 247 + return nil, fmt.Errorf("DeleteRepoFile: %w", err) 248 + } 249 + 250 + // Find the file we want to delete in the index 251 + inFilelist := false 252 + for _, indexFile := range filesInIndex { 253 + if indexFile == file.TreePath { 254 + inFilelist = true 255 + break 256 + } 257 + } 258 + if !inFilelist { 259 + return nil, models.ErrRepoFileDoesNotExist{ 260 + Path: file.TreePath, 261 + } 262 + } 263 + } 264 + } 215 265 216 266 if hasOldBranch { 217 267 // Get the commit of the original branch ··· 232 282 233 283 } 234 284 235 - if !opts.IsNewFile { 236 - fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) 237 - if err != nil { 285 + for _, file := range opts.Files { 286 + if err := handleCheckErrors(file, commit, opts, repo); err != nil { 238 287 return nil, err 239 288 } 240 - if opts.SHA != "" { 241 - // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error 242 - if opts.SHA != fromEntry.ID.String() { 243 - return nil, models.ErrSHADoesNotMatch{ 244 - Path: treePath, 245 - GivenSHA: opts.SHA, 246 - CurrentSHA: fromEntry.ID.String(), 247 - } 289 + } 290 + } 291 + 292 + contentStore := lfs.NewContentStore() 293 + for _, file := range opts.Files { 294 + switch file.Operation { 295 + case "create", "update": 296 + if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil { 297 + return nil, err 298 + } 299 + case "delete": 300 + // Remove the file from the index 301 + if err := t.RemoveFilesFromIndex(file.TreePath); err != nil { 302 + return nil, err 303 + } 304 + default: 305 + return nil, fmt.Errorf("Invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath) 306 + } 307 + } 308 + 309 + // Now write the tree 310 + treeHash, err := t.WriteTree() 311 + if err != nil { 312 + return nil, err 313 + } 314 + 315 + // Now commit the tree 316 + var commitHash string 317 + if opts.Dates != nil { 318 + commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) 319 + } else { 320 + commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff) 321 + } 322 + if err != nil { 323 + return nil, err 324 + } 325 + 326 + // Then push this tree to NewBranch 327 + if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { 328 + log.Error("%T %v", err, err) 329 + return nil, err 330 + } 331 + 332 + commit, err := t.GetCommit(commitHash) 333 + if err != nil { 334 + return nil, err 335 + } 336 + 337 + filesReponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths) 338 + if err != nil { 339 + return nil, err 340 + } 341 + 342 + if repo.IsEmpty { 343 + _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty") 344 + } 345 + 346 + return filesReponse, nil 347 + } 348 + 349 + // handles the check for various issues for ChangeRepoFiles 350 + func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions, repo *repo_model.Repository) error { 351 + if file.Operation == "update" || file.Operation == "delete" { 352 + fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) 353 + if err != nil { 354 + return err 355 + } 356 + if file.SHA != "" { 357 + // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error 358 + if file.SHA != fromEntry.ID.String() { 359 + return models.ErrSHADoesNotMatch{ 360 + Path: file.Options.treePath, 361 + GivenSHA: file.SHA, 362 + CurrentSHA: fromEntry.ID.String(), 248 363 } 249 - } else if opts.LastCommitID != "" { 250 - // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw 251 - // an error, but only if we aren't creating a new branch. 252 - if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { 253 - if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil { 254 - return nil, err 255 - } else if changed { 256 - return nil, models.ErrCommitIDDoesNotMatch{ 257 - GivenCommitID: opts.LastCommitID, 258 - CurrentCommitID: opts.LastCommitID, 259 - } 364 + } 365 + } else if opts.LastCommitID != "" { 366 + // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw 367 + // an error, but only if we aren't creating a new branch. 368 + if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { 369 + if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil { 370 + return err 371 + } else if changed { 372 + return models.ErrCommitIDDoesNotMatch{ 373 + GivenCommitID: opts.LastCommitID, 374 + CurrentCommitID: opts.LastCommitID, 260 375 } 261 - // The file wasn't modified, so we are good to delete it 262 376 } 263 - } else { 264 - // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits 265 - // haven't been made. We throw an error if one wasn't provided. 266 - return nil, models.ErrSHAOrCommitIDNotProvided{} 377 + // The file wasn't modified, so we are good to delete it 267 378 } 268 - encoding, bom = detectEncodingAndBOM(fromEntry, repo) 269 - executable = fromEntry.IsExecutable() 379 + } else { 380 + // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits 381 + // haven't been made. We throw an error if one wasn't provided. 382 + return models.ErrSHAOrCommitIDNotProvided{} 270 383 } 271 - 384 + file.Options.encoding, file.Options.bom = detectEncodingAndBOM(fromEntry, repo) 385 + file.Options.executable = fromEntry.IsExecutable() 386 + } 387 + if file.Operation == "create" || file.Operation == "update" { 272 388 // For the path where this file will be created/updated, we need to make 273 389 // sure no parts of the path are existing files or links except for the last 274 390 // item in the path which is the file name, and that shouldn't exist IF it is 275 391 // a new file OR is being moved to a new path. 276 - treePathParts := strings.Split(treePath, "/") 392 + treePathParts := strings.Split(file.Options.treePath, "/") 277 393 subTreePath := "" 278 394 for index, part := range treePathParts { 279 395 subTreePath = path.Join(subTreePath, part) ··· 283 399 // Means there is no item with that name, so we're good 284 400 break 285 401 } 286 - return nil, err 402 + return err 287 403 } 288 404 if index < len(treePathParts)-1 { 289 405 if !entry.IsDir() { 290 - return nil, models.ErrFilePathInvalid{ 406 + return models.ErrFilePathInvalid{ 291 407 Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), 292 408 Path: subTreePath, 293 409 Name: part, ··· 295 411 } 296 412 } 297 413 } else if entry.IsLink() { 298 - return nil, models.ErrFilePathInvalid{ 414 + return models.ErrFilePathInvalid{ 299 415 Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), 300 416 Path: subTreePath, 301 417 Name: part, 302 418 Type: git.EntryModeSymlink, 303 419 } 304 420 } else if entry.IsDir() { 305 - return nil, models.ErrFilePathInvalid{ 421 + return models.ErrFilePathInvalid{ 306 422 Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath), 307 423 Path: subTreePath, 308 424 Name: part, 309 425 Type: git.EntryModeTree, 310 426 } 311 - } else if fromTreePath != treePath || opts.IsNewFile { 427 + } else if file.Options.fromTreePath != file.Options.treePath || file.Operation == "create" { 312 428 // The entry shouldn't exist if we are creating new file or moving to a new path 313 - return nil, models.ErrRepoFileAlreadyExists{ 314 - Path: treePath, 429 + return models.ErrRepoFileAlreadyExists{ 430 + Path: file.Options.treePath, 315 431 } 316 432 } 317 433 318 434 } 319 435 } 320 436 437 + return nil 438 + } 439 + 440 + // handle creating or updating a file for ChangeRepoFiles 441 + func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error { 321 442 // Get the two paths (might be the same if not moving) from the index if they exist 322 - filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath) 443 + filesInIndex, err := t.LsFiles(file.TreePath, file.FromTreePath) 323 444 if err != nil { 324 - return nil, fmt.Errorf("UpdateRepoFile: %w", err) 445 + return fmt.Errorf("UpdateRepoFile: %w", err) 325 446 } 326 447 // If is a new file (not updating) then the given path shouldn't exist 327 - if opts.IsNewFile { 328 - for _, file := range filesInIndex { 329 - if file == opts.TreePath { 330 - return nil, models.ErrRepoFileAlreadyExists{ 331 - Path: opts.TreePath, 448 + if file.Operation == "create" { 449 + for _, indexFile := range filesInIndex { 450 + if indexFile == file.TreePath { 451 + return models.ErrRepoFileAlreadyExists{ 452 + Path: file.TreePath, 332 453 } 333 454 } 334 455 } 335 456 } 336 457 337 458 // Remove the old path from the tree 338 - if fromTreePath != treePath && len(filesInIndex) > 0 { 339 - for _, file := range filesInIndex { 340 - if file == fromTreePath { 341 - if err := t.RemoveFilesFromIndex(opts.FromTreePath); err != nil { 342 - return nil, err 459 + if file.Options.fromTreePath != file.Options.treePath && len(filesInIndex) > 0 { 460 + for _, indexFile := range filesInIndex { 461 + if indexFile == file.Options.fromTreePath { 462 + if err := t.RemoveFilesFromIndex(file.FromTreePath); err != nil { 463 + return err 343 464 } 344 465 } 345 466 } 346 467 } 347 468 348 - content := opts.Content 349 - if bom { 469 + content := file.Content 470 + if file.Options.bom { 350 471 content = string(charset.UTF8BOM) + content 351 472 } 352 - if encoding != "UTF-8" { 353 - charsetEncoding, _ := stdcharset.Lookup(encoding) 473 + if file.Options.encoding != "UTF-8" { 474 + charsetEncoding, _ := stdcharset.Lookup(file.Options.encoding) 354 475 if charsetEncoding != nil { 355 476 result, _, err := transform.String(charsetEncoding.NewEncoder(), content) 356 477 if err != nil { 357 478 // Look if we can't encode back in to the original we should just stick with utf-8 358 - log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", opts.TreePath, opts.FromTreePath, encoding, err) 479 + log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", file.TreePath, file.FromTreePath, file.Options.encoding, err) 359 480 result = content 360 481 } 361 482 content = result 362 483 } else { 363 - log.Error("Unknown encoding: %s", encoding) 484 + log.Error("Unknown encoding: %s", file.Options.encoding) 364 485 } 365 486 } 366 487 // Reset the opts.Content to our adjusted content to ensure that LFS gets the correct content 367 - opts.Content = content 488 + file.Content = content 368 489 var lfsMetaObject *git_model.LFSMetaObject 369 490 370 491 if setting.LFS.StartServer && hasOldBranch { 371 492 // Check there is no way this can return multiple infos 372 493 filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ 373 494 Attributes: []string{"filter"}, 374 - Filenames: []string{treePath}, 495 + Filenames: []string{file.Options.treePath}, 375 496 CachedOnly: true, 376 497 }) 377 498 if err != nil { 378 - return nil, err 499 + return err 379 500 } 380 501 381 - if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { 502 + if filename2attribute2info[file.Options.treePath] != nil && filename2attribute2info[file.Options.treePath]["filter"] == "lfs" { 382 503 // OK so we are supposed to LFS this data! 383 - pointer, err := lfs.GeneratePointer(strings.NewReader(opts.Content)) 504 + pointer, err := lfs.GeneratePointer(strings.NewReader(file.Content)) 384 505 if err != nil { 385 - return nil, err 506 + return err 386 507 } 387 - lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID} 508 + lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID} 388 509 content = pointer.StringContent() 389 510 } 390 511 } 512 + 391 513 // Add the object to the database 392 514 objectHash, err := t.HashObject(strings.NewReader(content)) 393 515 if err != nil { 394 - return nil, err 516 + return err 395 517 } 396 518 397 519 // Add the object to the index 398 - if executable { 399 - if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil { 400 - return nil, err 520 + if file.Options.executable { 521 + if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil { 522 + return err 401 523 } 402 524 } else { 403 - if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil { 404 - return nil, err 525 + if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil { 526 + return err 405 527 } 406 528 } 407 529 408 - // Now write the tree 409 - treeHash, err := t.WriteTree() 410 - if err != nil { 411 - return nil, err 412 - } 413 - 414 - // Now commit the tree 415 - var commitHash string 416 - if opts.Dates != nil { 417 - commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) 418 - } else { 419 - commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff) 420 - } 421 - if err != nil { 422 - return nil, err 423 - } 424 - 425 530 if lfsMetaObject != nil { 426 531 // We have an LFS object - create it 427 532 lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject) 428 533 if err != nil { 429 - return nil, err 534 + return err 430 535 } 431 - contentStore := lfs.NewContentStore() 432 536 exist, err := contentStore.Exists(lfsMetaObject.Pointer) 433 537 if err != nil { 434 - return nil, err 538 + return err 435 539 } 436 540 if !exist { 437 - if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(opts.Content)); err != nil { 438 - if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsMetaObject.Oid); err2 != nil { 439 - return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) 541 + if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(file.Content)); err != nil { 542 + if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil { 543 + return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) 440 544 } 441 - return nil, err 545 + return err 442 546 } 443 547 } 444 548 } 445 549 446 - // Then push this tree to NewBranch 447 - if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { 448 - log.Error("%T %v", err, err) 449 - return nil, err 450 - } 451 - 452 - commit, err := t.GetCommit(commitHash) 453 - if err != nil { 454 - return nil, err 455 - } 456 - 457 - file, err := GetFileResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePath) 458 - if err != nil { 459 - return nil, err 460 - } 461 - 462 - if repo.IsEmpty { 463 - _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false}, "is_empty") 464 - } 465 - 466 - return file, nil 550 + return nil 467 551 } 468 552 469 553 // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch 470 - func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, treePath string) error { 554 + func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error { 471 555 protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) 472 556 if err != nil { 473 557 return err 474 558 } 475 559 if protectedBranch != nil { 476 560 protectedBranch.Repo = repo 477 - isUnprotectedFile := false 478 - glob := protectedBranch.GetUnprotectedFilePatterns() 479 - if len(glob) != 0 { 480 - isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath) 481 - } 482 - if !protectedBranch.CanUserPush(ctx, doer) && !isUnprotectedFile { 483 - return models.ErrUserCannotCommit{ 484 - UserName: doer.LowerName, 561 + globUnprotected := protectedBranch.GetUnprotectedFilePatterns() 562 + globProtected := protectedBranch.GetProtectedFilePatterns() 563 + canUserPush := protectedBranch.CanUserPush(ctx, doer) 564 + for _, treePath := range treePaths { 565 + isUnprotectedFile := false 566 + if len(globUnprotected) != 0 { 567 + isUnprotectedFile = protectedBranch.IsUnprotectedFile(globUnprotected, treePath) 568 + } 569 + if !canUserPush && !isUnprotectedFile { 570 + return models.ErrUserCannotCommit{ 571 + UserName: doer.LowerName, 572 + } 573 + } 574 + if protectedBranch.IsProtectedFile(globProtected, treePath) { 575 + return models.ErrFilePathProtected{ 576 + Path: treePath, 577 + } 485 578 } 486 579 } 487 580 if protectedBranch.RequireSignedCommits { ··· 492 585 } 493 586 return models.ErrUserCannotCommit{ 494 587 UserName: doer.LowerName, 495 - } 496 - } 497 - } 498 - patterns := protectedBranch.GetProtectedFilePatterns() 499 - for _, pat := range patterns { 500 - if pat.Match(strings.ToLower(treePath)) { 501 - return models.ErrFilePathProtected{ 502 - Path: treePath, 503 588 } 504 589 } 505 590 }
+161
templates/swagger/v1_json.tmpl
··· 4063 4063 "$ref": "#/responses/notFound" 4064 4064 } 4065 4065 } 4066 + }, 4067 + "post": { 4068 + "consumes": [ 4069 + "application/json" 4070 + ], 4071 + "produces": [ 4072 + "application/json" 4073 + ], 4074 + "tags": [ 4075 + "repository" 4076 + ], 4077 + "summary": "Create or update multiple files in a repository", 4078 + "operationId": "repoChangeFiles", 4079 + "parameters": [ 4080 + { 4081 + "type": "string", 4082 + "description": "owner of the repo", 4083 + "name": "owner", 4084 + "in": "path", 4085 + "required": true 4086 + }, 4087 + { 4088 + "type": "string", 4089 + "description": "name of the repo", 4090 + "name": "repo", 4091 + "in": "path", 4092 + "required": true 4093 + }, 4094 + { 4095 + "name": "body", 4096 + "in": "body", 4097 + "required": true, 4098 + "schema": { 4099 + "$ref": "#/definitions/ChangeFilesOptions" 4100 + } 4101 + } 4102 + ], 4103 + "responses": { 4104 + "201": { 4105 + "$ref": "#/responses/FilesResponse" 4106 + }, 4107 + "403": { 4108 + "$ref": "#/responses/error" 4109 + }, 4110 + "404": { 4111 + "$ref": "#/responses/notFound" 4112 + }, 4113 + "422": { 4114 + "$ref": "#/responses/error" 4115 + } 4116 + } 4066 4117 } 4067 4118 }, 4068 4119 "/repos/{owner}/{repo}/contents/{filepath}": { ··· 15891 15942 }, 15892 15943 "x-go-package": "code.gitea.io/gitea/modules/structs" 15893 15944 }, 15945 + "ChangeFileOperation": { 15946 + "description": "ChangeFileOperation for creating, updating or deleting a file", 15947 + "type": "object", 15948 + "required": [ 15949 + "operation", 15950 + "content" 15951 + ], 15952 + "properties": { 15953 + "content": { 15954 + "description": "content must be base64 encoded", 15955 + "type": "string", 15956 + "x-go-name": "Content" 15957 + }, 15958 + "from_path": { 15959 + "description": "old path of the file to move", 15960 + "type": "string", 15961 + "x-go-name": "FromPath" 15962 + }, 15963 + "operation": { 15964 + "description": "indicates what to do with the file", 15965 + "type": "string", 15966 + "enum": [ 15967 + "create", 15968 + "update", 15969 + "delete" 15970 + ], 15971 + "x-go-name": "Operation" 15972 + }, 15973 + "path": { 15974 + "description": "path to the existing or new file", 15975 + "type": "string", 15976 + "x-go-name": "Path" 15977 + }, 15978 + "sha": { 15979 + "description": "sha is the SHA for the file that already exists, required for update, delete", 15980 + "type": "string", 15981 + "x-go-name": "SHA" 15982 + } 15983 + }, 15984 + "x-go-package": "code.gitea.io/gitea/modules/structs" 15985 + }, 15986 + "ChangeFilesOptions": { 15987 + "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)", 15988 + "type": "object", 15989 + "properties": { 15990 + "author": { 15991 + "$ref": "#/definitions/Identity" 15992 + }, 15993 + "branch": { 15994 + "description": "branch (optional) to base this file from. if not given, the default branch is used", 15995 + "type": "string", 15996 + "x-go-name": "BranchName" 15997 + }, 15998 + "committer": { 15999 + "$ref": "#/definitions/Identity" 16000 + }, 16001 + "dates": { 16002 + "$ref": "#/definitions/CommitDateOptions" 16003 + }, 16004 + "files": { 16005 + "type": "array", 16006 + "items": { 16007 + "$ref": "#/definitions/ChangeFileOperation" 16008 + }, 16009 + "x-go-name": "Files" 16010 + }, 16011 + "message": { 16012 + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", 16013 + "type": "string", 16014 + "x-go-name": "Message" 16015 + }, 16016 + "new_branch": { 16017 + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", 16018 + "type": "string", 16019 + "x-go-name": "NewBranchName" 16020 + }, 16021 + "signoff": { 16022 + "description": "Add a Signed-off-by trailer by the committer at the end of the commit log message.", 16023 + "type": "boolean", 16024 + "x-go-name": "Signoff" 16025 + } 16026 + }, 16027 + "x-go-package": "code.gitea.io/gitea/modules/structs" 16028 + }, 15894 16029 "ChangedFile": { 15895 16030 "description": "ChangedFile store information about files affected by the pull request", 15896 16031 "type": "object", ··· 18319 18454 }, 18320 18455 "content": { 18321 18456 "$ref": "#/definitions/ContentsResponse" 18457 + }, 18458 + "verification": { 18459 + "$ref": "#/definitions/PayloadCommitVerification" 18460 + } 18461 + }, 18462 + "x-go-package": "code.gitea.io/gitea/modules/structs" 18463 + }, 18464 + "FilesResponse": { 18465 + "description": "FilesResponse contains information about multiple files from a repo", 18466 + "type": "object", 18467 + "properties": { 18468 + "commit": { 18469 + "$ref": "#/definitions/FileCommitResponse" 18470 + }, 18471 + "files": { 18472 + "type": "array", 18473 + "items": { 18474 + "$ref": "#/definitions/ContentsResponse" 18475 + }, 18476 + "x-go-name": "Files" 18322 18477 }, 18323 18478 "verification": { 18324 18479 "$ref": "#/definitions/PayloadCommitVerification" ··· 21994 22149 "description": "FileResponse", 21995 22150 "schema": { 21996 22151 "$ref": "#/definitions/FileResponse" 22152 + } 22153 + }, 22154 + "FilesResponse": { 22155 + "description": "FilesResponse", 22156 + "schema": { 22157 + "$ref": "#/definitions/FilesResponse" 21997 22158 } 21998 22159 }, 21999 22160 "GPGKey": {
+11 -7
tests/integration/api_repo_file_helpers.go
··· 11 11 files_service "code.gitea.io/gitea/services/repository/files" 12 12 ) 13 13 14 - func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FileResponse, error) { 15 - opts := &files_service.UpdateRepoFileOptions{ 14 + func createFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) (*api.FilesResponse, error) { 15 + opts := &files_service.ChangeRepoFilesOptions{ 16 + Files: []*files_service.ChangeRepoFile{ 17 + { 18 + Operation: "create", 19 + TreePath: treePath, 20 + Content: content, 21 + }, 22 + }, 16 23 OldBranch: branchName, 17 - TreePath: treePath, 18 - Content: content, 19 - IsNewFile: true, 20 24 Author: nil, 21 25 Committer: nil, 22 26 } 23 - return files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, user, opts) 27 + return files_service.ChangeRepoFiles(git.DefaultContext, repo, user, opts) 24 28 } 25 29 26 - func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FileResponse, error) { 30 + func createFile(user *user_model.User, repo *repo_model.Repository, treePath string) (*api.FilesResponse, error) { 27 31 return createFileInBranch(user, repo, treePath, repo.DefaultBranch, "This is a NEW file") 28 32 }
+309
tests/integration/api_repo_files_change_test.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + stdCtx "context" 8 + "encoding/base64" 9 + "fmt" 10 + "net/http" 11 + "net/url" 12 + "testing" 13 + 14 + auth_model "code.gitea.io/gitea/models/auth" 15 + repo_model "code.gitea.io/gitea/models/repo" 16 + "code.gitea.io/gitea/models/unittest" 17 + user_model "code.gitea.io/gitea/models/user" 18 + "code.gitea.io/gitea/modules/context" 19 + "code.gitea.io/gitea/modules/git" 20 + "code.gitea.io/gitea/modules/setting" 21 + api "code.gitea.io/gitea/modules/structs" 22 + 23 + "github.com/stretchr/testify/assert" 24 + ) 25 + 26 + func getChangeFilesOptions() *api.ChangeFilesOptions { 27 + newContent := "This is new text" 28 + updateContent := "This is updated text" 29 + newContentEncoded := base64.StdEncoding.EncodeToString([]byte(newContent)) 30 + updateContentEncoded := base64.StdEncoding.EncodeToString([]byte(updateContent)) 31 + return &api.ChangeFilesOptions{ 32 + FileOptions: api.FileOptions{ 33 + BranchName: "master", 34 + NewBranchName: "master", 35 + Message: "My update of new/file.txt", 36 + Author: api.Identity{ 37 + Name: "Anne Doe", 38 + Email: "annedoe@example.com", 39 + }, 40 + Committer: api.Identity{ 41 + Name: "John Doe", 42 + Email: "johndoe@example.com", 43 + }, 44 + }, 45 + Files: []*api.ChangeFileOperation{ 46 + { 47 + Operation: "create", 48 + Content: newContentEncoded, 49 + }, 50 + { 51 + Operation: "update", 52 + Content: updateContentEncoded, 53 + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", 54 + }, 55 + { 56 + Operation: "delete", 57 + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", 58 + }, 59 + }, 60 + } 61 + } 62 + 63 + func TestAPIChangeFiles(t *testing.T) { 64 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 65 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16 66 + user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org 67 + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos 68 + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo 69 + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo 70 + repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo 71 + fileID := 0 72 + 73 + // Get user2's token 74 + session := loginUser(t, user2.Name) 75 + token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo) 76 + // Get user4's token 77 + session = loginUser(t, user4.Name) 78 + token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo) 79 + 80 + // Test changing files in repo1 which user2 owns, try both with branch and empty branch 81 + for _, branch := range [...]string{ 82 + "master", // Branch 83 + "", // Empty branch 84 + } { 85 + fileID++ 86 + createTreePath := fmt.Sprintf("new/file%d.txt", fileID) 87 + updateTreePath := fmt.Sprintf("update/file%d.txt", fileID) 88 + deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID) 89 + createFile(user2, repo1, updateTreePath) 90 + createFile(user2, repo1, deleteTreePath) 91 + changeFilesOptions := getChangeFilesOptions() 92 + changeFilesOptions.BranchName = branch 93 + changeFilesOptions.Files[0].Path = createTreePath 94 + changeFilesOptions.Files[1].Path = updateTreePath 95 + changeFilesOptions.Files[2].Path = deleteTreePath 96 + url := fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token2) 97 + req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 98 + resp := MakeRequest(t, req, http.StatusCreated) 99 + gitRepo, _ := git.OpenRepository(stdCtx.Background(), repo1.RepoPath()) 100 + commitID, _ := gitRepo.GetBranchCommitID(changeFilesOptions.NewBranchName) 101 + createLasCommit, _ := gitRepo.GetCommitByPath(createTreePath) 102 + updateLastCommit, _ := gitRepo.GetCommitByPath(updateTreePath) 103 + expectedCreateFileResponse := getExpectedFileResponseForCreate(fmt.Sprintf("%v/%v", user2.Name, repo1.Name), commitID, createTreePath, createLasCommit.ID.String()) 104 + expectedUpdateFileResponse := getExpectedFileResponseForUpdate(commitID, updateTreePath, updateLastCommit.ID.String()) 105 + var filesResponse api.FilesResponse 106 + DecodeJSON(t, resp, &filesResponse) 107 + 108 + // check create file 109 + assert.EqualValues(t, expectedCreateFileResponse.Content, filesResponse.Files[0]) 110 + 111 + // check update file 112 + assert.EqualValues(t, expectedUpdateFileResponse.Content, filesResponse.Files[1]) 113 + 114 + // test commit info 115 + assert.EqualValues(t, expectedCreateFileResponse.Commit.SHA, filesResponse.Commit.SHA) 116 + assert.EqualValues(t, expectedCreateFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) 117 + assert.EqualValues(t, expectedCreateFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email) 118 + assert.EqualValues(t, expectedCreateFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name) 119 + assert.EqualValues(t, expectedCreateFileResponse.Commit.Committer.Email, filesResponse.Commit.Committer.Email) 120 + assert.EqualValues(t, expectedCreateFileResponse.Commit.Committer.Name, filesResponse.Commit.Committer.Name) 121 + 122 + // test delete file 123 + assert.Nil(t, filesResponse.Files[2]) 124 + 125 + gitRepo.Close() 126 + } 127 + 128 + // Test changing files in a new branch 129 + changeFilesOptions := getChangeFilesOptions() 130 + changeFilesOptions.BranchName = repo1.DefaultBranch 131 + changeFilesOptions.NewBranchName = "new_branch" 132 + fileID++ 133 + createTreePath := fmt.Sprintf("new/file%d.txt", fileID) 134 + updateTreePath := fmt.Sprintf("update/file%d.txt", fileID) 135 + deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID) 136 + changeFilesOptions.Files[0].Path = createTreePath 137 + changeFilesOptions.Files[1].Path = updateTreePath 138 + changeFilesOptions.Files[2].Path = deleteTreePath 139 + createFile(user2, repo1, updateTreePath) 140 + createFile(user2, repo1, deleteTreePath) 141 + url := fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token2) 142 + req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 143 + resp := MakeRequest(t, req, http.StatusCreated) 144 + var filesResponse api.FilesResponse 145 + DecodeJSON(t, resp, &filesResponse) 146 + expectedCreateSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf" 147 + expectedCreateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID) 148 + expectedCreateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID) 149 + expectedUpdateSHA := "08bd14b2e2852529157324de9c226b3364e76136" 150 + expectedUpdateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID) 151 + expectedUpdateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID) 152 + assert.EqualValues(t, expectedCreateSHA, filesResponse.Files[0].SHA) 153 + assert.EqualValues(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL) 154 + assert.EqualValues(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL) 155 + assert.EqualValues(t, expectedUpdateSHA, filesResponse.Files[1].SHA) 156 + assert.EqualValues(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL) 157 + assert.EqualValues(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL) 158 + assert.Nil(t, filesResponse.Files[2]) 159 + 160 + assert.EqualValues(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message) 161 + 162 + // Test updating a file and renaming it 163 + changeFilesOptions = getChangeFilesOptions() 164 + changeFilesOptions.BranchName = repo1.DefaultBranch 165 + fileID++ 166 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 167 + createFile(user2, repo1, updateTreePath) 168 + changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]} 169 + changeFilesOptions.Files[0].FromPath = updateTreePath 170 + changeFilesOptions.Files[0].Path = "rename/" + updateTreePath 171 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 172 + resp = MakeRequest(t, req, http.StatusCreated) 173 + DecodeJSON(t, resp, &filesResponse) 174 + expectedUpdateSHA = "08bd14b2e2852529157324de9c226b3364e76136" 175 + expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID) 176 + expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID) 177 + assert.EqualValues(t, expectedUpdateSHA, filesResponse.Files[0].SHA) 178 + assert.EqualValues(t, expectedUpdateHTMLURL, *filesResponse.Files[0].HTMLURL) 179 + assert.EqualValues(t, expectedUpdateDownloadURL, *filesResponse.Files[0].DownloadURL) 180 + 181 + // Test updating a file without a message 182 + changeFilesOptions = getChangeFilesOptions() 183 + changeFilesOptions.Message = "" 184 + changeFilesOptions.BranchName = repo1.DefaultBranch 185 + fileID++ 186 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 187 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 188 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 189 + changeFilesOptions.Files[0].Path = createTreePath 190 + changeFilesOptions.Files[1].Path = updateTreePath 191 + changeFilesOptions.Files[2].Path = deleteTreePath 192 + createFile(user2, repo1, updateTreePath) 193 + createFile(user2, repo1, deleteTreePath) 194 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 195 + resp = MakeRequest(t, req, http.StatusCreated) 196 + DecodeJSON(t, resp, &filesResponse) 197 + expectedMessage := fmt.Sprintf("Add %v\nUpdate %v\nDelete %v\n", createTreePath, updateTreePath, deleteTreePath) 198 + assert.EqualValues(t, expectedMessage, filesResponse.Commit.Message) 199 + 200 + // Test updating a file with the wrong SHA 201 + fileID++ 202 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 203 + createFile(user2, repo1, updateTreePath) 204 + changeFilesOptions = getChangeFilesOptions() 205 + changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]} 206 + changeFilesOptions.Files[0].Path = updateTreePath 207 + correctSHA := changeFilesOptions.Files[0].SHA 208 + changeFilesOptions.Files[0].SHA = "badsha" 209 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 210 + resp = MakeRequest(t, req, http.StatusUnprocessableEntity) 211 + expectedAPIError := context.APIError{ 212 + Message: "sha does not match [given: " + changeFilesOptions.Files[0].SHA + ", expected: " + correctSHA + "]", 213 + URL: setting.API.SwaggerURL, 214 + } 215 + var apiError context.APIError 216 + DecodeJSON(t, resp, &apiError) 217 + assert.Equal(t, expectedAPIError, apiError) 218 + 219 + // Test creating a file in repo1 by user4 who does not have write access 220 + fileID++ 221 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 222 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 223 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 224 + createFile(user2, repo16, updateTreePath) 225 + createFile(user2, repo16, deleteTreePath) 226 + changeFilesOptions = getChangeFilesOptions() 227 + changeFilesOptions.Files[0].Path = createTreePath 228 + changeFilesOptions.Files[1].Path = updateTreePath 229 + changeFilesOptions.Files[2].Path = deleteTreePath 230 + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo16.Name, token4) 231 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 232 + MakeRequest(t, req, http.StatusNotFound) 233 + 234 + // Tests a repo with no token given so will fail 235 + fileID++ 236 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 237 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 238 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 239 + createFile(user2, repo16, updateTreePath) 240 + createFile(user2, repo16, deleteTreePath) 241 + changeFilesOptions = getChangeFilesOptions() 242 + changeFilesOptions.Files[0].Path = createTreePath 243 + changeFilesOptions.Files[1].Path = updateTreePath 244 + changeFilesOptions.Files[2].Path = deleteTreePath 245 + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name) 246 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 247 + MakeRequest(t, req, http.StatusNotFound) 248 + 249 + // Test using access token for a private repo that the user of the token owns 250 + fileID++ 251 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 252 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 253 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 254 + createFile(user2, repo16, updateTreePath) 255 + createFile(user2, repo16, deleteTreePath) 256 + changeFilesOptions = getChangeFilesOptions() 257 + changeFilesOptions.Files[0].Path = createTreePath 258 + changeFilesOptions.Files[1].Path = updateTreePath 259 + changeFilesOptions.Files[2].Path = deleteTreePath 260 + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo16.Name, token2) 261 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 262 + MakeRequest(t, req, http.StatusCreated) 263 + 264 + // Test using org repo "user3/repo3" where user2 is a collaborator 265 + fileID++ 266 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 267 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 268 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 269 + createFile(user3, repo3, updateTreePath) 270 + createFile(user3, repo3, deleteTreePath) 271 + changeFilesOptions = getChangeFilesOptions() 272 + changeFilesOptions.Files[0].Path = createTreePath 273 + changeFilesOptions.Files[1].Path = updateTreePath 274 + changeFilesOptions.Files[2].Path = deleteTreePath 275 + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user3.Name, repo3.Name, token2) 276 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 277 + MakeRequest(t, req, http.StatusCreated) 278 + 279 + // Test using org repo "user3/repo3" with no user token 280 + fileID++ 281 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 282 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 283 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 284 + createFile(user3, repo3, updateTreePath) 285 + createFile(user3, repo3, deleteTreePath) 286 + changeFilesOptions = getChangeFilesOptions() 287 + changeFilesOptions.Files[0].Path = createTreePath 288 + changeFilesOptions.Files[1].Path = updateTreePath 289 + changeFilesOptions.Files[2].Path = deleteTreePath 290 + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user3.Name, repo3.Name) 291 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 292 + MakeRequest(t, req, http.StatusNotFound) 293 + 294 + // Test using repo "user2/repo1" where user4 is a NOT collaborator 295 + fileID++ 296 + createTreePath = fmt.Sprintf("new/file%d.txt", fileID) 297 + updateTreePath = fmt.Sprintf("update/file%d.txt", fileID) 298 + deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID) 299 + createFile(user2, repo1, updateTreePath) 300 + createFile(user2, repo1, deleteTreePath) 301 + changeFilesOptions = getChangeFilesOptions() 302 + changeFilesOptions.Files[0].Path = createTreePath 303 + changeFilesOptions.Files[1].Path = updateTreePath 304 + changeFilesOptions.Files[2].Path = deleteTreePath 305 + url = fmt.Sprintf("/api/v1/repos/%s/%s/contents?token=%s", user2.Name, repo1.Name, token4) 306 + req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions) 307 + MakeRequest(t, req, http.StatusForbidden) 308 + }) 309 + }
+16 -8
tests/integration/pull_merge_test.go
··· 367 367 assert.NotEmpty(t, baseRepo) 368 368 369 369 // create a commit on new branch. 370 - _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{ 371 - TreePath: "important_file", 370 + _, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{ 371 + Files: []*files_service.ChangeRepoFile{ 372 + { 373 + Operation: "create", 374 + TreePath: "important_file", 375 + Content: "Just a non-important file", 376 + }, 377 + }, 372 378 Message: "Add a important file", 373 - Content: "Just a non-important file", 374 - IsNewFile: true, 375 379 OldBranch: "main", 376 380 NewBranch: "important-secrets", 377 381 }) 378 382 assert.NoError(t, err) 379 383 380 384 // create a commit on main branch. 381 - _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{ 382 - TreePath: "important_file", 385 + _, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{ 386 + Files: []*files_service.ChangeRepoFile{ 387 + { 388 + Operation: "create", 389 + TreePath: "important_file", 390 + Content: "Not the same content :P", 391 + }, 392 + }, 383 393 Message: "Add a important file", 384 - Content: "Not the same content :P", 385 - IsNewFile: true, 386 394 OldBranch: "main", 387 395 NewBranch: "main", 388 396 })
+16 -8
tests/integration/pull_update_test.go
··· 101 101 assert.NotEmpty(t, headRepo) 102 102 103 103 // create a commit on base Repo 104 - _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, actor, &files_service.UpdateRepoFileOptions{ 105 - TreePath: "File_A", 104 + _, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, actor, &files_service.ChangeRepoFilesOptions{ 105 + Files: []*files_service.ChangeRepoFile{ 106 + { 107 + Operation: "create", 108 + TreePath: "File_A", 109 + Content: "File A", 110 + }, 111 + }, 106 112 Message: "Add File A", 107 - Content: "File A", 108 - IsNewFile: true, 109 113 OldBranch: "master", 110 114 NewBranch: "master", 111 115 Author: &files_service.IdentityOptions{ ··· 124 128 assert.NoError(t, err) 125 129 126 130 // create a commit on head Repo 127 - _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, headRepo, actor, &files_service.UpdateRepoFileOptions{ 128 - TreePath: "File_B", 131 + _, err = files_service.ChangeRepoFiles(git.DefaultContext, headRepo, actor, &files_service.ChangeRepoFilesOptions{ 132 + Files: []*files_service.ChangeRepoFile{ 133 + { 134 + Operation: "create", 135 + TreePath: "File_B", 136 + Content: "File B", 137 + }, 138 + }, 129 139 Message: "Add File on PR branch", 130 - Content: "File B", 131 - IsNewFile: true, 132 140 OldBranch: "master", 133 141 NewBranch: "newBranch", 134 142 Author: &files_service.IdentityOptions{
+546
tests/integration/repofiles_change_test.go
··· 1 + // Copyright 2019 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "net/url" 8 + "path/filepath" 9 + "testing" 10 + "time" 11 + 12 + repo_model "code.gitea.io/gitea/models/repo" 13 + "code.gitea.io/gitea/models/unittest" 14 + "code.gitea.io/gitea/modules/git" 15 + "code.gitea.io/gitea/modules/setting" 16 + api "code.gitea.io/gitea/modules/structs" 17 + "code.gitea.io/gitea/modules/test" 18 + files_service "code.gitea.io/gitea/services/repository/files" 19 + 20 + "github.com/stretchr/testify/assert" 21 + ) 22 + 23 + func getCreateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { 24 + return &files_service.ChangeRepoFilesOptions{ 25 + Files: []*files_service.ChangeRepoFile{ 26 + { 27 + Operation: "create", 28 + TreePath: "new/file.txt", 29 + Content: "This is a NEW file", 30 + }, 31 + }, 32 + OldBranch: repo.DefaultBranch, 33 + NewBranch: repo.DefaultBranch, 34 + Message: "Creates new/file.txt", 35 + Author: nil, 36 + Committer: nil, 37 + } 38 + } 39 + 40 + func getUpdateRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { 41 + return &files_service.ChangeRepoFilesOptions{ 42 + Files: []*files_service.ChangeRepoFile{ 43 + { 44 + Operation: "update", 45 + TreePath: "README.md", 46 + SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", 47 + Content: "This is UPDATED content for the README file", 48 + }, 49 + }, 50 + OldBranch: repo.DefaultBranch, 51 + NewBranch: repo.DefaultBranch, 52 + Message: "Updates README.md", 53 + Author: nil, 54 + Committer: nil, 55 + } 56 + } 57 + 58 + func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { 59 + return &files_service.ChangeRepoFilesOptions{ 60 + Files: []*files_service.ChangeRepoFile{ 61 + { 62 + Operation: "delete", 63 + TreePath: "README.md", 64 + SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", 65 + }, 66 + }, 67 + LastCommitID: "", 68 + OldBranch: repo.DefaultBranch, 69 + NewBranch: repo.DefaultBranch, 70 + Message: "Deletes README.md", 71 + Author: &files_service.IdentityOptions{ 72 + Name: "Bob Smith", 73 + Email: "bob@smith.com", 74 + }, 75 + Committer: nil, 76 + } 77 + } 78 + 79 + func getExpectedFileResponseForRepofilesDelete(u *url.URL) *api.FileResponse { 80 + // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined 81 + return &api.FileResponse{ 82 + Content: nil, 83 + Commit: &api.FileCommitResponse{ 84 + Author: &api.CommitUser{ 85 + Identity: api.Identity{ 86 + Name: "Bob Smith", 87 + Email: "bob@smith.com", 88 + }, 89 + }, 90 + Committer: &api.CommitUser{ 91 + Identity: api.Identity{ 92 + Name: "Bob Smith", 93 + Email: "bob@smith.com", 94 + }, 95 + }, 96 + Message: "Deletes README.md\n", 97 + }, 98 + Verification: &api.PayloadCommitVerification{ 99 + Verified: false, 100 + Reason: "gpg.error.not_signed_commit", 101 + Signature: "", 102 + Payload: "", 103 + }, 104 + } 105 + } 106 + 107 + func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse { 108 + treePath := "new/file.txt" 109 + encoding := "base64" 110 + content := "VGhpcyBpcyBhIE5FVyBmaWxl" 111 + selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master" 112 + htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath 113 + gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885" 114 + downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath 115 + return &api.FileResponse{ 116 + Content: &api.ContentsResponse{ 117 + Name: filepath.Base(treePath), 118 + Path: treePath, 119 + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", 120 + LastCommitSHA: lastCommitSHA, 121 + Type: "file", 122 + Size: 18, 123 + Encoding: &encoding, 124 + Content: &content, 125 + URL: &selfURL, 126 + HTMLURL: &htmlURL, 127 + GitURL: &gitURL, 128 + DownloadURL: &downloadURL, 129 + Links: &api.FileLinksResponse{ 130 + Self: &selfURL, 131 + GitURL: &gitURL, 132 + HTMLURL: &htmlURL, 133 + }, 134 + }, 135 + Commit: &api.FileCommitResponse{ 136 + CommitMeta: api.CommitMeta{ 137 + URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, 138 + SHA: commitID, 139 + }, 140 + HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, 141 + Author: &api.CommitUser{ 142 + Identity: api.Identity{ 143 + Name: "User Two", 144 + Email: "user2@noreply.example.org", 145 + }, 146 + Date: time.Now().UTC().Format(time.RFC3339), 147 + }, 148 + Committer: &api.CommitUser{ 149 + Identity: api.Identity{ 150 + Name: "User Two", 151 + Email: "user2@noreply.example.org", 152 + }, 153 + Date: time.Now().UTC().Format(time.RFC3339), 154 + }, 155 + Parents: []*api.CommitMeta{ 156 + { 157 + URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", 158 + SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", 159 + }, 160 + }, 161 + Message: "Updates README.md\n", 162 + Tree: &api.CommitMeta{ 163 + URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", 164 + SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc", 165 + }, 166 + }, 167 + Verification: &api.PayloadCommitVerification{ 168 + Verified: false, 169 + Reason: "gpg.error.not_signed_commit", 170 + Signature: "", 171 + Payload: "", 172 + }, 173 + } 174 + } 175 + 176 + func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA string) *api.FileResponse { 177 + encoding := "base64" 178 + content := "VGhpcyBpcyBVUERBVEVEIGNvbnRlbnQgZm9yIHRoZSBSRUFETUUgZmlsZQ==" 179 + selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + filename + "?ref=master" 180 + htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + filename 181 + gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647" 182 + downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + filename 183 + return &api.FileResponse{ 184 + Content: &api.ContentsResponse{ 185 + Name: filename, 186 + Path: filename, 187 + SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647", 188 + LastCommitSHA: lastCommitSHA, 189 + Type: "file", 190 + Size: 43, 191 + Encoding: &encoding, 192 + Content: &content, 193 + URL: &selfURL, 194 + HTMLURL: &htmlURL, 195 + GitURL: &gitURL, 196 + DownloadURL: &downloadURL, 197 + Links: &api.FileLinksResponse{ 198 + Self: &selfURL, 199 + GitURL: &gitURL, 200 + HTMLURL: &htmlURL, 201 + }, 202 + }, 203 + Commit: &api.FileCommitResponse{ 204 + CommitMeta: api.CommitMeta{ 205 + URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, 206 + SHA: commitID, 207 + }, 208 + HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, 209 + Author: &api.CommitUser{ 210 + Identity: api.Identity{ 211 + Name: "User Two", 212 + Email: "user2@noreply.example.org", 213 + }, 214 + Date: time.Now().UTC().Format(time.RFC3339), 215 + }, 216 + Committer: &api.CommitUser{ 217 + Identity: api.Identity{ 218 + Name: "User Two", 219 + Email: "user2@noreply.example.org", 220 + }, 221 + Date: time.Now().UTC().Format(time.RFC3339), 222 + }, 223 + Parents: []*api.CommitMeta{ 224 + { 225 + URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", 226 + SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", 227 + }, 228 + }, 229 + Message: "Updates README.md\n", 230 + Tree: &api.CommitMeta{ 231 + URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", 232 + SHA: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc", 233 + }, 234 + }, 235 + Verification: &api.PayloadCommitVerification{ 236 + Verified: false, 237 + Reason: "gpg.error.not_signed_commit", 238 + Signature: "", 239 + Payload: "", 240 + }, 241 + } 242 + } 243 + 244 + func TestChangeRepoFilesForCreate(t *testing.T) { 245 + // setup 246 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 247 + ctx := test.MockContext(t, "user2/repo1") 248 + ctx.SetParams(":id", "1") 249 + test.LoadRepo(t, ctx, 1) 250 + test.LoadRepoCommit(t, ctx) 251 + test.LoadUser(t, ctx, 2) 252 + test.LoadGitRepo(t, ctx) 253 + defer ctx.Repo.GitRepo.Close() 254 + 255 + repo := ctx.Repo.Repository 256 + doer := ctx.Doer 257 + opts := getCreateRepoFilesOptions(repo) 258 + 259 + // test 260 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 261 + 262 + // asserts 263 + assert.NoError(t, err) 264 + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 265 + defer gitRepo.Close() 266 + 267 + commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch) 268 + lastCommit, _ := gitRepo.GetCommitByPath("new/file.txt") 269 + expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String()) 270 + assert.NotNil(t, expectedFileResponse) 271 + if expectedFileResponse != nil { 272 + assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0]) 273 + assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA) 274 + assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) 275 + assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email) 276 + assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name) 277 + } 278 + }) 279 + } 280 + 281 + func TestChangeRepoFilesForUpdate(t *testing.T) { 282 + // setup 283 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 284 + ctx := test.MockContext(t, "user2/repo1") 285 + ctx.SetParams(":id", "1") 286 + test.LoadRepo(t, ctx, 1) 287 + test.LoadRepoCommit(t, ctx) 288 + test.LoadUser(t, ctx, 2) 289 + test.LoadGitRepo(t, ctx) 290 + defer ctx.Repo.GitRepo.Close() 291 + 292 + repo := ctx.Repo.Repository 293 + doer := ctx.Doer 294 + opts := getUpdateRepoFilesOptions(repo) 295 + 296 + // test 297 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 298 + 299 + // asserts 300 + assert.NoError(t, err) 301 + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 302 + defer gitRepo.Close() 303 + 304 + commit, _ := gitRepo.GetBranchCommit(opts.NewBranch) 305 + lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath) 306 + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String()) 307 + assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0]) 308 + assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA) 309 + assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) 310 + assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email) 311 + assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name) 312 + }) 313 + } 314 + 315 + func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) { 316 + // setup 317 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 318 + ctx := test.MockContext(t, "user2/repo1") 319 + ctx.SetParams(":id", "1") 320 + test.LoadRepo(t, ctx, 1) 321 + test.LoadRepoCommit(t, ctx) 322 + test.LoadUser(t, ctx, 2) 323 + test.LoadGitRepo(t, ctx) 324 + defer ctx.Repo.GitRepo.Close() 325 + 326 + repo := ctx.Repo.Repository 327 + doer := ctx.Doer 328 + opts := getUpdateRepoFilesOptions(repo) 329 + opts.Files[0].FromTreePath = "README.md" 330 + opts.Files[0].TreePath = "README_new.md" // new file name, README_new.md 331 + 332 + // test 333 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 334 + 335 + // asserts 336 + assert.NoError(t, err) 337 + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 338 + defer gitRepo.Close() 339 + 340 + commit, _ := gitRepo.GetBranchCommit(opts.NewBranch) 341 + lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath) 342 + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String()) 343 + // assert that the old file no longer exists in the last commit of the branch 344 + fromEntry, err := commit.GetTreeEntryByPath(opts.Files[0].FromTreePath) 345 + switch err.(type) { 346 + case git.ErrNotExist: 347 + // correct, continue 348 + default: 349 + t.Fatalf("expected git.ErrNotExist, got:%v", err) 350 + } 351 + toEntry, err := commit.GetTreeEntryByPath(opts.Files[0].TreePath) 352 + assert.NoError(t, err) 353 + assert.Nil(t, fromEntry) // Should no longer exist here 354 + assert.NotNil(t, toEntry) // Should exist here 355 + // assert SHA has remained the same but paths use the new file name 356 + assert.EqualValues(t, expectedFileResponse.Content.SHA, filesResponse.Files[0].SHA) 357 + assert.EqualValues(t, expectedFileResponse.Content.Name, filesResponse.Files[0].Name) 358 + assert.EqualValues(t, expectedFileResponse.Content.Path, filesResponse.Files[0].Path) 359 + assert.EqualValues(t, expectedFileResponse.Content.URL, filesResponse.Files[0].URL) 360 + assert.EqualValues(t, expectedFileResponse.Commit.SHA, filesResponse.Commit.SHA) 361 + assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL) 362 + }) 363 + } 364 + 365 + // Test opts with branch names removed, should get same results as above test 366 + func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { 367 + // setup 368 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 369 + ctx := test.MockContext(t, "user2/repo1") 370 + ctx.SetParams(":id", "1") 371 + test.LoadRepo(t, ctx, 1) 372 + test.LoadRepoCommit(t, ctx) 373 + test.LoadUser(t, ctx, 2) 374 + test.LoadGitRepo(t, ctx) 375 + defer ctx.Repo.GitRepo.Close() 376 + 377 + repo := ctx.Repo.Repository 378 + doer := ctx.Doer 379 + opts := getUpdateRepoFilesOptions(repo) 380 + opts.OldBranch = "" 381 + opts.NewBranch = "" 382 + 383 + // test 384 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 385 + 386 + // asserts 387 + assert.NoError(t, err) 388 + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 389 + defer gitRepo.Close() 390 + 391 + commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch) 392 + lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath) 393 + expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.Files[0].TreePath, lastCommit.ID.String()) 394 + assert.EqualValues(t, expectedFileResponse.Content, filesResponse.Files[0]) 395 + }) 396 + } 397 + 398 + func TestChangeRepoFilesForDelete(t *testing.T) { 399 + onGiteaRun(t, testDeleteRepoFiles) 400 + } 401 + 402 + func testDeleteRepoFiles(t *testing.T, u *url.URL) { 403 + // setup 404 + unittest.PrepareTestEnv(t) 405 + ctx := test.MockContext(t, "user2/repo1") 406 + ctx.SetParams(":id", "1") 407 + test.LoadRepo(t, ctx, 1) 408 + test.LoadRepoCommit(t, ctx) 409 + test.LoadUser(t, ctx, 2) 410 + test.LoadGitRepo(t, ctx) 411 + defer ctx.Repo.GitRepo.Close() 412 + repo := ctx.Repo.Repository 413 + doer := ctx.Doer 414 + opts := getDeleteRepoFilesOptions(repo) 415 + 416 + t.Run("Delete README.md file", func(t *testing.T) { 417 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 418 + assert.NoError(t, err) 419 + expectedFileResponse := getExpectedFileResponseForRepofilesDelete(u) 420 + assert.NotNil(t, filesResponse) 421 + assert.Nil(t, filesResponse.Files[0]) 422 + assert.EqualValues(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message) 423 + assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity) 424 + assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity) 425 + assert.EqualValues(t, expectedFileResponse.Verification, filesResponse.Verification) 426 + }) 427 + 428 + t.Run("Verify README.md has been deleted", func(t *testing.T) { 429 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 430 + assert.Nil(t, filesResponse) 431 + expectedError := "repository file does not exist [path: " + opts.Files[0].TreePath + "]" 432 + assert.EqualError(t, err, expectedError) 433 + }) 434 + } 435 + 436 + // Test opts with branch names removed, same results 437 + func TestChangeRepoFilesForDeleteWithoutBranchNames(t *testing.T) { 438 + onGiteaRun(t, testDeleteRepoFilesWithoutBranchNames) 439 + } 440 + 441 + func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) { 442 + // setup 443 + unittest.PrepareTestEnv(t) 444 + ctx := test.MockContext(t, "user2/repo1") 445 + ctx.SetParams(":id", "1") 446 + test.LoadRepo(t, ctx, 1) 447 + test.LoadRepoCommit(t, ctx) 448 + test.LoadUser(t, ctx, 2) 449 + test.LoadGitRepo(t, ctx) 450 + defer ctx.Repo.GitRepo.Close() 451 + 452 + repo := ctx.Repo.Repository 453 + doer := ctx.Doer 454 + opts := getDeleteRepoFilesOptions(repo) 455 + opts.OldBranch = "" 456 + opts.NewBranch = "" 457 + 458 + t.Run("Delete README.md without Branch Name", func(t *testing.T) { 459 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 460 + assert.NoError(t, err) 461 + expectedFileResponse := getExpectedFileResponseForRepofilesDelete(u) 462 + assert.NotNil(t, filesResponse) 463 + assert.Nil(t, filesResponse.Files[0]) 464 + assert.EqualValues(t, expectedFileResponse.Commit.Message, filesResponse.Commit.Message) 465 + assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, filesResponse.Commit.Author.Identity) 466 + assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, filesResponse.Commit.Committer.Identity) 467 + assert.EqualValues(t, expectedFileResponse.Verification, filesResponse.Verification) 468 + }) 469 + } 470 + 471 + func TestChangeRepoFilesErrors(t *testing.T) { 472 + // setup 473 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 474 + ctx := test.MockContext(t, "user2/repo1") 475 + ctx.SetParams(":id", "1") 476 + test.LoadRepo(t, ctx, 1) 477 + test.LoadRepoCommit(t, ctx) 478 + test.LoadUser(t, ctx, 2) 479 + test.LoadGitRepo(t, ctx) 480 + defer ctx.Repo.GitRepo.Close() 481 + 482 + repo := ctx.Repo.Repository 483 + doer := ctx.Doer 484 + 485 + t.Run("bad branch", func(t *testing.T) { 486 + opts := getUpdateRepoFilesOptions(repo) 487 + opts.OldBranch = "bad_branch" 488 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 489 + assert.Error(t, err) 490 + assert.Nil(t, filesResponse) 491 + expectedError := "branch does not exist [name: " + opts.OldBranch + "]" 492 + assert.EqualError(t, err, expectedError) 493 + }) 494 + 495 + t.Run("bad SHA", func(t *testing.T) { 496 + opts := getUpdateRepoFilesOptions(repo) 497 + origSHA := opts.Files[0].SHA 498 + opts.Files[0].SHA = "bad_sha" 499 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 500 + assert.Nil(t, filesResponse) 501 + assert.Error(t, err) 502 + expectedError := "sha does not match [given: " + opts.Files[0].SHA + ", expected: " + origSHA + "]" 503 + assert.EqualError(t, err, expectedError) 504 + }) 505 + 506 + t.Run("new branch already exists", func(t *testing.T) { 507 + opts := getUpdateRepoFilesOptions(repo) 508 + opts.NewBranch = "develop" 509 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 510 + assert.Nil(t, filesResponse) 511 + assert.Error(t, err) 512 + expectedError := "branch already exists [name: " + opts.NewBranch + "]" 513 + assert.EqualError(t, err, expectedError) 514 + }) 515 + 516 + t.Run("treePath is empty:", func(t *testing.T) { 517 + opts := getUpdateRepoFilesOptions(repo) 518 + opts.Files[0].TreePath = "" 519 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 520 + assert.Nil(t, filesResponse) 521 + assert.Error(t, err) 522 + expectedError := "path contains a malformed path component [path: ]" 523 + assert.EqualError(t, err, expectedError) 524 + }) 525 + 526 + t.Run("treePath is a git directory:", func(t *testing.T) { 527 + opts := getUpdateRepoFilesOptions(repo) 528 + opts.Files[0].TreePath = ".git" 529 + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 530 + assert.Nil(t, filesResponse) 531 + assert.Error(t, err) 532 + expectedError := "path contains a malformed path component [path: " + opts.Files[0].TreePath + "]" 533 + assert.EqualError(t, err, expectedError) 534 + }) 535 + 536 + t.Run("create file that already exists", func(t *testing.T) { 537 + opts := getCreateRepoFilesOptions(repo) 538 + opts.Files[0].TreePath = "README.md" // already exists 539 + fileResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) 540 + assert.Nil(t, fileResponse) 541 + assert.Error(t, err) 542 + expectedError := "repository file already exists [path: " + opts.Files[0].TreePath + "]" 543 + assert.EqualError(t, err, expectedError) 544 + }) 545 + }) 546 + }
-201
tests/integration/repofiles_delete_test.go
··· 1 - // Copyright 2019 The Gitea Authors. All rights reserved. 2 - // SPDX-License-Identifier: MIT 3 - 4 - package integration 5 - 6 - import ( 7 - "net/url" 8 - "testing" 9 - 10 - repo_model "code.gitea.io/gitea/models/repo" 11 - "code.gitea.io/gitea/models/unittest" 12 - "code.gitea.io/gitea/modules/git" 13 - api "code.gitea.io/gitea/modules/structs" 14 - "code.gitea.io/gitea/modules/test" 15 - files_service "code.gitea.io/gitea/services/repository/files" 16 - 17 - "github.com/stretchr/testify/assert" 18 - ) 19 - 20 - func getDeleteRepoFileOptions(repo *repo_model.Repository) *files_service.DeleteRepoFileOptions { 21 - return &files_service.DeleteRepoFileOptions{ 22 - LastCommitID: "", 23 - OldBranch: repo.DefaultBranch, 24 - NewBranch: repo.DefaultBranch, 25 - TreePath: "README.md", 26 - Message: "Deletes README.md", 27 - SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", 28 - Author: &files_service.IdentityOptions{ 29 - Name: "Bob Smith", 30 - Email: "bob@smith.com", 31 - }, 32 - Committer: nil, 33 - } 34 - } 35 - 36 - func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse { 37 - // Just returns fields that don't change, i.e. fields with commit SHAs and dates can't be determined 38 - return &api.FileResponse{ 39 - Content: nil, 40 - Commit: &api.FileCommitResponse{ 41 - Author: &api.CommitUser{ 42 - Identity: api.Identity{ 43 - Name: "Bob Smith", 44 - Email: "bob@smith.com", 45 - }, 46 - }, 47 - Committer: &api.CommitUser{ 48 - Identity: api.Identity{ 49 - Name: "Bob Smith", 50 - Email: "bob@smith.com", 51 - }, 52 - }, 53 - Message: "Deletes README.md\n", 54 - }, 55 - Verification: &api.PayloadCommitVerification{ 56 - Verified: false, 57 - Reason: "gpg.error.not_signed_commit", 58 - Signature: "", 59 - Payload: "", 60 - }, 61 - } 62 - } 63 - 64 - func TestDeleteRepoFile(t *testing.T) { 65 - onGiteaRun(t, testDeleteRepoFile) 66 - } 67 - 68 - func testDeleteRepoFile(t *testing.T, u *url.URL) { 69 - // setup 70 - unittest.PrepareTestEnv(t) 71 - ctx := test.MockContext(t, "user2/repo1") 72 - ctx.SetParams(":id", "1") 73 - test.LoadRepo(t, ctx, 1) 74 - test.LoadRepoCommit(t, ctx) 75 - test.LoadUser(t, ctx, 2) 76 - test.LoadGitRepo(t, ctx) 77 - defer ctx.Repo.GitRepo.Close() 78 - repo := ctx.Repo.Repository 79 - doer := ctx.Doer 80 - opts := getDeleteRepoFileOptions(repo) 81 - 82 - t.Run("Delete README.md file", func(t *testing.T) { 83 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 84 - assert.NoError(t, err) 85 - expectedFileResponse := getExpectedDeleteFileResponse(u) 86 - assert.NotNil(t, fileResponse) 87 - assert.Nil(t, fileResponse.Content) 88 - assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message) 89 - assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity) 90 - assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity) 91 - assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification) 92 - }) 93 - 94 - t.Run("Verify README.md has been deleted", func(t *testing.T) { 95 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 96 - assert.Nil(t, fileResponse) 97 - expectedError := "repository file does not exist [path: " + opts.TreePath + "]" 98 - assert.EqualError(t, err, expectedError) 99 - }) 100 - } 101 - 102 - // Test opts with branch names removed, same results 103 - func TestDeleteRepoFileWithoutBranchNames(t *testing.T) { 104 - onGiteaRun(t, testDeleteRepoFileWithoutBranchNames) 105 - } 106 - 107 - func testDeleteRepoFileWithoutBranchNames(t *testing.T, u *url.URL) { 108 - // setup 109 - unittest.PrepareTestEnv(t) 110 - ctx := test.MockContext(t, "user2/repo1") 111 - ctx.SetParams(":id", "1") 112 - test.LoadRepo(t, ctx, 1) 113 - test.LoadRepoCommit(t, ctx) 114 - test.LoadUser(t, ctx, 2) 115 - test.LoadGitRepo(t, ctx) 116 - defer ctx.Repo.GitRepo.Close() 117 - 118 - repo := ctx.Repo.Repository 119 - doer := ctx.Doer 120 - opts := getDeleteRepoFileOptions(repo) 121 - opts.OldBranch = "" 122 - opts.NewBranch = "" 123 - 124 - t.Run("Delete README.md without Branch Name", func(t *testing.T) { 125 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 126 - assert.NoError(t, err) 127 - expectedFileResponse := getExpectedDeleteFileResponse(u) 128 - assert.NotNil(t, fileResponse) 129 - assert.Nil(t, fileResponse.Content) 130 - assert.EqualValues(t, expectedFileResponse.Commit.Message, fileResponse.Commit.Message) 131 - assert.EqualValues(t, expectedFileResponse.Commit.Author.Identity, fileResponse.Commit.Author.Identity) 132 - assert.EqualValues(t, expectedFileResponse.Commit.Committer.Identity, fileResponse.Commit.Committer.Identity) 133 - assert.EqualValues(t, expectedFileResponse.Verification, fileResponse.Verification) 134 - }) 135 - } 136 - 137 - func TestDeleteRepoFileErrors(t *testing.T) { 138 - // setup 139 - unittest.PrepareTestEnv(t) 140 - ctx := test.MockContext(t, "user2/repo1") 141 - ctx.SetParams(":id", "1") 142 - test.LoadRepo(t, ctx, 1) 143 - test.LoadRepoCommit(t, ctx) 144 - test.LoadUser(t, ctx, 2) 145 - test.LoadGitRepo(t, ctx) 146 - defer ctx.Repo.GitRepo.Close() 147 - 148 - repo := ctx.Repo.Repository 149 - doer := ctx.Doer 150 - 151 - t.Run("Bad branch", func(t *testing.T) { 152 - opts := getDeleteRepoFileOptions(repo) 153 - opts.OldBranch = "bad_branch" 154 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 155 - assert.Error(t, err) 156 - assert.Nil(t, fileResponse) 157 - expectedError := "branch does not exist [name: " + opts.OldBranch + "]" 158 - assert.EqualError(t, err, expectedError) 159 - }) 160 - 161 - t.Run("Bad SHA", func(t *testing.T) { 162 - opts := getDeleteRepoFileOptions(repo) 163 - origSHA := opts.SHA 164 - opts.SHA = "bad_sha" 165 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 166 - assert.Nil(t, fileResponse) 167 - assert.Error(t, err) 168 - expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]" 169 - assert.EqualError(t, err, expectedError) 170 - }) 171 - 172 - t.Run("New branch already exists", func(t *testing.T) { 173 - opts := getDeleteRepoFileOptions(repo) 174 - opts.NewBranch = "develop" 175 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 176 - assert.Nil(t, fileResponse) 177 - assert.Error(t, err) 178 - expectedError := "branch already exists [name: " + opts.NewBranch + "]" 179 - assert.EqualError(t, err, expectedError) 180 - }) 181 - 182 - t.Run("TreePath is empty:", func(t *testing.T) { 183 - opts := getDeleteRepoFileOptions(repo) 184 - opts.TreePath = "" 185 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 186 - assert.Nil(t, fileResponse) 187 - assert.Error(t, err) 188 - expectedError := "path contains a malformed path component [path: ]" 189 - assert.EqualError(t, err, expectedError) 190 - }) 191 - 192 - t.Run("TreePath is a git directory:", func(t *testing.T) { 193 - opts := getDeleteRepoFileOptions(repo) 194 - opts.TreePath = ".git" 195 - fileResponse, err := files_service.DeleteRepoFile(git.DefaultContext, repo, doer, opts) 196 - assert.Nil(t, fileResponse) 197 - assert.Error(t, err) 198 - expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]" 199 - assert.EqualError(t, err, expectedError) 200 - }) 201 - }
-415
tests/integration/repofiles_update_test.go
··· 1 - // Copyright 2019 The Gitea Authors. All rights reserved. 2 - // SPDX-License-Identifier: MIT 3 - 4 - package integration 5 - 6 - import ( 7 - "net/url" 8 - "path/filepath" 9 - "testing" 10 - "time" 11 - 12 - repo_model "code.gitea.io/gitea/models/repo" 13 - "code.gitea.io/gitea/modules/git" 14 - "code.gitea.io/gitea/modules/setting" 15 - api "code.gitea.io/gitea/modules/structs" 16 - "code.gitea.io/gitea/modules/test" 17 - files_service "code.gitea.io/gitea/services/repository/files" 18 - 19 - "github.com/stretchr/testify/assert" 20 - ) 21 - 22 - func getCreateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions { 23 - return &files_service.UpdateRepoFileOptions{ 24 - OldBranch: repo.DefaultBranch, 25 - NewBranch: repo.DefaultBranch, 26 - TreePath: "new/file.txt", 27 - Message: "Creates new/file.txt", 28 - Content: "This is a NEW file", 29 - IsNewFile: true, 30 - Author: nil, 31 - Committer: nil, 32 - } 33 - } 34 - 35 - func getUpdateRepoFileOptions(repo *repo_model.Repository) *files_service.UpdateRepoFileOptions { 36 - return &files_service.UpdateRepoFileOptions{ 37 - OldBranch: repo.DefaultBranch, 38 - NewBranch: repo.DefaultBranch, 39 - TreePath: "README.md", 40 - Message: "Updates README.md", 41 - SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", 42 - Content: "This is UPDATED content for the README file", 43 - IsNewFile: false, 44 - Author: nil, 45 - Committer: nil, 46 - } 47 - } 48 - 49 - func getExpectedFileResponseForRepofilesCreate(commitID, lastCommitSHA string) *api.FileResponse { 50 - treePath := "new/file.txt" 51 - encoding := "base64" 52 - content := "VGhpcyBpcyBhIE5FVyBmaWxl" 53 - selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master" 54 - htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + treePath 55 - gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885" 56 - downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath 57 - return &api.FileResponse{ 58 - Content: &api.ContentsResponse{ 59 - Name: filepath.Base(treePath), 60 - Path: treePath, 61 - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", 62 - LastCommitSHA: lastCommitSHA, 63 - Type: "file", 64 - Size: 18, 65 - Encoding: &encoding, 66 - Content: &content, 67 - URL: &selfURL, 68 - HTMLURL: &htmlURL, 69 - GitURL: &gitURL, 70 - DownloadURL: &downloadURL, 71 - Links: &api.FileLinksResponse{ 72 - Self: &selfURL, 73 - GitURL: &gitURL, 74 - HTMLURL: &htmlURL, 75 - }, 76 - }, 77 - Commit: &api.FileCommitResponse{ 78 - CommitMeta: api.CommitMeta{ 79 - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, 80 - SHA: commitID, 81 - }, 82 - HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, 83 - Author: &api.CommitUser{ 84 - Identity: api.Identity{ 85 - Name: "User Two", 86 - Email: "user2@noreply.example.org", 87 - }, 88 - Date: time.Now().UTC().Format(time.RFC3339), 89 - }, 90 - Committer: &api.CommitUser{ 91 - Identity: api.Identity{ 92 - Name: "User Two", 93 - Email: "user2@noreply.example.org", 94 - }, 95 - Date: time.Now().UTC().Format(time.RFC3339), 96 - }, 97 - Parents: []*api.CommitMeta{ 98 - { 99 - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", 100 - SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", 101 - }, 102 - }, 103 - Message: "Updates README.md\n", 104 - Tree: &api.CommitMeta{ 105 - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", 106 - SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc", 107 - }, 108 - }, 109 - Verification: &api.PayloadCommitVerification{ 110 - Verified: false, 111 - Reason: "gpg.error.not_signed_commit", 112 - Signature: "", 113 - Payload: "", 114 - }, 115 - } 116 - } 117 - 118 - func getExpectedFileResponseForRepofilesUpdate(commitID, filename, lastCommitSHA string) *api.FileResponse { 119 - encoding := "base64" 120 - content := "VGhpcyBpcyBVUERBVEVEIGNvbnRlbnQgZm9yIHRoZSBSRUFETUUgZmlsZQ==" 121 - selfURL := setting.AppURL + "api/v1/repos/user2/repo1/contents/" + filename + "?ref=master" 122 - htmlURL := setting.AppURL + "user2/repo1/src/branch/master/" + filename 123 - gitURL := setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647" 124 - downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + filename 125 - return &api.FileResponse{ 126 - Content: &api.ContentsResponse{ 127 - Name: filename, 128 - Path: filename, 129 - SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647", 130 - LastCommitSHA: lastCommitSHA, 131 - Type: "file", 132 - Size: 43, 133 - Encoding: &encoding, 134 - Content: &content, 135 - URL: &selfURL, 136 - HTMLURL: &htmlURL, 137 - GitURL: &gitURL, 138 - DownloadURL: &downloadURL, 139 - Links: &api.FileLinksResponse{ 140 - Self: &selfURL, 141 - GitURL: &gitURL, 142 - HTMLURL: &htmlURL, 143 - }, 144 - }, 145 - Commit: &api.FileCommitResponse{ 146 - CommitMeta: api.CommitMeta{ 147 - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, 148 - SHA: commitID, 149 - }, 150 - HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, 151 - Author: &api.CommitUser{ 152 - Identity: api.Identity{ 153 - Name: "User Two", 154 - Email: "user2@noreply.example.org", 155 - }, 156 - Date: time.Now().UTC().Format(time.RFC3339), 157 - }, 158 - Committer: &api.CommitUser{ 159 - Identity: api.Identity{ 160 - Name: "User Two", 161 - Email: "user2@noreply.example.org", 162 - }, 163 - Date: time.Now().UTC().Format(time.RFC3339), 164 - }, 165 - Parents: []*api.CommitMeta{ 166 - { 167 - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", 168 - SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", 169 - }, 170 - }, 171 - Message: "Updates README.md\n", 172 - Tree: &api.CommitMeta{ 173 - URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", 174 - SHA: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc", 175 - }, 176 - }, 177 - Verification: &api.PayloadCommitVerification{ 178 - Verified: false, 179 - Reason: "gpg.error.not_signed_commit", 180 - Signature: "", 181 - Payload: "", 182 - }, 183 - } 184 - } 185 - 186 - func TestCreateOrUpdateRepoFileForCreate(t *testing.T) { 187 - // setup 188 - onGiteaRun(t, func(t *testing.T, u *url.URL) { 189 - ctx := test.MockContext(t, "user2/repo1") 190 - ctx.SetParams(":id", "1") 191 - test.LoadRepo(t, ctx, 1) 192 - test.LoadRepoCommit(t, ctx) 193 - test.LoadUser(t, ctx, 2) 194 - test.LoadGitRepo(t, ctx) 195 - defer ctx.Repo.GitRepo.Close() 196 - 197 - repo := ctx.Repo.Repository 198 - doer := ctx.Doer 199 - opts := getCreateRepoFileOptions(repo) 200 - 201 - // test 202 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 203 - 204 - // asserts 205 - assert.NoError(t, err) 206 - gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 207 - defer gitRepo.Close() 208 - 209 - commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch) 210 - lastCommit, _ := gitRepo.GetCommitByPath("new/file.txt") 211 - expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID, lastCommit.ID.String()) 212 - assert.NotNil(t, expectedFileResponse) 213 - if expectedFileResponse != nil { 214 - assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) 215 - assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) 216 - assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) 217 - assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) 218 - assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) 219 - } 220 - }) 221 - } 222 - 223 - func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) { 224 - // setup 225 - onGiteaRun(t, func(t *testing.T, u *url.URL) { 226 - ctx := test.MockContext(t, "user2/repo1") 227 - ctx.SetParams(":id", "1") 228 - test.LoadRepo(t, ctx, 1) 229 - test.LoadRepoCommit(t, ctx) 230 - test.LoadUser(t, ctx, 2) 231 - test.LoadGitRepo(t, ctx) 232 - defer ctx.Repo.GitRepo.Close() 233 - 234 - repo := ctx.Repo.Repository 235 - doer := ctx.Doer 236 - opts := getUpdateRepoFileOptions(repo) 237 - 238 - // test 239 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 240 - 241 - // asserts 242 - assert.NoError(t, err) 243 - gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 244 - defer gitRepo.Close() 245 - 246 - commit, _ := gitRepo.GetBranchCommit(opts.NewBranch) 247 - lastCommit, _ := commit.GetCommitByPath(opts.TreePath) 248 - expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String()) 249 - assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) 250 - assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) 251 - assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) 252 - assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) 253 - assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) 254 - }) 255 - } 256 - 257 - func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) { 258 - // setup 259 - onGiteaRun(t, func(t *testing.T, u *url.URL) { 260 - ctx := test.MockContext(t, "user2/repo1") 261 - ctx.SetParams(":id", "1") 262 - test.LoadRepo(t, ctx, 1) 263 - test.LoadRepoCommit(t, ctx) 264 - test.LoadUser(t, ctx, 2) 265 - test.LoadGitRepo(t, ctx) 266 - defer ctx.Repo.GitRepo.Close() 267 - 268 - repo := ctx.Repo.Repository 269 - doer := ctx.Doer 270 - opts := getUpdateRepoFileOptions(repo) 271 - opts.FromTreePath = "README.md" 272 - opts.TreePath = "README_new.md" // new file name, README_new.md 273 - 274 - // test 275 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 276 - 277 - // asserts 278 - assert.NoError(t, err) 279 - gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 280 - defer gitRepo.Close() 281 - 282 - commit, _ := gitRepo.GetBranchCommit(opts.NewBranch) 283 - lastCommit, _ := commit.GetCommitByPath(opts.TreePath) 284 - expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String()) 285 - // assert that the old file no longer exists in the last commit of the branch 286 - fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath) 287 - switch err.(type) { 288 - case git.ErrNotExist: 289 - // correct, continue 290 - default: 291 - t.Fatalf("expected git.ErrNotExist, got:%v", err) 292 - } 293 - toEntry, err := commit.GetTreeEntryByPath(opts.TreePath) 294 - assert.NoError(t, err) 295 - assert.Nil(t, fromEntry) // Should no longer exist here 296 - assert.NotNil(t, toEntry) // Should exist here 297 - // assert SHA has remained the same but paths use the new file name 298 - assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA) 299 - assert.EqualValues(t, expectedFileResponse.Content.Name, fileResponse.Content.Name) 300 - assert.EqualValues(t, expectedFileResponse.Content.Path, fileResponse.Content.Path) 301 - assert.EqualValues(t, expectedFileResponse.Content.URL, fileResponse.Content.URL) 302 - assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) 303 - assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) 304 - }) 305 - } 306 - 307 - // Test opts with branch names removed, should get same results as above test 308 - func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) { 309 - // setup 310 - onGiteaRun(t, func(t *testing.T, u *url.URL) { 311 - ctx := test.MockContext(t, "user2/repo1") 312 - ctx.SetParams(":id", "1") 313 - test.LoadRepo(t, ctx, 1) 314 - test.LoadRepoCommit(t, ctx) 315 - test.LoadUser(t, ctx, 2) 316 - test.LoadGitRepo(t, ctx) 317 - defer ctx.Repo.GitRepo.Close() 318 - 319 - repo := ctx.Repo.Repository 320 - doer := ctx.Doer 321 - opts := getUpdateRepoFileOptions(repo) 322 - opts.OldBranch = "" 323 - opts.NewBranch = "" 324 - 325 - // test 326 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 327 - 328 - // asserts 329 - assert.NoError(t, err) 330 - gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) 331 - defer gitRepo.Close() 332 - 333 - commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch) 334 - lastCommit, _ := commit.GetCommitByPath(opts.TreePath) 335 - expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath, lastCommit.ID.String()) 336 - assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) 337 - }) 338 - } 339 - 340 - func TestCreateOrUpdateRepoFileErrors(t *testing.T) { 341 - // setup 342 - onGiteaRun(t, func(t *testing.T, u *url.URL) { 343 - ctx := test.MockContext(t, "user2/repo1") 344 - ctx.SetParams(":id", "1") 345 - test.LoadRepo(t, ctx, 1) 346 - test.LoadRepoCommit(t, ctx) 347 - test.LoadUser(t, ctx, 2) 348 - test.LoadGitRepo(t, ctx) 349 - defer ctx.Repo.GitRepo.Close() 350 - 351 - repo := ctx.Repo.Repository 352 - doer := ctx.Doer 353 - 354 - t.Run("bad branch", func(t *testing.T) { 355 - opts := getUpdateRepoFileOptions(repo) 356 - opts.OldBranch = "bad_branch" 357 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 358 - assert.Error(t, err) 359 - assert.Nil(t, fileResponse) 360 - expectedError := "branch does not exist [name: " + opts.OldBranch + "]" 361 - assert.EqualError(t, err, expectedError) 362 - }) 363 - 364 - t.Run("bad SHA", func(t *testing.T) { 365 - opts := getUpdateRepoFileOptions(repo) 366 - origSHA := opts.SHA 367 - opts.SHA = "bad_sha" 368 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 369 - assert.Nil(t, fileResponse) 370 - assert.Error(t, err) 371 - expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]" 372 - assert.EqualError(t, err, expectedError) 373 - }) 374 - 375 - t.Run("new branch already exists", func(t *testing.T) { 376 - opts := getUpdateRepoFileOptions(repo) 377 - opts.NewBranch = "develop" 378 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 379 - assert.Nil(t, fileResponse) 380 - assert.Error(t, err) 381 - expectedError := "branch already exists [name: " + opts.NewBranch + "]" 382 - assert.EqualError(t, err, expectedError) 383 - }) 384 - 385 - t.Run("treePath is empty:", func(t *testing.T) { 386 - opts := getUpdateRepoFileOptions(repo) 387 - opts.TreePath = "" 388 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 389 - assert.Nil(t, fileResponse) 390 - assert.Error(t, err) 391 - expectedError := "path contains a malformed path component [path: ]" 392 - assert.EqualError(t, err, expectedError) 393 - }) 394 - 395 - t.Run("treePath is a git directory:", func(t *testing.T) { 396 - opts := getUpdateRepoFileOptions(repo) 397 - opts.TreePath = ".git" 398 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 399 - assert.Nil(t, fileResponse) 400 - assert.Error(t, err) 401 - expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]" 402 - assert.EqualError(t, err, expectedError) 403 - }) 404 - 405 - t.Run("create file that already exists", func(t *testing.T) { 406 - opts := getCreateRepoFileOptions(repo) 407 - opts.TreePath = "README.md" // already exists 408 - fileResponse, err := files_service.CreateOrUpdateRepoFile(git.DefaultContext, repo, doer, opts) 409 - assert.Nil(t, fileResponse) 410 - assert.Error(t, err) 411 - expectedError := "repository file already exists [path: " + opts.TreePath + "]" 412 - assert.EqualError(t, err, expectedError) 413 - }) 414 - }) 415 - }