gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package pull
  4. import (
  5. "bufio"
  6. "bytes"
  7. "context"
  8. "fmt"
  9. "io"
  10. "os"
  11. "path/filepath"
  12. "strings"
  13. "time"
  14. issues_model "code.gitea.io/gitea/models/issues"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/git/gitcmd"
  19. "code.gitea.io/gitea/modules/log"
  20. asymkey_service "code.gitea.io/gitea/services/asymkey"
  21. )
  22. type mergeContext struct {
  23. *prTmpRepoContext
  24. doer *user_model.User
  25. sig *git.Signature
  26. committer *git.Signature
  27. signKey *git.SigningKey
  28. env []string
  29. }
  30. func (ctx *mergeContext) RunOpts() *gitcmd.RunOpts {
  31. ctx.outbuf.Reset()
  32. ctx.errbuf.Reset()
  33. return &gitcmd.RunOpts{
  34. Env: ctx.env,
  35. Dir: ctx.tmpBasePath,
  36. Stdout: ctx.outbuf,
  37. Stderr: ctx.errbuf,
  38. }
  39. }
  40. // ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
  41. type ErrSHADoesNotMatch struct {
  42. Path string
  43. GivenSHA string
  44. CurrentSHA string
  45. }
  46. // IsErrSHADoesNotMatch checks if an error is a ErrSHADoesNotMatch.
  47. func IsErrSHADoesNotMatch(err error) bool {
  48. _, ok := err.(ErrSHADoesNotMatch)
  49. return ok
  50. }
  51. func (err ErrSHADoesNotMatch) Error() string {
  52. return fmt.Sprintf("sha does not match [given: %s, expected: %s]", err.GivenSHA, err.CurrentSHA)
  53. }
  54. func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, expectedHeadCommitID string) (mergeCtx *mergeContext, cancel context.CancelFunc, err error) {
  55. // Clone base repo.
  56. prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
  57. if err != nil {
  58. log.Error("createTemporaryRepoForPR: %v", err)
  59. return nil, cancel, err
  60. }
  61. mergeCtx = &mergeContext{
  62. prTmpRepoContext: prCtx,
  63. doer: doer,
  64. }
  65. if expectedHeadCommitID != "" {
  66. trackingCommitID, _, err := gitcmd.NewCommand("show-ref", "--hash").AddDynamicArguments(git.BranchPrefix+trackingBranch).RunStdString(ctx, &gitcmd.RunOpts{Dir: mergeCtx.tmpBasePath})
  67. if err != nil {
  68. defer cancel()
  69. log.Error("failed to get sha of head branch in %-v: show-ref[%s] --hash refs/heads/tracking: %v", mergeCtx.pr, mergeCtx.tmpBasePath, err)
  70. return nil, nil, fmt.Errorf("unable to get sha of head branch in %v %w", pr, err)
  71. }
  72. if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID {
  73. defer cancel()
  74. return nil, nil, ErrSHADoesNotMatch{
  75. GivenSHA: expectedHeadCommitID,
  76. CurrentSHA: trackingCommitID,
  77. }
  78. }
  79. }
  80. mergeCtx.outbuf.Reset()
  81. mergeCtx.errbuf.Reset()
  82. if err := prepareTemporaryRepoForMerge(mergeCtx); err != nil {
  83. defer cancel()
  84. return nil, nil, err
  85. }
  86. mergeCtx.sig = doer.NewGitSig()
  87. mergeCtx.committer = mergeCtx.sig
  88. // Determine if we should sign
  89. sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch)
  90. if sign {
  91. mergeCtx.signKey = key
  92. if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
  93. mergeCtx.committer = signer
  94. }
  95. }
  96. commitTimeStr := time.Now().Format(time.RFC3339)
  97. // Because this may call hooks we should pass in the environment
  98. mergeCtx.env = append(os.Environ(),
  99. "GIT_AUTHOR_NAME="+mergeCtx.sig.Name,
  100. "GIT_AUTHOR_EMAIL="+mergeCtx.sig.Email,
  101. "GIT_AUTHOR_DATE="+commitTimeStr,
  102. "GIT_COMMITTER_NAME="+mergeCtx.committer.Name,
  103. "GIT_COMMITTER_EMAIL="+mergeCtx.committer.Email,
  104. "GIT_COMMITTER_DATE="+commitTimeStr,
  105. )
  106. return mergeCtx, cancel, nil
  107. }
  108. // prepareTemporaryRepoForMerge takes a repository that has been created using createTemporaryRepo
  109. // it then sets up the sparse-checkout and other things
  110. func prepareTemporaryRepoForMerge(ctx *mergeContext) error {
  111. infoPath := filepath.Join(ctx.tmpBasePath, ".git", "info")
  112. if err := os.MkdirAll(infoPath, 0o700); err != nil {
  113. log.Error("%-v Unable to create .git/info in %s: %v", ctx.pr, ctx.tmpBasePath, err)
  114. return fmt.Errorf("Unable to create .git/info in tmpBasePath: %w", err)
  115. }
  116. // Enable sparse-checkout
  117. // Here we use the .git/info/sparse-checkout file as described in the git documentation
  118. sparseCheckoutListFile, err := os.OpenFile(filepath.Join(infoPath, "sparse-checkout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
  119. if err != nil {
  120. log.Error("%-v Unable to write .git/info/sparse-checkout file in %s: %v", ctx.pr, ctx.tmpBasePath, err)
  121. return fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %w", err)
  122. }
  123. defer sparseCheckoutListFile.Close() // we will close it earlier but we need to ensure it is closed if there is an error
  124. if err := getDiffTree(ctx, ctx.tmpBasePath, baseBranch, trackingBranch, sparseCheckoutListFile); err != nil {
  125. log.Error("%-v getDiffTree(%s, %s, %s): %v", ctx.pr, ctx.tmpBasePath, baseBranch, trackingBranch, err)
  126. return fmt.Errorf("getDiffTree: %w", err)
  127. }
  128. if err := sparseCheckoutListFile.Close(); err != nil {
  129. log.Error("%-v Unable to close .git/info/sparse-checkout file in %s: %v", ctx.pr, ctx.tmpBasePath, err)
  130. return fmt.Errorf("Unable to close .git/info/sparse-checkout file in tmpBasePath: %w", err)
  131. }
  132. setConfig := func(key, value string) error {
  133. if err := gitcmd.NewCommand("config", "--local").AddDynamicArguments(key, value).
  134. Run(ctx, ctx.RunOpts()); err != nil {
  135. log.Error("git config [%s -> %q]: %v\n%s\n%s", key, value, err, ctx.outbuf.String(), ctx.errbuf.String())
  136. return fmt.Errorf("git config [%s -> %q]: %w\n%s\n%s", key, value, err, ctx.outbuf.String(), ctx.errbuf.String())
  137. }
  138. ctx.outbuf.Reset()
  139. ctx.errbuf.Reset()
  140. return nil
  141. }
  142. // Switch off LFS process (set required, clean and smudge here also)
  143. if err := setConfig("filter.lfs.process", ""); err != nil {
  144. return err
  145. }
  146. if err := setConfig("filter.lfs.required", "false"); err != nil {
  147. return err
  148. }
  149. if err := setConfig("filter.lfs.clean", ""); err != nil {
  150. return err
  151. }
  152. if err := setConfig("filter.lfs.smudge", ""); err != nil {
  153. return err
  154. }
  155. if err := setConfig("core.sparseCheckout", "true"); err != nil {
  156. return err
  157. }
  158. // Read base branch index
  159. if err := gitcmd.NewCommand("read-tree", "HEAD").
  160. Run(ctx, ctx.RunOpts()); err != nil {
  161. log.Error("git read-tree HEAD: %v\n%s\n%s", err, ctx.outbuf.String(), ctx.errbuf.String())
  162. return fmt.Errorf("Unable to read base branch in to the index: %w\n%s\n%s", err, ctx.outbuf.String(), ctx.errbuf.String())
  163. }
  164. ctx.outbuf.Reset()
  165. ctx.errbuf.Reset()
  166. return nil
  167. }
  168. // getDiffTree returns a string containing all the files that were changed between headBranch and baseBranch
  169. // the filenames are escaped so as to fit the format required for .git/info/sparse-checkout
  170. func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string, out io.Writer) error {
  171. diffOutReader, diffOutWriter, err := os.Pipe()
  172. if err != nil {
  173. log.Error("Unable to create os.Pipe for %s", repoPath)
  174. return err
  175. }
  176. defer func() {
  177. _ = diffOutReader.Close()
  178. _ = diffOutWriter.Close()
  179. }()
  180. scanNullTerminatedStrings := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
  181. if atEOF && len(data) == 0 {
  182. return 0, nil, nil
  183. }
  184. if i := bytes.IndexByte(data, '\x00'); i >= 0 {
  185. return i + 1, data[0:i], nil
  186. }
  187. if atEOF {
  188. return len(data), data, nil
  189. }
  190. return 0, nil, nil
  191. }
  192. err = gitcmd.NewCommand("diff-tree", "--no-commit-id", "--name-only", "-r", "-r", "-z", "--root").AddDynamicArguments(baseBranch, headBranch).
  193. Run(ctx, &gitcmd.RunOpts{
  194. Dir: repoPath,
  195. Stdout: diffOutWriter,
  196. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  197. // Close the writer end of the pipe to begin processing
  198. _ = diffOutWriter.Close()
  199. defer func() {
  200. // Close the reader on return to terminate the git command if necessary
  201. _ = diffOutReader.Close()
  202. }()
  203. // Now scan the output from the command
  204. scanner := bufio.NewScanner(diffOutReader)
  205. scanner.Split(scanNullTerminatedStrings)
  206. for scanner.Scan() {
  207. filepath := scanner.Text()
  208. // escape '*', '?', '[', spaces and '!' prefix
  209. filepath = escapedSymbols.ReplaceAllString(filepath, `\$1`)
  210. // no necessary to escape the first '#' symbol because the first symbol is '/'
  211. fmt.Fprintf(out, "/%s\n", filepath)
  212. }
  213. return scanner.Err()
  214. },
  215. })
  216. return err
  217. }
  218. // ErrRebaseConflicts represents an error if rebase fails with a conflict
  219. type ErrRebaseConflicts struct {
  220. Style repo_model.MergeStyle
  221. CommitSHA string
  222. StdOut string
  223. StdErr string
  224. Err error
  225. }
  226. // IsErrRebaseConflicts checks if an error is a ErrRebaseConflicts.
  227. func IsErrRebaseConflicts(err error) bool {
  228. _, ok := err.(ErrRebaseConflicts)
  229. return ok
  230. }
  231. func (err ErrRebaseConflicts) Error() string {
  232. return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut)
  233. }
  234. // rebaseTrackingOnToBase checks out the tracking branch as staging and rebases it on to the base branch
  235. // if there is a conflict it will return an ErrRebaseConflicts
  236. func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error {
  237. // Checkout head branch
  238. if err := gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch).
  239. Run(ctx, ctx.RunOpts()); err != nil {
  240. return fmt.Errorf("unable to git checkout tracking as staging in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
  241. }
  242. ctx.outbuf.Reset()
  243. ctx.errbuf.Reset()
  244. // Rebase before merging
  245. if err := gitcmd.NewCommand("rebase").AddDynamicArguments(baseBranch).
  246. Run(ctx, ctx.RunOpts()); err != nil {
  247. // Rebase will leave a REBASE_HEAD file in .git if there is a conflict
  248. if _, statErr := os.Stat(filepath.Join(ctx.tmpBasePath, ".git", "REBASE_HEAD")); statErr == nil {
  249. var commitSha string
  250. ok := false
  251. failingCommitPaths := []string{
  252. filepath.Join(ctx.tmpBasePath, ".git", "rebase-apply", "original-commit"), // Git < 2.26
  253. filepath.Join(ctx.tmpBasePath, ".git", "rebase-merge", "stopped-sha"), // Git >= 2.26
  254. }
  255. for _, failingCommitPath := range failingCommitPaths {
  256. if _, statErr := os.Stat(failingCommitPath); statErr == nil {
  257. commitShaBytes, readErr := os.ReadFile(failingCommitPath)
  258. if readErr != nil {
  259. // Abandon this attempt to handle the error
  260. return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
  261. }
  262. commitSha = strings.TrimSpace(string(commitShaBytes))
  263. ok = true
  264. break
  265. }
  266. }
  267. if !ok {
  268. log.Error("Unable to determine failing commit sha for failing rebase in temp repo for %-v. Cannot cast as ErrRebaseConflicts.", ctx.pr)
  269. return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
  270. }
  271. log.Debug("Conflict when rebasing staging on to base in %-v at %s: %v\n%s\n%s", ctx.pr, commitSha, err, ctx.outbuf.String(), ctx.errbuf.String())
  272. return ErrRebaseConflicts{
  273. CommitSHA: commitSha,
  274. Style: mergeStyle,
  275. StdOut: ctx.outbuf.String(),
  276. StdErr: ctx.errbuf.String(),
  277. Err: err,
  278. }
  279. }
  280. return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
  281. }
  282. ctx.outbuf.Reset()
  283. ctx.errbuf.Reset()
  284. return nil
  285. }