Files
gitea/services/pull/temp_repo.go
wxiaoguang 7065637e61 refactor: hide git repo path details from more packages (#38601)
Remove `RepoPath` from "models/repo" package, remove `UserPath` from
"models/user" package, use `WithRepo` for more places, fine tune tests.
2026-07-23 17:42:55 +00:00

166 lines
7.1 KiB
Go

// Copyright 2019 The Gitea Authors.
// All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/git/gitrepo"
repo_module "gitea.dev/modules/repository"
)
// Temporary repos created here use standard branch names to help simplify
// merging code
const (
tmpRepoBaseBranch = "base" // equivalent to pr.BaseBranch
tmpRepoTrackingBranch = "tracking" // equivalent to pr.HeadBranch
tmpRepoStagingBranch = "staging" // this is used for a working branch
)
type prTmpRepoContext struct {
context.Context
tmpBasePath string
tmpRepo git.RepositoryFacade
pr *issues_model.PullRequest
outbuf *bytes.Buffer // we keep these around to help reduce needless buffer recreation, 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()
return cmd.WithRepo(ctx.tmpRepo).WithStdoutBuffer(ctx.outbuf)
}
// 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, retErr error) {
defer func() {
if retErr != nil && cancel != nil {
cancel()
}
}()
if err := pr.LoadHeadRepo(ctx); err != nil {
return nil, nil, fmt.Errorf("LoadHeadRepo[PR:%d]: %w", pr.ID, err)
} else if pr.HeadRepo == nil {
return nil, nil, &repo_model.ErrRepoNotExist{ID: pr.HeadRepoID}
} else if err := pr.LoadBaseRepo(ctx); err != nil {
return nil, nil, fmt.Errorf("LoadBaseRepo[PR:%d]: %w", pr.ID, err)
} else if pr.BaseRepo == nil {
return nil, nil, &repo_model.ErrRepoNotExist{ID: pr.BaseRepoID}
} else if err := pr.HeadRepo.LoadOwner(ctx); err != nil {
return nil, nil, fmt.Errorf("HeadRepo.LoadOwner[PR:%d]: %w", pr.ID, err)
} else if err := pr.BaseRepo.LoadOwner(ctx); err != nil {
return nil, nil, fmt.Errorf("BaseRepo.LoadOwner[PR:%d]: %w", pr.ID, err)
}
// Clone base repo.
tmpBasePath, tmpRepo, cleanup, err := repo_module.CreateTemporaryGitRepo("pull")
if err != nil {
return nil, nil, fmt.Errorf("CreateTemporaryPath[PR:%d]: %w", pr.ID, err)
}
cancel = cleanup
prCtx = &prTmpRepoContext{
Context: ctx,
tmpBasePath: tmpBasePath,
tmpRepo: tmpRepo,
pr: pr,
outbuf: &bytes.Buffer{},
}
baseRepoPath := gitrepo.RepoLocalPath(pr.BaseRepo.CodeStorageRepo())
headRepoPath := gitrepo.RepoLocalPath(pr.HeadRepo.CodeStorageRepo())
if err := git.InitRepositoryLocal(ctx, tmpBasePath, false, pr.BaseRepo.ObjectFormatName); err != nil {
return nil, nil, fmt.Errorf("InitRepository[PR:%d]: %w", pr.ID, err)
}
remoteRepoName := "head_repo"
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 {
const altFileRelPath = ".git/objects/info/alternates"
gitInfoAltFile := filepath.Join(repoPath, filepath.FromSlash(altFileRelPath))
newAltLine := filepath.Join(cacheRepoPath, "objects")
f, err := os.OpenFile(gitInfoAltFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("unable to open %s file in %s: %w", altFileRelPath, filepath.Base(repoPath), err)
}
defer f.Close()
if _, err := fmt.Fprintln(f, newAltLine); err != nil {
return fmt.Errorf("unable to write %s file in %s: %w", altFileRelPath, filepath.Base(repoPath), err)
}
return nil
}
// Add head repo remote.
if err := addCacheRepo(tmpBasePath, baseRepoPath); err != nil {
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)).
RunWithStderr(ctx); err != nil {
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(), err.Stderr())
}
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("fetch", "origin").AddArguments(fetchArgs...).
AddDashesAndList(git.BranchPrefix+pr.BaseBranch+":"+git.BranchPrefix+tmpRepoBaseBranch, git.BranchPrefix+pr.BaseBranch+":"+git.BranchPrefix+"original_"+tmpRepoBaseBranch)).
RunWithStderr(ctx); err != nil {
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(), err.Stderr())
}
if err := prCtx.PrepareGitCmd(gitcmd.NewCommand("symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+tmpRepoBaseBranch)).
RunWithStderr(ctx); err != nil {
return nil, nil, fmt.Errorf("unable to set HEAD as base branch in tmpBasePath: %w\n%s\n%s", err, prCtx.outbuf.String(), err.Stderr())
}
if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil {
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)).
RunWithStderr(ctx); err != nil {
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(), err.Stderr())
}
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+":"+tmpRepoTrackingBranch)).
RunWithStderr(ctx); err != nil {
if exist, _ := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch); !exist {
return nil, nil, git_model.ErrBranchNotExist{BranchName: pr.HeadBranch}
}
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(), err.Stderr())
}
prCtx.outbuf.Reset()
return prCtx, cancel, nil
}