Skip to content
quarry Artifact [8edc300fdd]
Login

Artifact [8edc300fdd]

Artifact 8edc300fdded6ac031d9be563f108c2f61edd6c793ec2e613de6cf0cf29fedbc:


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

// A mode only the branch changed survives into the merged tree's
// archive: a pipeline that runs ./run.sh must find it executable.
func TestMergeArchiveKeepsTheBranchsExecuteBit(t *testing.T) {
	repo := t.TempDir()
	run(t, repo, "init", ".")
	writeZ(t, repo, "run.sh", "#!/bin/sh\necho ok\n")
	writeZ(t, repo, "f.txt", "a\n")
	run(t, repo, "add", "run.sh", "f.txt")
	run(t, repo, "commit", "-m", "root", "--user", "u")
	if err := os.Chmod(filepath.Join(repo, "run.sh"), 0o755); err != nil {
		t.Fatal(err)
	}
	run(t, repo, "commit", "-m", "exec", "--branch", "feat", "--user", "u")
	run(t, repo, "update", "trunk")
	writeZ(t, repo, "f.txt", "b\n")
	run(t, repo, "commit", "-m", "t", "--user", "u")

	dst := filepath.Join(t.TempDir(), "m.tar.gz")
	if err := (&Client{repo: repo}).MergeArchiveTo(context.Background(), "feat", "trunk", "r-pair", dst, 1<<20); err != nil {
		t.Fatal(err)
	}
	f, _ := os.Open(dst)
	defer f.Close()
	gz, err := gzip.NewReader(f)
	if err != nil {
		t.Fatal(err)
	}
	tr := tar.NewReader(gz)
	for {
		h, err := tr.Next()
		if err != nil {
			t.Fatal("FAIL: run.sh is not in the merged tree")
		}
		if strings.HasSuffix(h.Name, "/run.sh") {
			if h.Mode&0o111 == 0 {
				t.Fatalf("FAIL: run.sh is %o in the merged tree, want executable", h.Mode)
			}
			t.Log("ok: the branch's execute bit survives the merge")
			return
		}
	}
}