code.gitea.io/gitea@v1.22.3/services/repository/files/patch.go (about) 1 // Copyright 2021 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 git_model "code.gitea.io/gitea/models/git" 13 repo_model "code.gitea.io/gitea/models/repo" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/git" 16 "code.gitea.io/gitea/modules/gitrepo" 17 "code.gitea.io/gitea/modules/log" 18 "code.gitea.io/gitea/modules/structs" 19 asymkey_service "code.gitea.io/gitea/services/asymkey" 20 ) 21 22 // ApplyDiffPatchOptions holds the repository diff patch update options 23 type ApplyDiffPatchOptions struct { 24 LastCommitID string 25 OldBranch string 26 NewBranch string 27 Message string 28 Content string 29 SHA string 30 Author *IdentityOptions 31 Committer *IdentityOptions 32 Dates *CommitDateOptions 33 Signoff bool 34 } 35 36 // Validate validates the provided options 37 func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { 38 // If no branch name is set, assume master 39 if opts.OldBranch == "" { 40 opts.OldBranch = repo.DefaultBranch 41 } 42 if opts.NewBranch == "" { 43 opts.NewBranch = opts.OldBranch 44 } 45 46 gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) 47 if err != nil { 48 return err 49 } 50 defer closer.Close() 51 52 // oldBranch must exist for this operation 53 if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil { 54 return err 55 } 56 // A NewBranch can be specified for the patch to be applied to. 57 // Check to make sure the branch does not already exist, otherwise we can't proceed. 58 // If we aren't branching to a new branch, make sure user can commit to the given branch 59 if opts.NewBranch != opts.OldBranch { 60 existingBranch, err := gitRepo.GetBranch(opts.NewBranch) 61 if existingBranch != nil { 62 return git_model.ErrBranchAlreadyExists{ 63 BranchName: opts.NewBranch, 64 } 65 } 66 if err != nil && !git.IsErrBranchNotExist(err) { 67 return err 68 } 69 } else { 70 protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, opts.OldBranch) 71 if err != nil { 72 return err 73 } 74 if protectedBranch != nil { 75 protectedBranch.Repo = repo 76 if !protectedBranch.CanUserPush(ctx, doer) { 77 return models.ErrUserCannotCommit{ 78 UserName: doer.LowerName, 79 } 80 } 81 } 82 if protectedBranch != nil && protectedBranch.RequireSignedCommits { 83 _, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), opts.OldBranch) 84 if err != nil { 85 if !asymkey_service.IsErrWontSign(err) { 86 return err 87 } 88 return models.ErrUserCannotCommit{ 89 UserName: doer.LowerName, 90 } 91 } 92 } 93 } 94 return nil 95 } 96 97 // ApplyDiffPatch applies a patch to the given repository 98 func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) { 99 err := repo.MustNotBeArchived() 100 if err != nil { 101 return nil, err 102 } 103 104 if err := opts.Validate(ctx, repo, doer); err != nil { 105 return nil, err 106 } 107 108 message := strings.TrimSpace(opts.Message) 109 110 author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) 111 112 t, err := NewTemporaryUploadRepository(ctx, repo) 113 if err != nil { 114 log.Error("NewTemporaryUploadRepository failed: %v", err) 115 } 116 defer t.Close() 117 if err := t.Clone(opts.OldBranch, true); err != nil { 118 return nil, err 119 } 120 if err := t.SetDefaultIndex(); err != nil { 121 return nil, err 122 } 123 124 // Get the commit of the original branch 125 commit, err := t.GetBranchCommit(opts.OldBranch) 126 if err != nil { 127 return nil, err // Couldn't get a commit for the branch 128 } 129 130 // Assigned LastCommitID in opts if it hasn't been set 131 if opts.LastCommitID == "" { 132 opts.LastCommitID = commit.ID.String() 133 } else { 134 lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID) 135 if err != nil { 136 return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %w", err) 137 } 138 opts.LastCommitID = lastCommitID.String() 139 if commit.ID.String() != opts.LastCommitID { 140 return nil, models.ErrCommitIDDoesNotMatch{ 141 GivenCommitID: opts.LastCommitID, 142 CurrentCommitID: opts.LastCommitID, 143 } 144 } 145 } 146 147 stdout := &strings.Builder{} 148 stderr := &strings.Builder{} 149 150 cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary") 151 if git.DefaultFeatures().CheckVersionAtLeast("2.32") { 152 cmdApply.AddArguments("-3") 153 } 154 155 if err := cmdApply.Run(&git.RunOpts{ 156 Dir: t.basePath, 157 Stdout: stdout, 158 Stderr: stderr, 159 Stdin: strings.NewReader(opts.Content), 160 }); err != nil { 161 return nil, fmt.Errorf("Error: Stdout: %s\nStderr: %s\nErr: %w", stdout.String(), stderr.String(), err) 162 } 163 164 // Now write the tree 165 treeHash, err := t.WriteTree() 166 if err != nil { 167 return nil, err 168 } 169 170 // Now commit the tree 171 var commitHash string 172 if opts.Dates != nil { 173 commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) 174 } else { 175 commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) 176 } 177 if err != nil { 178 return nil, err 179 } 180 181 // Then push this tree to NewBranch 182 if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { 183 return nil, err 184 } 185 186 commit, err = t.GetCommit(commitHash) 187 if err != nil { 188 return nil, err 189 } 190 191 fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil 192 verification := GetPayloadCommitVerification(ctx, commit) 193 fileResponse := &structs.FileResponse{ 194 Commit: fileCommitResponse, 195 Verification: verification, 196 } 197 198 return fileResponse, nil 199 }