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

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