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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  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. "os"
  12. "path/filepath"
  13. "strings"
  14. "code.gitea.io/gitea/models"
  15. "code.gitea.io/gitea/models/unit"
  16. "code.gitea.io/gitea/modules/git"
  17. "code.gitea.io/gitea/modules/graceful"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/process"
  20. repo_module "code.gitea.io/gitea/modules/repository"
  21. "code.gitea.io/gitea/modules/util"
  22. "github.com/gobwas/glob"
  23. )
  24. // DownloadDiffOrPatch will write the patch for the pr to the writer
  25. func DownloadDiffOrPatch(ctx context.Context, pr *models.PullRequest, w io.Writer, patch, binary bool) error {
  26. if err := pr.LoadBaseRepoCtx(ctx); err != nil {
  27. log.Error("Unable to load base repository ID %d for pr #%d [%d]", pr.BaseRepoID, pr.Index, pr.ID)
  28. return err
  29. }
  30. gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo.RepoPath())
  31. if err != nil {
  32. return fmt.Errorf("OpenRepository: %v", err)
  33. }
  34. defer closer.Close()
  35. if err := gitRepo.GetDiffOrPatch(pr.MergeBase, pr.GetGitRefName(), w, patch, binary); err != nil {
  36. log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  37. return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  38. }
  39. return nil
  40. }
  41. var patchErrorSuffices = []string{
  42. ": already exists in index",
  43. ": patch does not apply",
  44. ": already exists in working directory",
  45. "unrecognized input",
  46. }
  47. // TestPatch will test whether a simple patch will apply
  48. func TestPatch(pr *models.PullRequest) error {
  49. ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: Repo[%d]#%d", pr.BaseRepoID, pr.Index))
  50. defer finished()
  51. // Clone base repo.
  52. tmpBasePath, err := createTemporaryRepo(ctx, pr)
  53. if err != nil {
  54. log.Error("CreateTemporaryPath: %v", err)
  55. return err
  56. }
  57. defer func() {
  58. if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
  59. log.Error("Merge: RemoveTemporaryPath: %s", err)
  60. }
  61. }()
  62. gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
  63. if err != nil {
  64. return fmt.Errorf("OpenRepository: %v", err)
  65. }
  66. defer gitRepo.Close()
  67. // 1. update merge base
  68. pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base", "--", "base", "tracking").RunStdString(&git.RunOpts{Dir: tmpBasePath})
  69. if err != nil {
  70. var err2 error
  71. pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base")
  72. if err2 != nil {
  73. return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %v", err, err2)
  74. }
  75. }
  76. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  77. // 2. Check for conflicts
  78. if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
  79. return err
  80. }
  81. // 3. Check for protected files changes
  82. if err = checkPullFilesProtection(pr, gitRepo); err != nil {
  83. return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
  84. }
  85. if len(pr.ChangedProtectedFiles) > 0 {
  86. log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
  87. }
  88. pr.Status = models.PullRequestStatusMergeable
  89. return nil
  90. }
  91. type errMergeConflict struct {
  92. filename string
  93. }
  94. func (e *errMergeConflict) Error() string {
  95. return fmt.Sprintf("conflict detected at: %s", e.filename)
  96. }
  97. func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, gitRepo *git.Repository) error {
  98. switch {
  99. case file.stage1 != nil && (file.stage2 == nil || file.stage3 == nil):
  100. // 1. Deleted in one or both:
  101. //
  102. // Conflict <==> the stage1 !SameAs to the undeleted one
  103. if (file.stage2 != nil && !file.stage1.SameAs(file.stage2)) || (file.stage3 != nil && !file.stage1.SameAs(file.stage3)) {
  104. // Conflict!
  105. return &errMergeConflict{file.stage1.path}
  106. }
  107. // Not a genuine conflict and we can simply remove the file from the index
  108. return gitRepo.RemoveFilesFromIndex(file.stage1.path)
  109. case file.stage1 == nil && file.stage2 != nil && (file.stage3 == nil || file.stage2.SameAs(file.stage3)):
  110. // 2. Added in ours but not in theirs or identical in both
  111. //
  112. // Not a genuine conflict just add to the index
  113. if err := gitRepo.AddObjectToIndex(file.stage2.mode, git.MustIDFromString(file.stage2.sha), file.stage2.path); err != nil {
  114. return err
  115. }
  116. return nil
  117. case file.stage1 == nil && file.stage2 != nil && file.stage3 != nil && file.stage2.sha == file.stage3.sha && file.stage2.mode != file.stage3.mode:
  118. // 3. Added in both with the same sha but the modes are different
  119. //
  120. // Conflict! (Not sure that this can actually happen but we should handle)
  121. return &errMergeConflict{file.stage2.path}
  122. case file.stage1 == nil && file.stage2 == nil && file.stage3 != nil:
  123. // 4. Added in theirs but not ours:
  124. //
  125. // Not a genuine conflict just add to the index
  126. return gitRepo.AddObjectToIndex(file.stage3.mode, git.MustIDFromString(file.stage3.sha), file.stage3.path)
  127. case file.stage1 == nil:
  128. // 5. Created by new in both
  129. //
  130. // Conflict!
  131. return &errMergeConflict{file.stage2.path}
  132. case file.stage2 != nil && file.stage3 != nil:
  133. // 5. Modified in both - we should try to merge in the changes but first:
  134. //
  135. if file.stage2.mode == "120000" || file.stage3.mode == "120000" {
  136. // 5a. Conflicting symbolic link change
  137. return &errMergeConflict{file.stage2.path}
  138. }
  139. if file.stage2.mode == "160000" || file.stage3.mode == "160000" {
  140. // 5b. Conflicting submodule change
  141. return &errMergeConflict{file.stage2.path}
  142. }
  143. if file.stage2.mode != file.stage3.mode {
  144. // 5c. Conflicting mode change
  145. return &errMergeConflict{file.stage2.path}
  146. }
  147. // Need to get the objects from the object db to attempt to merge
  148. root, _, err := git.NewCommand(ctx, "unpack-file", file.stage1.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath})
  149. if err != nil {
  150. return fmt.Errorf("unable to get root object: %s at path: %s for merging. Error: %w", file.stage1.sha, file.stage1.path, err)
  151. }
  152. root = strings.TrimSpace(root)
  153. defer func() {
  154. _ = util.Remove(filepath.Join(tmpBasePath, root))
  155. }()
  156. base, _, err := git.NewCommand(ctx, "unpack-file", file.stage2.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath})
  157. if err != nil {
  158. return fmt.Errorf("unable to get base object: %s at path: %s for merging. Error: %w", file.stage2.sha, file.stage2.path, err)
  159. }
  160. base = strings.TrimSpace(filepath.Join(tmpBasePath, base))
  161. defer func() {
  162. _ = util.Remove(base)
  163. }()
  164. head, _, err := git.NewCommand(ctx, "unpack-file", file.stage3.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath})
  165. if err != nil {
  166. return fmt.Errorf("unable to get head object:%s at path: %s for merging. Error: %w", file.stage3.sha, file.stage3.path, err)
  167. }
  168. head = strings.TrimSpace(head)
  169. defer func() {
  170. _ = util.Remove(filepath.Join(tmpBasePath, head))
  171. }()
  172. // now git merge-file annoyingly takes a different order to the merge-tree ...
  173. _, _, conflictErr := git.NewCommand(ctx, "merge-file", base, root, head).RunStdString(&git.RunOpts{Dir: tmpBasePath})
  174. if conflictErr != nil {
  175. return &errMergeConflict{file.stage2.path}
  176. }
  177. // base now contains the merged data
  178. hash, _, err := git.NewCommand(ctx, "hash-object", "-w", "--path", file.stage2.path, base).RunStdString(&git.RunOpts{Dir: tmpBasePath})
  179. if err != nil {
  180. return err
  181. }
  182. hash = strings.TrimSpace(hash)
  183. return gitRepo.AddObjectToIndex(file.stage2.mode, git.MustIDFromString(hash), file.stage2.path)
  184. default:
  185. if file.stage1 != nil {
  186. return &errMergeConflict{file.stage1.path}
  187. } else if file.stage2 != nil {
  188. return &errMergeConflict{file.stage2.path}
  189. } else if file.stage3 != nil {
  190. return &errMergeConflict{file.stage3.path}
  191. }
  192. }
  193. return nil
  194. }
  195. // AttemptThreeWayMerge will attempt to three way merge using git read-tree and then follow the git merge-one-file algorithm to attempt to resolve basic conflicts
  196. func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) {
  197. ctx, cancel := context.WithCancel(ctx)
  198. defer cancel()
  199. // First we use read-tree to do a simple three-way merge
  200. if _, _, err := git.NewCommand(ctx, "read-tree", "-m", base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitPath}); err != nil {
  201. log.Error("Unable to run read-tree -m! Error: %v", err)
  202. return false, nil, fmt.Errorf("unable to run read-tree -m! Error: %v", err)
  203. }
  204. // Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
  205. unmerged := make(chan *unmergedFile)
  206. go unmergedFiles(ctx, gitPath, unmerged)
  207. defer func() {
  208. cancel()
  209. for range unmerged {
  210. // empty the unmerged channel
  211. }
  212. }()
  213. numberOfConflicts := 0
  214. conflict := false
  215. conflictedFiles := make([]string, 0, 5)
  216. for file := range unmerged {
  217. if file == nil {
  218. break
  219. }
  220. if file.err != nil {
  221. cancel()
  222. return false, nil, file.err
  223. }
  224. // OK now we have the unmerged file triplet attempt to merge it
  225. if err := attemptMerge(ctx, file, gitPath, gitRepo); err != nil {
  226. if conflictErr, ok := err.(*errMergeConflict); ok {
  227. log.Trace("Conflict: %s in %s", conflictErr.filename, description)
  228. conflict = true
  229. if numberOfConflicts < 10 {
  230. conflictedFiles = append(conflictedFiles, conflictErr.filename)
  231. }
  232. numberOfConflicts++
  233. continue
  234. }
  235. return false, nil, err
  236. }
  237. }
  238. return conflict, conflictedFiles, nil
  239. }
  240. func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
  241. // 1. checkConflicts resets the conflict status - therefore - reset the conflict status
  242. pr.ConflictedFiles = nil
  243. // 2. AttemptThreeWayMerge first - this is much quicker than plain patch to base
  244. description := fmt.Sprintf("PR[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index)
  245. conflict, _, err := AttemptThreeWayMerge(ctx,
  246. tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description)
  247. if err != nil {
  248. return false, err
  249. }
  250. if !conflict {
  251. var treeHash string
  252. treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: tmpBasePath})
  253. if err != nil {
  254. return false, err
  255. }
  256. treeHash = strings.TrimSpace(treeHash)
  257. baseTree, err := gitRepo.GetTree("base")
  258. if err != nil {
  259. return false, err
  260. }
  261. if treeHash == baseTree.ID.String() {
  262. log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
  263. pr.Status = models.PullRequestStatusEmpty
  264. }
  265. return false, nil
  266. }
  267. // 3. OK read-tree has failed so we need to try a different thing - this might actually succeed where the above fails due to whitespace handling.
  268. // 3a. Create a plain patch from head to base
  269. tmpPatchFile, err := os.CreateTemp("", "patch")
  270. if err != nil {
  271. log.Error("Unable to create temporary patch file! Error: %v", err)
  272. return false, fmt.Errorf("unable to create temporary patch file! Error: %v", err)
  273. }
  274. defer func() {
  275. _ = util.Remove(tmpPatchFile.Name())
  276. }()
  277. if err := gitRepo.GetDiffBinary(pr.MergeBase, "tracking", tmpPatchFile); err != nil {
  278. tmpPatchFile.Close()
  279. log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
  280. 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)
  281. }
  282. stat, err := tmpPatchFile.Stat()
  283. if err != nil {
  284. tmpPatchFile.Close()
  285. return false, fmt.Errorf("unable to stat patch file: %v", err)
  286. }
  287. patchPath := tmpPatchFile.Name()
  288. tmpPatchFile.Close()
  289. // 3b. if the size of that patch is 0 - there can be no conflicts!
  290. if stat.Size() == 0 {
  291. log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
  292. pr.Status = models.PullRequestStatusEmpty
  293. return false, nil
  294. }
  295. log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
  296. // 4. Read the base branch in to the index of the temporary repository
  297. _, _, err = git.NewCommand(gitRepo.Ctx, "read-tree", "base").RunStdString(&git.RunOpts{Dir: tmpBasePath})
  298. if err != nil {
  299. return false, fmt.Errorf("git read-tree %s: %v", pr.BaseBranch, err)
  300. }
  301. // 5. Now get the pull request configuration to check if we need to ignore whitespace
  302. prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
  303. if err != nil {
  304. return false, err
  305. }
  306. prConfig := prUnit.PullRequestsConfig()
  307. // 6. Prepare the arguments to apply the patch against the index
  308. args := []string{"apply", "--check", "--cached"}
  309. if prConfig.IgnoreWhitespaceConflicts {
  310. args = append(args, "--ignore-whitespace")
  311. }
  312. is3way := false
  313. if git.CheckGitVersionAtLeast("2.32.0") == nil {
  314. args = append(args, "--3way")
  315. is3way = true
  316. }
  317. args = append(args, patchPath)
  318. // 7. Prep the pipe:
  319. // - Here we could do the equivalent of:
  320. // `git apply --check --cached patch_file > conflicts`
  321. // Then iterate through the conflicts. However, that means storing all the conflicts
  322. // in memory - which is very wasteful.
  323. // - alternatively we can do the equivalent of:
  324. // `git apply --check ... | grep ...`
  325. // meaning we don't store all of the conflicts unnecessarily.
  326. stderrReader, stderrWriter, err := os.Pipe()
  327. if err != nil {
  328. log.Error("Unable to open stderr pipe: %v", err)
  329. return false, fmt.Errorf("unable to open stderr pipe: %v", err)
  330. }
  331. defer func() {
  332. _ = stderrReader.Close()
  333. _ = stderrWriter.Close()
  334. }()
  335. // 8. Run the check command
  336. conflict = false
  337. err = git.NewCommand(gitRepo.Ctx, args...).
  338. Run(&git.RunOpts{
  339. Dir: tmpBasePath,
  340. Stderr: stderrWriter,
  341. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  342. // Close the writer end of the pipe to begin processing
  343. _ = stderrWriter.Close()
  344. defer func() {
  345. // Close the reader on return to terminate the git command if necessary
  346. _ = stderrReader.Close()
  347. }()
  348. const prefix = "error: patch failed:"
  349. const errorPrefix = "error: "
  350. const threewayFailed = "Failed to perform three-way merge..."
  351. const appliedPatchPrefix = "Applied patch to '"
  352. const withConflicts = "' with conflicts."
  353. conflictMap := map[string]bool{}
  354. // Now scan the output from the command
  355. scanner := bufio.NewScanner(stderrReader)
  356. for scanner.Scan() {
  357. line := scanner.Text()
  358. if strings.HasPrefix(line, prefix) {
  359. conflict = true
  360. filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
  361. conflictMap[filepath] = true
  362. } else if is3way && line == threewayFailed {
  363. conflict = true
  364. } else if strings.HasPrefix(line, errorPrefix) {
  365. conflict = true
  366. for _, suffix := range patchErrorSuffices {
  367. if strings.HasSuffix(line, suffix) {
  368. filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix))
  369. if filepath != "" {
  370. conflictMap[filepath] = true
  371. }
  372. break
  373. }
  374. }
  375. } else if is3way && strings.HasPrefix(line, appliedPatchPrefix) && strings.HasSuffix(line, withConflicts) {
  376. conflict = true
  377. filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix)
  378. if filepath != "" {
  379. conflictMap[filepath] = true
  380. }
  381. }
  382. // only list 10 conflicted files
  383. if len(conflictMap) >= 10 {
  384. break
  385. }
  386. }
  387. if len(conflictMap) > 0 {
  388. pr.ConflictedFiles = make([]string, 0, len(conflictMap))
  389. for key := range conflictMap {
  390. pr.ConflictedFiles = append(pr.ConflictedFiles, key)
  391. }
  392. }
  393. return nil
  394. },
  395. })
  396. // 9. Check if the found conflictedfiles is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts.
  397. // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts.
  398. if len(pr.ConflictedFiles) > 0 {
  399. if conflict {
  400. pr.Status = models.PullRequestStatusConflict
  401. log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
  402. return true, nil
  403. }
  404. } else if err != nil {
  405. return false, fmt.Errorf("git apply --check: %v", err)
  406. }
  407. return false, nil
  408. }
  409. // CheckFileProtection check file Protection
  410. func CheckFileProtection(repo *git.Repository, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) {
  411. if len(patterns) == 0 {
  412. return nil, nil
  413. }
  414. affectedFiles, err := git.GetAffectedFiles(repo, oldCommitID, newCommitID, env)
  415. if err != nil {
  416. return nil, err
  417. }
  418. changedProtectedFiles := make([]string, 0, limit)
  419. for _, affectedFile := range affectedFiles {
  420. lpath := strings.ToLower(affectedFile)
  421. for _, pat := range patterns {
  422. if pat.Match(lpath) {
  423. changedProtectedFiles = append(changedProtectedFiles, lpath)
  424. break
  425. }
  426. }
  427. if len(changedProtectedFiles) >= limit {
  428. break
  429. }
  430. }
  431. if len(changedProtectedFiles) > 0 {
  432. err = models.ErrFilePathProtected{
  433. Path: changedProtectedFiles[0],
  434. }
  435. }
  436. return changedProtectedFiles, err
  437. }
  438. // CheckUnprotectedFiles check if the commit only touches unprotected files
  439. func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) {
  440. if len(patterns) == 0 {
  441. return false, nil
  442. }
  443. affectedFiles, err := git.GetAffectedFiles(repo, oldCommitID, newCommitID, env)
  444. if err != nil {
  445. return false, err
  446. }
  447. for _, affectedFile := range affectedFiles {
  448. lpath := strings.ToLower(affectedFile)
  449. unprotected := false
  450. for _, pat := range patterns {
  451. if pat.Match(lpath) {
  452. unprotected = true
  453. break
  454. }
  455. }
  456. if !unprotected {
  457. return false, nil
  458. }
  459. }
  460. return true, nil
  461. }
  462. // checkPullFilesProtection check if pr changed protected files and save results
  463. func checkPullFilesProtection(pr *models.PullRequest, gitRepo *git.Repository) error {
  464. if pr.Status == models.PullRequestStatusEmpty {
  465. pr.ChangedProtectedFiles = nil
  466. return nil
  467. }
  468. if err := pr.LoadProtectedBranch(); err != nil {
  469. return err
  470. }
  471. if pr.ProtectedBranch == nil {
  472. pr.ChangedProtectedFiles = nil
  473. return nil
  474. }
  475. var err error
  476. pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pr.ProtectedBranch.GetProtectedFilePatterns(), 10, os.Environ())
  477. if err != nil && !models.IsErrFilePathProtected(err) {
  478. return err
  479. }
  480. return nil
  481. }