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.

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