You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

automerge.go 7.7KB

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