gitea源码

automerge.go 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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. "code.gitea.io/gitea/services/automergequeue"
  23. notify_service "code.gitea.io/gitea/services/notify"
  24. pull_service "code.gitea.io/gitea/services/pull"
  25. repo_service "code.gitea.io/gitea/services/repository"
  26. )
  27. // Init runs the task queue to that handles auto merges
  28. func Init() error {
  29. notify_service.RegisterNotifier(NewNotifier())
  30. automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
  31. if automergequeue.AutoMergeQueue == nil {
  32. return errors.New("unable to create pr_auto_merge queue")
  33. }
  34. go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
  35. return nil
  36. }
  37. // handle passed PR IDs and test the PRs
  38. func handler(items ...string) []string {
  39. for _, s := range items {
  40. var id int64
  41. var sha string
  42. if _, err := fmt.Sscanf(s, "%d_%s", &id, &sha); err != nil {
  43. log.Error("could not parse data from pr_auto_merge queue (%v): %v", s, err)
  44. continue
  45. }
  46. handlePullRequestAutoMerge(id, sha)
  47. }
  48. return nil
  49. }
  50. // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
  51. func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) {
  52. err = db.WithTx(ctx, func(ctx context.Context) error {
  53. if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil {
  54. return err
  55. }
  56. _, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
  57. return err
  58. })
  59. // Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
  60. // If the transaction rolls back, then the pull request is not scheduled to auto merge.
  61. // So we should only set "scheduled" to true if there is no error.
  62. scheduled = err == nil
  63. if scheduled {
  64. log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
  65. automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
  66. }
  67. return scheduled, err
  68. }
  69. // RemoveScheduledAutoMerge cancels a previously scheduled pull request
  70. func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
  71. return db.WithTx(ctx, func(ctx context.Context) error {
  72. if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil {
  73. return err
  74. }
  75. _, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer)
  76. return err
  77. })
  78. }
  79. // StartPRCheckAndAutoMergeBySHA start an automerge check and auto merge task for all pull requests of repository and SHA
  80. func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_model.Repository) error {
  81. pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
  82. return !pr.HasMerged && pr.CanAutoMerge()
  83. })
  84. if err != nil {
  85. return err
  86. }
  87. for _, pr := range pulls {
  88. automergequeue.AddToQueue(pr, sha)
  89. }
  90. return nil
  91. }
  92. func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
  93. gitRepo, err := gitrepo.OpenRepository(ctx, repo)
  94. if err != nil {
  95. return nil, err
  96. }
  97. defer gitRepo.Close()
  98. refs, err := gitRepo.GetRefsBySha(sha, "")
  99. if err != nil {
  100. return nil, err
  101. }
  102. pulls := make(map[int64]*issues_model.PullRequest)
  103. for _, ref := range refs {
  104. // Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
  105. // use that to get the pr.
  106. if strings.HasPrefix(ref, git.PullPrefix) {
  107. parts := strings.Split(ref[len(git.PullPrefix):], "/")
  108. // e.g. 'refs/pull/1/head' would be []string{"1", "head"}
  109. if len(parts) != 2 {
  110. log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
  111. continue
  112. }
  113. prIndex, err := strconv.ParseInt(parts[0], 10, 64)
  114. if err != nil {
  115. log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
  116. continue
  117. }
  118. p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
  119. if err != nil {
  120. // If there is no pull request for this branch, we don't try to merge it.
  121. if issues_model.IsErrPullRequestNotExist(err) {
  122. continue
  123. }
  124. return nil, err
  125. }
  126. if filter(p) {
  127. pulls[p.ID] = p
  128. }
  129. }
  130. }
  131. return pulls, nil
  132. }
  133. // handlePullRequestAutoMerge merge the pull request if all checks are successful
  134. func handlePullRequestAutoMerge(pullID int64, sha string) {
  135. ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
  136. fmt.Sprintf("Handle AutoMerge of PR[%d] with sha[%s]", pullID, sha))
  137. defer finished()
  138. pr, err := issues_model.GetPullRequestByID(ctx, pullID)
  139. if err != nil {
  140. log.Error("GetPullRequestByID[%d]: %v", pullID, err)
  141. return
  142. }
  143. // Check if there is a scheduled pr in the db
  144. exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID)
  145. if err != nil {
  146. log.Error("%-v GetScheduledMergeByPullID: %v", pr, err)
  147. return
  148. }
  149. if !exists {
  150. return
  151. }
  152. if err = pr.LoadBaseRepo(ctx); err != nil {
  153. log.Error("%-v LoadBaseRepo: %v", pr, err)
  154. return
  155. }
  156. // check the sha is the same as pull request head commit id
  157. baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
  158. if err != nil {
  159. log.Error("OpenRepository: %v", err)
  160. return
  161. }
  162. defer baseGitRepo.Close()
  163. headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  164. if err != nil {
  165. log.Error("GetRefCommitID: %v", err)
  166. return
  167. }
  168. if headCommitID != sha {
  169. 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)
  170. return
  171. }
  172. // Get all checks for this pr
  173. // We get the latest sha commit hash again to handle the case where the check of a previous push
  174. // did not succeed or was not finished yet.
  175. if err = pr.LoadHeadRepo(ctx); err != nil {
  176. log.Error("%-v LoadHeadRepo: %v", pr, err)
  177. return
  178. }
  179. var headGitRepo *git.Repository
  180. if pr.BaseRepoID == pr.HeadRepoID {
  181. headGitRepo = baseGitRepo
  182. } else {
  183. headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
  184. if err != nil {
  185. log.Error("OpenRepository %-v: %v", pr.HeadRepo, err)
  186. return
  187. }
  188. defer headGitRepo.Close()
  189. }
  190. switch pr.Flow {
  191. case issues_model.PullRequestFlowGithub:
  192. headBranchExist := pr.HeadRepo != nil && gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch)
  193. if !headBranchExist {
  194. log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch: %s]", pr, pr.HeadRepoID, pr.HeadBranch)
  195. return
  196. }
  197. case issues_model.PullRequestFlowAGit:
  198. headBranchExist := gitrepo.IsReferenceExist(ctx, pr.BaseRepo, pr.GetGitHeadRefName())
  199. if !headBranchExist {
  200. log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch(Agit): %s]", pr, pr.HeadRepoID, pr.HeadBranch)
  201. return
  202. }
  203. default:
  204. log.Error("wrong flow type %d", pr.Flow)
  205. return
  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. // Merge if all checks succeeded
  218. doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
  219. if err != nil {
  220. log.Error("Unable to get scheduled User[%d]: %v", scheduledPRM.DoerID, err)
  221. return
  222. }
  223. perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
  224. if err != nil {
  225. log.Error("GetUserRepoPermission %-v: %v", pr.HeadRepo, err)
  226. return
  227. }
  228. if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
  229. if errors.Is(err, pull_service.ErrNotReadyToMerge) {
  230. log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
  231. return
  232. }
  233. log.Error("%-v CheckPullMergeable: %v", pr, err)
  234. return
  235. }
  236. if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
  237. log.Error("pull_service.Merge: %v", err)
  238. // FIXME: if merge failed, we should display some error message to the pull request page.
  239. // The resolution is add a new column on automerge table named `error_message` to store the error message and displayed
  240. // on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch.
  241. return
  242. }
  243. if pr.Flow == issues_model.PullRequestFlowGithub && scheduledPRM.DeleteBranchAfterMerge {
  244. if err := repo_service.DeleteBranch(ctx, doer, pr.HeadRepo, headGitRepo, pr.HeadBranch, pr); err != nil {
  245. log.Error("DeletePullRequestHeadBranch: %v", err)
  246. }
  247. }
  248. }