191 lines
5.3 KiB
Go
191 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type conflictFileLoader func(
|
|
context.Context, string, int, string, string, string, string,
|
|
) ([]string, error)
|
|
|
|
type conflictFileResult struct {
|
|
files []string
|
|
err error
|
|
checkedAt time.Time
|
|
}
|
|
|
|
func (c *GitHubClient) loadConflictFiles(
|
|
ctx context.Context,
|
|
repositoryURL string,
|
|
number int,
|
|
baseRef, baseOID, headOID string,
|
|
) ([]string, error) {
|
|
key := strings.Join([]string{repositoryURL, baseOID, headOID}, "\x00")
|
|
c.conflictMu.Lock()
|
|
cached, ok := c.conflictCache[key]
|
|
c.conflictMu.Unlock()
|
|
if ok && (cached.err == nil || time.Since(cached.checkedAt) < time.Minute) {
|
|
return append([]string(nil), cached.files...), cached.err
|
|
}
|
|
|
|
files, err := c.conflicts(ctx, repositoryURL, number, baseRef, baseOID, headOID, c.token)
|
|
result := conflictFileResult{
|
|
files: append([]string(nil), files...), err: err, checkedAt: time.Now(),
|
|
}
|
|
c.conflictMu.Lock()
|
|
c.conflictCache[key] = result
|
|
c.conflictMu.Unlock()
|
|
return files, err
|
|
}
|
|
|
|
func analyzeConflictFiles(
|
|
ctx context.Context,
|
|
repositoryURL string,
|
|
number int,
|
|
baseRef, _, _, token string,
|
|
) ([]string, error) {
|
|
if repositoryURL == "" || baseRef == "" || number <= 0 {
|
|
return nil, errors.New("missing repository merge metadata")
|
|
}
|
|
gitDir, err := os.MkdirTemp("", "diple-conflicts-*")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create temporary merge repository: %w", err)
|
|
}
|
|
defer os.RemoveAll(gitDir)
|
|
|
|
run := func(args ...string) ([]byte, error) {
|
|
command := exec.CommandContext(ctx, "git", args...)
|
|
command.Env = gitAuthenticationEnvironment(callerEnvironment(), token)
|
|
return command.CombinedOutput()
|
|
}
|
|
if output, runErr := run("init", "--bare", gitDir); runErr != nil {
|
|
return nil, commandError("initialize merge analysis", output, runErr)
|
|
}
|
|
cloneURL := strings.TrimSuffix(strings.TrimSuffix(repositoryURL, "/"), ".git") + ".git"
|
|
if output, runErr := run("-C", gitDir, "remote", "add", "origin", cloneURL); runErr != nil {
|
|
return nil, commandError("configure merge analysis remote", output, runErr)
|
|
}
|
|
if output, runErr := run("-C", gitDir, "config", "remote.origin.promisor", "true"); runErr != nil {
|
|
return nil, commandError("configure partial clone", output, runErr)
|
|
}
|
|
if output, runErr := run("-C", gitDir, "config", "remote.origin.partialclonefilter", "blob:none"); runErr != nil {
|
|
return nil, commandError("configure partial clone filter", output, runErr)
|
|
}
|
|
|
|
refspecs := []string{
|
|
"+refs/heads/" + baseRef + ":refs/diple/base",
|
|
"+refs/pull/" + strconv.Itoa(number) + "/head:refs/diple/head",
|
|
}
|
|
fetch := func(depthArgs ...string) error {
|
|
args := []string{"-C", gitDir, "fetch", "--quiet", "--no-tags", "--filter=blob:none"}
|
|
args = append(args, depthArgs...)
|
|
args = append(args, "origin")
|
|
args = append(args, refspecs...)
|
|
output, runErr := run(args...)
|
|
return commandError("fetch merge inputs", output, runErr)
|
|
}
|
|
if err := fetch("--depth=64"); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, deepen := range []string{"192", "768"} {
|
|
if mergeBaseExists(run, gitDir) {
|
|
break
|
|
}
|
|
if err := fetch("--deepen=" + deepen); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if !mergeBaseExists(run, gitDir) {
|
|
if err := fetch("--unshallow"); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
output, runErr := run(
|
|
"-C", gitDir, "merge-tree", "--write-tree", "--name-only", "--no-messages", "-z",
|
|
"refs/diple/base", "refs/diple/head",
|
|
)
|
|
if runErr == nil {
|
|
return nil, nil
|
|
}
|
|
var exitErr *exec.ExitError
|
|
if !errors.As(runErr, &exitErr) || exitErr.ExitCode() != 1 {
|
|
return nil, commandError("analyze merge conflicts", output, runErr)
|
|
}
|
|
return parseConflictFiles(output)
|
|
}
|
|
|
|
func mergeBaseExists(
|
|
run func(...string) ([]byte, error),
|
|
gitDir string,
|
|
) bool {
|
|
_, err := run(
|
|
"-C", gitDir, "merge-base", "refs/diple/base", "refs/diple/head",
|
|
)
|
|
return err == nil
|
|
}
|
|
|
|
func parseConflictFiles(output []byte) ([]string, error) {
|
|
parts := bytes.Split(output, []byte{0})
|
|
if len(parts) < 2 || len(parts[0]) == 0 {
|
|
return nil, errors.New("git merge-tree returned malformed conflict data")
|
|
}
|
|
files := make([]string, 0, len(parts)-2)
|
|
seen := make(map[string]bool)
|
|
for _, raw := range parts[1:] {
|
|
name := string(raw)
|
|
if name == "" || seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
files = append(files, name)
|
|
}
|
|
sort.Strings(files)
|
|
return files, nil
|
|
}
|
|
|
|
func commandError(action string, output []byte, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
message := strings.TrimSpace(string(output))
|
|
if message == "" {
|
|
return fmt.Errorf("%s: %w", action, err)
|
|
}
|
|
return fmt.Errorf("%s: %s", action, message)
|
|
}
|
|
|
|
func callerEnvironment() []string {
|
|
const prefix = "GIT_CONFIG_"
|
|
environment := make([]string, 0, len(os.Environ())+5)
|
|
for _, item := range os.Environ() {
|
|
if !strings.HasPrefix(item, prefix) && !strings.HasPrefix(item, "GIT_TERMINAL_PROMPT=") {
|
|
environment = append(environment, item)
|
|
}
|
|
}
|
|
return environment
|
|
}
|
|
|
|
func gitAuthenticationEnvironment(environment []string, token string) []string {
|
|
environment = append(environment, "GIT_TERMINAL_PROMPT=0")
|
|
if token == "" {
|
|
return environment
|
|
}
|
|
credentials := base64.StdEncoding.EncodeToString([]byte("x-access-token:" + token))
|
|
return append(environment,
|
|
"GIT_CONFIG_COUNT=1",
|
|
"GIT_CONFIG_KEY_0=http.extraHeader",
|
|
"GIT_CONFIG_VALUE_0=Authorization: Basic "+credentials,
|
|
)
|
|
}
|