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

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