mirror of https://github.com/go-gitea/gitea.git
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.
196 lines
8.5 KiB
196 lines
8.5 KiB
// Copyright 2019 The Gitea Authors. |
|
// All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package pull |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"os" |
|
"path/filepath" |
|
"strings" |
|
|
|
git_model "code.gitea.io/gitea/models/git" |
|
issues_model "code.gitea.io/gitea/models/issues" |
|
repo_model "code.gitea.io/gitea/models/repo" |
|
"code.gitea.io/gitea/modules/git" |
|
"code.gitea.io/gitea/modules/git/gitcmd" |
|
"code.gitea.io/gitea/modules/log" |
|
repo_module "code.gitea.io/gitea/modules/repository" |
|
) |
|
|
|
// Temporary repos created here use standard branch names to help simplify |
|
// merging code |
|
const ( |
|
baseBranch = "base" // equivalent to pr.BaseBranch |
|
trackingBranch = "tracking" // equivalent to pr.HeadBranch |
|
stagingBranch = "staging" // this is used for a working branch |
|
) |
|
|
|
type prTmpRepoContext struct { |
|
context.Context |
|
tmpBasePath string |
|
pr *issues_model.PullRequest |
|
outbuf *strings.Builder // we keep these around to help reduce needless buffer recreation, |
|
errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use |
|
} |
|
|
|
// PrepareGitCmd prepares a git command with the correct directory, environment, and output buffers |
|
// This function can only be called with gitcmd.Run() |
|
// Do NOT use it with gitcmd.RunStd*() functions, otherwise it will panic |
|
func (ctx *prTmpRepoContext) PrepareGitCmd(cmd *gitcmd.Command) *gitcmd.Command { |
|
ctx.outbuf.Reset() |
|
ctx.errbuf.Reset() |
|
return cmd.WithDir(ctx.tmpBasePath). |
|
WithStdout(ctx.outbuf). |
|
WithStderr(ctx.errbuf) |
|
} |
|
|
|
// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch |
|
// it also create a second base branch called "original_base" |
|
func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) (prCtx *prTmpRepoContext, cancel context.CancelFunc, err error) { |
|
if err := pr.LoadHeadRepo(ctx); err != nil { |
|
log.Error("%-v LoadHeadRepo: %v", pr, err) |
|
return nil, nil, fmt.Errorf("%v LoadHeadRepo: %w", pr, err) |
|
} else if pr.HeadRepo == nil { |
|
log.Error("%-v HeadRepo %d does not exist", pr, pr.HeadRepoID) |
|
return nil, nil, &repo_model.ErrRepoNotExist{ |
|
ID: pr.HeadRepoID, |
|
} |
|
} else if err := pr.LoadBaseRepo(ctx); err != nil { |
|
log.Error("%-v LoadBaseRepo: %v", pr, err) |
|
return nil, nil, fmt.Errorf("%v LoadBaseRepo: %w", pr, err) |
|
} else if pr.BaseRepo == nil { |
|
log.Error("%-v BaseRepo %d does not exist", pr, pr.BaseRepoID) |
|
return nil, nil, &repo_model.ErrRepoNotExist{ |
|
ID: pr.BaseRepoID, |
|
} |
|
} else if err := pr.HeadRepo.LoadOwner(ctx); err != nil { |
|
log.Error("%-v HeadRepo.LoadOwner: %v", pr, err) |
|
return nil, nil, fmt.Errorf("%v HeadRepo.LoadOwner: %w", pr, err) |
|
} else if err := pr.BaseRepo.LoadOwner(ctx); err != nil { |
|
log.Error("%-v BaseRepo.LoadOwner: %v", pr, err) |
|
return nil, nil, fmt.Errorf("%v BaseRepo.LoadOwner: %w", pr, err) |
|
} |
|
|
|
// Clone base repo. |
|
tmpBasePath, cleanup, err := repo_module.CreateTemporaryPath("pull") |
|
if err != nil { |
|
log.Error("CreateTemporaryPath[%-v]: %v", pr, err) |
|
return nil, nil, err |
|
} |
|
cancel = cleanup |
|
|
|
prCtx = &prTmpRepoContext{ |
|
Context: ctx, |
|
tmpBasePath: tmpBasePath, |
|
pr: pr, |
|
outbuf: &strings.Builder{}, |
|
errbuf: &strings.Builder{}, |
|
} |
|
|
|
baseRepoPath := pr.BaseRepo.RepoPath() |
|
headRepoPath := pr.HeadRepo.RepoPath() |
|
|
|
if err := git.InitRepository(ctx, tmpBasePath, false, pr.BaseRepo.ObjectFormatName); err != nil { |
|
log.Error("Unable to init tmpBasePath for %-v: %v", pr, err) |
|
cancel() |
|
return nil, nil, err |
|
} |
|
|
|
remoteRepoName := "head_repo" |
|
baseBranch := "base" |
|
|
|
fetchArgs := gitcmd.TrustedCmdArgs{"--no-tags"} |
|
if git.DefaultFeatures().CheckVersionAtLeast("2.25.0") { |
|
// Writing the commit graph can be slow and is not needed here |
|
fetchArgs = append(fetchArgs, "--no-write-commit-graph") |
|
} |
|
|
|
// addCacheRepo adds git alternatives for the cacheRepoPath in the repoPath |
|
addCacheRepo := func(repoPath, cacheRepoPath string) error { |
|
p := filepath.Join(repoPath, ".git", "objects", "info", "alternates") |
|
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) |
|
if err != nil { |
|
log.Error("Could not create .git/objects/info/alternates file in %s: %v", repoPath, err) |
|
return err |
|
} |
|
defer f.Close() |
|
data := filepath.Join(cacheRepoPath, "objects") |
|
if _, err := fmt.Fprintln(f, data); err != nil { |
|
log.Error("Could not write to .git/objects/info/alternates file in %s: %v", repoPath, err) |
|
return err |
|
} |
|
return nil |
|
} |
|
|
|
// Add head repo remote. |
|
if err := addCacheRepo(tmpBasePath, baseRepoPath); err != nil { |
|
log.Error("%-v Unable to add base repository to temporary repo [%s -> %s]: %v", pr, pr.BaseRepo.FullName(), tmpBasePath, err) |
|
cancel() |
|
return nil, nil, fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepo.FullName(), err) |
|
} |
|
|
|
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("remote", "add", "-t").AddDynamicArguments(pr.BaseBranch).AddArguments("-m").AddDynamicArguments(pr.BaseBranch).AddDynamicArguments("origin", baseRepoPath)). |
|
Run(ctx); err != nil { |
|
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()) |
|
cancel() |
|
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()) |
|
} |
|
|
|
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("fetch", "origin").AddArguments(fetchArgs...). |
|
AddDashesAndList(git.BranchPrefix+pr.BaseBranch+":"+git.BranchPrefix+baseBranch, git.BranchPrefix+pr.BaseBranch+":"+git.BranchPrefix+"original_"+baseBranch)). |
|
Run(ctx); err != nil { |
|
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()) |
|
cancel() |
|
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()) |
|
} |
|
|
|
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseBranch)). |
|
Run(ctx); err != nil { |
|
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()) |
|
cancel() |
|
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()) |
|
} |
|
|
|
if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil { |
|
log.Error("%-v Unable to add head repository to temporary repo [%s -> %s]: %v", pr, pr.HeadRepo.FullName(), tmpBasePath, err) |
|
cancel() |
|
return nil, nil, fmt.Errorf("Unable to add head base repository to temporary repo [%s -> tmpBasePath]: %w", pr.HeadRepo.FullName(), err) |
|
} |
|
|
|
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("remote", "add").AddDynamicArguments(remoteRepoName, headRepoPath)). |
|
Run(ctx); err != nil { |
|
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()) |
|
cancel() |
|
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()) |
|
} |
|
|
|
trackingBranch := "tracking" |
|
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) |
|
// Fetch head branch |
|
var headBranch string |
|
if pr.Flow == issues_model.PullRequestFlowGithub { |
|
headBranch = git.BranchPrefix + pr.HeadBranch |
|
} else if len(pr.HeadCommitID) == objectFormat.FullLength() { // for not created pull request |
|
headBranch = pr.HeadCommitID |
|
} else { |
|
headBranch = pr.GetGitHeadRefName() |
|
} |
|
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("fetch").AddArguments(fetchArgs...).AddDynamicArguments(remoteRepoName, headBranch+":"+trackingBranch)). |
|
Run(ctx); err != nil { |
|
cancel() |
|
if exist, _ := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch); !exist { |
|
return nil, nil, git_model.ErrBranchNotExist{ |
|
BranchName: pr.HeadBranch, |
|
} |
|
} |
|
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()) |
|
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()) |
|
} |
|
prCtx.outbuf.Reset() |
|
prCtx.errbuf.Reset() |
|
|
|
return prCtx, cancel, nil |
|
}
|
|
|