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.

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