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.

temp_repo.go 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // Copyright 2019 The Gitea Authors.
  2. // All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package pull
  5. import (
  6. "context"
  7. "fmt"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. git_model "code.gitea.io/gitea/models/git"
  12. issues_model "code.gitea.io/gitea/models/issues"
  13. repo_model "code.gitea.io/gitea/models/repo"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/log"
  16. repo_module "code.gitea.io/gitea/modules/repository"
  17. )
  18. // Temporary repos created here use standard branch names to help simplify
  19. // merging code
  20. const (
  21. baseBranch = "base" // equivalent to pr.BaseBranch
  22. trackingBranch = "tracking" // equivalent to pr.HeadBranch
  23. stagingBranch = "staging" // this is used for a working branch
  24. )
  25. type prContext struct {
  26. context.Context
  27. tmpBasePath string
  28. pr *issues_model.PullRequest
  29. outbuf *strings.Builder // we keep these around to help reduce needless buffer recreation,
  30. errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use
  31. }
  32. func (ctx *prContext) RunOpts() *git.RunOpts {
  33. ctx.outbuf.Reset()
  34. ctx.errbuf.Reset()
  35. return &git.RunOpts{
  36. Dir: ctx.tmpBasePath,
  37. Stdout: ctx.outbuf,
  38. Stderr: ctx.errbuf,
  39. }
  40. }
  41. // createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
  42. // it also create a second base branch called "original_base"
  43. func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) (prCtx *prContext, cancel context.CancelFunc, err error) {
  44. if err := pr.LoadHeadRepo(ctx); err != nil {
  45. log.Error("%-v LoadHeadRepo: %v", pr, err)
  46. return nil, nil, fmt.Errorf("%v LoadHeadRepo: %w", pr, err)
  47. } else if pr.HeadRepo == nil {
  48. log.Error("%-v HeadRepo %d does not exist", pr, pr.HeadRepoID)
  49. return nil, nil, &repo_model.ErrRepoNotExist{
  50. ID: pr.HeadRepoID,
  51. }
  52. } else if err := pr.LoadBaseRepo(ctx); err != nil {
  53. log.Error("%-v LoadBaseRepo: %v", pr, err)
  54. return nil, nil, fmt.Errorf("%v LoadBaseRepo: %w", pr, err)
  55. } else if pr.BaseRepo == nil {
  56. log.Error("%-v BaseRepo %d does not exist", pr, pr.BaseRepoID)
  57. return nil, nil, &repo_model.ErrRepoNotExist{
  58. ID: pr.BaseRepoID,
  59. }
  60. } else if err := pr.HeadRepo.LoadOwner(ctx); err != nil {
  61. log.Error("%-v HeadRepo.LoadOwner: %v", pr, err)
  62. return nil, nil, fmt.Errorf("%v HeadRepo.LoadOwner: %w", pr, err)
  63. } else if err := pr.BaseRepo.LoadOwner(ctx); err != nil {
  64. log.Error("%-v BaseRepo.LoadOwner: %v", pr, err)
  65. return nil, nil, fmt.Errorf("%v BaseRepo.LoadOwner: %w", pr, err)
  66. }
  67. // Clone base repo.
  68. tmpBasePath, err := repo_module.CreateTemporaryPath("pull")
  69. if err != nil {
  70. log.Error("CreateTemporaryPath[%-v]: %v", pr, err)
  71. return nil, nil, err
  72. }
  73. prCtx = &prContext{
  74. Context: ctx,
  75. tmpBasePath: tmpBasePath,
  76. pr: pr,
  77. outbuf: &strings.Builder{},
  78. errbuf: &strings.Builder{},
  79. }
  80. cancel = func() {
  81. if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
  82. log.Error("Error whilst removing removing temporary repo for %-v: %v", pr, err)
  83. }
  84. }
  85. baseRepoPath := pr.BaseRepo.RepoPath()
  86. headRepoPath := pr.HeadRepo.RepoPath()
  87. if err := git.InitRepository(ctx, tmpBasePath, false, pr.BaseRepo.ObjectFormatName); err != nil {
  88. log.Error("Unable to init tmpBasePath for %-v: %v", pr, err)
  89. cancel()
  90. return nil, nil, err
  91. }
  92. remoteRepoName := "head_repo"
  93. baseBranch := "base"
  94. fetchArgs := git.TrustedCmdArgs{"--no-tags"}
  95. if git.DefaultFeatures().CheckVersionAtLeast("2.25.0") {
  96. // Writing the commit graph can be slow and is not needed here
  97. fetchArgs = append(fetchArgs, "--no-write-commit-graph")
  98. }
  99. // addCacheRepo adds git alternatives for the cacheRepoPath in the repoPath
  100. addCacheRepo := func(repoPath, cacheRepoPath string) error {
  101. p := filepath.Join(repoPath, ".git", "objects", "info", "alternates")
  102. f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
  103. if err != nil {
  104. log.Error("Could not create .git/objects/info/alternates file in %s: %v", repoPath, err)
  105. return err
  106. }
  107. defer f.Close()
  108. data := filepath.Join(cacheRepoPath, "objects")
  109. if _, err := fmt.Fprintln(f, data); err != nil {
  110. log.Error("Could not write to .git/objects/info/alternates file in %s: %v", repoPath, err)
  111. return err
  112. }
  113. return nil
  114. }
  115. // Add head repo remote.
  116. if err := addCacheRepo(tmpBasePath, baseRepoPath); err != nil {
  117. log.Error("%-v Unable to add base repository to temporary repo [%s -> %s]: %v", pr, pr.BaseRepo.FullName(), tmpBasePath, err)
  118. cancel()
  119. return nil, nil, fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepo.FullName(), err)
  120. }
  121. if err := git.NewCommand(ctx, "remote", "add", "-t").AddDynamicArguments(pr.BaseBranch).AddArguments("-m").AddDynamicArguments(pr.BaseBranch).AddDynamicArguments("origin", baseRepoPath).
  122. Run(prCtx.RunOpts()); err != nil {
  123. log.Error("%-v Unable to add base repository as origin [%s -> %s]: %v\n%s\n%s", pr, pr.BaseRepo.FullName(), tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  124. cancel()
  125. return nil, nil, fmt.Errorf("Unable to add base repository as origin [%s -> tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), err, prCtx.outbuf.String(), prCtx.errbuf.String())
  126. }
  127. if err := git.NewCommand(ctx, "fetch", "origin").AddArguments(fetchArgs...).AddDashesAndList(pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch).
  128. Run(prCtx.RunOpts()); err != nil {
  129. log.Error("%-v Unable to fetch origin base branch [%s:%s -> base, original_base in %s]: %v:\n%s\n%s", pr, pr.BaseRepo.FullName(), pr.BaseBranch, tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  130. cancel()
  131. return nil, nil, fmt.Errorf("Unable to fetch origin base branch [%s:%s -> base, original_base in tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  132. }
  133. if err := git.NewCommand(ctx, "symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseBranch).
  134. Run(prCtx.RunOpts()); err != nil {
  135. log.Error("%-v Unable to set HEAD as base branch in [%s]: %v\n%s\n%s", pr, tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  136. cancel()
  137. return nil, nil, fmt.Errorf("Unable to set HEAD as base branch in tmpBasePath: %w\n%s\n%s", err, prCtx.outbuf.String(), prCtx.errbuf.String())
  138. }
  139. if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil {
  140. log.Error("%-v Unable to add head repository to temporary repo [%s -> %s]: %v", pr, pr.HeadRepo.FullName(), tmpBasePath, err)
  141. cancel()
  142. return nil, nil, fmt.Errorf("Unable to add head base repository to temporary repo [%s -> tmpBasePath]: %w", pr.HeadRepo.FullName(), err)
  143. }
  144. if err := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteRepoName, headRepoPath).
  145. Run(prCtx.RunOpts()); err != nil {
  146. log.Error("%-v Unable to add head repository as head_repo [%s -> %s]: %v\n%s\n%s", pr, pr.HeadRepo.FullName(), tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  147. cancel()
  148. return nil, nil, fmt.Errorf("Unable to add head repository as head_repo [%s -> tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), err, prCtx.outbuf.String(), prCtx.errbuf.String())
  149. }
  150. trackingBranch := "tracking"
  151. objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
  152. // Fetch head branch
  153. var headBranch string
  154. if pr.Flow == issues_model.PullRequestFlowGithub {
  155. headBranch = git.BranchPrefix + pr.HeadBranch
  156. } else if len(pr.HeadCommitID) == objectFormat.FullLength() { // for not created pull request
  157. headBranch = pr.HeadCommitID
  158. } else {
  159. headBranch = pr.GetGitRefName()
  160. }
  161. if err := git.NewCommand(ctx, "fetch").AddArguments(fetchArgs...).AddDynamicArguments(remoteRepoName, headBranch+":"+trackingBranch).
  162. Run(prCtx.RunOpts()); err != nil {
  163. cancel()
  164. if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) {
  165. return nil, nil, git_model.ErrBranchNotExist{
  166. BranchName: pr.HeadBranch,
  167. }
  168. }
  169. log.Error("%-v Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr, pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  170. return nil, nil, fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, prCtx.outbuf.String(), prCtx.errbuf.String())
  171. }
  172. prCtx.outbuf.Reset()
  173. prCtx.errbuf.Reset()
  174. return prCtx, cancel, nil
  175. }