package zitcli
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
)
// untarZ reads a tarball into path -> contents, dropping the top
// directory.
func untarZ(t *testing.T, path string) map[string]string {
t.Helper()
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
t.Fatal(err)
}
out := map[string]string{}
tr := tar.NewReader(gz)
for {
h, err := tr.Next()
if errors.Is(err, io.EOF) {
return out
}
if err != nil {
t.Fatal(err)
}
_, rel, _ := strings.Cut(h.Name, "/")
if h.Typeflag != tar.TypeReg {
continue
}
b, _ := io.ReadAll(tr)
out[rel] = string(b)
}
}
// THE MERGED TREE IS WHAT AN ACCEPT WOULD WRITE, AND ASKING WRITES
// NOTHING. A pair that merges cleanly packs the three-way result, not
// either side; a conflicting pair answers the paths instead of a tree;
// and the store is byte-identical after both.
func TestMergeArchiveIsTheMergeAndWritesNothing(t *testing.T) {
repo := t.TempDir()
run(t, repo, "init", ".")
writeZ(t, repo, "f.txt", "top\nMID\nbottom\n")
writeZ(t, repo, "g.txt", "base\n")
run(t, repo, "add", "f.txt", "g.txt")
run(t, repo, "commit", "-m", "root", "--user", "u")
writeZ(t, repo, "f.txt", "top\nMID\nBOTTOM\n")
writeZ(t, repo, "new.txt", "from the branch\n")
run(t, repo, "add", "new.txt")
run(t, repo, "commit", "-m", "b", "--branch", "feat/ok", "--user", "u")
run(t, repo, "update", "trunk")
writeZ(t, repo, "g.txt", "THEIRS\n")
run(t, repo, "commit", "-m", "b2", "--branch", "feat/bad", "--user", "u")
run(t, repo, "update", "trunk")
writeZ(t, repo, "f.txt", "TOP\nMID\nbottom\n")
writeZ(t, repo, "g.txt", "OURS\n")
run(t, repo, "commit", "-m", "t", "--user", "u")
before, err := os.ReadFile(filepath.Join(repo, ".zit"))
if err != nil {
t.Fatal(err)
}
c := &Client{repo: repo}
dst := filepath.Join(t.TempDir(), "m.tar.gz")
if err := c.MergeArchiveTo(context.Background(), "feat/ok", "trunk", "r-pair", dst, 1<<20); err != nil {
t.Fatalf("FAIL: a clean pair did not pack: %v", err)
}
tree := untarZ(t, dst)
if tree["f.txt"] != "TOP\nMID\nBOTTOM\n" {
t.Errorf("FAIL: f.txt = %q, want both sides' edits merged", tree["f.txt"])
}
if tree["new.txt"] != "from the branch\n" || tree["g.txt"] != "OURS\n" {
t.Errorf("FAIL: tree = %v, want the branch's new file and trunk's g.txt", tree)
}
for name := range tree {
if strings.HasPrefix(name, ".zit") && name != ".zitignore" {
t.Errorf("FAIL: checkout state %q is in the tree", name)
}
}
var mc *MergeConflict
err = c.MergeArchiveTo(context.Background(), "feat/bad", "trunk", "r-pair", filepath.Join(t.TempDir(), "x.tar.gz"), 1<<20)
if !errors.As(err, &mc) || len(mc.Paths) == 0 || mc.Paths[0] != "g.txt" {
t.Fatalf("FAIL: a conflicting pair answered %v, want a MergeConflict naming g.txt", err)
}
after, err := os.ReadFile(filepath.Join(repo, ".zit"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(before, after) {
t.Fatalf("FAIL: the store changed (%d -> %d bytes); a merged tree must only read it", len(before), len(after))
}
t.Log("ok: the clean pair packs the three-way merge, the conflict names its file, the store is unchanged")
}