package zitcli
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"craftdesign.group/quarry/internal/lease"
)
// zit reports a conflicting merge region by region, and the report is
// the answer: exit 5 is a conflict, not a failure. A clean pair reports
// no files.
func TestMergeRegionsReportsAConflict(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
run := func(args ...string) string {
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)
}
write := func(body string) {
if err := os.WriteFile(filepath.Join(dir, "f.txt"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
tip := func(branch string) string {
h := regexp.MustCompile(`[0-9a-f]{64}`).FindString(run("branch", "info", branch))
if h == "" {
t.Fatalf("no tip for %s", branch)
}
return h
}
run("init", ".")
write("a\nb\nc\nd\ne\nf\ng\n")
run("add", "f.txt")
run("commit", "-m", "base", "--user", "u")
base := tip("trunk")
run("branch", "new", "feat")
write("a\nB-feat\nc\nd\ne\nf\ng\nadded-by-feat\n")
run("commit", "-m", "feat", "--user", "u")
run("update", "trunk")
write("A-trunk\nb-trunk\nc\nd\ne\nF-trunk\ng\n")
run("commit", "-m", "trunk", "--user", "u")
c := New(dir)
rep, err := c.MergeRegions(context.Background(), tip("feat"), tip("trunk"))
if err != nil {
t.Fatal(err)
}
if rep.Verdict != "conflict" || len(rep.Files) != 1 || rep.Files[0].Path != "f.txt" {
t.Fatalf("FAIL: report %+v", rep)
}
kinds := ""
for _, r := range rep.Files[0].Regions {
kinds += r.Kind + " "
}
if kinds != "conflict same current same other " {
t.Errorf("FAIL: regions %q", kinds)
}
if f := rep.Files[0]; f.Base == "" || f.Current == "" || f.Other == "" {
t.Errorf("FAIL: a side has no blob: %+v", f)
}
clean, err := c.MergeRegions(context.Background(), base, tip("trunk"))
if err != nil || len(clean.Files) != 0 {
t.Errorf("FAIL: a clean pair reported %+v (%v)", clean, err)
}
}
// A resolved merge lands on the branch as the merge of the target into
// it, with the answers' content; a tip that moved, or an answer still
// holding a conflict marker, is refused with nothing written.
func TestMergeCommitRecordsTheResolution(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
run := func(args ...string) string {
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)
}
write := func(body string) {
if err := os.WriteFile(filepath.Join(dir, "f.txt"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
tip := func(branch string) string {
return regexp.MustCompile(`[0-9a-f]{64}`).FindString(run("branch", "info", branch))
}
run("init", ".")
write("a\nb\nc\n")
run("add", "f.txt")
run("commit", "-m", "base", "--user", "u")
run("branch", "new", "feat")
write("a\nB-feat\nc\n")
run("commit", "-m", "feat", "--user", "u")
run("update", "trunk")
write("a\nB-trunk\nc\n")
run("commit", "-m", "trunk", "--user", "u")
feat, trunk := tip("feat"), tip("trunk")
c := New(dir)
tok := lease.Unguarded("test")
marked := map[string][]byte{"f.txt": []byte("a\n<<<<<<< feat\nB-feat\n=======\nB-trunk\n>>>>>>> trunk\nc\n")}
if _, err := c.MergeCommit(tok, "trunk", "feat", marked, nil, "merge", "dan", feat, trunk); err == nil {
t.Error("FAIL: an answer holding conflict markers was recorded")
}
if _, err := c.MergeCommit(tok, "trunk", "feat", map[string][]byte{"f.txt": []byte("a\nB\nc\n")}, nil, "merge", "dan", trunk, trunk); !errors.Is(err, MergeMoved) {
t.Errorf("FAIL: answers against the wrong branch tip gave %v, want MergeMoved", err)
}
if tip("feat") != feat {
t.Fatal("FAIL: a refused answer wrote to the branch")
}
if _, err := c.MergeCommit(tok, "trunk", "feat", map[string][]byte{"f.txt": []byte("a\nB-both\nc\n")}, nil, "merge trunk into feat", "dan", feat, trunk); err != nil {
t.Fatalf("FAIL: the resolution was refused: %v", err)
}
if tip("feat") == feat {
t.Fatal("FAIL: the branch did not move")
}
run("update", "feat")
if b, _ := os.ReadFile(filepath.Join(dir, "f.txt")); !strings.Contains(string(b), "B-both") {
t.Errorf("FAIL: the branch holds %q", b)
}
if rep, err := c.MergeRegions(context.Background(), tip("feat"), tip("trunk")); err != nil || len(rep.Files) != 0 {
t.Errorf("FAIL: the branch still conflicts with its target: %+v %v", rep, err)
}
}
// A resolved merge can be committed onto the target too, as `fossil
// merge` on the target would, and the source may be a bare check-in
// rather than a branch tip: only that check-in and its ancestors come.
func TestMergeCommitOntoTheTarget(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
run := func(args ...string) string {
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)
}
write := func(name, body string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
tip := func(branch string) string {
return regexp.MustCompile(`[0-9a-f]{64}`).FindString(run("branch", "info", branch))
}
run("init", ".")
write("f.txt", "a\nb\nc\n")
run("add", "f.txt")
run("commit", "-m", "base", "--user", "u")
run("branch", "new", "feat")
write("f.txt", "a\nB-feat\nc\n")
run("commit", "-m", "feat one", "--user", "u")
one := tip("feat")
write("h.txt", "h\n")
run("add", "h.txt")
run("commit", "-m", "feat two", "--user", "u")
two := tip("feat")
run("update", "trunk")
write("f.txt", "a\nB-trunk\nc\n")
run("commit", "-m", "trunk", "--user", "u")
trunk := tip("trunk")
c := New(dir)
tok := lease.Unguarded("test")
answer := map[string][]byte{"f.txt": []byte("a\nB-both\nc\n")}
if _, err := c.MergeCommit(tok, one, "trunk", answer, nil, "merge feat one into trunk", "dan", trunk, one); err != nil {
t.Fatalf("FAIL: the merge onto the target was refused: %v", err)
}
if tip("trunk") == trunk {
t.Fatal("FAIL: the target did not move")
}
if tip("feat") != two {
t.Fatal("FAIL: the branch moved")
}
run("update", "trunk")
if b, _ := os.ReadFile(filepath.Join(dir, "f.txt")); !strings.Contains(string(b), "B-both") {
t.Errorf("FAIL: the target holds %q", b)
}
if _, err := os.Stat(filepath.Join(dir, "h.txt")); err == nil {
t.Error("FAIL: a later check-in on the branch came along")
}
// The rest of the branch now merges cleanly: nothing to answer, and
// zit records it all the same (zit 0.3.41, zit#235).
merged := tip("trunk")
if _, err := c.MergeCommit(tok, two, "trunk", nil, nil, "merge feat into trunk", "dan", merged, two); err != nil {
t.Fatalf("FAIL: a clean merge onto the target was refused: %v", err)
}
if tip("trunk") == merged {
t.Fatal("FAIL: the target did not move on a clean merge")
}
run("update", "trunk")
if _, err := os.Stat(filepath.Join(dir, "h.txt")); err != nil {
t.Error("FAIL: the clean merge did not bring the branch's file")
}
}
// Two sides that changed different files merge with nothing to answer
// (zit 0.3.42, danmestas/zit#236; 0.3.41 could not scope the merge).
func TestMergeCommitOfDisjointChanges(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
run := func(args ...string) string {
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)
}
write := func(name, body string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
tip := func(branch string) string {
return regexp.MustCompile(`[0-9a-f]{64}`).FindString(run("branch", "info", branch))
}
run("init", ".")
write("f.txt", "a\n")
write("g.txt", "g\n")
run("add", "f.txt", "g.txt")
run("commit", "-m", "base", "--user", "u")
run("branch", "new", "feat")
write("g.txt", "g-feat\n")
run("commit", "-m", "feat", "--user", "u")
feat := tip("feat")
run("update", "trunk")
write("f.txt", "a-trunk\n")
run("commit", "-m", "trunk", "--user", "u")
trunk := tip("trunk")
c := New(dir)
if _, err := c.MergeCommit(lease.Unguarded("test"), feat, "trunk", nil, nil, "merge feat into trunk", "dan", trunk, feat); err != nil {
t.Fatalf("FAIL: a clean merge of disjoint changes was refused: %v", err)
}
if tip("trunk") == trunk {
t.Fatal("FAIL: the target did not move")
}
run("update", "trunk")
f, _ := os.ReadFile(filepath.Join(dir, "f.txt"))
g, _ := os.ReadFile(filepath.Join(dir, "g.txt"))
if string(f) != "a-trunk\n" || string(g) != "g-feat\n" {
t.Errorf("FAIL: the merge holds f=%q g=%q", f, g)
}
}