package zitcli
import (
"context"
"os"
"os/exec"
"path/filepath"
"slices"
"testing"
)
// A live fold takes a tree check-in's changed paths from ChangedFiles
// and a rebuild from crosslink, so the two must name the same paths for
// every kind of change: an add, an edit, a rename, a mode change, a
// whole directory removed, a binary file and an empty one, a file
// replaced by a directory, a merge (both diff against the primary
// parent) and the root (its whole file list). The store is a Git import
// with sub-manifests, so the check-ins are tree check-ins.
func TestChangedFilesAgreesWithCrosslink(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not on PATH")
}
tmp := t.TempDir()
g := filepath.Join(tmp, "git")
env := append(os.Environ(), "HOME="+tmp,
"GIT_AUTHOR_NAME=Ana", "[email protected]",
"GIT_COMMITTER_NAME=Ana", "[email protected]")
in := func(dir, name string, args ...string) {
t.Helper()
c := exec.Command(name, args...)
c.Dir, c.Env = dir, env
if out, err := c.CombinedOutput(); err != nil {
t.Fatalf("%s %v: %v\n%s", name, args, err, out)
}
}
write := func(path, body string) {
t.Helper()
full := filepath.Join(g, path)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
if err := os.MkdirAll(g, 0o755); err != nil {
t.Fatal(err)
}
in(g, "git", "init", "-q", "-b", "main")
for _, p := range []string{"a/1.txt", "a/2.txt", "b/3.txt", "b/4.txt", "c/5.txt", "c/6.txt", "c/7.txt", "d/8.txt"} {
write(p, p+"\n")
}
in(g, "git", "add", "-A")
in(g, "git", "commit", "-qm", "base")
in(g, "git", "mv", "a/1.txt", "d/moved.txt")
in(g, "git", "commit", "-qm", "rename")
in(g, "git", "rm", "-rq", "c")
in(g, "git", "commit", "-qm", "remove a directory")
if err := os.Chmod(filepath.Join(g, "b/3.txt"), 0o755); err != nil {
t.Fatal(err)
}
in(g, "git", "commit", "-qam", "mode")
write("b/bin.dat", "\x00\x01\x02")
write("b/empty.txt", "")
write("a/2.txt", "edited\n")
in(g, "git", "add", "-A")
in(g, "git", "commit", "-qm", "binary, empty, edit")
// A file replaced by a directory of the same name.
in(g, "git", "rm", "-q", "d/8.txt")
write("d/8.txt/inner.txt", "inner\n")
in(g, "git", "add", "-A")
in(g, "git", "commit", "-qm", "file to directory")
// A merge: a side branch and trunk each change a file.
in(g, "git", "checkout", "-qb", "side")
write("b/side.txt", "side\n")
in(g, "git", "add", "-A")
in(g, "git", "commit", "-qm", "side work")
in(g, "git", "checkout", "-q", "main")
write("a/2.txt", "edited on main\n")
in(g, "git", "commit", "-qam", "main work")
in(g, "git", "merge", "-q", "--no-ff", "-m", "merge side", "side")
store := filepath.Join(tmp, "store")
in(tmp, "zit", "import-git", g, store, "--submanifest-k", "1", "--quiet")
z := New(store)
ctx := context.Background()
checked, roots, merges := 0, 0, 0
if err := z.Crosslink(ctx, func(r CrosslinkRecord) error {
if !r.FilesKnown {
t.Errorf("FAIL: crosslink knows no files for %.12s (%q)", r.Hash, r.Comment)
return nil
}
var got []string
var err error
if len(r.Parents) == 0 {
// A root's changes are its files: the live fold lists them.
roots++
var entries []LsEntry
if entries, err = z.LsNames(ctx, r.Hash); err == nil {
for _, e := range entries {
got = append(got, e.Name)
}
slices.Sort(got)
}
} else {
if len(r.Parents) > 1 {
merges++
}
got, err = z.ChangedFiles(ctx, r.Hash)
}
if err != nil {
t.Errorf("FAIL: changed files of %.12s: %v", r.Hash, err)
return nil
}
want := slices.Sorted(slices.Values(r.Changes))
if !slices.Equal(got, want) {
t.Errorf("FAIL: %.12s (%q) changed %q, crosslink says %q", r.Hash, r.Comment, got, want)
}
checked++
return nil
}); err != nil {
t.Fatal(err)
}
if checked != 9 || roots != 1 || merges != 1 {
t.Errorf("FAIL: compared %d check-ins (%d roots, %d merges), want 9 (1, 1)", checked, roots, merges)
}
if !t.Failed() {
t.Log("ok")
}
}