gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package pull
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "code.gitea.io/gitea/models/db"
  12. git_model "code.gitea.io/gitea/models/git"
  13. issues_model "code.gitea.io/gitea/models/issues"
  14. access_model "code.gitea.io/gitea/models/perm/access"
  15. "code.gitea.io/gitea/models/pull"
  16. repo_model "code.gitea.io/gitea/models/repo"
  17. "code.gitea.io/gitea/models/unit"
  18. user_model "code.gitea.io/gitea/models/user"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/git/gitcmd"
  21. "code.gitea.io/gitea/modules/gitrepo"
  22. "code.gitea.io/gitea/modules/globallock"
  23. "code.gitea.io/gitea/modules/graceful"
  24. "code.gitea.io/gitea/modules/log"
  25. "code.gitea.io/gitea/modules/process"
  26. "code.gitea.io/gitea/modules/queue"
  27. "code.gitea.io/gitea/modules/setting"
  28. "code.gitea.io/gitea/modules/timeutil"
  29. asymkey_service "code.gitea.io/gitea/services/asymkey"
  30. "code.gitea.io/gitea/services/automergequeue"
  31. notify_service "code.gitea.io/gitea/services/notify"
  32. )
  33. // prPatchCheckerQueue represents a queue to handle update pull request tests
  34. var prPatchCheckerQueue *queue.WorkerPoolQueue[string]
  35. var (
  36. ErrIsClosed = errors.New("pull is closed")
  37. ErrNoPermissionToMerge = errors.New("no permission to merge")
  38. ErrNotReadyToMerge = errors.New("not ready to merge")
  39. ErrHasMerged = errors.New("has already been merged")
  40. ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
  41. ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
  42. ErrNotMergeableState = errors.New("not in mergeable state")
  43. ErrDependenciesLeft = errors.New("is blocked by an open dependency")
  44. )
  45. func markPullRequestStatusAsChecking(ctx context.Context, pr *issues_model.PullRequest) bool {
  46. pr.Status = issues_model.PullRequestStatusChecking
  47. err := pr.UpdateColsIfNotMerged(ctx, "status")
  48. if err != nil {
  49. log.Error("UpdateColsIfNotMerged failed, pr: %-v, err: %v", pr, err)
  50. return false
  51. }
  52. pr, err = issues_model.GetPullRequestByID(ctx, pr.ID)
  53. if err != nil {
  54. log.Error("GetPullRequestByID failed, pr: %-v, err: %v", pr, err)
  55. return false
  56. }
  57. return pr.Status == issues_model.PullRequestStatusChecking
  58. }
  59. var AddPullRequestToCheckQueue = realAddPullRequestToCheckQueue
  60. func realAddPullRequestToCheckQueue(prID int64) {
  61. err := prPatchCheckerQueue.Push(strconv.FormatInt(prID, 10))
  62. if err != nil && !errors.Is(err, queue.ErrAlreadyInQueue) {
  63. log.Error("Error adding %v to the pull requests check queue: %v", prID, err)
  64. }
  65. }
  66. func StartPullRequestCheckImmediately(ctx context.Context, pr *issues_model.PullRequest) {
  67. if !markPullRequestStatusAsChecking(ctx, pr) {
  68. return
  69. }
  70. AddPullRequestToCheckQueue(pr.ID)
  71. }
  72. // StartPullRequestCheckDelayable will delay the check if the pull request was not updated recently.
  73. // When the "base" branch gets updated, all PRs targeting that "base" branch need to re-check whether
  74. // they are mergeable.
  75. // When there are too many stale PRs, each "base" branch update will consume a lot of system resources.
  76. // So we can delay the checks for PRs that were not updated recently, only mark their status as
  77. // "checking", and then next time when these PRs are updated or viewed, the real checks will run.
  78. func StartPullRequestCheckDelayable(ctx context.Context, pr *issues_model.PullRequest) {
  79. if !markPullRequestStatusAsChecking(ctx, pr) {
  80. return
  81. }
  82. if setting.Repository.PullRequest.DelayCheckForInactiveDays >= 0 {
  83. if err := pr.LoadIssue(ctx); err != nil {
  84. return
  85. }
  86. duration := 24 * time.Hour * time.Duration(setting.Repository.PullRequest.DelayCheckForInactiveDays)
  87. if pr.Issue.UpdatedUnix.AddDuration(duration) <= timeutil.TimeStampNow() {
  88. return
  89. }
  90. }
  91. AddPullRequestToCheckQueue(pr.ID)
  92. }
  93. func StartPullRequestCheckOnView(ctx context.Context, pr *issues_model.PullRequest) {
  94. // TODO: its correctness totally depends on the "unique queue" feature and the global lock.
  95. // So duplicate "start" requests will be ignored if there is already a task in the queue or one is running.
  96. // Ideally in the future we should decouple the "unique queue" feature from the "start" request.
  97. if pr.Status == issues_model.PullRequestStatusChecking {
  98. if setting.IsInTesting {
  99. // In testing mode, there might be an "immediate" queue, which is not a real queue, everything is executed in the same goroutine
  100. // So we can't use the global lock here, otherwise it will cause a deadlock.
  101. AddPullRequestToCheckQueue(pr.ID)
  102. } else {
  103. // When a PR check starts, the task is popped from the queue and the task handler acquires the global lock
  104. // So we need to acquire the global lock here to prevent from duplicate tasks
  105. _, _ = globallock.TryLockAndDo(ctx, getPullWorkingLockKey(pr.ID), func(ctx context.Context) error {
  106. AddPullRequestToCheckQueue(pr.ID) // the queue is a unique queue and won't add the same task again
  107. return nil
  108. })
  109. }
  110. }
  111. }
  112. type MergeCheckType int
  113. const (
  114. MergeCheckTypeGeneral MergeCheckType = iota // general merge checks for "merge", "rebase", "squash", etc
  115. MergeCheckTypeManually // Manually Merged button (mark a PR as merged manually)
  116. MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed
  117. )
  118. // CheckPullMergeable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
  119. func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminForceMerge bool) error {
  120. return db.WithTx(stdCtx, func(ctx context.Context) error {
  121. if pr.HasMerged {
  122. return ErrHasMerged
  123. }
  124. if err := pr.LoadIssue(ctx); err != nil {
  125. log.Error("Unable to load issue[%d] for %-v: %v", pr.IssueID, pr, err)
  126. return err
  127. } else if pr.Issue.IsClosed {
  128. return ErrIsClosed
  129. }
  130. if allowedMerge, err := IsUserAllowedToMerge(ctx, pr, *perm, doer); err != nil {
  131. log.Error("Error whilst checking if %-v is allowed to merge %-v: %v", doer, pr, err)
  132. return err
  133. } else if !allowedMerge {
  134. return ErrNoPermissionToMerge
  135. }
  136. if mergeCheckType == MergeCheckTypeManually {
  137. // if doer is doing "manually merge" (mark as merged manually), do not check anything
  138. return nil
  139. }
  140. if pr.IsWorkInProgress(ctx) {
  141. return ErrIsWorkInProgress
  142. }
  143. if !pr.CanAutoMerge() && !pr.IsEmpty() {
  144. return ErrNotMergeableState
  145. }
  146. if pr.IsChecking() {
  147. return ErrIsChecking
  148. }
  149. if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
  150. if !errors.Is(err, ErrNotReadyToMerge) {
  151. log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
  152. return err
  153. }
  154. // Now the branch protection check failed, check whether the failure could be skipped (skip by setting err = nil)
  155. // * when doing Auto Merge (Scheduled Merge After Checks Succeed), skip the branch protection check
  156. if mergeCheckType == MergeCheckTypeAuto {
  157. err = nil
  158. }
  159. // * if admin tries to "Force Merge", they could sometimes skip the branch protection check
  160. if adminForceMerge {
  161. isRepoAdmin, errForceMerge := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer)
  162. if errForceMerge != nil {
  163. return fmt.Errorf("IsUserRepoAdmin failed, repo: %v, doer: %v, err: %w", pr.BaseRepoID, doer.ID, errForceMerge)
  164. }
  165. protectedBranchRule, errForceMerge := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
  166. if errForceMerge != nil {
  167. return fmt.Errorf("GetFirstMatchProtectedBranchRule failed, repo: %v, base branch: %v, err: %w", pr.BaseRepoID, pr.BaseBranch, errForceMerge)
  168. }
  169. // if doer is admin and the "Force Merge" is not blocked, then clear the branch protection check error
  170. blockAdminForceMerge := protectedBranchRule != nil && protectedBranchRule.BlockAdminMergeOverride
  171. if isRepoAdmin && !blockAdminForceMerge {
  172. err = nil
  173. }
  174. }
  175. // If there is still a branch protection check error, return it
  176. if err != nil {
  177. return err
  178. }
  179. }
  180. if _, err := isSignedIfRequired(ctx, pr, doer); err != nil {
  181. return err
  182. }
  183. if noDeps, err := issues_model.IssueNoDependenciesLeft(ctx, pr.Issue); err != nil {
  184. return err
  185. } else if !noDeps {
  186. return ErrDependenciesLeft
  187. }
  188. return nil
  189. })
  190. }
  191. // isSignedIfRequired check if merge will be signed if required
  192. func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) {
  193. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
  194. if err != nil {
  195. return false, err
  196. }
  197. if pb == nil || !pb.RequireSignedCommits {
  198. return true, nil
  199. }
  200. sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName())
  201. return sign, err
  202. }
  203. // markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
  204. // and set to be either conflict or mergeable.
  205. func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
  206. // If the status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
  207. if pr.Status == issues_model.PullRequestStatusChecking {
  208. pr.Status = issues_model.PullRequestStatusMergeable
  209. }
  210. // Make sure there is no waiting test to process before leaving the checking status.
  211. has, err := prPatchCheckerQueue.Has(strconv.FormatInt(pr.ID, 10))
  212. if err != nil {
  213. log.Error("Unable to check if the queue is waiting to reprocess %-v. Error: %v", pr, err)
  214. }
  215. if has {
  216. log.Trace("Not updating status for %-v as it is due to be rechecked", pr)
  217. return
  218. }
  219. if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
  220. log.Error("Update[%-v]: %v", pr, err)
  221. }
  222. // if there is a scheduled merge for this pull request, start the auto merge check (again)
  223. exist, _, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
  224. if err != nil {
  225. log.Error("GetScheduledMergeByPullID[%-v]: %v", pr, err)
  226. return
  227. } else if !exist {
  228. return
  229. }
  230. automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
  231. }
  232. // getMergeCommit checks if a pull request has been merged
  233. // Returns the git.Commit of the pull request if merged
  234. func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Commit, error) {
  235. if err := pr.LoadBaseRepo(ctx); err != nil {
  236. return nil, fmt.Errorf("unable to load base repo for %s: %w", pr, err)
  237. }
  238. prHeadRef := pr.GetGitHeadRefName()
  239. // Check if the pull request is merged into BaseBranch
  240. if _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").
  241. AddDynamicArguments(prHeadRef, pr.BaseBranch).
  242. RunStdString(ctx, &gitcmd.RunOpts{Dir: pr.BaseRepo.RepoPath()}); err != nil {
  243. if strings.Contains(err.Error(), "exit status 1") {
  244. // prHeadRef is not an ancestor of the base branch
  245. return nil, nil
  246. }
  247. // Errors are signaled by a non-zero status that is not 1
  248. return nil, fmt.Errorf("%-v git merge-base --is-ancestor: %w", pr, err)
  249. }
  250. // If merge-base successfully exits then prHeadRef is an ancestor of pr.BaseBranch
  251. // Find the head commit id
  252. prHeadCommitID, err := git.GetFullCommitID(ctx, pr.BaseRepo.RepoPath(), prHeadRef)
  253. if err != nil {
  254. return nil, fmt.Errorf("GetFullCommitID(%s) in %s: %w", prHeadRef, pr.BaseRepo.FullName(), err)
  255. }
  256. gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
  257. if err != nil {
  258. return nil, fmt.Errorf("%-v OpenRepository: %w", pr.BaseRepo, err)
  259. }
  260. defer gitRepo.Close()
  261. objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
  262. // Get the commit from BaseBranch where the pull request got merged
  263. mergeCommit, _, err := gitcmd.NewCommand("rev-list", "--ancestry-path", "--merges", "--reverse").
  264. AddDynamicArguments(prHeadCommitID+".."+pr.BaseBranch).
  265. RunStdString(ctx, &gitcmd.RunOpts{Dir: pr.BaseRepo.RepoPath()})
  266. if err != nil {
  267. return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %w", err)
  268. } else if len(mergeCommit) < objectFormat.FullLength() {
  269. // PR was maybe fast-forwarded, so just use last commit of PR
  270. mergeCommit = prHeadCommitID
  271. }
  272. mergeCommit = strings.TrimSpace(mergeCommit)
  273. commit, err := gitRepo.GetCommit(mergeCommit)
  274. if err != nil {
  275. return nil, fmt.Errorf("GetMergeCommit[%s]: %w", mergeCommit, err)
  276. }
  277. return commit, nil
  278. }
  279. // manuallyMerged checks if a pull request got manually merged
  280. // When a pull request got manually merged mark the pull request as merged
  281. func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool {
  282. if err := pr.LoadBaseRepo(ctx); err != nil {
  283. log.Error("%-v LoadBaseRepo: %v", pr, err)
  284. return false
  285. }
  286. if unit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests); err == nil {
  287. config := unit.PullRequestsConfig()
  288. if !config.AutodetectManualMerge {
  289. return false
  290. }
  291. } else {
  292. log.Error("%-v BaseRepo.GetUnit(unit.TypePullRequests): %v", pr, err)
  293. return false
  294. }
  295. commit, err := getMergeCommit(ctx, pr)
  296. if err != nil {
  297. log.Error("%-v getMergeCommit: %v", pr, err)
  298. return false
  299. }
  300. if commit == nil {
  301. // no merge commit found
  302. return false
  303. }
  304. merger, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
  305. // When the commit author is unknown set the BaseRepo owner as merger
  306. if merger == nil {
  307. if pr.BaseRepo.Owner == nil {
  308. if err = pr.BaseRepo.LoadOwner(ctx); err != nil {
  309. log.Error("%-v BaseRepo.LoadOwner: %v", pr, err)
  310. return false
  311. }
  312. }
  313. merger = pr.BaseRepo.Owner
  314. }
  315. if merged, err := SetMerged(ctx, pr, commit.ID.String(), timeutil.TimeStamp(commit.Author.When.Unix()), merger, issues_model.PullRequestStatusManuallyMerged); err != nil {
  316. log.Error("%-v setMerged : %v", pr, err)
  317. return false
  318. } else if !merged {
  319. return false
  320. }
  321. notify_service.MergePullRequest(ctx, merger, pr)
  322. log.Info("manuallyMerged[%-v]: Marked as manually merged into %s/%s by commit id: %s", pr, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
  323. return true
  324. }
  325. // InitializePullRequests checks and tests untested patches of pull requests.
  326. func InitializePullRequests(ctx context.Context) {
  327. // If we prefer to delay the checks, then no need to do any check during startup, there should be not much difference
  328. if setting.Repository.PullRequest.DelayCheckForInactiveDays >= 0 {
  329. return
  330. }
  331. prs, err := issues_model.GetPullRequestIDsByCheckStatus(ctx, issues_model.PullRequestStatusChecking)
  332. if err != nil {
  333. log.Error("Find Checking PRs: %v", err)
  334. return
  335. }
  336. for _, prID := range prs {
  337. select {
  338. case <-ctx.Done():
  339. return
  340. default:
  341. AddPullRequestToCheckQueue(prID)
  342. }
  343. }
  344. }
  345. func checkPullRequestMergeable(id int64) {
  346. ctx := graceful.GetManager().HammerContext()
  347. releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(id))
  348. if err != nil {
  349. log.Error("lock.Lock(): %v", err)
  350. return
  351. }
  352. defer releaser()
  353. ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Test PR[%d] from patch checking queue", id))
  354. defer finished()
  355. pr, err := issues_model.GetPullRequestByID(ctx, id)
  356. if err != nil {
  357. log.Error("Unable to GetPullRequestByID[%d] for checkPullRequestMergeable: %v", id, err)
  358. return
  359. }
  360. log.Trace("Testing %-v", pr)
  361. defer func() {
  362. log.Trace("Done testing %-v (status: %s)", pr, pr.Status)
  363. }()
  364. if pr.HasMerged {
  365. log.Trace("%-v is already merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
  366. return
  367. }
  368. if manuallyMerged(ctx, pr) {
  369. log.Trace("%-v is manually merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
  370. return
  371. }
  372. if err := testPullRequestBranchMergeable(pr); err != nil {
  373. log.Error("testPullRequestTmpRepoBranchMergeable[%-v]: %v", pr, err)
  374. pr.Status = issues_model.PullRequestStatusError
  375. if err := pr.UpdateCols(ctx, "status"); err != nil {
  376. log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err)
  377. }
  378. return
  379. }
  380. markPullRequestAsMergeable(ctx, pr)
  381. }
  382. // CheckPRsForBaseBranch check all pulls with baseBrannch
  383. func CheckPRsForBaseBranch(ctx context.Context, baseRepo *repo_model.Repository, baseBranchName string) error {
  384. prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, baseRepo.ID, baseBranchName)
  385. if err != nil {
  386. return err
  387. }
  388. for _, pr := range prs {
  389. StartPullRequestCheckImmediately(ctx, pr)
  390. }
  391. return nil
  392. }
  393. // Init runs the task queue to test all the checking status pull requests
  394. func Init() error {
  395. prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string {
  396. for _, s := range items {
  397. id, _ := strconv.ParseInt(s, 10, 64)
  398. checkPullRequestMergeable(id)
  399. }
  400. return nil
  401. })
  402. if prPatchCheckerQueue == nil {
  403. return errors.New("unable to create pr_patch_checker queue")
  404. }
  405. go graceful.GetManager().RunWithCancel(prPatchCheckerQueue)
  406. go graceful.GetManager().RunWithShutdownContext(InitializePullRequests)
  407. return nil
  408. }