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.

744 lines
22 KiB

Change target branch for pull request (#6488) * Adds functionality to change target branch of created pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use const instead of var in JavaScript additions Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Check if branches are equal and if PR already exists before changing target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Make sure to check all commits Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Print error messages for user as error flash message Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Disallow changing target branch of closed or merged pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Resolve conflicts after merge of upstream/master Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Change order of branch select fields Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes duplicate check Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use ctx.Tr for translations Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Recompile JS Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use correct translation namespace Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove redundant if condition Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves most change branch logic into pull service Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Completes comment Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Add Ref to ChangesPayload for logging changed target branches instead of creating a new struct Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Revert changes to go.mod Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Directly use createComment method Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return 404 if pull request is not found. Move written check up Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove variable declaration Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return client errors on change pull request target errors Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return error in commit.HasPreviousCommit Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds blank line Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Test patch before persisting new target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update patch before testing (not working) Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes patch calls when changeing pull request target Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes unneeded check for base name Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves ChangeTargetBranch completely to pull service. Update patch status. Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Set webhook mode after errors were validated Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update PR in one transaction Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Move logic for check if head is equal with branch to pull model Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds missing comment and simplify return Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adjust CreateComment method call Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>
4 years ago
Change target branch for pull request (#6488) * Adds functionality to change target branch of created pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use const instead of var in JavaScript additions Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Check if branches are equal and if PR already exists before changing target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Make sure to check all commits Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Print error messages for user as error flash message Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Disallow changing target branch of closed or merged pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Resolve conflicts after merge of upstream/master Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Change order of branch select fields Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes duplicate check Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use ctx.Tr for translations Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Recompile JS Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use correct translation namespace Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove redundant if condition Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves most change branch logic into pull service Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Completes comment Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Add Ref to ChangesPayload for logging changed target branches instead of creating a new struct Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Revert changes to go.mod Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Directly use createComment method Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return 404 if pull request is not found. Move written check up Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove variable declaration Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return client errors on change pull request target errors Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return error in commit.HasPreviousCommit Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds blank line Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Test patch before persisting new target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update patch before testing (not working) Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes patch calls when changeing pull request target Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes unneeded check for base name Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves ChangeTargetBranch completely to pull service. Update patch status. Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Set webhook mode after errors were validated Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update PR in one transaction Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Move logic for check if head is equal with branch to pull model Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds missing comment and simplify return Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adjust CreateComment method call Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>
4 years ago
Change target branch for pull request (#6488) * Adds functionality to change target branch of created pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use const instead of var in JavaScript additions Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Check if branches are equal and if PR already exists before changing target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Make sure to check all commits Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Print error messages for user as error flash message Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Disallow changing target branch of closed or merged pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Resolve conflicts after merge of upstream/master Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Change order of branch select fields Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes duplicate check Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use ctx.Tr for translations Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Recompile JS Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use correct translation namespace Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove redundant if condition Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves most change branch logic into pull service Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Completes comment Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Add Ref to ChangesPayload for logging changed target branches instead of creating a new struct Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Revert changes to go.mod Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Directly use createComment method Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return 404 if pull request is not found. Move written check up Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove variable declaration Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return client errors on change pull request target errors Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return error in commit.HasPreviousCommit Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds blank line Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Test patch before persisting new target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update patch before testing (not working) Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes patch calls when changeing pull request target Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes unneeded check for base name Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves ChangeTargetBranch completely to pull service. Update patch status. Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Set webhook mode after errors were validated Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update PR in one transaction Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Move logic for check if head is equal with branch to pull model Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds missing comment and simplify return Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adjust CreateComment method call Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>
4 years ago
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package pull
  5. import (
  6. "bufio"
  7. "bytes"
  8. "context"
  9. "encoding/json"
  10. "fmt"
  11. "os"
  12. "path"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/models"
  16. "code.gitea.io/gitea/modules/git"
  17. "code.gitea.io/gitea/modules/graceful"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/notification"
  20. "code.gitea.io/gitea/modules/setting"
  21. issue_service "code.gitea.io/gitea/services/issue"
  22. "github.com/unknwon/com"
  23. )
  24. // NewPullRequest creates new pull request with labels for repository.
  25. func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, assigneeIDs []int64) error {
  26. if err := TestPatch(pr); err != nil {
  27. return err
  28. }
  29. divergence, err := GetDiverging(pr)
  30. if err != nil {
  31. return err
  32. }
  33. pr.CommitsAhead = divergence.Ahead
  34. pr.CommitsBehind = divergence.Behind
  35. if err := models.NewPullRequest(repo, pull, labelIDs, uuids, pr); err != nil {
  36. return err
  37. }
  38. for _, assigneeID := range assigneeIDs {
  39. if err := issue_service.AddAssigneeIfNotAssigned(pull, pull.Poster, assigneeID); err != nil {
  40. return err
  41. }
  42. }
  43. pr.Issue = pull
  44. pull.PullRequest = pr
  45. if err := PushToBaseRepo(pr); err != nil {
  46. return err
  47. }
  48. notification.NotifyNewPullRequest(pr)
  49. // add first push codes comment
  50. baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
  51. if err != nil {
  52. return err
  53. }
  54. defer baseGitRepo.Close()
  55. compareInfo, err := baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(),
  56. git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
  57. if err != nil {
  58. return err
  59. }
  60. if compareInfo.Commits.Len() > 0 {
  61. data := models.PushActionContent{IsForcePush: false}
  62. data.CommitIDs = make([]string, 0, compareInfo.Commits.Len())
  63. for e := compareInfo.Commits.Back(); e != nil; e = e.Prev() {
  64. data.CommitIDs = append(data.CommitIDs, e.Value.(*git.Commit).ID.String())
  65. }
  66. dataJSON, err := json.Marshal(data)
  67. if err != nil {
  68. return err
  69. }
  70. ops := &models.CreateCommentOptions{
  71. Type: models.CommentTypePullPush,
  72. Doer: pull.Poster,
  73. Repo: repo,
  74. Issue: pr.Issue,
  75. IsForcePush: false,
  76. Content: string(dataJSON),
  77. }
  78. _, _ = models.CreateComment(ops)
  79. }
  80. return nil
  81. }
  82. // ChangeTargetBranch changes the target branch of this pull request, as the given user.
  83. func ChangeTargetBranch(pr *models.PullRequest, doer *models.User, targetBranch string) (err error) {
  84. // Current target branch is already the same
  85. if pr.BaseBranch == targetBranch {
  86. return nil
  87. }
  88. if pr.Issue.IsClosed {
  89. return models.ErrIssueIsClosed{
  90. ID: pr.Issue.ID,
  91. RepoID: pr.Issue.RepoID,
  92. Index: pr.Issue.Index,
  93. }
  94. }
  95. if pr.HasMerged {
  96. return models.ErrPullRequestHasMerged{
  97. ID: pr.ID,
  98. IssueID: pr.Index,
  99. HeadRepoID: pr.HeadRepoID,
  100. BaseRepoID: pr.BaseRepoID,
  101. HeadBranch: pr.HeadBranch,
  102. BaseBranch: pr.BaseBranch,
  103. }
  104. }
  105. // Check if branches are equal
  106. branchesEqual, err := IsHeadEqualWithBranch(pr, targetBranch)
  107. if err != nil {
  108. return err
  109. }
  110. if branchesEqual {
  111. return models.ErrBranchesEqual{
  112. HeadBranchName: pr.HeadBranch,
  113. BaseBranchName: targetBranch,
  114. }
  115. }
  116. // Check if pull request for the new target branch already exists
  117. existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch)
  118. if existingPr != nil {
  119. return models.ErrPullRequestAlreadyExists{
  120. ID: existingPr.ID,
  121. IssueID: existingPr.Index,
  122. HeadRepoID: existingPr.HeadRepoID,
  123. BaseRepoID: existingPr.BaseRepoID,
  124. HeadBranch: existingPr.HeadBranch,
  125. BaseBranch: existingPr.BaseBranch,
  126. }
  127. }
  128. if err != nil && !models.IsErrPullRequestNotExist(err) {
  129. return err
  130. }
  131. // Set new target branch
  132. oldBranch := pr.BaseBranch
  133. pr.BaseBranch = targetBranch
  134. // Refresh patch
  135. if err := TestPatch(pr); err != nil {
  136. return err
  137. }
  138. // Update target branch, PR diff and status
  139. // This is the same as checkAndUpdateStatus in check service, but also updates base_branch
  140. if pr.Status == models.PullRequestStatusChecking {
  141. pr.Status = models.PullRequestStatusMergeable
  142. }
  143. // Update Commit Divergence
  144. divergence, err := GetDiverging(pr)
  145. if err != nil {
  146. return err
  147. }
  148. pr.CommitsAhead = divergence.Ahead
  149. pr.CommitsBehind = divergence.Behind
  150. if err := pr.UpdateColsIfNotMerged("merge_base", "status", "conflicted_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
  151. return err
  152. }
  153. // Create comment
  154. options := &models.CreateCommentOptions{
  155. Type: models.CommentTypeChangeTargetBranch,
  156. Doer: doer,
  157. Repo: pr.Issue.Repo,
  158. Issue: pr.Issue,
  159. OldRef: oldBranch,
  160. NewRef: targetBranch,
  161. }
  162. if _, err = models.CreateComment(options); err != nil {
  163. return fmt.Errorf("CreateChangeTargetBranchComment: %v", err)
  164. }
  165. return nil
  166. }
  167. func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *models.User, branch string) error {
  168. repo, err := models.GetRepositoryByID(repoID)
  169. if err != nil {
  170. return fmt.Errorf("GetRepositoryByID: %v", err)
  171. }
  172. gitRepo, err := git.OpenRepository(repo.RepoPath())
  173. if err != nil {
  174. return fmt.Errorf("git.OpenRepository: %v", err)
  175. }
  176. go func() {
  177. // FIXME: graceful: We need to tell the manager we're doing something...
  178. err := requests.InvalidateCodeComments(doer, gitRepo, branch)
  179. if err != nil {
  180. log.Error("PullRequestList.InvalidateCodeComments: %v", err)
  181. }
  182. gitRepo.Close()
  183. }()
  184. return nil
  185. }
  186. func addHeadRepoTasks(prs []*models.PullRequest) {
  187. for _, pr := range prs {
  188. log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
  189. if err := PushToBaseRepo(pr); err != nil {
  190. log.Error("PushToBaseRepo: %v", err)
  191. continue
  192. }
  193. AddToTaskQueue(pr)
  194. }
  195. }
  196. // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
  197. // and generate new patch for testing as needed.
  198. func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
  199. log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
  200. graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
  201. // There is no sensible way to shut this down ":-("
  202. // If you don't let it run all the way then you will lose data
  203. // FIXME: graceful: AddTestPullRequestTask needs to become a queue!
  204. prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
  205. if err != nil {
  206. log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
  207. return
  208. }
  209. if isSync {
  210. requests := models.PullRequestList(prs)
  211. if err = requests.LoadAttributes(); err != nil {
  212. log.Error("PullRequestList.LoadAttributes: %v", err)
  213. }
  214. if invalidationErr := checkForInvalidation(requests, repoID, doer, branch); invalidationErr != nil {
  215. log.Error("checkForInvalidation: %v", invalidationErr)
  216. }
  217. if err == nil {
  218. for _, pr := range prs {
  219. if newCommitID != "" && newCommitID != git.EmptySHA {
  220. changed, err := checkIfPRContentChanged(pr, oldCommitID, newCommitID)
  221. if err != nil {
  222. log.Error("checkIfPRContentChanged: %v", err)
  223. }
  224. if changed {
  225. // Mark old reviews as stale if diff to mergebase has changed
  226. if err := models.MarkReviewsAsStale(pr.IssueID); err != nil {
  227. log.Error("MarkReviewsAsStale: %v", err)
  228. }
  229. }
  230. if err := models.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
  231. log.Error("MarkReviewsAsNotStale: %v", err)
  232. }
  233. divergence, err := GetDiverging(pr)
  234. if err != nil {
  235. log.Error("GetDiverging: %v", err)
  236. } else {
  237. err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind)
  238. if err != nil {
  239. log.Error("UpdateCommitDivergence: %v", err)
  240. }
  241. }
  242. }
  243. pr.Issue.PullRequest = pr
  244. notification.NotifyPullRequestSynchronized(doer, pr)
  245. }
  246. }
  247. }
  248. addHeadRepoTasks(prs)
  249. for _, pr := range prs {
  250. comment, err := models.CreatePushPullComment(doer, pr, oldCommitID, newCommitID)
  251. if err == nil && comment != nil {
  252. notification.NotifyPullRequestPushCommits(doer, pr, comment)
  253. }
  254. }
  255. log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
  256. prs, err = models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
  257. if err != nil {
  258. log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
  259. return
  260. }
  261. for _, pr := range prs {
  262. divergence, err := GetDiverging(pr)
  263. if err != nil {
  264. log.Error("GetDiverging: %v", err)
  265. } else {
  266. err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind)
  267. if err != nil {
  268. log.Error("UpdateCommitDivergence: %v", err)
  269. }
  270. }
  271. AddToTaskQueue(pr)
  272. }
  273. })
  274. }
  275. // checkIfPRContentChanged checks if diff to target branch has changed by push
  276. // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
  277. func checkIfPRContentChanged(pr *models.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
  278. if err = pr.LoadHeadRepo(); err != nil {
  279. return false, fmt.Errorf("LoadHeadRepo: %v", err)
  280. } else if pr.HeadRepo == nil {
  281. // corrupt data assumed changed
  282. return true, nil
  283. }
  284. if err = pr.LoadBaseRepo(); err != nil {
  285. return false, fmt.Errorf("LoadBaseRepo: %v", err)
  286. }
  287. headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
  288. if err != nil {
  289. return false, fmt.Errorf("OpenRepository: %v", err)
  290. }
  291. defer headGitRepo.Close()
  292. // Add a temporary remote.
  293. tmpRemote := "checkIfPRContentChanged-" + com.ToStr(time.Now().UnixNano())
  294. if err = headGitRepo.AddRemote(tmpRemote, pr.BaseRepo.RepoPath(), true); err != nil {
  295. return false, fmt.Errorf("AddRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
  296. }
  297. defer func() {
  298. if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
  299. log.Error("checkIfPRContentChanged: RemoveRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
  300. }
  301. }()
  302. // To synchronize repo and get a base ref
  303. _, base, err := headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch)
  304. if err != nil {
  305. return false, fmt.Errorf("GetMergeBase: %v", err)
  306. }
  307. diffBefore := &bytes.Buffer{}
  308. diffAfter := &bytes.Buffer{}
  309. if err := headGitRepo.GetDiffFromMergeBase(base, oldCommitID, diffBefore); err != nil {
  310. // If old commit not found, assume changed.
  311. log.Debug("GetDiffFromMergeBase: %v", err)
  312. return true, nil
  313. }
  314. if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil {
  315. // New commit should be found
  316. return false, fmt.Errorf("GetDiffFromMergeBase: %v", err)
  317. }
  318. diffBeforeLines := bufio.NewScanner(diffBefore)
  319. diffAfterLines := bufio.NewScanner(diffAfter)
  320. for diffBeforeLines.Scan() && diffAfterLines.Scan() {
  321. if strings.HasPrefix(diffBeforeLines.Text(), "index") && strings.HasPrefix(diffAfterLines.Text(), "index") {
  322. // file hashes can change without the diff changing
  323. continue
  324. } else if strings.HasPrefix(diffBeforeLines.Text(), "@@") && strings.HasPrefix(diffAfterLines.Text(), "@@") {
  325. // the location of the difference may change
  326. continue
  327. } else if !bytes.Equal(diffBeforeLines.Bytes(), diffAfterLines.Bytes()) {
  328. return true, nil
  329. }
  330. }
  331. if diffBeforeLines.Scan() || diffAfterLines.Scan() {
  332. // Diffs not of equal length
  333. return true, nil
  334. }
  335. return false, nil
  336. }
  337. // PushToBaseRepo pushes commits from branches of head repository to
  338. // corresponding branches of base repository.
  339. // FIXME: Only push branches that are actually updates?
  340. func PushToBaseRepo(pr *models.PullRequest) (err error) {
  341. log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
  342. // Clone base repo.
  343. tmpBasePath, err := models.CreateTemporaryPath("pull")
  344. if err != nil {
  345. log.Error("CreateTemporaryPath: %v", err)
  346. return err
  347. }
  348. defer func() {
  349. err := models.RemoveTemporaryPath(tmpBasePath)
  350. if err != nil {
  351. log.Error("Error whilst removing temporary path: %s Error: %v", tmpBasePath, err)
  352. }
  353. }()
  354. if err := pr.LoadHeadRepo(); err != nil {
  355. log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
  356. return err
  357. }
  358. headRepoPath := pr.HeadRepo.RepoPath()
  359. if err := git.Clone(headRepoPath, tmpBasePath, git.CloneRepoOptions{
  360. Bare: true,
  361. Shared: true,
  362. Branch: pr.HeadBranch,
  363. Quiet: true,
  364. }); err != nil {
  365. log.Error("git clone tmpBasePath: %v", err)
  366. return err
  367. }
  368. gitRepo, err := git.OpenRepository(tmpBasePath)
  369. if err != nil {
  370. return fmt.Errorf("OpenRepository: %v", err)
  371. }
  372. if err := pr.LoadBaseRepo(); err != nil {
  373. log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
  374. return err
  375. }
  376. if err := gitRepo.AddRemote("base", pr.BaseRepo.RepoPath(), false); err != nil {
  377. return fmt.Errorf("tmpGitRepo.AddRemote: %v", err)
  378. }
  379. defer gitRepo.Close()
  380. headFile := pr.GetGitRefName()
  381. // Remove head in case there is a conflict.
  382. file := path.Join(pr.BaseRepo.RepoPath(), headFile)
  383. _ = os.Remove(file)
  384. if err = pr.LoadIssue(); err != nil {
  385. return fmt.Errorf("unable to load issue %d for pr %d: %v", pr.IssueID, pr.ID, err)
  386. }
  387. if err = pr.Issue.LoadPoster(); err != nil {
  388. return fmt.Errorf("unable to load poster %d for pr %d: %v", pr.Issue.PosterID, pr.ID, err)
  389. }
  390. if err = git.Push(tmpBasePath, git.PushOptions{
  391. Remote: "base",
  392. Branch: fmt.Sprintf("%s:%s", pr.HeadBranch, headFile),
  393. Force: true,
  394. // Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
  395. Env: models.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
  396. }); err != nil {
  397. if git.IsErrPushOutOfDate(err) {
  398. // This should not happen as we're using force!
  399. log.Error("Unable to push PR head for %s#%d (%-v:%s) due to ErrPushOfDate: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, headFile, err)
  400. return err
  401. } else if git.IsErrPushRejected(err) {
  402. rejectErr := err.(*git.ErrPushRejected)
  403. 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, headFile, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
  404. return err
  405. }
  406. log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, headFile, err)
  407. return fmt.Errorf("Push: %s:%s %s:%s %v", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), headFile, err)
  408. }
  409. return nil
  410. }
  411. type errlist []error
  412. func (errs errlist) Error() string {
  413. if len(errs) > 0 {
  414. var buf strings.Builder
  415. for i, err := range errs {
  416. if i > 0 {
  417. buf.WriteString(", ")
  418. }
  419. buf.WriteString(err.Error())
  420. }
  421. return buf.String()
  422. }
  423. return ""
  424. }
  425. // CloseBranchPulls close all the pull requests who's head branch is the branch
  426. func CloseBranchPulls(doer *models.User, repoID int64, branch string) error {
  427. prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
  428. if err != nil {
  429. return err
  430. }
  431. prs2, err := models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
  432. if err != nil {
  433. return err
  434. }
  435. prs = append(prs, prs2...)
  436. if err := models.PullRequestList(prs).LoadAttributes(); err != nil {
  437. return err
  438. }
  439. var errs errlist
  440. for _, pr := range prs {
  441. if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) {
  442. errs = append(errs, err)
  443. }
  444. }
  445. if len(errs) > 0 {
  446. return errs
  447. }
  448. return nil
  449. }
  450. // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository
  451. func CloseRepoBranchesPulls(doer *models.User, repo *models.Repository) error {
  452. branches, err := git.GetBranchesByPath(repo.RepoPath())
  453. if err != nil {
  454. return err
  455. }
  456. var errs errlist
  457. for _, branch := range branches {
  458. prs, err := models.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name)
  459. if err != nil {
  460. return err
  461. }
  462. if err = models.PullRequestList(prs).LoadAttributes(); err != nil {
  463. return err
  464. }
  465. for _, pr := range prs {
  466. if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) {
  467. errs = append(errs, err)
  468. }
  469. }
  470. }
  471. if len(errs) > 0 {
  472. return errs
  473. }
  474. return nil
  475. }
  476. // GetCommitMessages returns the commit messages between head and merge base (if there is one)
  477. func GetCommitMessages(pr *models.PullRequest) string {
  478. if err := pr.LoadIssue(); err != nil {
  479. log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
  480. return ""
  481. }
  482. if err := pr.Issue.LoadPoster(); err != nil {
  483. log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
  484. return ""
  485. }
  486. if pr.HeadRepo == nil {
  487. var err error
  488. pr.HeadRepo, err = models.GetRepositoryByID(pr.HeadRepoID)
  489. if err != nil {
  490. log.Error("GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
  491. return ""
  492. }
  493. }
  494. gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
  495. if err != nil {
  496. log.Error("Unable to open head repository: Error: %v", err)
  497. return ""
  498. }
  499. defer gitRepo.Close()
  500. headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch)
  501. if err != nil {
  502. log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
  503. return ""
  504. }
  505. mergeBase, err := gitRepo.GetCommit(pr.MergeBase)
  506. if err != nil {
  507. log.Error("Unable to get merge base commit: %s Error: %v", pr.MergeBase, err)
  508. return ""
  509. }
  510. limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit
  511. list, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, 0)
  512. if err != nil {
  513. log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
  514. return ""
  515. }
  516. maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
  517. posterSig := pr.Issue.Poster.NewGitSig().String()
  518. authorsMap := map[string]bool{}
  519. authors := make([]string, 0, list.Len())
  520. stringBuilder := strings.Builder{}
  521. element := list.Front()
  522. for element != nil {
  523. commit := element.Value.(*git.Commit)
  524. if maxSize < 0 || stringBuilder.Len() < maxSize {
  525. toWrite := []byte(commit.CommitMessage)
  526. if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
  527. toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
  528. }
  529. if _, err := stringBuilder.Write(toWrite); err != nil {
  530. log.Error("Unable to write commit message Error: %v", err)
  531. return ""
  532. }
  533. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  534. log.Error("Unable to write commit message Error: %v", err)
  535. return ""
  536. }
  537. }
  538. authorString := commit.Author.String()
  539. if !authorsMap[authorString] && authorString != posterSig {
  540. authors = append(authors, authorString)
  541. authorsMap[authorString] = true
  542. }
  543. element = element.Next()
  544. }
  545. // Consider collecting the remaining authors
  546. if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
  547. skip := limit
  548. limit = 30
  549. for {
  550. list, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
  551. if err != nil {
  552. log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
  553. return ""
  554. }
  555. if list.Len() == 0 {
  556. break
  557. }
  558. element := list.Front()
  559. for element != nil {
  560. commit := element.Value.(*git.Commit)
  561. authorString := commit.Author.String()
  562. if !authorsMap[authorString] && authorString != posterSig {
  563. authors = append(authors, authorString)
  564. authorsMap[authorString] = true
  565. }
  566. element = element.Next()
  567. }
  568. }
  569. }
  570. if len(authors) > 0 {
  571. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  572. log.Error("Unable to write to string builder Error: %v", err)
  573. return ""
  574. }
  575. }
  576. for _, author := range authors {
  577. if _, err := stringBuilder.Write([]byte("Co-authored-by: ")); err != nil {
  578. log.Error("Unable to write to string builder Error: %v", err)
  579. return ""
  580. }
  581. if _, err := stringBuilder.Write([]byte(author)); err != nil {
  582. log.Error("Unable to write to string builder Error: %v", err)
  583. return ""
  584. }
  585. if _, err := stringBuilder.WriteRune('\n'); err != nil {
  586. log.Error("Unable to write to string builder Error: %v", err)
  587. return ""
  588. }
  589. }
  590. return stringBuilder.String()
  591. }
  592. // GetLastCommitStatus returns the last commit status for this pull request.
  593. func GetLastCommitStatus(pr *models.PullRequest) (status *models.CommitStatus, err error) {
  594. if err = pr.LoadHeadRepo(); err != nil {
  595. return nil, err
  596. }
  597. if pr.HeadRepo == nil {
  598. return nil, models.ErrPullRequestHeadRepoMissing{ID: pr.ID, HeadRepoID: pr.HeadRepoID}
  599. }
  600. headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
  601. if err != nil {
  602. return nil, err
  603. }
  604. defer headGitRepo.Close()
  605. lastCommitID, err := headGitRepo.GetBranchCommitID(pr.HeadBranch)
  606. if err != nil {
  607. return nil, err
  608. }
  609. err = pr.LoadBaseRepo()
  610. if err != nil {
  611. return nil, err
  612. }
  613. statusList, err := models.GetLatestCommitStatus(pr.BaseRepo, lastCommitID, 0)
  614. if err != nil {
  615. return nil, err
  616. }
  617. return models.CalcCommitStatus(statusList), nil
  618. }
  619. // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
  620. func IsHeadEqualWithBranch(pr *models.PullRequest, branchName string) (bool, error) {
  621. var err error
  622. if err = pr.LoadBaseRepo(); err != nil {
  623. return false, err
  624. }
  625. baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
  626. if err != nil {
  627. return false, err
  628. }
  629. baseCommit, err := baseGitRepo.GetBranchCommit(branchName)
  630. if err != nil {
  631. return false, err
  632. }
  633. if err = pr.LoadHeadRepo(); err != nil {
  634. return false, err
  635. }
  636. headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
  637. if err != nil {
  638. return false, err
  639. }
  640. headCommit, err := headGitRepo.GetBranchCommit(pr.HeadBranch)
  641. if err != nil {
  642. return false, err
  643. }
  644. return baseCommit.HasPreviousCommit(headCommit.ID)
  645. }