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

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