gitea源码

patch.go 21KB

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