code.gitea.io/gitea@v1.22.3/services/automerge/automerge.go (about) 1 // Copyright 2021 Gitea. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package automerge 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strconv" 11 "strings" 12 13 "code.gitea.io/gitea/models/db" 14 issues_model "code.gitea.io/gitea/models/issues" 15 access_model "code.gitea.io/gitea/models/perm/access" 16 pull_model "code.gitea.io/gitea/models/pull" 17 repo_model "code.gitea.io/gitea/models/repo" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/git" 20 "code.gitea.io/gitea/modules/gitrepo" 21 "code.gitea.io/gitea/modules/graceful" 22 "code.gitea.io/gitea/modules/log" 23 "code.gitea.io/gitea/modules/process" 24 "code.gitea.io/gitea/modules/queue" 25 notify_service "code.gitea.io/gitea/services/notify" 26 pull_service "code.gitea.io/gitea/services/pull" 27 ) 28 29 // prAutoMergeQueue represents a queue to handle update pull request tests 30 var prAutoMergeQueue *queue.WorkerPoolQueue[string] 31 32 // Init runs the task queue to that handles auto merges 33 func Init() error { 34 notify_service.RegisterNotifier(NewNotifier()) 35 36 prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler) 37 if prAutoMergeQueue == nil { 38 return fmt.Errorf("unable to create pr_auto_merge queue") 39 } 40 go graceful.GetManager().RunWithCancel(prAutoMergeQueue) 41 return nil 42 } 43 44 // handle passed PR IDs and test the PRs 45 func handler(items ...string) []string { 46 for _, s := range items { 47 var id int64 48 var sha string 49 if _, err := fmt.Sscanf(s, "%d_%s", &id, &sha); err != nil { 50 log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err) 51 continue 52 } 53 handlePullRequestAutoMerge(id, sha) 54 } 55 return nil 56 } 57 58 func addToQueue(pr *issues_model.PullRequest, sha string) { 59 log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) 60 if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil { 61 log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err) 62 } 63 } 64 65 // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly 66 func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { 67 err = db.WithTx(ctx, func(ctx context.Context) error { 68 if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil { 69 return err 70 } 71 scheduled = true 72 73 _, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer) 74 return err 75 }) 76 return scheduled, err 77 } 78 79 // RemoveScheduledAutoMerge cancels a previously scheduled pull request 80 func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error { 81 return db.WithTx(ctx, func(ctx context.Context) error { 82 if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil { 83 return err 84 } 85 86 _, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer) 87 return err 88 }) 89 } 90 91 // StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA 92 func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error { 93 pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool { 94 return !pr.HasMerged && pr.CanAutoMerge() 95 }) 96 if err != nil { 97 return err 98 } 99 100 for _, pr := range pulls { 101 addToQueue(pr, sha) 102 } 103 104 return nil 105 } 106 107 // StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request 108 func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) { 109 if pull == nil || pull.HasMerged || !pull.CanAutoMerge() { 110 return 111 } 112 113 if err := pull.LoadBaseRepo(ctx); err != nil { 114 log.Error("LoadBaseRepo: %v", err) 115 return 116 } 117 118 gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) 119 if err != nil { 120 log.Error("OpenRepository: %v", err) 121 return 122 } 123 defer gitRepo.Close() 124 commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) 125 if err != nil { 126 log.Error("GetRefCommitID: %v", err) 127 return 128 } 129 130 addToQueue(pull, commitID) 131 } 132 133 func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { 134 gitRepo, err := gitrepo.OpenRepository(ctx, repo) 135 if err != nil { 136 return nil, err 137 } 138 defer gitRepo.Close() 139 140 refs, err := gitRepo.GetRefsBySha(sha, "") 141 if err != nil { 142 return nil, err 143 } 144 145 pulls := make(map[int64]*issues_model.PullRequest) 146 147 for _, ref := range refs { 148 // Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then 149 // use that to get the pr. 150 if strings.HasPrefix(ref, git.PullPrefix) { 151 parts := strings.Split(ref[len(git.PullPrefix):], "/") 152 153 // e.g. 'refs/pull/1/head' would be []string{"1", "head"} 154 if len(parts) != 2 { 155 log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo) 156 continue 157 } 158 159 prIndex, err := strconv.ParseInt(parts[0], 10, 64) 160 if err != nil { 161 log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo) 162 continue 163 } 164 165 p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex) 166 if err != nil { 167 // If there is no pull request for this branch, we don't try to merge it. 168 if issues_model.IsErrPullRequestNotExist(err) { 169 continue 170 } 171 return nil, err 172 } 173 174 if filter(p) { 175 pulls[p.ID] = p 176 } 177 } 178 } 179 180 return pulls, nil 181 } 182 183 // handlePullRequestAutoMerge merge the pull request if all checks are successful 184 func handlePullRequestAutoMerge(pullID int64, sha string) { 185 ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), 186 fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha)) 187 defer finished() 188 189 pr, err := issues_model.GetPullRequestByID(ctx, pullID) 190 if err != nil { 191 log.Error("GetPullRequestByID[%d]: %v", pullID, err) 192 return 193 } 194 195 // Check if there is a scheduled pr in the db 196 exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID) 197 if err != nil { 198 log.Error("%-v GetScheduledMergeByPullID: %v", pr, err) 199 return 200 } 201 if !exists { 202 return 203 } 204 205 if err = pr.LoadBaseRepo(ctx); err != nil { 206 log.Error("%-v LoadBaseRepo: %v", pr, err) 207 return 208 } 209 210 // check the sha is the same as pull request head commit id 211 baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) 212 if err != nil { 213 log.Error("OpenRepository: %v", err) 214 return 215 } 216 defer baseGitRepo.Close() 217 218 headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) 219 if err != nil { 220 log.Error("GetRefCommitID: %v", err) 221 return 222 } 223 if headCommitID != sha { 224 log.Warn("Head commit id of auto merge %-v does not match sha [%s], it may means the head branch has been updated. Just ignore this request because a new request expected in the queue", pr, sha) 225 return 226 } 227 228 // Get all checks for this pr 229 // We get the latest sha commit hash again to handle the case where the check of a previous push 230 // did not succeed or was not finished yet. 231 if err = pr.LoadHeadRepo(ctx); err != nil { 232 log.Error("%-v LoadHeadRepo: %v", pr, err) 233 return 234 } 235 236 var headGitRepo *git.Repository 237 if pr.BaseRepoID == pr.HeadRepoID { 238 headGitRepo = baseGitRepo 239 } else { 240 headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo) 241 if err != nil { 242 log.Error("OpenRepository %-v: %v", pr.HeadRepo, err) 243 return 244 } 245 defer headGitRepo.Close() 246 } 247 248 switch pr.Flow { 249 case issues_model.PullRequestFlowGithub: 250 headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch) 251 if pr.HeadRepo == nil || !headBranchExist { 252 log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch) 253 return 254 } 255 case issues_model.PullRequestFlowAGit: 256 headBranchExist := git.IsReferenceExist(ctx, baseGitRepo.Path, pr.GetGitRefName()) 257 if !headBranchExist { 258 log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch(Agit): %s]", pr, pr.HeadRepoID, pr.HeadBranch) 259 return 260 } 261 default: 262 log.Error("wrong flow type %d", pr.Flow) 263 return 264 } 265 266 // Check if all checks succeeded 267 pass, err := pull_service.IsPullCommitStatusPass(ctx, pr) 268 if err != nil { 269 log.Error("%-v IsPullCommitStatusPass: %v", pr, err) 270 return 271 } 272 if !pass { 273 log.Info("Scheduled auto merge %-v has unsuccessful status checks", pr) 274 return 275 } 276 277 // Merge if all checks succeeded 278 doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID) 279 if err != nil { 280 log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err) 281 return 282 } 283 284 perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) 285 if err != nil { 286 log.Error("GetUserRepoPermission %-v: %v", pr.HeadRepo, err) 287 return 288 } 289 290 if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { 291 if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) { 292 log.Info("%-v was scheduled to automerge by an unauthorized user", pr) 293 return 294 } 295 log.Error("%-v CheckPullMergeable: %v", pr, err) 296 return 297 } 298 299 if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil { 300 log.Error("pull_service.Merge: %v", err) 301 // FIXME: if merge failed, we should display some error message to the pull request page. 302 // The resolution is add a new column on automerge table named `error_message` to store the error message and displayed 303 // on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch. 304 return 305 } 306 }