gitea源码

pull.go 36KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package pull
  4. import (
  5. "bytes"
  6. "context"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "os"
  11. "regexp"
  12. "strings"
  13. "time"
  14. "code.gitea.io/gitea/models/db"
  15. git_model "code.gitea.io/gitea/models/git"
  16. issues_model "code.gitea.io/gitea/models/issues"
  17. "code.gitea.io/gitea/models/organization"
  18. access_model "code.gitea.io/gitea/models/perm/access"
  19. repo_model "code.gitea.io/gitea/models/repo"
  20. "code.gitea.io/gitea/models/unit"
  21. user_model "code.gitea.io/gitea/models/user"
  22. "code.gitea.io/gitea/modules/base"
  23. "code.gitea.io/gitea/modules/container"
  24. "code.gitea.io/gitea/modules/git"
  25. "code.gitea.io/gitea/modules/git/gitcmd"
  26. "code.gitea.io/gitea/modules/gitrepo"
  27. "code.gitea.io/gitea/modules/globallock"
  28. "code.gitea.io/gitea/modules/graceful"
  29. "code.gitea.io/gitea/modules/log"
  30. repo_module "code.gitea.io/gitea/modules/repository"
  31. "code.gitea.io/gitea/modules/setting"
  32. "code.gitea.io/gitea/modules/util"
  33. issue_service "code.gitea.io/gitea/services/issue"
  34. notify_service "code.gitea.io/gitea/services/notify"
  35. )
  36. func getPullWorkingLockKey(prID int64) string {
  37. return fmt.Sprintf("pull_working_%d", prID)
  38. }
  39. type NewPullRequestOptions struct {
  40. Repo *repo_model.Repository
  41. Issue *issues_model.Issue
  42. LabelIDs []int64
  43. AttachmentUUIDs []string
  44. PullRequest *issues_model.PullRequest
  45. AssigneeIDs []int64
  46. Reviewers []*user_model.User
  47. TeamReviewers []*organization.Team
  48. }
  49. // NewPullRequest creates new pull request with labels for repository.
  50. func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
  51. repo, issue, labelIDs, uuids, pr, assigneeIDs := opts.Repo, opts.Issue, opts.LabelIDs, opts.AttachmentUUIDs, opts.PullRequest, opts.AssigneeIDs
  52. if err := issue.LoadPoster(ctx); err != nil {
  53. return err
  54. }
  55. if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
  56. return user_model.ErrBlockedUser
  57. }
  58. // user should be a collaborator or a member of the organization for base repo
  59. canCreate := issue.Poster.IsAdmin || pr.Flow == issues_model.PullRequestFlowAGit
  60. if !canCreate {
  61. canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID)
  62. if err != nil {
  63. return err
  64. }
  65. if !canCreate {
  66. // or user should have write permission in the head repo
  67. if err := pr.LoadHeadRepo(ctx); err != nil {
  68. return err
  69. }
  70. perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, issue.Poster)
  71. if err != nil {
  72. return err
  73. }
  74. if !perm.CanWrite(unit.TypeCode) {
  75. return issues_model.ErrMustCollaborator
  76. }
  77. }
  78. }
  79. prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
  80. if err != nil {
  81. if !git_model.IsErrBranchNotExist(err) {
  82. log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
  83. }
  84. return err
  85. }
  86. defer cancel()
  87. if err := testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr); err != nil {
  88. return err
  89. }
  90. divergence, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
  91. if err != nil {
  92. return err
  93. }
  94. pr.CommitsAhead = divergence.Ahead
  95. pr.CommitsBehind = divergence.Behind
  96. assigneeCommentMap := make(map[int64]*issues_model.Comment)
  97. var reviewNotifiers []*issue_service.ReviewRequestNotifier
  98. if err := db.WithTx(ctx, func(ctx context.Context) error {
  99. if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
  100. return err
  101. }
  102. for _, assigneeID := range assigneeIDs {
  103. comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, false)
  104. if err != nil {
  105. return err
  106. }
  107. assigneeCommentMap[assigneeID] = comment
  108. }
  109. pr.Issue = issue
  110. issue.PullRequest = pr
  111. if pr.Flow == issues_model.PullRequestFlowGithub {
  112. err = PushToBaseRepo(ctx, pr)
  113. } else {
  114. err = UpdateRef(ctx, pr)
  115. }
  116. if err != nil {
  117. return err
  118. }
  119. // add first push codes comment
  120. if _, err := CreatePushPullComment(ctx, issue.Poster, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitHeadRefName(), false); err != nil {
  121. return err
  122. }
  123. if !pr.IsWorkInProgress(ctx) {
  124. reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
  125. if err != nil {
  126. return err
  127. }
  128. }
  129. return nil
  130. }); err != nil {
  131. // cleanup: this will only remove the reference, the real commit will be clean up when next GC
  132. if err1 := gitrepo.RemoveRef(ctx, pr.BaseRepo, pr.GetGitHeadRefName()); err1 != nil {
  133. log.Error("RemoveRef: %v", err1)
  134. }
  135. return err
  136. }
  137. issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifiers)
  138. mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
  139. if err != nil {
  140. return err
  141. }
  142. notify_service.NewPullRequest(ctx, pr, mentions)
  143. if len(issue.Labels) > 0 {
  144. notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
  145. }
  146. if issue.Milestone != nil {
  147. notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
  148. }
  149. for _, assigneeID := range assigneeIDs {
  150. assignee, err := user_model.GetUserByID(ctx, assigneeID)
  151. if err != nil {
  152. return ErrDependenciesLeft
  153. }
  154. notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
  155. }
  156. permDoer, err := access_model.GetUserRepoPermission(ctx, repo, issue.Poster)
  157. for _, reviewer := range opts.Reviewers {
  158. if _, err = issue_service.ReviewRequest(ctx, pr.Issue, issue.Poster, &permDoer, reviewer, true); err != nil {
  159. return err
  160. }
  161. }
  162. for _, teamReviewer := range opts.TeamReviewers {
  163. if _, err = issue_service.TeamReviewRequest(ctx, pr.Issue, issue.Poster, teamReviewer, true); err != nil {
  164. return err
  165. }
  166. }
  167. return nil
  168. }
  169. // ErrPullRequestHasMerged represents a "PullRequestHasMerged"-error
  170. type ErrPullRequestHasMerged struct {
  171. ID int64
  172. IssueID int64
  173. HeadRepoID int64
  174. BaseRepoID int64
  175. HeadBranch string
  176. BaseBranch string
  177. }
  178. // IsErrPullRequestHasMerged checks if an error is a ErrPullRequestHasMerged.
  179. func IsErrPullRequestHasMerged(err error) bool {
  180. _, ok := err.(ErrPullRequestHasMerged)
  181. return ok
  182. }
  183. // Error does pretty-printing :D
  184. func (err ErrPullRequestHasMerged) Error() string {
  185. return fmt.Sprintf("pull request has merged [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
  186. err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
  187. }
  188. // ChangeTargetBranch changes the target branch of this pull request, as the given user.
  189. func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) {
  190. releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID))
  191. if err != nil {
  192. log.Error("lock.Lock(): %v", err)
  193. return fmt.Errorf("lock.Lock: %w", err)
  194. }
  195. defer releaser()
  196. // Current target branch is already the same
  197. if pr.BaseBranch == targetBranch {
  198. return nil
  199. }
  200. if pr.Issue.IsClosed {
  201. return issues_model.ErrIssueIsClosed{
  202. ID: pr.Issue.ID,
  203. RepoID: pr.Issue.RepoID,
  204. Index: pr.Issue.Index,
  205. IsPull: true,
  206. }
  207. }
  208. if pr.HasMerged {
  209. return ErrPullRequestHasMerged{
  210. ID: pr.ID,
  211. IssueID: pr.Index,
  212. HeadRepoID: pr.HeadRepoID,
  213. BaseRepoID: pr.BaseRepoID,
  214. HeadBranch: pr.HeadBranch,
  215. BaseBranch: pr.BaseBranch,
  216. }
  217. }
  218. // Check if branches are equal
  219. branchesEqual, err := IsHeadEqualWithBranch(ctx, pr, targetBranch)
  220. if err != nil {
  221. return err
  222. }
  223. if branchesEqual {
  224. return git_model.ErrBranchesEqual{
  225. HeadBranchName: pr.HeadBranch,
  226. BaseBranchName: targetBranch,
  227. }
  228. }
  229. // Check if pull request for the new target branch already exists
  230. existingPr, err := issues_model.GetUnmergedPullRequest(ctx, pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, issues_model.PullRequestFlowGithub)
  231. if existingPr != nil {
  232. return issues_model.ErrPullRequestAlreadyExists{
  233. ID: existingPr.ID,
  234. IssueID: existingPr.Index,
  235. HeadRepoID: existingPr.HeadRepoID,
  236. BaseRepoID: existingPr.BaseRepoID,
  237. HeadBranch: existingPr.HeadBranch,
  238. BaseBranch: existingPr.BaseBranch,
  239. }
  240. }
  241. if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
  242. return err
  243. }
  244. // Set new target branch
  245. oldBranch := pr.BaseBranch
  246. pr.BaseBranch = targetBranch
  247. // Refresh patch
  248. if err := testPullRequestBranchMergeable(pr); err != nil {
  249. return err
  250. }
  251. // Update target branch, PR diff and status
  252. // This is the same as markPullRequestAsMergeable in check service, but also updates base_branch
  253. if pr.Status == issues_model.PullRequestStatusChecking {
  254. pr.Status = issues_model.PullRequestStatusMergeable
  255. }
  256. // Update Commit Divergence
  257. divergence, err := GetDiverging(ctx, pr)
  258. if err != nil {
  259. return err
  260. }
  261. pr.CommitsAhead = divergence.Ahead
  262. pr.CommitsBehind = divergence.Behind
  263. // add first push codes comment
  264. baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
  265. if err != nil {
  266. return err
  267. }
  268. defer baseGitRepo.Close()
  269. return db.WithTx(ctx, func(ctx context.Context) error {
  270. if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
  271. return err
  272. }
  273. // Create comment
  274. options := &issues_model.CreateCommentOptions{
  275. Type: issues_model.CommentTypeChangeTargetBranch,
  276. Doer: doer,
  277. Repo: pr.Issue.Repo,
  278. Issue: pr.Issue,
  279. OldRef: oldBranch,
  280. NewRef: targetBranch,
  281. }
  282. if _, err = issues_model.CreateComment(ctx, options); err != nil {
  283. return fmt.Errorf("CreateChangeTargetBranchComment: %w", err)
  284. }
  285. // Delete all old push comments and insert new push comments
  286. if _, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
  287. And("type = ?", issues_model.CommentTypePullRequestPush).
  288. NoAutoCondition().
  289. Delete(new(issues_model.Comment)); err != nil {
  290. return err
  291. }
  292. _, err = CreatePushPullComment(ctx, doer, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitHeadRefName(), false)
  293. return err
  294. })
  295. }
  296. func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
  297. repo, err := repo_model.GetRepositoryByID(ctx, repoID)
  298. if err != nil {
  299. return fmt.Errorf("GetRepositoryByIDCtx: %w", err)
  300. }
  301. gitRepo, err := gitrepo.OpenRepository(ctx, repo)
  302. if err != nil {
  303. return fmt.Errorf("gitrepo.OpenRepository: %w", err)
  304. }
  305. go func() {
  306. // FIXME: graceful: We need to tell the manager we're doing something...
  307. err := InvalidateCodeComments(ctx, requests, doer, gitRepo, branch)
  308. if err != nil {
  309. log.Error("PullRequestList.InvalidateCodeComments: %v", err)
  310. }
  311. gitRepo.Close()
  312. }()
  313. return nil
  314. }
  315. type TestPullRequestOptions struct {
  316. RepoID int64
  317. Doer *user_model.User
  318. Branch string
  319. IsSync bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
  320. IsForcePush bool
  321. OldCommitID string
  322. NewCommitID string
  323. }
  324. // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
  325. // and generate new patch for testing as needed.
  326. func AddTestPullRequestTask(opts TestPullRequestOptions) {
  327. log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
  328. graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
  329. // There is no sensible way to shut this down ":-("
  330. // If you don't let it run all the way then you will lose data
  331. // TODO: graceful: AddTestPullRequestTask needs to become a queue!
  332. // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
  333. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
  334. if err != nil {
  335. log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
  336. return
  337. }
  338. for _, pr := range prs {
  339. log.Trace("Updating PR[%d]: composing new test task", pr.ID)
  340. if pr.Flow == issues_model.PullRequestFlowGithub {
  341. if err := PushToBaseRepo(ctx, pr); err != nil {
  342. log.Error("PushToBaseRepo: %v", err)
  343. continue
  344. }
  345. } else {
  346. continue
  347. }
  348. StartPullRequestCheckImmediately(ctx, pr)
  349. comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID, opts.IsForcePush)
  350. if err == nil && comment != nil {
  351. notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
  352. }
  353. }
  354. if opts.IsSync {
  355. if err = prs.LoadAttributes(ctx); err != nil {
  356. log.Error("PullRequestList.LoadAttributes: %v", err)
  357. }
  358. if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
  359. log.Error("checkForInvalidation: %v", invalidationErr)
  360. }
  361. if err == nil {
  362. for _, pr := range prs {
  363. objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
  364. if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
  365. changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
  366. if err != nil {
  367. log.Error("checkIfPRContentChanged: %v", err)
  368. }
  369. if changed {
  370. // Mark old reviews as stale if diff to mergebase has changed
  371. if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
  372. log.Error("MarkReviewsAsStale: %v", err)
  373. }
  374. // dismiss all approval reviews if protected branch rule item enabled.
  375. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
  376. if err != nil {
  377. log.Error("GetFirstMatchProtectedBranchRule: %v", err)
  378. }
  379. if pb != nil && pb.DismissStaleApprovals {
  380. if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil {
  381. log.Error("DismissApprovalReviews: %v", err)
  382. }
  383. }
  384. }
  385. if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
  386. log.Error("MarkReviewsAsNotStale: %v", err)
  387. }
  388. divergence, err := GetDiverging(ctx, pr)
  389. if err != nil {
  390. log.Error("GetDiverging: %v", err)
  391. } else {
  392. err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
  393. if err != nil {
  394. log.Error("UpdateCommitDivergence: %v", err)
  395. }
  396. }
  397. }
  398. if !pr.IsWorkInProgress(ctx) {
  399. reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr)
  400. if err != nil {
  401. log.Error("PullRequestCodeOwnersReview: %v", err)
  402. }
  403. if len(reviewNotifiers) > 0 {
  404. issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers)
  405. }
  406. }
  407. notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
  408. }
  409. }
  410. }
  411. log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
  412. prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
  413. if err != nil {
  414. log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
  415. return
  416. }
  417. for _, pr := range prs {
  418. divergence, err := GetDiverging(ctx, pr)
  419. if err != nil {
  420. if git_model.IsErrBranchNotExist(err) && !gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch) {
  421. log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch)
  422. } else {
  423. log.Error("GetDiverging: %v", err)
  424. }
  425. } else {
  426. err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
  427. if err != nil {
  428. log.Error("UpdateCommitDivergence: %v", err)
  429. }
  430. }
  431. StartPullRequestCheckDelayable(ctx, pr)
  432. }
  433. })
  434. }
  435. // checkIfPRContentChanged checks if diff to target branch has changed by push
  436. // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
  437. func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
  438. prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
  439. if err != nil {
  440. log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
  441. return false, err
  442. }
  443. defer cancel()
  444. tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
  445. if err != nil {
  446. return false, fmt.Errorf("OpenRepository: %w", err)
  447. }
  448. defer tmpRepo.Close()
  449. // Find the merge-base
  450. _, base, err := tmpRepo.GetMergeBase("", "base", "tracking")
  451. if err != nil {
  452. return false, fmt.Errorf("GetMergeBase: %w", err)
  453. }
  454. cmd := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base)
  455. stdoutReader, stdoutWriter, err := os.Pipe()
  456. if err != nil {
  457. return false, fmt.Errorf("unable to open pipe for to run diff: %w", err)
  458. }
  459. stderr := new(bytes.Buffer)
  460. if err := cmd.Run(ctx, &gitcmd.RunOpts{
  461. Dir: prCtx.tmpBasePath,
  462. Stdout: stdoutWriter,
  463. Stderr: stderr,
  464. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  465. _ = stdoutWriter.Close()
  466. defer func() {
  467. _ = stdoutReader.Close()
  468. }()
  469. return util.IsEmptyReader(stdoutReader)
  470. },
  471. }); err != nil {
  472. if err == util.ErrNotEmpty {
  473. return true, nil
  474. }
  475. err = gitcmd.ConcatenateError(err, stderr.String())
  476. log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v",
  477. newCommitID, oldCommitID, base,
  478. pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
  479. err)
  480. return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err)
  481. }
  482. return false, nil
  483. }
  484. // PushToBaseRepo pushes commits from branches of head repository to
  485. // corresponding branches of base repository.
  486. // FIXME: Only push branches that are actually updates?
  487. func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
  488. return pushToBaseRepoHelper(ctx, pr, "")
  489. }
  490. func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
  491. log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitHeadRefName())
  492. if err := pr.LoadHeadRepo(ctx); err != nil {
  493. log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
  494. return err
  495. }
  496. headRepoPath := pr.HeadRepo.RepoPath()
  497. if err := pr.LoadBaseRepo(ctx); err != nil {
  498. log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
  499. return err
  500. }
  501. baseRepoPath := pr.BaseRepo.RepoPath()
  502. if err = pr.LoadIssue(ctx); err != nil {
  503. return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
  504. }
  505. if err = pr.Issue.LoadPoster(ctx); err != nil {
  506. return fmt.Errorf("unable to load poster %d for pr %d: %w", pr.Issue.PosterID, pr.ID, err)
  507. }
  508. gitRefName := pr.GetGitHeadRefName()
  509. if err := git.Push(ctx, headRepoPath, git.PushOptions{
  510. Remote: baseRepoPath,
  511. Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
  512. Force: true,
  513. // Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
  514. Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
  515. }); err != nil {
  516. if git.IsErrPushOutOfDate(err) {
  517. // This should not happen as we're using force!
  518. 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)
  519. return err
  520. } else if git.IsErrPushRejected(err) {
  521. rejectErr := err.(*git.ErrPushRejected)
  522. 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)
  523. return err
  524. } else if git.IsErrMoreThanOne(err) {
  525. if prefixHeadBranch != "" {
  526. log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch)
  527. return err
  528. }
  529. log.Info("Retrying to push with %s%s", git.BranchPrefix, pr.HeadBranch)
  530. err = pushToBaseRepoHelper(ctx, pr, git.BranchPrefix)
  531. return err
  532. }
  533. 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)
  534. return fmt.Errorf("Push: %s:%s %s:%s %w", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
  535. }
  536. return nil
  537. }
  538. // UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations
  539. func UpdatePullsRefs(ctx context.Context, repo *repo_model.Repository, update *repo_module.PushUpdateOptions) {
  540. branch := update.RefFullName.BranchName()
  541. // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
  542. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch)
  543. if err != nil {
  544. log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repo.ID, branch, err)
  545. } else {
  546. for _, pr := range prs {
  547. log.Trace("Updating PR[%d]: composing new test task", pr.ID)
  548. if pr.Flow == issues_model.PullRequestFlowGithub {
  549. if err := PushToBaseRepo(ctx, pr); err != nil {
  550. log.Error("PushToBaseRepo: %v", err)
  551. }
  552. }
  553. }
  554. }
  555. }
  556. // UpdateRef update refs/pull/id/head directly for agit flow pull request
  557. func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
  558. log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitHeadRefName())
  559. if err := pr.LoadBaseRepo(ctx); err != nil {
  560. log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
  561. return err
  562. }
  563. if err := gitrepo.UpdateRef(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), pr.HeadCommitID); err != nil {
  564. log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
  565. }
  566. return err
  567. }
  568. // retargetBranchPulls change target branch for all pull requests whose base branch is the branch
  569. // Both branch and targetBranch must be in the same repo (for security reasons)
  570. func retargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch, targetBranch string) error {
  571. prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
  572. if err != nil {
  573. return err
  574. }
  575. if err := prs.LoadAttributes(ctx); err != nil {
  576. return err
  577. }
  578. var errs []error
  579. for _, pr := range prs {
  580. if err = pr.Issue.LoadRepo(ctx); err != nil {
  581. errs = append(errs, err)
  582. } else if err = ChangeTargetBranch(ctx, pr, doer, targetBranch); err != nil &&
  583. !issues_model.IsErrIssueIsClosed(err) && !IsErrPullRequestHasMerged(err) &&
  584. !issues_model.IsErrPullRequestAlreadyExists(err) {
  585. errs = append(errs, err)
  586. }
  587. }
  588. return errors.Join(errs...)
  589. }
  590. // AdjustPullsCausedByBranchDeleted close all the pull requests who's head branch is the branch
  591. // Or Close all the plls who's base branch is the branch if setting.Repository.PullRequest.RetargetChildrenOnMerge is false.
  592. // If it's true, Retarget all these pulls to the default branch.
  593. func AdjustPullsCausedByBranchDeleted(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) error {
  594. // branch as head branch
  595. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch)
  596. if err != nil {
  597. return err
  598. }
  599. if err := prs.LoadAttributes(ctx); err != nil {
  600. return err
  601. }
  602. prs.SetHeadRepo(repo)
  603. if err := prs.LoadRepositories(ctx); err != nil {
  604. return err
  605. }
  606. var errs []error
  607. for _, pr := range prs {
  608. if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
  609. errs = append(errs, err)
  610. }
  611. if err == nil {
  612. if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
  613. log.Error("AddDeletePRBranchComment: %v", err)
  614. errs = append(errs, err)
  615. }
  616. }
  617. }
  618. if setting.Repository.PullRequest.RetargetChildrenOnMerge {
  619. if err := retargetBranchPulls(ctx, doer, repo.ID, branch, repo.DefaultBranch); err != nil {
  620. log.Error("retargetBranchPulls failed: %v", err)
  621. errs = append(errs, err)
  622. }
  623. return errors.Join(errs...)
  624. }
  625. // branch as base branch
  626. prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repo.ID, branch)
  627. if err != nil {
  628. return err
  629. }
  630. if err := prs.LoadAttributes(ctx); err != nil {
  631. return err
  632. }
  633. prs.SetBaseRepo(repo)
  634. if err := prs.LoadRepositories(ctx); err != nil {
  635. return err
  636. }
  637. errs = nil
  638. for _, pr := range prs {
  639. if err = issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.BaseBranch); err != nil {
  640. log.Error("AddDeletePRBranchComment: %v", err)
  641. errs = append(errs, err)
  642. }
  643. if err == nil {
  644. if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
  645. errs = append(errs, err)
  646. }
  647. }
  648. }
  649. return errors.Join(errs...)
  650. }
  651. // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository, but only whose base repo is not in the given repository
  652. func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) error {
  653. branches, _, err := gitrepo.GetBranchesByPath(ctx, repo, 0, 0)
  654. if err != nil {
  655. return err
  656. }
  657. var errs []error
  658. for _, branch := range branches {
  659. prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch)
  660. if err != nil {
  661. return err
  662. }
  663. if err = prs.LoadAttributes(ctx); err != nil {
  664. return err
  665. }
  666. for _, pr := range prs {
  667. // If the base repository for this pr is this repository there is no need to close it
  668. // as it is going to be deleted anyway
  669. if pr.BaseRepoID == repo.ID {
  670. continue
  671. }
  672. if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) {
  673. errs = append(errs, err)
  674. }
  675. }
  676. }
  677. return errors.Join(errs...)
  678. }
  679. var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
  680. // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
  681. func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
  682. if err := pr.LoadIssue(ctx); err != nil {
  683. log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
  684. return ""
  685. }
  686. if err := pr.Issue.LoadPoster(ctx); err != nil {
  687. log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
  688. return ""
  689. }
  690. if pr.HeadRepo == nil {
  691. var err error
  692. pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
  693. if err != nil {
  694. log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err)
  695. return ""
  696. }
  697. }
  698. gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
  699. if err != nil {
  700. log.Error("Unable to open head repository: Error: %v", err)
  701. return ""
  702. }
  703. defer closer.Close()
  704. var headCommit *git.Commit
  705. if pr.Flow == issues_model.PullRequestFlowGithub {
  706. headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
  707. } else {
  708. pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  709. if err != nil {
  710. log.Error("Unable to get head commit: %s Error: %v", pr.GetGitHeadRefName(), err)
  711. return ""
  712. }
  713. headCommit, err = gitRepo.GetCommit(pr.HeadCommitID)
  714. }
  715. if err != nil {
  716. log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
  717. return ""
  718. }
  719. mergeBase, err := gitRepo.GetCommit(pr.MergeBase)
  720. if err != nil {
  721. log.Error("Unable to get merge base commit: %s Error: %v", pr.MergeBase, err)
  722. return ""
  723. }
  724. limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit
  725. commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, 0)
  726. if err != nil {
  727. log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
  728. return ""
  729. }
  730. posterSig := pr.Issue.Poster.NewGitSig().String()
  731. uniqueAuthors := make(container.Set[string])
  732. authors := make([]string, 0, len(commits))
  733. stringBuilder := strings.Builder{}
  734. if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
  735. message := strings.TrimSpace(pr.Issue.Content)
  736. stringBuilder.WriteString(message)
  737. if stringBuilder.Len() > 0 {
  738. stringBuilder.WriteRune('\n')
  739. if !commitMessageTrailersPattern.MatchString(message) {
  740. stringBuilder.WriteRune('\n')
  741. }
  742. }
  743. }
  744. // commits list is in reverse chronological order
  745. first := true
  746. for i := len(commits) - 1; i >= 0; i-- {
  747. commit := commits[i]
  748. if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
  749. maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
  750. if maxSize < 0 || stringBuilder.Len() < maxSize {
  751. var toWrite []byte
  752. if first {
  753. first = false
  754. toWrite = []byte(strings.TrimPrefix(commit.CommitMessage, pr.Issue.Title))
  755. } else {
  756. toWrite = []byte(commit.CommitMessage)
  757. }
  758. if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
  759. toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
  760. }
  761. if _, err := stringBuilder.Write(toWrite); err != nil {
  762. log.Error("Unable to write commit message Error: %v", err)
  763. return ""
  764. }
  765. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  766. log.Error("Unable to write commit message Error: %v", err)
  767. return ""
  768. }
  769. }
  770. }
  771. authorString := commit.Author.String()
  772. if uniqueAuthors.Add(authorString) && authorString != posterSig {
  773. // Compare use account as well to avoid adding the same author multiple times
  774. // times when email addresses are private or multiple emails are used.
  775. commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
  776. if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
  777. authors = append(authors, authorString)
  778. }
  779. }
  780. }
  781. // Consider collecting the remaining authors
  782. if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
  783. skip := limit
  784. limit = 30
  785. for {
  786. commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
  787. if err != nil {
  788. log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
  789. return ""
  790. }
  791. if len(commits) == 0 {
  792. break
  793. }
  794. for _, commit := range commits {
  795. authorString := commit.Author.String()
  796. if uniqueAuthors.Add(authorString) && authorString != posterSig {
  797. commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
  798. if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
  799. authors = append(authors, authorString)
  800. }
  801. }
  802. }
  803. skip += limit
  804. }
  805. }
  806. for _, author := range authors {
  807. if _, err := stringBuilder.WriteString("Co-authored-by: "); err != nil {
  808. log.Error("Unable to write to string builder Error: %v", err)
  809. return ""
  810. }
  811. if _, err := stringBuilder.WriteString(author); err != nil {
  812. log.Error("Unable to write to string builder Error: %v", err)
  813. return ""
  814. }
  815. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  816. log.Error("Unable to write to string builder Error: %v", err)
  817. return ""
  818. }
  819. }
  820. return stringBuilder.String()
  821. }
  822. // 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
  823. func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
  824. if err := issues.LoadPullRequests(ctx); err != nil {
  825. return nil, nil, err
  826. }
  827. if _, err := issues.LoadRepositories(ctx); err != nil {
  828. return nil, nil, err
  829. }
  830. var (
  831. gitRepos = make(map[int64]*git.Repository)
  832. res = make(map[int64][]*git_model.CommitStatus)
  833. lastRes = make(map[int64]*git_model.CommitStatus)
  834. err error
  835. )
  836. defer func() {
  837. for _, gitRepo := range gitRepos {
  838. gitRepo.Close()
  839. }
  840. }()
  841. for _, issue := range issues {
  842. if !issue.IsPull {
  843. continue
  844. }
  845. gitRepo, ok := gitRepos[issue.RepoID]
  846. if !ok {
  847. gitRepo, err = gitrepo.OpenRepository(ctx, issue.Repo)
  848. if err != nil {
  849. log.Error("Cannot open git repository %-v for issue #%d[%d]. Error: %v", issue.Repo, issue.Index, issue.ID, err)
  850. continue
  851. }
  852. gitRepos[issue.RepoID] = gitRepo
  853. }
  854. statuses, lastStatus, err := getAllCommitStatus(ctx, gitRepo, issue.PullRequest)
  855. if err != nil {
  856. log.Error("getAllCommitStatus: cant get commit statuses of pull [%d]: %v", issue.PullRequest.ID, err)
  857. continue
  858. }
  859. res[issue.PullRequest.ID] = statuses
  860. lastRes[issue.PullRequest.ID] = lastStatus
  861. }
  862. return res, lastRes, nil
  863. }
  864. // getAllCommitStatus get pr's commit statuses.
  865. func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) {
  866. sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  867. if shaErr != nil {
  868. return nil, nil, shaErr
  869. }
  870. statuses, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
  871. lastStatus = git_model.CalcCommitStatus(statuses)
  872. return statuses, lastStatus, err
  873. }
  874. // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
  875. func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) {
  876. var err error
  877. if err = pr.LoadBaseRepo(ctx); err != nil {
  878. return false, err
  879. }
  880. baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
  881. if err != nil {
  882. return false, err
  883. }
  884. defer closer.Close()
  885. baseCommit, err := baseGitRepo.GetBranchCommit(branchName)
  886. if err != nil {
  887. return false, err
  888. }
  889. if err = pr.LoadHeadRepo(ctx); err != nil {
  890. return false, err
  891. }
  892. var headGitRepo *git.Repository
  893. if pr.HeadRepoID == pr.BaseRepoID {
  894. headGitRepo = baseGitRepo
  895. } else {
  896. var closer io.Closer
  897. headGitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo)
  898. if err != nil {
  899. return false, err
  900. }
  901. defer closer.Close()
  902. }
  903. var headCommit *git.Commit
  904. if pr.Flow == issues_model.PullRequestFlowGithub {
  905. headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch)
  906. if err != nil {
  907. return false, err
  908. }
  909. } else {
  910. pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  911. if err != nil {
  912. return false, err
  913. }
  914. if headCommit, err = baseGitRepo.GetCommit(pr.HeadCommitID); err != nil {
  915. return false, err
  916. }
  917. }
  918. return baseCommit.HasPreviousCommit(headCommit.ID)
  919. }
  920. type CommitInfo struct {
  921. Summary string `json:"summary"`
  922. CommitterOrAuthorName string `json:"committer_or_author_name"`
  923. ID string `json:"id"`
  924. ShortSha string `json:"short_sha"`
  925. Time string `json:"time"`
  926. }
  927. // GetPullCommits returns all commits on given pull request and the last review commit sha
  928. // Attention: The last review commit sha must be from the latest review whose commit id is not empty.
  929. // So the type of the latest review cannot be "ReviewTypeRequest".
  930. func GetPullCommits(ctx context.Context, baseGitRepo *git.Repository, doer *user_model.User, issue *issues_model.Issue) ([]CommitInfo, string, error) {
  931. pull := issue.PullRequest
  932. if err := pull.LoadBaseRepo(ctx); err != nil {
  933. return nil, "", err
  934. }
  935. baseBranch := pull.BaseBranch
  936. if pull.HasMerged {
  937. baseBranch = pull.MergeBase
  938. }
  939. prInfo, err := GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, baseBranch, pull.GetGitHeadRefName(), true, false)
  940. if err != nil {
  941. return nil, "", err
  942. }
  943. commits := make([]CommitInfo, 0, len(prInfo.Commits))
  944. for _, commit := range prInfo.Commits {
  945. var committerOrAuthorName string
  946. var commitTime time.Time
  947. if commit.Author != nil {
  948. committerOrAuthorName = commit.Author.Name
  949. commitTime = commit.Author.When
  950. } else {
  951. committerOrAuthorName = commit.Committer.Name
  952. commitTime = commit.Committer.When
  953. }
  954. commits = append(commits, CommitInfo{
  955. Summary: commit.Summary(),
  956. CommitterOrAuthorName: committerOrAuthorName,
  957. ID: commit.ID.String(),
  958. ShortSha: base.ShortSha(commit.ID.String()),
  959. Time: commitTime.Format(time.RFC3339),
  960. })
  961. }
  962. var lastReviewCommitID string
  963. if doer != nil {
  964. // get last review of current user and store information in context (if available)
  965. lastreview, err := issues_model.FindLatestReviews(ctx, issues_model.FindReviewOptions{
  966. IssueID: issue.ID,
  967. ReviewerID: doer.ID,
  968. Types: []issues_model.ReviewType{
  969. issues_model.ReviewTypeApprove,
  970. issues_model.ReviewTypeComment,
  971. issues_model.ReviewTypeReject,
  972. },
  973. })
  974. if err != nil && !issues_model.IsErrReviewNotExist(err) {
  975. return nil, "", err
  976. }
  977. if len(lastreview) > 0 {
  978. lastReviewCommitID = lastreview[0].CommitID
  979. }
  980. }
  981. return commits, lastReviewCommitID, nil
  982. }