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, ) }