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.

patch.go 5.7KB

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