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.

pull.go 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package pull
  4. import (
  5. "context"
  6. "fmt"
  7. "io"
  8. "os"
  9. "regexp"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/models/db"
  13. git_model "code.gitea.io/gitea/models/git"
  14. issues_model "code.gitea.io/gitea/models/issues"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/container"
  18. "code.gitea.io/gitea/modules/git"
  19. "code.gitea.io/gitea/modules/graceful"
  20. "code.gitea.io/gitea/modules/json"
  21. "code.gitea.io/gitea/modules/log"
  22. "code.gitea.io/gitea/modules/notification"
  23. repo_module "code.gitea.io/gitea/modules/repository"
  24. "code.gitea.io/gitea/modules/setting"
  25. "code.gitea.io/gitea/modules/sync"
  26. "code.gitea.io/gitea/modules/util"
  27. issue_service "code.gitea.io/gitea/services/issue"
  28. )
  29. // TODO: use clustered lock (unique queue? or *abuse* cache)
  30. var pullWorkingPool = sync.NewExclusivePool()
  31. // NewPullRequest creates new pull request with labels for repository.
  32. func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
  33. prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
  34. if err != nil {
  35. if !models.IsErrBranchDoesNotExist(err) {
  36. log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
  37. }
  38. return err
  39. }
  40. defer cancel()
  41. if err := testPatch(ctx, prCtx, pr); err != nil {
  42. return err
  43. }
  44. divergence, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
  45. if err != nil {
  46. return err
  47. }
  48. pr.CommitsAhead = divergence.Ahead
  49. pr.CommitsBehind = divergence.Behind
  50. assigneeCommentMap := make(map[int64]*issues_model.Comment)
  51. // add first push codes comment
  52. baseGitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
  53. if err != nil {
  54. return err
  55. }
  56. defer baseGitRepo.Close()
  57. if err := db.WithTx(ctx, func(ctx context.Context) error {
  58. if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
  59. return err
  60. }
  61. for _, assigneeID := range assigneeIDs {
  62. comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, false)
  63. if err != nil {
  64. return err
  65. }
  66. assigneeCommentMap[assigneeID] = comment
  67. }
  68. pr.Issue = issue
  69. issue.PullRequest = pr
  70. if pr.Flow == issues_model.PullRequestFlowGithub {
  71. err = PushToBaseRepo(ctx, pr)
  72. } else {
  73. err = UpdateRef(ctx, pr)
  74. }
  75. if err != nil {
  76. return err
  77. }
  78. compareInfo, err := baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(),
  79. git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false)
  80. if err != nil {
  81. return err
  82. }
  83. if len(compareInfo.Commits) == 0 {
  84. return nil
  85. }
  86. data := issues_model.PushActionContent{IsForcePush: false}
  87. data.CommitIDs = make([]string, 0, len(compareInfo.Commits))
  88. for i := len(compareInfo.Commits) - 1; i >= 0; i-- {
  89. data.CommitIDs = append(data.CommitIDs, compareInfo.Commits[i].ID.String())
  90. }
  91. dataJSON, err := json.Marshal(data)
  92. if err != nil {
  93. return err
  94. }
  95. ops := &issues_model.CreateCommentOptions{
  96. Type: issues_model.CommentTypePullRequestPush,
  97. Doer: issue.Poster,
  98. Repo: repo,
  99. Issue: pr.Issue,
  100. IsForcePush: false,
  101. Content: string(dataJSON),
  102. }
  103. if _, err = issues_model.CreateComment(ctx, ops); err != nil {
  104. return err
  105. }
  106. return nil
  107. }); err != nil {
  108. // cleanup: this will only remove the reference, the real commit will be clean up when next GC
  109. if err1 := baseGitRepo.RemoveReference(pr.GetGitRefName()); err1 != nil {
  110. log.Error("RemoveReference: %v", err1)
  111. }
  112. return err
  113. }
  114. baseGitRepo.Close() // close immediately to avoid notifications will open the repository again
  115. mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
  116. if err != nil {
  117. return err
  118. }
  119. notification.NotifyNewPullRequest(ctx, pr, mentions)
  120. if len(issue.Labels) > 0 {
  121. notification.NotifyIssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
  122. }
  123. if issue.Milestone != nil {
  124. notification.NotifyIssueChangeMilestone(ctx, issue.Poster, issue, 0)
  125. }
  126. if len(assigneeIDs) > 0 {
  127. for _, assigneeID := range assigneeIDs {
  128. assignee, err := user_model.GetUserByID(ctx, assigneeID)
  129. if err != nil {
  130. return ErrDependenciesLeft
  131. }
  132. notification.NotifyIssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
  133. }
  134. }
  135. return nil
  136. }
  137. // ChangeTargetBranch changes the target branch of this pull request, as the given user.
  138. func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) {
  139. pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
  140. defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
  141. // Current target branch is already the same
  142. if pr.BaseBranch == targetBranch {
  143. return nil
  144. }
  145. if pr.Issue.IsClosed {
  146. return issues_model.ErrIssueIsClosed{
  147. ID: pr.Issue.ID,
  148. RepoID: pr.Issue.RepoID,
  149. Index: pr.Issue.Index,
  150. }
  151. }
  152. if pr.HasMerged {
  153. return models.ErrPullRequestHasMerged{
  154. ID: pr.ID,
  155. IssueID: pr.Index,
  156. HeadRepoID: pr.HeadRepoID,
  157. BaseRepoID: pr.BaseRepoID,
  158. HeadBranch: pr.HeadBranch,
  159. BaseBranch: pr.BaseBranch,
  160. }
  161. }
  162. // Check if branches are equal
  163. branchesEqual, err := IsHeadEqualWithBranch(ctx, pr, targetBranch)
  164. if err != nil {
  165. return err
  166. }
  167. if branchesEqual {
  168. return models.ErrBranchesEqual{
  169. HeadBranchName: pr.HeadBranch,
  170. BaseBranchName: targetBranch,
  171. }
  172. }
  173. // Check if pull request for the new target branch already exists
  174. existingPr, err := issues_model.GetUnmergedPullRequest(ctx, pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, issues_model.PullRequestFlowGithub)
  175. if existingPr != nil {
  176. return issues_model.ErrPullRequestAlreadyExists{
  177. ID: existingPr.ID,
  178. IssueID: existingPr.Index,
  179. HeadRepoID: existingPr.HeadRepoID,
  180. BaseRepoID: existingPr.BaseRepoID,
  181. HeadBranch: existingPr.HeadBranch,
  182. BaseBranch: existingPr.BaseBranch,
  183. }
  184. }
  185. if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
  186. return err
  187. }
  188. // Set new target branch
  189. oldBranch := pr.BaseBranch
  190. pr.BaseBranch = targetBranch
  191. // Refresh patch
  192. if err := TestPatch(pr); err != nil {
  193. return err
  194. }
  195. // Update target branch, PR diff and status
  196. // This is the same as checkAndUpdateStatus in check service, but also updates base_branch
  197. if pr.Status == issues_model.PullRequestStatusChecking {
  198. pr.Status = issues_model.PullRequestStatusMergeable
  199. }
  200. // Update Commit Divergence
  201. divergence, err := GetDiverging(ctx, pr)
  202. if err != nil {
  203. return err
  204. }
  205. pr.CommitsAhead = divergence.Ahead
  206. pr.CommitsBehind = divergence.Behind
  207. if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
  208. return err
  209. }
  210. // Create comment
  211. options := &issues_model.CreateCommentOptions{
  212. Type: issues_model.CommentTypeChangeTargetBranch,
  213. Doer: doer,
  214. Repo: pr.Issue.Repo,
  215. Issue: pr.Issue,
  216. OldRef: oldBranch,
  217. NewRef: targetBranch,
  218. }
  219. if _, err = issue_service.CreateComment(ctx, options); err != nil {
  220. return fmt.Errorf("CreateChangeTargetBranchComment: %w", err)
  221. }
  222. return nil
  223. }
  224. func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
  225. repo, err := repo_model.GetRepositoryByID(ctx, repoID)
  226. if err != nil {
  227. return fmt.Errorf("GetRepositoryByIDCtx: %w", err)
  228. }
  229. gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
  230. if err != nil {
  231. return fmt.Errorf("git.OpenRepository: %w", err)
  232. }
  233. go func() {
  234. // FIXME: graceful: We need to tell the manager we're doing something...
  235. err := InvalidateCodeComments(ctx, requests, doer, gitRepo, branch)
  236. if err != nil {
  237. log.Error("PullRequestList.InvalidateCodeComments: %v", err)
  238. }
  239. gitRepo.Close()
  240. }()
  241. return nil
  242. }
  243. // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
  244. // and generate new patch for testing as needed.
  245. func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
  246. log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
  247. graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
  248. // There is no sensible way to shut this down ":-("
  249. // If you don't let it run all the way then you will lose data
  250. // TODO: graceful: AddTestPullRequestTask needs to become a queue!
  251. // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
  252. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
  253. if err != nil {
  254. log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
  255. return
  256. }
  257. for _, pr := range prs {
  258. log.Trace("Updating PR[%d]: composing new test task", pr.ID)
  259. if pr.Flow == issues_model.PullRequestFlowGithub {
  260. if err := PushToBaseRepo(ctx, pr); err != nil {
  261. log.Error("PushToBaseRepo: %v", err)
  262. continue
  263. }
  264. } else {
  265. continue
  266. }
  267. AddToTaskQueue(pr)
  268. comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
  269. if err == nil && comment != nil {
  270. notification.NotifyPullRequestPushCommits(ctx, doer, pr, comment)
  271. }
  272. }
  273. if isSync {
  274. requests := issues_model.PullRequestList(prs)
  275. if err = requests.LoadAttributes(); err != nil {
  276. log.Error("PullRequestList.LoadAttributes: %v", err)
  277. }
  278. if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil {
  279. log.Error("checkForInvalidation: %v", invalidationErr)
  280. }
  281. if err == nil {
  282. for _, pr := range prs {
  283. if newCommitID != "" && newCommitID != git.EmptySHA {
  284. changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
  285. if err != nil {
  286. log.Error("checkIfPRContentChanged: %v", err)
  287. }
  288. if changed {
  289. // Mark old reviews as stale if diff to mergebase has changed
  290. if err := issues_model.MarkReviewsAsStale(pr.IssueID); err != nil {
  291. log.Error("MarkReviewsAsStale: %v", err)
  292. }
  293. // dismiss all approval reviews if protected branch rule item enabled.
  294. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
  295. if err != nil {
  296. log.Error("GetFirstMatchProtectedBranchRule: %v", err)
  297. }
  298. if pb != nil && pb.DismissStaleApprovals {
  299. if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
  300. log.Error("DismissApprovalReviews: %v", err)
  301. }
  302. }
  303. }
  304. if err := issues_model.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
  305. log.Error("MarkReviewsAsNotStale: %v", err)
  306. }
  307. divergence, err := GetDiverging(ctx, pr)
  308. if err != nil {
  309. log.Error("GetDiverging: %v", err)
  310. } else {
  311. err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
  312. if err != nil {
  313. log.Error("UpdateCommitDivergence: %v", err)
  314. }
  315. }
  316. }
  317. notification.NotifyPullRequestSynchronized(ctx, doer, pr)
  318. }
  319. }
  320. }
  321. log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
  322. prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
  323. if err != nil {
  324. log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
  325. return
  326. }
  327. for _, pr := range prs {
  328. divergence, err := GetDiverging(ctx, pr)
  329. if err != nil {
  330. if models.IsErrBranchDoesNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) {
  331. log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch)
  332. } else {
  333. log.Error("GetDiverging: %v", err)
  334. }
  335. } else {
  336. err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
  337. if err != nil {
  338. log.Error("UpdateCommitDivergence: %v", err)
  339. }
  340. }
  341. AddToTaskQueue(pr)
  342. }
  343. })
  344. }
  345. // checkIfPRContentChanged checks if diff to target branch has changed by push
  346. // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
  347. func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
  348. prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
  349. if err != nil {
  350. log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
  351. return false, err
  352. }
  353. defer cancel()
  354. tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
  355. if err != nil {
  356. return false, fmt.Errorf("OpenRepository: %w", err)
  357. }
  358. defer tmpRepo.Close()
  359. // Find the merge-base
  360. _, base, err := tmpRepo.GetMergeBase("", "base", "tracking")
  361. if err != nil {
  362. return false, fmt.Errorf("GetMergeBase: %w", err)
  363. }
  364. cmd := git.NewCommand(ctx, "diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base)
  365. stdoutReader, stdoutWriter, err := os.Pipe()
  366. if err != nil {
  367. return false, fmt.Errorf("unable to open pipe for to run diff: %w", err)
  368. }
  369. if err := cmd.Run(&git.RunOpts{
  370. Dir: prCtx.tmpBasePath,
  371. Stdout: stdoutWriter,
  372. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  373. _ = stdoutWriter.Close()
  374. defer func() {
  375. _ = stdoutReader.Close()
  376. }()
  377. return util.IsEmptyReader(stdoutReader)
  378. },
  379. }); err != nil {
  380. if err == util.ErrNotEmpty {
  381. return true, nil
  382. }
  383. log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v",
  384. newCommitID, oldCommitID, base,
  385. pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
  386. err)
  387. return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err)
  388. }
  389. return false, nil
  390. }
  391. // PushToBaseRepo pushes commits from branches of head repository to
  392. // corresponding branches of base repository.
  393. // FIXME: Only push branches that are actually updates?
  394. func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
  395. return pushToBaseRepoHelper(ctx, pr, "")
  396. }
  397. func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
  398. log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
  399. if err := pr.LoadHeadRepo(ctx); err != nil {
  400. log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
  401. return err
  402. }
  403. headRepoPath := pr.HeadRepo.RepoPath()
  404. if err := pr.LoadBaseRepo(ctx); err != nil {
  405. log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
  406. return err
  407. }
  408. baseRepoPath := pr.BaseRepo.RepoPath()
  409. if err = pr.LoadIssue(ctx); err != nil {
  410. return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
  411. }
  412. if err = pr.Issue.LoadPoster(ctx); err != nil {
  413. return fmt.Errorf("unable to load poster %d for pr %d: %w", pr.Issue.PosterID, pr.ID, err)
  414. }
  415. gitRefName := pr.GetGitRefName()
  416. if err := git.Push(ctx, headRepoPath, git.PushOptions{
  417. Remote: baseRepoPath,
  418. Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
  419. Force: true,
  420. // Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
  421. Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
  422. }); err != nil {
  423. if git.IsErrPushOutOfDate(err) {
  424. // This should not happen as we're using force!
  425. log.Error("Unable to push PR head for %s#%d (%-v:%s) due to ErrPushOfDate: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
  426. return err
  427. } else if git.IsErrPushRejected(err) {
  428. rejectErr := err.(*git.ErrPushRejected)
  429. log.Info("Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
  430. return err
  431. } else if git.IsErrMoreThanOne(err) {
  432. if prefixHeadBranch != "" {
  433. log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch)
  434. return err
  435. }
  436. log.Info("Retrying to push with %s%s", git.BranchPrefix, pr.HeadBranch)
  437. err = pushToBaseRepoHelper(ctx, pr, git.BranchPrefix)
  438. return err
  439. }
  440. log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
  441. return fmt.Errorf("Push: %s:%s %s:%s %w", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
  442. }
  443. return nil
  444. }
  445. // UpdateRef update refs/pull/id/head directly for agit flow pull request
  446. func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
  447. log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
  448. if err := pr.LoadBaseRepo(ctx); err != nil {
  449. log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
  450. return err
  451. }
  452. _, _, err = git.NewCommand(ctx, "update-ref").AddDynamicArguments(pr.GetGitRefName(), pr.HeadCommitID).RunStdString(&git.RunOpts{Dir: pr.BaseRepo.RepoPath()})
  453. if err != nil {
  454. log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
  455. }
  456. return err
  457. }
  458. type errlist []error
  459. func (errs errlist) Error() string {
  460. if len(errs) > 0 {
  461. var buf strings.Builder
  462. for i, err := range errs {
  463. if i > 0 {
  464. buf.WriteString(", ")
  465. }
  466. buf.WriteString(err.Error())
  467. }
  468. return buf.String()
  469. }
  470. return ""
  471. }
  472. // CloseBranchPulls close all the pull requests who's head branch is the branch
  473. func CloseBranchPulls(doer *user_model.User, repoID int64, branch string) error {
  474. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
  475. if err != nil {
  476. return err
  477. }
  478. prs2, err := issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
  479. if err != nil {
  480. return err
  481. }
  482. prs = append(prs, prs2...)
  483. if err := issues_model.PullRequestList(prs).LoadAttributes(); err != nil {
  484. return err
  485. }
  486. var errs errlist
  487. for _, pr := range prs {
  488. if err = issue_service.ChangeStatus(pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
  489. errs = append(errs, err)
  490. }
  491. }
  492. if len(errs) > 0 {
  493. return errs
  494. }
  495. return nil
  496. }
  497. // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository, but only whose base repo is not in the given repository
  498. func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) error {
  499. branches, _, err := git.GetBranchesByPath(ctx, repo.RepoPath(), 0, 0)
  500. if err != nil {
  501. return err
  502. }
  503. var errs errlist
  504. for _, branch := range branches {
  505. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name)
  506. if err != nil {
  507. return err
  508. }
  509. if err = issues_model.PullRequestList(prs).LoadAttributes(); err != nil {
  510. return err
  511. }
  512. for _, pr := range prs {
  513. // If the base repository for this pr is this repository there is no need to close it
  514. // as it is going to be deleted anyway
  515. if pr.BaseRepoID == repo.ID {
  516. continue
  517. }
  518. if err = issue_service.ChangeStatus(pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) {
  519. errs = append(errs, err)
  520. }
  521. }
  522. }
  523. if len(errs) > 0 {
  524. return errs
  525. }
  526. return nil
  527. }
  528. var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
  529. // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
  530. func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
  531. if err := pr.LoadIssue(ctx); err != nil {
  532. log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
  533. return ""
  534. }
  535. if err := pr.Issue.LoadPoster(ctx); err != nil {
  536. log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
  537. return ""
  538. }
  539. if pr.HeadRepo == nil {
  540. var err error
  541. pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
  542. if err != nil {
  543. log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err)
  544. return ""
  545. }
  546. }
  547. gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.HeadRepo.RepoPath())
  548. if err != nil {
  549. log.Error("Unable to open head repository: Error: %v", err)
  550. return ""
  551. }
  552. defer closer.Close()
  553. var headCommit *git.Commit
  554. if pr.Flow == issues_model.PullRequestFlowGithub {
  555. headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
  556. } else {
  557. pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
  558. if err != nil {
  559. log.Error("Unable to get head commit: %s Error: %v", pr.GetGitRefName(), err)
  560. return ""
  561. }
  562. headCommit, err = gitRepo.GetCommit(pr.HeadCommitID)
  563. }
  564. if err != nil {
  565. log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
  566. return ""
  567. }
  568. mergeBase, err := gitRepo.GetCommit(pr.MergeBase)
  569. if err != nil {
  570. log.Error("Unable to get merge base commit: %s Error: %v", pr.MergeBase, err)
  571. return ""
  572. }
  573. limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit
  574. commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, 0)
  575. if err != nil {
  576. log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
  577. return ""
  578. }
  579. posterSig := pr.Issue.Poster.NewGitSig().String()
  580. uniqueAuthors := make(container.Set[string])
  581. authors := make([]string, 0, len(commits))
  582. stringBuilder := strings.Builder{}
  583. if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
  584. message := strings.TrimSpace(pr.Issue.Content)
  585. stringBuilder.WriteString(message)
  586. if stringBuilder.Len() > 0 {
  587. stringBuilder.WriteRune('\n')
  588. if !commitMessageTrailersPattern.MatchString(message) {
  589. stringBuilder.WriteRune('\n')
  590. }
  591. }
  592. }
  593. // commits list is in reverse chronological order
  594. first := true
  595. for i := len(commits) - 1; i >= 0; i-- {
  596. commit := commits[i]
  597. if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
  598. maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
  599. if maxSize < 0 || stringBuilder.Len() < maxSize {
  600. var toWrite []byte
  601. if first {
  602. first = false
  603. toWrite = []byte(strings.TrimPrefix(commit.CommitMessage, pr.Issue.Title))
  604. } else {
  605. toWrite = []byte(commit.CommitMessage)
  606. }
  607. if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
  608. toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
  609. }
  610. if _, err := stringBuilder.Write(toWrite); err != nil {
  611. log.Error("Unable to write commit message Error: %v", err)
  612. return ""
  613. }
  614. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  615. log.Error("Unable to write commit message Error: %v", err)
  616. return ""
  617. }
  618. }
  619. }
  620. authorString := commit.Author.String()
  621. if uniqueAuthors.Add(authorString) && authorString != posterSig {
  622. // Compare use account as well to avoid adding the same author multiple times
  623. // times when email addresses are private or multiple emails are used.
  624. commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
  625. if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
  626. authors = append(authors, authorString)
  627. }
  628. }
  629. }
  630. // Consider collecting the remaining authors
  631. if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
  632. skip := limit
  633. limit = 30
  634. for {
  635. commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
  636. if err != nil {
  637. log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
  638. return ""
  639. }
  640. if len(commits) == 0 {
  641. break
  642. }
  643. for _, commit := range commits {
  644. authorString := commit.Author.String()
  645. if uniqueAuthors.Add(authorString) && authorString != posterSig {
  646. commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
  647. if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
  648. authors = append(authors, authorString)
  649. }
  650. }
  651. }
  652. skip += limit
  653. }
  654. }
  655. for _, author := range authors {
  656. if _, err := stringBuilder.Write([]byte("Co-authored-by: ")); err != nil {
  657. log.Error("Unable to write to string builder Error: %v", err)
  658. return ""
  659. }
  660. if _, err := stringBuilder.Write([]byte(author)); err != nil {
  661. log.Error("Unable to write to string builder Error: %v", err)
  662. return ""
  663. }
  664. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  665. log.Error("Unable to write to string builder Error: %v", err)
  666. return ""
  667. }
  668. }
  669. return stringBuilder.String()
  670. }
  671. // GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status
  672. func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) {
  673. _, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues)
  674. return lastStatus, err
  675. }
  676. // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
  677. func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
  678. if err := issues.LoadPullRequests(ctx); err != nil {
  679. return nil, nil, err
  680. }
  681. if _, err := issues.LoadRepositories(ctx); err != nil {
  682. return nil, nil, err
  683. }
  684. var (
  685. gitRepos = make(map[int64]*git.Repository)
  686. res = make(map[int64][]*git_model.CommitStatus)
  687. lastRes = make(map[int64]*git_model.CommitStatus)
  688. err error
  689. )
  690. defer func() {
  691. for _, gitRepo := range gitRepos {
  692. gitRepo.Close()
  693. }
  694. }()
  695. for _, issue := range issues {
  696. if !issue.IsPull {
  697. continue
  698. }
  699. gitRepo, ok := gitRepos[issue.RepoID]
  700. if !ok {
  701. gitRepo, err = git.OpenRepository(ctx, issue.Repo.RepoPath())
  702. if err != nil {
  703. log.Error("Cannot open git repository %-v for issue #%d[%d]. Error: %v", issue.Repo, issue.Index, issue.ID, err)
  704. continue
  705. }
  706. gitRepos[issue.RepoID] = gitRepo
  707. }
  708. statuses, lastStatus, err := getAllCommitStatus(gitRepo, issue.PullRequest)
  709. if err != nil {
  710. log.Error("getAllCommitStatus: cant get commit statuses of pull [%d]: %v", issue.PullRequest.ID, err)
  711. continue
  712. }
  713. res[issue.PullRequest.ID] = statuses
  714. lastRes[issue.PullRequest.ID] = lastStatus
  715. }
  716. return res, lastRes, nil
  717. }
  718. // getAllCommitStatus get pr's commit statuses.
  719. func getAllCommitStatus(gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) {
  720. sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitRefName())
  721. if shaErr != nil {
  722. return nil, nil, shaErr
  723. }
  724. statuses, _, err = git_model.GetLatestCommitStatus(db.DefaultContext, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
  725. lastStatus = git_model.CalcCommitStatus(statuses)
  726. return statuses, lastStatus, err
  727. }
  728. // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
  729. func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) {
  730. var err error
  731. if err = pr.LoadBaseRepo(ctx); err != nil {
  732. return false, err
  733. }
  734. baseGitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo.RepoPath())
  735. if err != nil {
  736. return false, err
  737. }
  738. defer closer.Close()
  739. baseCommit, err := baseGitRepo.GetBranchCommit(branchName)
  740. if err != nil {
  741. return false, err
  742. }
  743. if err = pr.LoadHeadRepo(ctx); err != nil {
  744. return false, err
  745. }
  746. var headGitRepo *git.Repository
  747. if pr.HeadRepoID == pr.BaseRepoID {
  748. headGitRepo = baseGitRepo
  749. } else {
  750. var closer io.Closer
  751. headGitRepo, closer, err = git.RepositoryFromContextOrOpen(ctx, pr.HeadRepo.RepoPath())
  752. if err != nil {
  753. return false, err
  754. }
  755. defer closer.Close()
  756. }
  757. var headCommit *git.Commit
  758. if pr.Flow == issues_model.PullRequestFlowGithub {
  759. headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch)
  760. if err != nil {
  761. return false, err
  762. }
  763. } else {
  764. pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitRefName())
  765. if err != nil {
  766. return false, err
  767. }
  768. if headCommit, err = baseGitRepo.GetCommit(pr.HeadCommitID); err != nil {
  769. return false, err
  770. }
  771. }
  772. return baseCommit.HasPreviousCommit(headCommit.ID)
  773. }