Skip to content
quarry Artifact [0b02ce0109]
Login

Artifact [0b02ce0109]

Artifact 0b02ce0109ca6730937e6b81d2006a8c859e3d799728c084a919d03394a27234:


// Package premerge prepares the tree an accept WOULD create, without
// creating anything (RFC 0009 §premerge). It is the quarry-shaped step
// a CI pipeline runs first in premerge mode: clone read-only, open the
// target, merge the branch tip into the working tree — zit's merge
// writes no check-in — and hand the tree to the tests. Nothing here
// ever commits or pushes; ledger safety is structural, and the
// integration test asserts the store's artifact count is untouched
// even when a pipeline tries.
package premerge

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"
)

// Result says what the preparation produced.
type Result struct {
	// Dir holds the merged tree, ready for the pipeline.
	Dir string
	// Pair identifies what was tested: branch tip and target tip. A
	// verdict belongs to this pair and nothing else — the merge
	// check-in an accept later writes will hash differently.
	Pair string
	// Conflict is the other verdict: the merge does not apply. Files
	// names where, as zit reported them.
	Conflict bool
	Files    []string
}

// Pair is the canonical spelling of a premerge subject.
func Pair(branchTip, targetTip string) string {
	return "pair:" + branchTip + ":" + targetTip
}

// Prepare clones cloneURL into dir, opens targetTip, and merges
// branchTip into the working tree. It never commits: the caller runs
// its pipeline in Result.Dir and discards it.
func Prepare(cloneURL, branchTip, targetTip, dir string) (Result, error) {
	res := Result{Dir: dir, Pair: Pair(branchTip, targetTip)}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return res, err
	}
	if err := run(dir, "zit", "clone", cloneURL, "."); err != nil {
		return res, fmt.Errorf("premerge: clone: %w", err)
	}
	if err := run(dir, "zit", "open", targetTip); err != nil {
		// zit 0.2.0 opens the tree as part of clone; moving to the
		// target is then an update, not a second open.
		if uerr := run(dir, "zit", "update", targetTip); uerr != nil {
			return res, fmt.Errorf("premerge: open target: %w", err)
		}
	}
	// Belt to the credentials' braces: the clone saved its origin as
	// the default remote, and zit autosyncs on commit by default — a
	// pipeline that commits in this tree must strand that commit HERE,
	// not push it home. The integration test commits on purpose and
	// asserts the source ledger did not move.
	if err := run(dir, "zit", "settings", "autosync", "off"); err != nil {
		return res, fmt.Errorf("premerge: autosync off: %w", err)
	}
	out, err := runOut(dir, "zit", "merge", "--no-sync", branchTip)
	if err != nil {
		// A refused merge IS the verdict, not an error in the runner.
		res.Conflict = true
		for _, line := range strings.Split(out, "\n") {
			line = strings.TrimSpace(line)
			if strings.HasPrefix(line, "CONFLICT") || strings.HasSuffix(line, ": conflict") {
				res.Files = append(res.Files, line)
			}
		}
		if len(res.Files) == 0 && strings.TrimSpace(out) != "" {
			res.Files = []string{strings.TrimSpace(out)}
		}
		return res, nil
	}
	return res, nil
}

func run(dir string, name string, args ...string) error {
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	var buf bytes.Buffer
	cmd.Stdout, cmd.Stderr = &buf, &buf
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, strings.TrimSpace(buf.String()))
	}
	return nil
}

func runOut(dir string, name string, args ...string) (string, error) {
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	var buf bytes.Buffer
	cmd.Stdout, cmd.Stderr = &buf, &buf
	err := cmd.Run()
	return buf.String(), err
}