drone/gitrpc/internal/gitea/merge.go

276 lines
8.0 KiB
Go

// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package gitea
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/harness/gitness/gitrpc/internal/tempdir"
"github.com/harness/gitness/gitrpc/internal/types"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
)
// CreateTemporaryRepo creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
// it also create a second base branch called "original_base".
//
//nolint:funlen,gocognit // need refactor
func (g Adapter) CreateTemporaryRepoForPR(
ctx context.Context,
reposTempPath string,
pr *types.PullRequest,
) (string, error) {
if pr.BaseRepoPath == "" && pr.HeadRepoPath != "" {
pr.BaseRepoPath = pr.HeadRepoPath
}
if pr.HeadRepoPath == "" && pr.BaseRepoPath != "" {
pr.HeadRepoPath = pr.BaseRepoPath
}
if pr.BaseBranch == "" {
return "", errors.New("empty base branch")
}
if pr.HeadBranch == "" {
return "", errors.New("empty head branch")
}
baseRepoPath := pr.BaseRepoPath
headRepoPath := pr.HeadRepoPath
// Clone base repo.
tmpBasePath, err := tempdir.CreateTemporaryPath(reposTempPath, "pull")
if err != nil {
return "", err
}
if err = g.InitRepository(ctx, tmpBasePath, false); err != nil {
// log.Error("git init tmpBasePath: %v", err)
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", err
}
remoteRepoName := "head_repo"
baseBranch := "base"
// Add head repo remote.
addCacheRepo := func(staging, cache string) error {
var f *os.File
alternates := filepath.Join(staging, ".git", "objects", "info", "alternates")
f, err = os.OpenFile(alternates, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return err
}
defer f.Close()
data := filepath.Join(cache, "objects")
if _, err = fmt.Fprintln(f, data); err != nil {
return err
}
return nil
}
if err = addCacheRepo(tmpBasePath, baseRepoPath); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", fmt.Errorf("unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepoPath, err)
}
var outbuf, errbuf strings.Builder
if err = git.NewCommand(ctx, "remote", "add", "-t", pr.BaseBranch, "-m", pr.BaseBranch, "origin", baseRepoPath).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", fmt.Errorf("unable to add base repository as origin "+
"[%s -> tmpBasePath]: %w\n%s\n%s", pr.BaseRepoPath, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
if err = git.NewCommand(ctx, "fetch", "origin", "--no-tags", "--",
pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", fmt.Errorf("unable to fetch origin base branch "+
"[%s:%s -> base, original_base in tmpBasePath]: %w\n%s\n%s",
pr.BaseRepoPath, pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
if err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+baseBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", fmt.Errorf("unable to set HEAD as base "+
"branch [tmpBasePath]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
if err = addCacheRepo(tmpBasePath, headRepoPath); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", fmt.Errorf("unable to head base repository "+
"to temporary repo [%s -> tmpBasePath]: %w", pr.HeadRepoPath, err)
}
if err = git.NewCommand(ctx, "remote", "add", remoteRepoName, headRepoPath).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
return "", fmt.Errorf("unable to add head repository as head_repo "+
"[%s -> tmpBasePath]: %w\n%s\n%s", pr.HeadRepoPath, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
trackingBranch := "tracking"
headBranch := git.BranchPrefix + pr.HeadBranch
if err = git.NewCommand(ctx, "fetch", "--no-tags", remoteRepoName, headBranch+":"+trackingBranch).
Run(&git.RunOpts{
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
_ = tempdir.RemoveTemporaryPath(tmpBasePath)
if !git.IsBranchExist(ctx, pr.HeadRepoPath, headBranch) {
return "", models.ErrBranchDoesNotExist{
BranchName: headBranch,
}
}
return "", fmt.Errorf("unable to fetch head_repo head branch "+
"[%s:%s -> tracking in tmpBasePath]: %w\n%s\n%s",
pr.HeadRepoPath, headBranch, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
return tmpBasePath, nil
}
func (g Adapter) Merge(
ctx context.Context,
pr *types.PullRequest,
mergeMethod string,
trackingBranch string,
tmpBasePath string,
mergeMsg string,
env []string,
) error {
// TODO: mergeMethod should be an enum.
if mergeMethod != "merge" {
return fmt.Errorf("merge method '%s' is not supported", mergeMethod)
}
var outbuf, errbuf strings.Builder
args := []string{
mergeMethod,
"--no-ff",
trackingBranch,
}
// override message for merging iff mergeMsg was provided (only for merge for now)
if mergeMethod == "merge" && mergeMsg != "" {
args = append(args, "-m", mergeMsg)
}
cmd := git.NewCommand(ctx, args...)
if err := cmd.Run(&git.RunOpts{
Env: env,
Dir: tmpBasePath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
// Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict
if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil {
// We have a merge conflict error
return types.MergeConflictsError{
Method: mergeMethod,
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
} else if strings.Contains(errbuf.String(), "refusing to merge unrelated histories") {
return types.MergeUnrelatedHistoriesError{
Method: mergeMethod,
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
}
return fmt.Errorf("git merge [%s -> %s]: %w\n%s\n%s",
pr.HeadBranch, pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
return nil
}
func (g Adapter) GetDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string) (string, error) {
getDiffTreeFromBranch := func(repoPath, baseBranch, headBranch string) (string, error) {
var outbuf, errbuf strings.Builder
if err := git.NewCommand(ctx, "diff-tree", "--no-commit-id",
"--name-only", "-r", "-z", "--root", baseBranch, headBranch, "--").
Run(&git.RunOpts{
Dir: repoPath,
Stdout: &outbuf,
Stderr: &errbuf,
}); err != nil {
return "", fmt.Errorf("git diff-tree [%s base:%s head:%s]: %s", repoPath, baseBranch, headBranch, errbuf.String())
}
return outbuf.String(), nil
}
scanNullTerminatedStrings := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\x00'); i >= 0 {
return i + 1, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
list, err := getDiffTreeFromBranch(repoPath, baseBranch, headBranch)
if err != nil {
return "", err
}
// Prefixing '/' for each entry, otherwise all files with the same name in subdirectories would be matched.
out := bytes.Buffer{}
scanner := bufio.NewScanner(strings.NewReader(list))
scanner.Split(scanNullTerminatedStrings)
for scanner.Scan() {
filepath := scanner.Text()
// escape '*', '?', '[', spaces and '!' prefix
filepath = escapedSymbols.ReplaceAllString(filepath, `\$1`)
// no necessary to escape the first '#' symbol because the first symbol is '/'
fmt.Fprintf(&out, "/%s\n", filepath)
}
return out.String(), nil
}