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

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