Skip to content
quarry Artifact [14f5d943b1]
Login

Artifact [14f5d943b1]

Artifact 14f5d943b176babdc12879f767d12909666ea5e45d97871e427caeba115e7403:


package premerge

import (
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"
)

// serveStore serves a checkout's repository over HTTP — zit clone
// speaks only http(s) — and returns the sync URL.
func serveStore(t *testing.T, dir string) string {
	t.Helper()
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	port := l.Addr().(*net.TCPAddr).Port
	l.Close()
	cmd := exec.Command("zit", "serve", "--port", strconv.Itoa(port))
	cmd.Dir = dir
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { cmd.Process.Kill(); cmd.Wait() })
	url := "http://127.0.0.1:" + strconv.Itoa(port) + "/xfer"
	for i := 0; i < 50; i++ {
		c, derr := net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(port), 100*time.Millisecond)
		if derr == nil {
			c.Close()
			return url
		}
		time.Sleep(50 * time.Millisecond)
	}
	t.Fatal("zit serve did not come up")
	return url
}

func zrun(t *testing.T, dir string, args ...string) string {
	t.Helper()
	cmd := exec.Command("zit", args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("zit %v: %v\n%s", args, err, out)
	}
	return string(out)
}

func artifactCount(t *testing.T, checkout string) string {
	t.Helper()
	return zrun(t, checkout, "stat")
}

// RFC 0009 §premerge, the whole contract in one test: the prepared
// tree is the merge, a conflicted pair says so instead of erroring,
// and the source store's ledger is byte-identical afterwards — even
// though nothing here ever held credentials at all.
func TestPrepareMergesWithoutTouchingTheLedger(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	src := t.TempDir()
	zrun(t, src, "init")
	os.WriteFile(filepath.Join(src, "a.txt"), []byte("base\n"), 0o644)
	zrun(t, src, "add", "a.txt")
	zrun(t, src, "commit", "-m", "base")
	// Branch: edits b.txt. Trunk: edits a.txt. Merge applies cleanly.
	os.WriteFile(filepath.Join(src, "b.txt"), []byte("branch work\n"), 0o644)
	zrun(t, src, "add", "b.txt")
	zrun(t, src, "commit", "-m", "branch work", "--branch", "feature")
	branchTip := strings.Fields(zrun(t, src, "info", "feature"))[0]
	zrun(t, src, "update", "trunk")
	os.WriteFile(filepath.Join(src, "a.txt"), []byte("trunk moved\n"), 0o644)
	zrun(t, src, "commit", "-m", "trunk moved")
	targetTip := strings.Fields(zrun(t, src, "info", "trunk"))[0]

	before := artifactCount(t, src)
	res, err := Prepare(serveStore(t, src), branchTip, targetTip, t.TempDir())
	if err != nil {
		t.Fatalf("prepare: %v", err)
	}
	if res.Conflict {
		t.Fatalf("clean pair read as conflict: %v", res.Files)
	}
	// The tree is the merge: both sides present.
	a, _ := os.ReadFile(filepath.Join(res.Dir, "a.txt"))
	b, _ := os.ReadFile(filepath.Join(res.Dir, "b.txt"))
	if string(a) != "trunk moved\n" || string(b) != "branch work\n" {
		t.Fatalf("not the merged tree: a=%q b=%q", a, b)
	}
	if res.Pair != "pair:"+branchTip+":"+targetTip {
		t.Fatalf("pair spelling: %q", res.Pair)
	}
	// A pipeline that tries to commit in the prepared tree changes the
	// CLONE, never the source ledger.
	os.WriteFile(filepath.Join(res.Dir, "evil.txt"), []byte("x\n"), 0o644)
	ec := exec.Command("zit", "add", "evil.txt")
	ec.Dir = res.Dir
	_ = ec.Run()
	cc := exec.Command("zit", "commit", "-m", "evil")
	cc.Dir = res.Dir
	_ = cc.Run()
	if after := artifactCount(t, src); after != before {
		t.Fatalf("the source ledger moved:\nbefore: %s\nafter: %s", before, after)
	}
}

func TestPrepareReportsAConflict(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	src := t.TempDir()
	zrun(t, src, "init")
	os.WriteFile(filepath.Join(src, "a.txt"), []byte("base\n"), 0o644)
	zrun(t, src, "add", "a.txt")
	zrun(t, src, "commit", "-m", "base")
	os.WriteFile(filepath.Join(src, "a.txt"), []byte("branch says\n"), 0o644)
	zrun(t, src, "commit", "-m", "branch side", "--branch", "feature")
	branchTip := strings.Fields(zrun(t, src, "info", "feature"))[0]
	zrun(t, src, "update", "trunk")
	os.WriteFile(filepath.Join(src, "a.txt"), []byte("trunk says\n"), 0o644)
	zrun(t, src, "commit", "-m", "trunk side")
	targetTip := strings.Fields(zrun(t, src, "info", "trunk"))[0]
	res, err := Prepare(serveStore(t, src), branchTip, targetTip, t.TempDir())
	if err != nil {
		t.Fatalf("prepare: %v", err)
	}
	if !res.Conflict {
		t.Fatal("conflicting pair read as clean")
	}
}