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.

216 lines
6.3 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. "bufio"
  8. "context"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "os"
  13. "strings"
  14. "code.gitea.io/gitea/models"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/log"
  17. )
  18. // DownloadDiff will write the patch for the pr to the writer
  19. func DownloadDiff(pr *models.PullRequest, w io.Writer, patch bool) error {
  20. return DownloadDiffOrPatch(pr, w, false)
  21. }
  22. // DownloadPatch will write the patch for the pr to the writer
  23. func DownloadPatch(pr *models.PullRequest, w io.Writer, patch bool) error {
  24. return DownloadDiffOrPatch(pr, w, true)
  25. }
  26. // DownloadDiffOrPatch will write the patch for the pr to the writer
  27. func DownloadDiffOrPatch(pr *models.PullRequest, w io.Writer, patch bool) error {
  28. // Clone base repo.
  29. tmpBasePath, err := createTemporaryRepo(pr)
  30. if err != nil {
  31. log.Error("CreateTemporaryPath: %v", err)
  32. return err
  33. }
  34. defer func() {
  35. if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
  36. log.Error("DownloadDiff: RemoveTemporaryPath: %s", err)
  37. }
  38. }()
  39. gitRepo, err := git.OpenRepository(tmpBasePath)
  40. if err != nil {
  41. return fmt.Errorf("OpenRepository: %v", err)
  42. }
  43. defer gitRepo.Close()
  44. pr.MergeBase, err = git.NewCommand("merge-base", "--", "base", "tracking").RunInDir(tmpBasePath)
  45. if err != nil {
  46. pr.MergeBase = "base"
  47. }
  48. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  49. if err := gitRepo.GetDiffOrPatch(pr.MergeBase, "tracking", w, patch); err != nil {
  50. log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  51. return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  52. }
  53. return nil
  54. }
  55. var patchErrorSuffices = []string{
  56. ": already exists in index",
  57. ": patch does not apply",
  58. ": already exists in working directory",
  59. "unrecognized input",
  60. }
  61. // TestPatch will test whether a simple patch will apply
  62. func TestPatch(pr *models.PullRequest) error {
  63. // Clone base repo.
  64. tmpBasePath, err := createTemporaryRepo(pr)
  65. if err != nil {
  66. log.Error("CreateTemporaryPath: %v", err)
  67. return err
  68. }
  69. defer func() {
  70. if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
  71. log.Error("Merge: RemoveTemporaryPath: %s", err)
  72. }
  73. }()
  74. gitRepo, err := git.OpenRepository(tmpBasePath)
  75. if err != nil {
  76. return fmt.Errorf("OpenRepository: %v", err)
  77. }
  78. defer gitRepo.Close()
  79. pr.MergeBase, err = git.NewCommand("merge-base", "--", "base", "tracking").RunInDir(tmpBasePath)
  80. if err != nil {
  81. var err2 error
  82. pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base")
  83. if err2 != nil {
  84. return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %v", err, err2)
  85. }
  86. }
  87. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  88. tmpPatchFile, err := ioutil.TempFile("", "patch")
  89. if err != nil {
  90. log.Error("Unable to create temporary patch file! Error: %v", err)
  91. return fmt.Errorf("Unable to create temporary patch file! Error: %v", err)
  92. }
  93. defer func() {
  94. _ = os.Remove(tmpPatchFile.Name())
  95. }()
  96. if err := gitRepo.GetDiff(pr.MergeBase, "tracking", tmpPatchFile); err != nil {
  97. tmpPatchFile.Close()
  98. log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  99. return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  100. }
  101. stat, err := tmpPatchFile.Stat()
  102. if err != nil {
  103. tmpPatchFile.Close()
  104. return fmt.Errorf("Unable to stat patch file: %v", err)
  105. }
  106. patchPath := tmpPatchFile.Name()
  107. tmpPatchFile.Close()
  108. if stat.Size() == 0 {
  109. log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
  110. pr.Status = models.PullRequestStatusMergeable
  111. pr.ConflictedFiles = []string{}
  112. return nil
  113. }
  114. log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
  115. pr.Status = models.PullRequestStatusChecking
  116. _, err = git.NewCommand("read-tree", "base").RunInDir(tmpBasePath)
  117. if err != nil {
  118. return fmt.Errorf("git read-tree %s: %v", pr.BaseBranch, err)
  119. }
  120. prUnit, err := pr.BaseRepo.GetUnit(models.UnitTypePullRequests)
  121. if err != nil {
  122. return err
  123. }
  124. prConfig := prUnit.PullRequestsConfig()
  125. args := []string{"apply", "--check", "--cached"}
  126. if prConfig.IgnoreWhitespaceConflicts {
  127. args = append(args, "--ignore-whitespace")
  128. }
  129. args = append(args, patchPath)
  130. pr.ConflictedFiles = make([]string, 0, 5)
  131. stderrReader, stderrWriter, err := os.Pipe()
  132. if err != nil {
  133. log.Error("Unable to open stderr pipe: %v", err)
  134. return fmt.Errorf("Unable to open stderr pipe: %v", err)
  135. }
  136. defer func() {
  137. _ = stderrReader.Close()
  138. _ = stderrWriter.Close()
  139. }()
  140. conflict := false
  141. err = git.NewCommand(args...).
  142. RunInDirTimeoutEnvFullPipelineFunc(
  143. nil, -1, tmpBasePath,
  144. nil, stderrWriter, nil,
  145. func(ctx context.Context, cancel context.CancelFunc) error {
  146. _ = stderrWriter.Close()
  147. const prefix = "error: patch failed:"
  148. const errorPrefix = "error: "
  149. conflictMap := map[string]bool{}
  150. scanner := bufio.NewScanner(stderrReader)
  151. for scanner.Scan() {
  152. line := scanner.Text()
  153. if strings.HasPrefix(line, prefix) {
  154. conflict = true
  155. filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
  156. conflictMap[filepath] = true
  157. } else if strings.HasPrefix(line, errorPrefix) {
  158. conflict = true
  159. for _, suffix := range patchErrorSuffices {
  160. if strings.HasSuffix(line, suffix) {
  161. filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix))
  162. if filepath != "" {
  163. conflictMap[filepath] = true
  164. }
  165. break
  166. }
  167. }
  168. }
  169. // only list 10 conflicted files
  170. if len(conflictMap) >= 10 {
  171. break
  172. }
  173. }
  174. if len(conflictMap) > 0 {
  175. pr.ConflictedFiles = make([]string, 0, len(conflictMap))
  176. for key := range conflictMap {
  177. pr.ConflictedFiles = append(pr.ConflictedFiles, key)
  178. }
  179. }
  180. _ = stderrReader.Close()
  181. return nil
  182. })
  183. if err != nil {
  184. if conflict {
  185. pr.Status = models.PullRequestStatusConflict
  186. log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
  187. return nil
  188. }
  189. return fmt.Errorf("git apply --check: %v", err)
  190. }
  191. pr.Status = models.PullRequestStatusMergeable
  192. return nil
  193. }