2019-12-13 22:21:06 +00:00
// Copyright 2019 The Gitea Authors.
// All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2019-12-13 22:21:06 +00:00
package pull
import (
2026-01-22 14:04:26 +08:00
"bytes"
2022-01-19 23:26:57 +00:00
"context"
2019-12-13 22:21:06 +00:00
"fmt"
"os"
"path/filepath"
2023-06-29 18:03:20 +08:00
git_model "code.gitea.io/gitea/models/git"
2022-06-13 17:37:59 +08:00
issues_model "code.gitea.io/gitea/models/issues"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2019-12-13 22:21:06 +00:00
"code.gitea.io/gitea/modules/git"
2025-09-15 23:33:12 -07:00
"code.gitea.io/gitea/modules/git/gitcmd"
2019-12-13 22:21:06 +00:00
"code.gitea.io/gitea/modules/log"
2022-05-09 00:46:32 +08:00
repo_module "code.gitea.io/gitea/modules/repository"
2019-12-13 22:21:06 +00:00
)
2023-03-07 20:07:35 +00:00
// Temporary repos created here use standard branch names to help simplify
// merging code
const (
2026-01-27 11:57:20 -08:00
tmpRepoBaseBranch = "base" // equivalent to pr.BaseBranch
tmpRepoTrackingBranch = "tracking" // equivalent to pr.HeadBranch
tmpRepoStagingBranch = "staging" // this is used for a working branch
2023-03-07 20:07:35 +00:00
)
2025-04-24 21:26:57 +02:00
type prTmpRepoContext struct {
2023-03-07 20:07:35 +00:00
context . Context
tmpBasePath string
pr * issues_model . PullRequest
2026-01-22 14:04:26 +08:00
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
2023-03-07 20:07:35 +00:00
}
2025-10-11 19:24:00 -07:00
// 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
2025-10-07 02:06:51 -07:00
func ( ctx * prTmpRepoContext ) PrepareGitCmd ( cmd * gitcmd . Command ) * gitcmd . Command {
2023-03-07 20:07:35 +00:00
ctx . outbuf . Reset ( )
2026-01-22 14:04:26 +08:00
return cmd . WithDir ( ctx . tmpBasePath ) . WithStdoutBuffer ( ctx . outbuf )
2023-03-07 20:07:35 +00:00
}
// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
2020-04-01 21:03:08 +02:00
// it also create a second base branch called "original_base"
2025-04-24 21:26:57 +02:00
func createTemporaryRepoForPR ( ctx context . Context , pr * issues_model . PullRequest ) ( prCtx * prTmpRepoContext , cancel context . CancelFunc , err error ) {
2022-11-19 09:12:33 +01:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "%-v LoadHeadRepo: %v" , pr , err )
return nil , nil , fmt . Errorf ( "%v LoadHeadRepo: %w" , pr , err )
2019-12-13 22:21:06 +00:00
} else if pr . HeadRepo == nil {
2023-03-07 20:07:35 +00:00
log . Error ( "%-v HeadRepo %d does not exist" , pr , pr . HeadRepoID )
return nil , nil , & repo_model . ErrRepoNotExist {
2019-12-13 22:21:06 +00:00
ID : pr . HeadRepoID ,
}
2022-11-19 09:12:33 +01:00
} else if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "%-v LoadBaseRepo: %v" , pr , err )
return nil , nil , fmt . Errorf ( "%v LoadBaseRepo: %w" , pr , err )
2019-12-13 22:21:06 +00:00
} else if pr . BaseRepo == nil {
2023-03-07 20:07:35 +00:00
log . Error ( "%-v BaseRepo %d does not exist" , pr , pr . BaseRepoID )
return nil , nil , & repo_model . ErrRepoNotExist {
2019-12-13 22:21:06 +00:00
ID : pr . BaseRepoID ,
}
2023-02-18 21:11:03 +09:00
} else if err := pr . HeadRepo . LoadOwner ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "%-v HeadRepo.LoadOwner: %v" , pr , err )
return nil , nil , fmt . Errorf ( "%v HeadRepo.LoadOwner: %w" , pr , err )
2023-02-18 21:11:03 +09:00
} else if err := pr . BaseRepo . LoadOwner ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "%-v BaseRepo.LoadOwner: %v" , pr , err )
return nil , nil , fmt . Errorf ( "%v BaseRepo.LoadOwner: %w" , pr , err )
2019-12-13 22:21:06 +00:00
}
// Clone base repo.
2025-04-08 09:15:28 -07:00
tmpBasePath , cleanup , err := repo_module . CreateTemporaryPath ( "pull" )
2019-12-13 22:21:06 +00:00
if err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "CreateTemporaryPath[%-v]: %v" , pr , err )
return nil , nil , err
}
2025-04-08 09:15:28 -07:00
cancel = cleanup
2025-04-24 21:26:57 +02:00
prCtx = & prTmpRepoContext {
2023-03-07 20:07:35 +00:00
Context : ctx ,
tmpBasePath : tmpBasePath ,
pr : pr ,
2026-01-22 14:04:26 +08:00
outbuf : & bytes . Buffer { } ,
2023-03-07 20:07:35 +00:00
}
2019-12-13 22:21:06 +00:00
baseRepoPath := pr . BaseRepo . RepoPath ( )
headRepoPath := pr . HeadRepo . RepoPath ( )
2023-12-17 19:56:08 +08:00
if err := git . InitRepository ( ctx , tmpBasePath , false , pr . BaseRepo . ObjectFormatName ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "Unable to init tmpBasePath for %-v: %v" , pr , err )
cancel ( )
return nil , nil , err
2019-12-13 22:21:06 +00:00
}
remoteRepoName := "head_repo"
2025-09-15 23:33:12 -07:00
fetchArgs := gitcmd . TrustedCmdArgs { "--no-tags" }
2024-05-07 00:34:16 +08:00
if git . DefaultFeatures ( ) . CheckVersionAtLeast ( "2.25.0" ) {
2023-03-01 20:19:04 +01:00
// Writing the commit graph can be slow and is not needed here
fetchArgs = append ( fetchArgs , "--no-write-commit-graph" )
}
2023-03-07 20:07:35 +00:00
// addCacheRepo adds git alternatives for the cacheRepoPath in the repoPath
addCacheRepo := func ( repoPath , cacheRepoPath string ) error {
p := filepath . Join ( repoPath , ".git" , "objects" , "info" , "alternates" )
2022-01-20 18:46:10 +01:00
f , err := os . OpenFile ( p , os . O_APPEND | os . O_CREATE | os . O_WRONLY , 0 o600 )
2019-12-13 22:21:06 +00:00
if err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "Could not create .git/objects/info/alternates file in %s: %v" , repoPath , err )
2019-12-13 22:21:06 +00:00
return err
}
defer f . Close ( )
2023-03-07 20:07:35 +00:00
data := filepath . Join ( cacheRepoPath , "objects" )
2019-12-13 22:21:06 +00:00
if _ , err := fmt . Fprintln ( f , data ) ; err != nil {
2023-03-07 20:07:35 +00:00
log . Error ( "Could not write to .git/objects/info/alternates file in %s: %v" , repoPath , err )
2019-12-13 22:21:06 +00:00
return err
}
return nil
}
2023-03-07 20:07:35 +00:00
// Add head repo remote.
2019-12-13 22:21:06 +00:00
if err := addCacheRepo ( tmpBasePath , baseRepoPath ) ; err != nil {
2023-03-07 20:07:35 +00:00
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 )
2019-12-13 22:21:06 +00:00
}
2025-10-07 02:06:51 -07:00
if err := prCtx . PrepareGitCmd ( gitcmd . NewCommand ( "remote" , "add" , "-t" ) . AddDynamicArguments ( pr . BaseBranch ) . AddArguments ( "-m" ) . AddDynamicArguments ( pr . BaseBranch ) . AddDynamicArguments ( "origin" , baseRepoPath ) ) .
2026-01-19 07:10:33 +08:00
RunWithStderr ( 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 ( ) , err . Stderr ( ) )
2023-03-07 20:07:35 +00:00
cancel ( )
2026-01-19 07:10:33 +08:00
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 ( ) )
2019-12-13 22:21:06 +00:00
}
2025-10-07 02:06:51 -07:00
if err := prCtx . PrepareGitCmd ( gitcmd . NewCommand ( "fetch" , "origin" ) . AddArguments ( fetchArgs ... ) .
2026-01-27 11:57:20 -08:00
AddDashesAndList ( git . BranchPrefix + pr . BaseBranch + ":" + git . BranchPrefix + tmpRepoBaseBranch , git . BranchPrefix + pr . BaseBranch + ":" + git . BranchPrefix + "original_" + tmpRepoBaseBranch ) ) .
2026-01-19 07:10:33 +08:00
RunWithStderr ( 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 ( ) , err . Stderr ( ) )
2023-03-07 20:07:35 +00:00
cancel ( )
2026-01-19 07:10:33 +08:00
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 ( ) )
2019-12-13 22:21:06 +00:00
}
2026-01-27 11:57:20 -08:00
if err := prCtx . PrepareGitCmd ( gitcmd . NewCommand ( "symbolic-ref" ) . AddDynamicArguments ( "HEAD" , git . BranchPrefix + tmpRepoBaseBranch ) ) .
2026-01-19 07:10:33 +08:00
RunWithStderr ( 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 ( ) , err . Stderr ( ) )
2023-03-07 20:07:35 +00:00
cancel ( )
2026-01-19 07:10:33 +08:00
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 ( ) )
2019-12-13 22:21:06 +00:00
}
if err := addCacheRepo ( tmpBasePath , headRepoPath ) ; err != nil {
2023-03-07 20:07:35 +00:00
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 )
2019-12-13 22:21:06 +00:00
}
2025-10-07 02:06:51 -07:00
if err := prCtx . PrepareGitCmd ( gitcmd . NewCommand ( "remote" , "add" ) . AddDynamicArguments ( remoteRepoName , headRepoPath ) ) .
2026-01-19 07:10:33 +08:00
RunWithStderr ( 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 ( ) , err . Stderr ( ) )
2023-03-07 20:07:35 +00:00
cancel ( )
2026-01-19 07:10:33 +08:00
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 ( ) )
2019-12-13 22:21:06 +00:00
}
2023-12-17 19:56:08 +08:00
objectFormat := git . ObjectFormatFromName ( pr . BaseRepo . ObjectFormatName )
2019-12-13 22:21:06 +00:00
// Fetch head branch
2021-07-28 17:42:56 +08:00
var headBranch string
2022-06-13 17:37:59 +08:00
if pr . Flow == issues_model . PullRequestFlowGithub {
2021-07-28 17:42:56 +08:00
headBranch = git . BranchPrefix + pr . HeadBranch
2023-12-13 21:02:00 +00:00
} else if len ( pr . HeadCommitID ) == objectFormat . FullLength ( ) { // for not created pull request
2021-07-28 17:42:56 +08:00
headBranch = pr . HeadCommitID
} else {
2025-07-16 21:33:33 +08:00
headBranch = pr . GetGitHeadRefName ( )
2021-07-28 17:42:56 +08:00
}
2026-01-27 11:57:20 -08:00
if err := prCtx . PrepareGitCmd ( gitcmd . NewCommand ( "fetch" ) . AddArguments ( fetchArgs ... ) . AddDynamicArguments ( remoteRepoName , headBranch + ":" + tmpRepoTrackingBranch ) ) .
2026-01-19 07:10:33 +08:00
RunWithStderr ( ctx ) ; err != nil {
2023-03-07 20:07:35 +00:00
cancel ( )
2025-10-25 10:08:25 -07:00
if exist , _ := git_model . IsBranchExist ( ctx , pr . HeadRepo . ID , pr . HeadBranch ) ; ! exist {
2023-06-29 18:03:20 +08:00
return nil , nil , git_model . ErrBranchNotExist {
2021-07-13 01:26:25 +02:00
BranchName : pr . HeadBranch ,
}
}
2026-01-19 07:10:33 +08:00
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 ( ) , err . Stderr ( ) )
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 ( ) )
2019-12-13 22:21:06 +00:00
}
2023-03-07 20:07:35 +00:00
prCtx . outbuf . Reset ( )
return prCtx , cancel , nil
2019-12-13 22:21:06 +00:00
}