code.gitea.io/gitea@v1.21.7/services/pull/temp_repo.go (about)

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