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.

215 lines
6.4 KiB

  1. // Copyright 2019 The Gitea Authors.
  2. // All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package pull
  6. import (
  7. "fmt"
  8. "io/ioutil"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "code.gitea.io/gitea/models"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/sync"
  19. "code.gitea.io/gitea/modules/timeutil"
  20. "github.com/unknwon/com"
  21. )
  22. // pullRequestQueue represents a queue to handle update pull request tests
  23. var pullRequestQueue = sync.NewUniqueQueue(setting.Repository.PullRequestQueueLength)
  24. // AddToTaskQueue adds itself to pull request test task queue.
  25. func AddToTaskQueue(pr *models.PullRequest) {
  26. go pullRequestQueue.AddFunc(pr.ID, func() {
  27. pr.Status = models.PullRequestStatusChecking
  28. if err := pr.UpdateCols("status"); err != nil {
  29. log.Error("AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err)
  30. }
  31. })
  32. }
  33. // checkAndUpdateStatus checks if pull request is possible to leaving checking status,
  34. // and set to be either conflict or mergeable.
  35. func checkAndUpdateStatus(pr *models.PullRequest) {
  36. // Status is not changed to conflict means mergeable.
  37. if pr.Status == models.PullRequestStatusChecking {
  38. pr.Status = models.PullRequestStatusMergeable
  39. }
  40. // Make sure there is no waiting test to process before leaving the checking status.
  41. if !pullRequestQueue.Exist(pr.ID) {
  42. if err := pr.UpdateCols("status, conflicted_files"); err != nil {
  43. log.Error("Update[%d]: %v", pr.ID, err)
  44. }
  45. }
  46. }
  47. // getMergeCommit checks if a pull request got merged
  48. // Returns the git.Commit of the pull request if merged
  49. func getMergeCommit(pr *models.PullRequest) (*git.Commit, error) {
  50. if pr.BaseRepo == nil {
  51. var err error
  52. pr.BaseRepo, err = models.GetRepositoryByID(pr.BaseRepoID)
  53. if err != nil {
  54. return nil, fmt.Errorf("GetRepositoryByID: %v", err)
  55. }
  56. }
  57. indexTmpPath := filepath.Join(os.TempDir(), "gitea-"+pr.BaseRepo.Name+"-"+strconv.Itoa(time.Now().Nanosecond()))
  58. defer os.Remove(indexTmpPath)
  59. headFile := pr.GetGitRefName()
  60. // Check if a pull request is merged into BaseBranch
  61. _, err := git.NewCommand("merge-base", "--is-ancestor", headFile, pr.BaseBranch).RunInDirWithEnv(pr.BaseRepo.RepoPath(), []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()})
  62. if err != nil {
  63. // Errors are signaled by a non-zero status that is not 1
  64. if strings.Contains(err.Error(), "exit status 1") {
  65. return nil, nil
  66. }
  67. return nil, fmt.Errorf("git merge-base --is-ancestor: %v", err)
  68. }
  69. commitIDBytes, err := ioutil.ReadFile(pr.BaseRepo.RepoPath() + "/" + headFile)
  70. if err != nil {
  71. return nil, fmt.Errorf("ReadFile(%s): %v", headFile, err)
  72. }
  73. commitID := string(commitIDBytes)
  74. if len(commitID) < 40 {
  75. return nil, fmt.Errorf(`ReadFile(%s): invalid commit-ID "%s"`, headFile, commitID)
  76. }
  77. cmd := commitID[:40] + ".." + pr.BaseBranch
  78. // Get the commit from BaseBranch where the pull request got merged
  79. mergeCommit, err := git.NewCommand("rev-list", "--ancestry-path", "--merges", "--reverse", cmd).RunInDirWithEnv("", []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()})
  80. if err != nil {
  81. return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %v", err)
  82. } else if len(mergeCommit) < 40 {
  83. // PR was fast-forwarded, so just use last commit of PR
  84. mergeCommit = commitID[:40]
  85. }
  86. gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
  87. if err != nil {
  88. return nil, fmt.Errorf("OpenRepository: %v", err)
  89. }
  90. defer gitRepo.Close()
  91. commit, err := gitRepo.GetCommit(mergeCommit[:40])
  92. if err != nil {
  93. return nil, fmt.Errorf("GetCommit: %v", err)
  94. }
  95. return commit, nil
  96. }
  97. // manuallyMerged checks if a pull request got manually merged
  98. // When a pull request got manually merged mark the pull request as merged
  99. func manuallyMerged(pr *models.PullRequest) bool {
  100. commit, err := getMergeCommit(pr)
  101. if err != nil {
  102. log.Error("PullRequest[%d].getMergeCommit: %v", pr.ID, err)
  103. return false
  104. }
  105. if commit != nil {
  106. pr.MergedCommitID = commit.ID.String()
  107. pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
  108. pr.Status = models.PullRequestStatusManuallyMerged
  109. merger, _ := models.GetUserByEmail(commit.Author.Email)
  110. // When the commit author is unknown set the BaseRepo owner as merger
  111. if merger == nil {
  112. if pr.BaseRepo.Owner == nil {
  113. if err = pr.BaseRepo.GetOwner(); err != nil {
  114. log.Error("BaseRepo.GetOwner[%d]: %v", pr.ID, err)
  115. return false
  116. }
  117. }
  118. merger = pr.BaseRepo.Owner
  119. }
  120. pr.Merger = merger
  121. pr.MergerID = merger.ID
  122. if err = pr.SetMerged(); err != nil {
  123. log.Error("PullRequest[%d].setMerged : %v", pr.ID, err)
  124. return false
  125. }
  126. log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
  127. return true
  128. }
  129. return false
  130. }
  131. // TestPullRequests checks and tests untested patches of pull requests.
  132. // TODO: test more pull requests at same time.
  133. func TestPullRequests() {
  134. prs, err := models.GetPullRequestsByCheckStatus(models.PullRequestStatusChecking)
  135. if err != nil {
  136. log.Error("Find Checking PRs: %v", err)
  137. return
  138. }
  139. var checkedPRs = make(map[int64]struct{})
  140. // Update pull request status.
  141. for _, pr := range prs {
  142. checkedPRs[pr.ID] = struct{}{}
  143. if err := pr.GetBaseRepo(); err != nil {
  144. log.Error("GetBaseRepo: %v", err)
  145. continue
  146. }
  147. if manuallyMerged(pr) {
  148. continue
  149. }
  150. if err := TestPatch(pr); err != nil {
  151. log.Error("testPatch: %v", err)
  152. continue
  153. }
  154. checkAndUpdateStatus(pr)
  155. }
  156. // Start listening on new test requests.
  157. for prID := range pullRequestQueue.Queue() {
  158. log.Trace("TestPullRequests[%v]: processing test task", prID)
  159. pullRequestQueue.Remove(prID)
  160. id := com.StrTo(prID).MustInt64()
  161. if _, ok := checkedPRs[id]; ok {
  162. continue
  163. }
  164. pr, err := models.GetPullRequestByID(id)
  165. if err != nil {
  166. log.Error("GetPullRequestByID[%s]: %v", prID, err)
  167. continue
  168. } else if manuallyMerged(pr) {
  169. continue
  170. }
  171. pr.Status = models.PullRequestStatusChecking
  172. if err := pr.Update(); err != nil {
  173. log.Error("testPatch[%d]: Unable to update status to Checking Status %v", pr.ID, err)
  174. continue
  175. }
  176. if err = TestPatch(pr); err != nil {
  177. log.Error("testPatch[%d]: %v", pr.ID, err)
  178. continue
  179. }
  180. checkAndUpdateStatus(pr)
  181. }
  182. }
  183. // Init runs the task queue to test all the checking status pull requests
  184. func Init() {
  185. go TestPullRequests()
  186. }