code.gitea.io/gitea@v1.21.7/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/graceful" 21 "code.gitea.io/gitea/modules/log" 22 "code.gitea.io/gitea/modules/process" 23 "code.gitea.io/gitea/modules/queue" 24 pull_service "code.gitea.io/gitea/services/pull" 25 ) 26 27 // prAutoMergeQueue represents a queue to handle update pull request tests 28 var prAutoMergeQueue *queue.WorkerPoolQueue[string] 29 30 // Init runs the task queue to that handles auto merges 31 func Init() error { 32 prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler) 33 if prAutoMergeQueue == nil { 34 return fmt.Errorf("unable to create pr_auto_merge queue") 35 } 36 go graceful.GetManager().RunWithCancel(prAutoMergeQueue) 37 return nil 38 } 39 40 // handle passed PR IDs and test the PRs 41 func handler(items ...string) []string { 42 for _, s := range items { 43 var id int64 44 var sha string 45 if _, err := fmt.Sscanf(s, "%d_%s", &id, &sha); err != nil { 46 log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err) 47 continue 48 } 49 handlePull(id, sha) 50 } 51 return nil 52 } 53 54 func addToQueue(pr *issues_model.PullRequest, sha string) { 55 log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha) 56 if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil { 57 log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err) 58 } 59 } 60 61 // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly 62 func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { 63 err = db.WithTx(ctx, func(ctx context.Context) error { 64 lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull) 65 if err != nil { 66 return err 67 } 68 69 // we don't need to schedule 70 if lastCommitStatus.IsSuccess() { 71 return nil 72 } 73 74 if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil { 75 return err 76 } 77 scheduled = true 78 79 _, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer) 80 return err 81 }) 82 return scheduled, err 83 } 84 85 // RemoveScheduledAutoMerge cancels a previously scheduled pull request 86 func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error { 87 return db.WithTx(ctx, func(ctx context.Context) error { 88 if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil { 89 return err 90 } 91 92 _, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer) 93 return err 94 }) 95 } 96 97 // MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded 98 func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error { 99 pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool { 100 return !pr.HasMerged && pr.CanAutoMerge() 101 }) 102 if err != nil { 103 return err 104 } 105 106 for _, pr := range pulls { 107 addToQueue(pr, sha) 108 } 109 110 return nil 111 } 112 113 func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { 114 gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) 115 if err != nil { 116 return nil, err 117 } 118 defer gitRepo.Close() 119 120 refs, err := gitRepo.GetRefsBySha(sha, "") 121 if err != nil { 122 return nil, err 123 } 124 125 pulls := make(map[int64]*issues_model.PullRequest) 126 127 for _, ref := range refs { 128 // Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then 129 // use that to get the pr. 130 if strings.HasPrefix(ref, git.PullPrefix) { 131 parts := strings.Split(ref[len(git.PullPrefix):], "/") 132 133 // e.g. 'refs/pull/1/head' would be []string{"1", "head"} 134 if len(parts) != 2 { 135 log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo) 136 continue 137 } 138 139 prIndex, err := strconv.ParseInt(parts[0], 10, 64) 140 if err != nil { 141 log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo) 142 continue 143 } 144 145 p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex) 146 if err != nil { 147 // If there is no pull request for this branch, we don't try to merge it. 148 if issues_model.IsErrPullRequestNotExist(err) { 149 continue 150 } 151 return nil, err 152 } 153 154 if filter(p) { 155 pulls[p.ID] = p 156 } 157 } 158 } 159 160 return pulls, nil 161 } 162 163 func handlePull(pullID int64, sha string) { 164 ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), 165 fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha)) 166 defer finished() 167 168 pr, err := issues_model.GetPullRequestByID(ctx, pullID) 169 if err != nil { 170 log.Error("GetPullRequestByID[%d]: %v", pullID, err) 171 return 172 } 173 174 // Check if there is a scheduled pr in the db 175 exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID) 176 if err != nil { 177 log.Error("%-v GetScheduledMergeByPullID: %v", pr, err) 178 return 179 } 180 if !exists { 181 return 182 } 183 184 // Get all checks for this pr 185 // We get the latest sha commit hash again to handle the case where the check of a previous push 186 // did not succeed or was not finished yet. 187 188 if err = pr.LoadHeadRepo(ctx); err != nil { 189 log.Error("%-v LoadHeadRepo: %v", pr, err) 190 return 191 } 192 193 headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) 194 if err != nil { 195 log.Error("OpenRepository %-v: %v", pr.HeadRepo, err) 196 return 197 } 198 defer headGitRepo.Close() 199 200 headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch) 201 202 if pr.HeadRepo == nil || !headBranchExist { 203 log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch) 204 return 205 } 206 207 // Check if all checks succeeded 208 pass, err := pull_service.IsPullCommitStatusPass(ctx, pr) 209 if err != nil { 210 log.Error("%-v IsPullCommitStatusPass: %v", pr, err) 211 return 212 } 213 if !pass { 214 log.Info("Scheduled auto merge %-v has unsuccessful status checks", pr) 215 return 216 } 217 218 // Merge if all checks succeeded 219 doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID) 220 if err != nil { 221 log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err) 222 return 223 } 224 225 perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) 226 if err != nil { 227 log.Error("GetUserRepoPermission %-v: %v", pr.HeadRepo, err) 228 return 229 } 230 231 if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil { 232 if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) { 233 log.Info("%-v was scheduled to automerge by an unauthorized user", pr) 234 return 235 } 236 log.Error("%-v CheckPullMergable: %v", pr, err) 237 return 238 } 239 240 var baseGitRepo *git.Repository 241 if pr.BaseRepoID == pr.HeadRepoID { 242 baseGitRepo = headGitRepo 243 } else { 244 if err = pr.LoadBaseRepo(ctx); err != nil { 245 log.Error("%-v LoadBaseRepo: %v", pr, err) 246 return 247 } 248 249 baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) 250 if err != nil { 251 log.Error("OpenRepository %-v: %v", pr.BaseRepo, err) 252 return 253 } 254 defer baseGitRepo.Close() 255 } 256 257 if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil { 258 log.Error("pull_service.Merge: %v", err) 259 return 260 } 261 }