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 30KB

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