You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

pull.go 7.4 kB

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>
5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. "context"
  7. "fmt"
  8. "os"
  9. "path"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/git"
  12. "code.gitea.io/gitea/modules/graceful"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/notification"
  15. issue_service "code.gitea.io/gitea/services/issue"
  16. )
  17. // NewPullRequest creates new pull request with labels for repository.
  18. func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, assigneeIDs []int64) error {
  19. if err := TestPatch(pr); err != nil {
  20. return err
  21. }
  22. if err := models.NewPullRequest(repo, pull, labelIDs, uuids, pr); err != nil {
  23. return err
  24. }
  25. for _, assigneeID := range assigneeIDs {
  26. if err := issue_service.AddAssigneeIfNotAssigned(pull, pull.Poster, assigneeID); err != nil {
  27. return err
  28. }
  29. }
  30. pr.Issue = pull
  31. pull.PullRequest = pr
  32. if err := PushToBaseRepo(pr); err != nil {
  33. return err
  34. }
  35. notification.NotifyNewPullRequest(pr)
  36. return nil
  37. }
  38. // ChangeTargetBranch changes the target branch of this pull request, as the given user.
  39. func ChangeTargetBranch(pr *models.PullRequest, doer *models.User, targetBranch string) (err error) {
  40. // Current target branch is already the same
  41. if pr.BaseBranch == targetBranch {
  42. return nil
  43. }
  44. if pr.Issue.IsClosed {
  45. return models.ErrIssueIsClosed{
  46. ID: pr.Issue.ID,
  47. RepoID: pr.Issue.RepoID,
  48. Index: pr.Issue.Index,
  49. }
  50. }
  51. if pr.HasMerged {
  52. return models.ErrPullRequestHasMerged{
  53. ID: pr.ID,
  54. IssueID: pr.Index,
  55. HeadRepoID: pr.HeadRepoID,
  56. BaseRepoID: pr.BaseRepoID,
  57. HeadBranch: pr.HeadBranch,
  58. BaseBranch: pr.BaseBranch,
  59. }
  60. }
  61. // Check if branches are equal
  62. branchesEqual, err := pr.IsHeadEqualWithBranch(targetBranch)
  63. if err != nil {
  64. return err
  65. }
  66. if branchesEqual {
  67. return models.ErrBranchesEqual{
  68. HeadBranchName: pr.HeadBranch,
  69. BaseBranchName: targetBranch,
  70. }
  71. }
  72. // Check if pull request for the new target branch already exists
  73. existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch)
  74. if existingPr != nil {
  75. return models.ErrPullRequestAlreadyExists{
  76. ID: existingPr.ID,
  77. IssueID: existingPr.Index,
  78. HeadRepoID: existingPr.HeadRepoID,
  79. BaseRepoID: existingPr.BaseRepoID,
  80. HeadBranch: existingPr.HeadBranch,
  81. BaseBranch: existingPr.BaseBranch,
  82. }
  83. }
  84. if err != nil && !models.IsErrPullRequestNotExist(err) {
  85. return err
  86. }
  87. // Set new target branch
  88. oldBranch := pr.BaseBranch
  89. pr.BaseBranch = targetBranch
  90. // Refresh patch
  91. if err := TestPatch(pr); err != nil {
  92. return err
  93. }
  94. // Update target branch, PR diff and status
  95. // This is the same as checkAndUpdateStatus in check service, but also updates base_branch
  96. if pr.Status == models.PullRequestStatusChecking {
  97. pr.Status = models.PullRequestStatusMergeable
  98. }
  99. if err := pr.UpdateCols("status, conflicted_files, base_branch"); err != nil {
  100. return err
  101. }
  102. // Create comment
  103. options := &models.CreateCommentOptions{
  104. Type: models.CommentTypeChangeTargetBranch,
  105. Doer: doer,
  106. Repo: pr.Issue.Repo,
  107. Issue: pr.Issue,
  108. OldRef: oldBranch,
  109. NewRef: targetBranch,
  110. }
  111. if _, err = models.CreateComment(options); err != nil {
  112. return fmt.Errorf("CreateChangeTargetBranchComment: %v", err)
  113. }
  114. return nil
  115. }
  116. func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *models.User, branch string) error {
  117. repo, err := models.GetRepositoryByID(repoID)
  118. if err != nil {
  119. return fmt.Errorf("GetRepositoryByID: %v", err)
  120. }
  121. gitRepo, err := git.OpenRepository(repo.RepoPath())
  122. if err != nil {
  123. return fmt.Errorf("git.OpenRepository: %v", err)
  124. }
  125. go func() {
  126. // FIXME: graceful: We need to tell the manager we're doing something...
  127. err := requests.InvalidateCodeComments(doer, gitRepo, branch)
  128. if err != nil {
  129. log.Error("PullRequestList.InvalidateCodeComments: %v", err)
  130. }
  131. gitRepo.Close()
  132. }()
  133. return nil
  134. }
  135. func addHeadRepoTasks(prs []*models.PullRequest) {
  136. for _, pr := range prs {
  137. log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
  138. if err := PushToBaseRepo(pr); err != nil {
  139. log.Error("PushToBaseRepo: %v", err)
  140. continue
  141. }
  142. AddToTaskQueue(pr)
  143. }
  144. }
  145. // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
  146. // and generate new patch for testing as needed.
  147. func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSync bool) {
  148. log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
  149. graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
  150. // There is no sensible way to shut this down ":-("
  151. // If you don't let it run all the way then you will lose data
  152. // FIXME: graceful: AddTestPullRequestTask needs to become a queue!
  153. prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
  154. if err != nil {
  155. log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
  156. return
  157. }
  158. if isSync {
  159. requests := models.PullRequestList(prs)
  160. if err = requests.LoadAttributes(); err != nil {
  161. log.Error("PullRequestList.LoadAttributes: %v", err)
  162. }
  163. if invalidationErr := checkForInvalidation(requests, repoID, doer, branch); invalidationErr != nil {
  164. log.Error("checkForInvalidation: %v", invalidationErr)
  165. }
  166. if err == nil {
  167. for _, pr := range prs {
  168. pr.Issue.PullRequest = pr
  169. notification.NotifyPullRequestSynchronized(doer, pr)
  170. }
  171. }
  172. }
  173. addHeadRepoTasks(prs)
  174. log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
  175. prs, err = models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
  176. if err != nil {
  177. log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
  178. return
  179. }
  180. for _, pr := range prs {
  181. AddToTaskQueue(pr)
  182. }
  183. })
  184. }
  185. // PushToBaseRepo pushes commits from branches of head repository to
  186. // corresponding branches of base repository.
  187. // FIXME: Only push branches that are actually updates?
  188. func PushToBaseRepo(pr *models.PullRequest) (err error) {
  189. log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
  190. headRepoPath := pr.HeadRepo.RepoPath()
  191. headGitRepo, err := git.OpenRepository(headRepoPath)
  192. if err != nil {
  193. return fmt.Errorf("OpenRepository: %v", err)
  194. }
  195. defer headGitRepo.Close()
  196. tmpRemoteName := fmt.Sprintf("tmp-pull-%d", pr.ID)
  197. if err = headGitRepo.AddRemote(tmpRemoteName, pr.BaseRepo.RepoPath(), false); err != nil {
  198. return fmt.Errorf("headGitRepo.AddRemote: %v", err)
  199. }
  200. // Make sure to remove the remote even if the push fails
  201. defer func() {
  202. if err := headGitRepo.RemoveRemote(tmpRemoteName); err != nil {
  203. log.Error("PushToBaseRepo: RemoveRemote: %s", err)
  204. }
  205. }()
  206. headFile := pr.GetGitRefName()
  207. // Remove head in case there is a conflict.
  208. file := path.Join(pr.BaseRepo.RepoPath(), headFile)
  209. _ = os.Remove(file)
  210. if err = git.Push(headRepoPath, git.PushOptions{
  211. Remote: tmpRemoteName,
  212. Branch: fmt.Sprintf("%s:%s", pr.HeadBranch, headFile),
  213. Force: true,
  214. }); err != nil {
  215. return fmt.Errorf("Push: %v", err)
  216. }
  217. return nil
  218. }