Skip to content
quarry Artifact [5b28c3d918]
Login

Artifact [5b28c3d918]

Artifact 5b28c3d918aa18f9a9b1d5a2a30915da948d8a3a215953de5af5edb3c73343f9:


package zitcli

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

// hashOf is the full hash a name resolves to in repo.
func hashOf(t *testing.T, repo, name string) string {
	t.Helper()
	cmd := exec.Command("zit", "info", name)
	cmd.Dir = repo
	out, err := cmd.Output()
	if err != nil {
		t.Fatalf("zit info %s: %v", name, err)
	}
	first, _, _ := strings.Cut(string(out), "\n")
	return strings.TrimSpace(first)
}

// A MERGE CHECK-IN HAS ITS PAIR'S TREE UNLESS SOMEBODY CHANGED IT.
//
// Equal means every path and its content hash: the merge zit writes
// from the two tips is its pair's tree, and the same merge with one
// more edit, or a check-in that is no merge of them, is not.
func TestSameTreeIsThePairsTree(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")
	if err := os.MkdirAll(filepath.Join(repo, "d"), 0o755); err != nil {
		t.Fatal(err)
	}
	writeZ(t, repo, "d/h.txt", "deep\n")
	run(t, repo, "add", "f.txt", "g.txt", "d/h.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", "--user", "u")
	branch := hashOf(t, repo, "feat")
	run(t, repo, "update", "trunk")
	writeZ(t, repo, "f.txt", "TOP\nMID\nbottom\n")
	run(t, repo, "commit", "-m", "t", "--user", "u")
	target := hashOf(t, repo, "trunk")

	run(t, repo, "merge", "feat")
	run(t, repo, "commit", "-m", "land feat", "--user", "u")
	merged := hashOf(t, repo, "trunk")

	c := New(repo)
	ctx := context.Background()
	if same, err := c.SameTree(ctx, merged, branch, target); err != nil || !same {
		t.Fatalf("FAIL: the merge of feat into trunk answered same=%v err=%v; want its pair's tree", same, err)
	}
	if same, err := c.SameTree(ctx, target, branch, target); err != nil || same {
		t.Fatalf("FAIL: trunk before the merge answered same=%v err=%v; want a different tree", same, err)
	}

	run(t, repo, "update", target)
	run(t, repo, "merge", "feat")
	writeZ(t, repo, "g.txt", "edited in the merge\n")
	run(t, repo, "commit", "-m", "land feat, edited", "--branch", "edited", "--user", "u")
	edited := hashOf(t, repo, "edited")
	if same, err := c.SameTree(ctx, edited, branch, target); err != nil || same {
		t.Fatalf("FAIL: a merge with an edit answered same=%v err=%v; want a different tree", same, err)
	}
	run(t, repo, "update", merged)
	if err := os.Chmod(filepath.Join(repo, "g.txt"), 0o755); err != nil {
		t.Fatal(err)
	}
	run(t, repo, "commit", "-m", "g is a script now", "--branch", "modes", "--user", "u")
	modes := hashOf(t, repo, "modes")
	if same, err := c.SameTree(ctx, modes, branch, target); err != nil || same {
		t.Fatalf("FAIL: the merge's files with g.txt made executable answered same=%v err=%v; want a different tree", same, err)
	}
	t.Log("ok: the merge is its pair's tree; an edit, a mode, or another check-in is not")
}

// A TREE MATCHES BY DISTINCT PATHS: a path the tar repeats is counted
// once, so a repeated path cannot stand in for a missing one.
func TestTreeMatchesCountsDistinctPaths(t *testing.T) {
	var buf bytes.Buffer
	gz := gzip.NewWriter(&buf)
	tw := tar.NewWriter(gz)
	for range 2 {
		tw.WriteHeader(&tar.Header{Name: "a.txt", Typeflag: tar.TypeReg, Mode: 0o644, Size: 2})
		tw.Write([]byte("a\n"))
	}
	tw.Close()
	gz.Close()
	want := map[string]treeEntry{
		"a.txt": {blob: blobHash([]byte("a\n"), 64)},
		"b.txt": {blob: blobHash([]byte("b\n"), 64)},
	}
	if same, err := treeMatches(&buf, want); err != nil || same {
		t.Fatalf("FAIL: a tar holding a.txt twice and no b.txt answered same=%v err=%v", same, err)
	}
	t.Log("ok: a repeated path does not stand in for a missing one")
}

// THE MERGED TREE'S FILES ARE THE MERGE'S, NOT EITHER SIDE'S.
//
// A file the target changed after the branch forked reads as the target
// changed it, a file the branch added is there, and only the paths
// asked for come back. A pair that does not merge has no files.
func TestMergedFilesAreTheMerge(t *testing.T) {
	repo := t.TempDir()
	run(t, repo, "init", ".")
	if err := os.MkdirAll(filepath.Join(repo, "ci"), 0o755); err != nil {
		t.Fatal(err)
	}
	writeZ(t, repo, "ci/test.yml", "old\n")
	writeZ(t, repo, "f.txt", "root\n")
	run(t, repo, "add", "ci/test.yml", "f.txt")
	run(t, repo, "commit", "-m", "root", "--user", "u")
	writeZ(t, repo, "ci/branch.yml", "branch\n")
	run(t, repo, "add", "ci/branch.yml")
	run(t, repo, "commit", "-m", "b", "--branch", "feat", "--user", "u")
	branch := hashOf(t, repo, "feat")
	run(t, repo, "update", "trunk")
	writeZ(t, repo, "ci/test.yml", "new\n")
	writeZ(t, repo, "f.txt", "trunk\n")
	run(t, repo, "commit", "-m", "t", "--user", "u")
	target := hashOf(t, repo, "trunk")

	c := New(repo)
	got, err := c.MergedFiles(context.Background(), branch, target, func(p string) bool { return strings.HasPrefix(p, "ci/") })
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 2 || string(got["ci/test.yml"]) != "new\n" || string(got["ci/branch.yml"]) != "branch\n" {
		t.Fatalf("FAIL: the merged tree's ci/ files are %q; want trunk's test.yml and the branch's branch.yml", got)
	}

	run(t, repo, "update", branch)
	writeZ(t, repo, "f.txt", "branch\n")
	run(t, repo, "commit", "-m", "clash", "--user", "u")
	clash := hashOf(t, repo, "feat")
	var mc *MergeConflict
	if _, err := c.MergedFiles(context.Background(), clash, target, func(string) bool { return true }); !errors.As(err, &mc) {
		t.Fatalf("FAIL: a conflicting pair answered %v; want a MergeConflict", err)
	}
	t.Log("ok: the merged tree's files, filtered; a conflict has none")
}