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 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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. "github.com/gobwas/glob"
  19. )
  20. // DownloadDiffOrPatch will write the patch for the pr to the writer
  21. func DownloadDiffOrPatch(pr *models.PullRequest, w io.Writer, patch bool) error {
  22. if err := pr.LoadBaseRepo(); err != nil {
  23. log.Error("Unable to load base repository ID %d for pr #%d [%d]", pr.BaseRepoID, pr.Index, pr.ID)
  24. return err
  25. }
  26. gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
  27. if err != nil {
  28. return fmt.Errorf("OpenRepository: %v", err)
  29. }
  30. defer gitRepo.Close()
  31. if err := gitRepo.GetDiffOrPatch(pr.MergeBase, pr.GetGitRefName(), w, patch); err != nil {
  32. log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  33. return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  34. }
  35. return nil
  36. }
  37. var patchErrorSuffices = []string{
  38. ": already exists in index",
  39. ": patch does not apply",
  40. ": already exists in working directory",
  41. "unrecognized input",
  42. }
  43. // TestPatch will test whether a simple patch will apply
  44. func TestPatch(pr *models.PullRequest) error {
  45. // Clone base repo.
  46. tmpBasePath, err := createTemporaryRepo(pr)
  47. if err != nil {
  48. log.Error("CreateTemporaryPath: %v", err)
  49. return err
  50. }
  51. defer func() {
  52. if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
  53. log.Error("Merge: RemoveTemporaryPath: %s", err)
  54. }
  55. }()
  56. gitRepo, err := git.OpenRepository(tmpBasePath)
  57. if err != nil {
  58. return fmt.Errorf("OpenRepository: %v", err)
  59. }
  60. defer gitRepo.Close()
  61. // 1. update merge base
  62. pr.MergeBase, err = git.NewCommand("merge-base", "--", "base", "tracking").RunInDir(tmpBasePath)
  63. if err != nil {
  64. var err2 error
  65. pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base")
  66. if err2 != nil {
  67. return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %v", err, err2)
  68. }
  69. }
  70. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  71. // 2. Check for conflicts
  72. if conflicts, err := checkConflicts(pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
  73. return err
  74. }
  75. // 3. Check for protected files changes
  76. if err = checkPullFilesProtection(pr, gitRepo); err != nil {
  77. return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
  78. }
  79. if len(pr.ChangedProtectedFiles) > 0 {
  80. log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
  81. }
  82. pr.Status = models.PullRequestStatusMergeable
  83. return nil
  84. }
  85. func checkConflicts(pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
  86. // 1. Create a plain patch from head to base
  87. tmpPatchFile, err := ioutil.TempFile("", "patch")
  88. if err != nil {
  89. log.Error("Unable to create temporary patch file! Error: %v", err)
  90. return false, fmt.Errorf("Unable to create temporary patch file! Error: %v", err)
  91. }
  92. defer func() {
  93. _ = util.Remove(tmpPatchFile.Name())
  94. }()
  95. if err := gitRepo.GetDiff(pr.MergeBase, "tracking", tmpPatchFile); err != nil {
  96. tmpPatchFile.Close()
  97. log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  98. return false, fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  99. }
  100. stat, err := tmpPatchFile.Stat()
  101. if err != nil {
  102. tmpPatchFile.Close()
  103. return false, fmt.Errorf("Unable to stat patch file: %v", err)
  104. }
  105. patchPath := tmpPatchFile.Name()
  106. tmpPatchFile.Close()
  107. // 1a. if the size of that patch is 0 - there can be no conflicts!
  108. if stat.Size() == 0 {
  109. log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
  110. pr.Status = models.PullRequestStatusEmpty
  111. pr.ConflictedFiles = []string{}
  112. pr.ChangedProtectedFiles = []string{}
  113. return false, nil
  114. }
  115. log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
  116. // 2. preset the pr.Status as checking (this is not save at present)
  117. pr.Status = models.PullRequestStatusChecking
  118. // 3. Read the base branch in to the index of the temporary repository
  119. _, err = git.NewCommand("read-tree", "base").RunInDir(tmpBasePath)
  120. if err != nil {
  121. return false, fmt.Errorf("git read-tree %s: %v", pr.BaseBranch, err)
  122. }
  123. // 4. Now get the pull request configuration to check if we need to ignore whitespace
  124. prUnit, err := pr.BaseRepo.GetUnit(models.UnitTypePullRequests)
  125. if err != nil {
  126. return false, err
  127. }
  128. prConfig := prUnit.PullRequestsConfig()
  129. // 5. Prepare the arguments to apply the patch against the index
  130. args := []string{"apply", "--check", "--cached"}
  131. if prConfig.IgnoreWhitespaceConflicts {
  132. args = append(args, "--ignore-whitespace")
  133. }
  134. args = append(args, patchPath)
  135. pr.ConflictedFiles = make([]string, 0, 5)
  136. // 6. Prep the pipe:
  137. // - Here we could do the equivalent of:
  138. // `git apply --check --cached patch_file > conflicts`
  139. // Then iterate through the conflicts. However, that means storing all the conflicts
  140. // in memory - which is very wasteful.
  141. // - alternatively we can do the equivalent of:
  142. // `git apply --check ... | grep ...`
  143. // meaning we don't store all of the conflicts unnecessarily.
  144. stderrReader, stderrWriter, err := os.Pipe()
  145. if err != nil {
  146. log.Error("Unable to open stderr pipe: %v", err)
  147. return false, fmt.Errorf("Unable to open stderr pipe: %v", err)
  148. }
  149. defer func() {
  150. _ = stderrReader.Close()
  151. _ = stderrWriter.Close()
  152. }()
  153. // 7. Run the check command
  154. conflict := false
  155. err = git.NewCommand(args...).
  156. RunInDirTimeoutEnvFullPipelineFunc(
  157. nil, -1, tmpBasePath,
  158. nil, stderrWriter, nil,
  159. func(ctx context.Context, cancel context.CancelFunc) error {
  160. // Close the writer end of the pipe to begin processing
  161. _ = stderrWriter.Close()
  162. defer func() {
  163. // Close the reader on return to terminate the git command if necessary
  164. _ = stderrReader.Close()
  165. }()
  166. const prefix = "error: patch failed:"
  167. const errorPrefix = "error: "
  168. conflictMap := map[string]bool{}
  169. // Now scan the output from the command
  170. scanner := bufio.NewScanner(stderrReader)
  171. for scanner.Scan() {
  172. line := scanner.Text()
  173. if strings.HasPrefix(line, prefix) {
  174. conflict = true
  175. filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
  176. conflictMap[filepath] = true
  177. } else if strings.HasPrefix(line, errorPrefix) {
  178. conflict = true
  179. for _, suffix := range patchErrorSuffices {
  180. if strings.HasSuffix(line, suffix) {
  181. filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix))
  182. if filepath != "" {
  183. conflictMap[filepath] = true
  184. }
  185. break
  186. }
  187. }
  188. }
  189. // only list 10 conflicted files
  190. if len(conflictMap) >= 10 {
  191. break
  192. }
  193. }
  194. if len(conflictMap) > 0 {
  195. pr.ConflictedFiles = make([]string, 0, len(conflictMap))
  196. for key := range conflictMap {
  197. pr.ConflictedFiles = append(pr.ConflictedFiles, key)
  198. }
  199. }
  200. return nil
  201. })
  202. // 8. If there is a conflict the `git apply` command will return a non-zero error code - so there will be a positive error.
  203. if err != nil {
  204. if conflict {
  205. pr.Status = models.PullRequestStatusConflict
  206. log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
  207. return true, nil
  208. }
  209. return false, fmt.Errorf("git apply --check: %v", err)
  210. }
  211. return false, nil
  212. }
  213. // CheckFileProtection check file Protection
  214. func CheckFileProtection(oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string, repo *git.Repository) ([]string, error) {
  215. if len(patterns) == 0 {
  216. return nil, nil
  217. }
  218. affectedFiles, err := git.GetAffectedFiles(oldCommitID, newCommitID, env, repo)
  219. if err != nil {
  220. return nil, err
  221. }
  222. changedProtectedFiles := make([]string, 0, limit)
  223. for _, affectedFile := range affectedFiles {
  224. lpath := strings.ToLower(affectedFile)
  225. for _, pat := range patterns {
  226. if pat.Match(lpath) {
  227. changedProtectedFiles = append(changedProtectedFiles, lpath)
  228. break
  229. }
  230. }
  231. if len(changedProtectedFiles) >= limit {
  232. break
  233. }
  234. }
  235. if len(changedProtectedFiles) > 0 {
  236. err = models.ErrFilePathProtected{
  237. Path: changedProtectedFiles[0],
  238. }
  239. }
  240. return changedProtectedFiles, err
  241. }
  242. // CheckUnprotectedFiles check if the commit only touches unprotected files
  243. func CheckUnprotectedFiles(oldCommitID, newCommitID string, patterns []glob.Glob, env []string, repo *git.Repository) (bool, error) {
  244. if len(patterns) == 0 {
  245. return false, nil
  246. }
  247. affectedFiles, err := git.GetAffectedFiles(oldCommitID, newCommitID, env, repo)
  248. if err != nil {
  249. return false, err
  250. }
  251. for _, affectedFile := range affectedFiles {
  252. lpath := strings.ToLower(affectedFile)
  253. unprotected := false
  254. for _, pat := range patterns {
  255. if pat.Match(lpath) {
  256. unprotected = true
  257. break
  258. }
  259. }
  260. if !unprotected {
  261. return false, nil
  262. }
  263. }
  264. return true, nil
  265. }
  266. // checkPullFilesProtection check if pr changed protected files and save results
  267. func checkPullFilesProtection(pr *models.PullRequest, gitRepo *git.Repository) error {
  268. if err := pr.LoadProtectedBranch(); err != nil {
  269. return err
  270. }
  271. if pr.ProtectedBranch == nil {
  272. pr.ChangedProtectedFiles = nil
  273. return nil
  274. }
  275. var err error
  276. pr.ChangedProtectedFiles, err = CheckFileProtection(pr.MergeBase, "tracking", pr.ProtectedBranch.GetProtectedFilePatterns(), 10, os.Environ(), gitRepo)
  277. if err != nil && !models.IsErrFilePathProtected(err) {
  278. return err
  279. }
  280. return nil
  281. }