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