Skip to content
quarry Artifact [38fd5c117b]
Login

Artifact [38fd5c117b]

Artifact 38fd5c117b58d272e819a9ea245160a98426fe4237e7228aa2e45b1d3e461330:


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