package gitx
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// The fixture builds a bare repository at <tmp>/~alice/demo with a history rich
// enough to exercise every command:
//
// root ── c2 ── rename ── binary ─┐
// \ merge (branch: main, HEAD)
// feature ─────────────┘ (branch: feature/with-slash)
//
// root : add a.txt (root commit)
// c2 : add b.txt, edit a.txt
// rename : git mv a.txt -> a-renamed.txt (tag: v0.9.0)
// binary : add bin.dat (4 NUL-ish bytes) (main^1)
// feature : add feature.txt (from c2) (branch feature/with-slash, main^2)
// merge : merge feature into main (main, HEAD; tag: v1.0.0)
//
// The commit dates increase monotonically so tag creator-date ordering is
// deterministic (v1.0.0 newer than v0.9.0).
const (
fxOwner = "alice"
fxName = "demo"
)
// gitTest runs git in dir with a hardened, deterministic environment, failing
// the test on error. It takes a testing.TB rather than a *testing.T because
// bench_test.go builds its own fixture repository with the same helper.
func gitTest(t testing.TB, dir string, date string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(),
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_CONFIG_SYSTEM=/dev/null",
"GIT_TERMINAL_PROMPT=0",
"LC_ALL=C",
"GIT_AUTHOR_NAME=Alice Example",
"GIT_AUTHOR_EMAIL=alice@example.com",
"GIT_COMMITTER_NAME=Alice Example",
"GIT_COMMITTER_EMAIL=alice@example.com",
"GIT_AUTHOR_DATE="+date,
"GIT_COMMITTER_DATE="+date,
)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
return string(out)
}
func writeFile(t testing.TB, dir, name string, data []byte) {
t.Helper()
p := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, data, 0o644); err != nil {
t.Fatal(err)
}
}
// newFixtureRepo builds the bare fixture repository and returns reposRoot (the
// directory that Open takes). The bare repo lives at reposRoot/~alice/demo.
func newFixtureRepo(t *testing.T) string {
t.Helper()
if _, err := exec.LookPath("git"); err != nil {
t.Skipf("git not available: %v", err)
}
reposRoot := t.TempDir()
work := t.TempDir()
d1 := "2024-01-01T00:00:00Z"
d2 := "2024-01-02T00:00:00Z"
d3 := "2024-01-03T00:00:00Z"
d4 := "2024-01-04T00:00:00Z"
d5 := "2024-01-05T00:00:00Z"
d6 := "2024-01-06T00:00:00Z"
gitTest(t, work, d1, "init", "-b", "main")
// root: add a.txt
writeFile(t, work, "a.txt", []byte("hello\nworld\n"))
gitTest(t, work, d1, "add", "a.txt")
gitTest(t, work, d1, "commit", "-m", "root: add a.txt")
// c2: add b.txt, edit a.txt
writeFile(t, work, "a.txt", []byte("hello\nworld\nmore\n"))
writeFile(t, work, "b.txt", []byte("bee\n"))
gitTest(t, work, d2, "add", "a.txt", "b.txt")
gitTest(t, work, d2, "commit", "-m", "c2: add b, edit a")
// feature branch from c2 (before rename/binary): add feature.txt
gitTest(t, work, d5, "branch", "feature/with-slash")
// rename: pure git mv a.txt -> a-renamed.txt
gitTest(t, work, d3, "mv", "a.txt", "a-renamed.txt")
gitTest(t, work, d3, "commit", "-m", "rename: a.txt -> a-renamed.txt")
gitTest(t, work, d3, "tag", "-a", "-m", "release 0.9.0", "v0.9.0")
// binary: add bin.dat (4 bytes, includes NUL)
writeFile(t, work, "bin.dat", []byte{0x00, 0x01, 0x02, 0x03})
gitTest(t, work, d4, "add", "bin.dat")
gitTest(t, work, d4, "commit", "-m", "binary: add bin.dat")
// build feature tip
gitTest(t, work, d5, "checkout", "feature/with-slash")
writeFile(t, work, "feature.txt", []byte("feature\n"))
gitTest(t, work, d5, "add", "feature.txt")
gitTest(t, work, d5, "commit", "-m", "feature: add feature.txt")
// merge feature into main (no-ff so a merge commit is created)
gitTest(t, work, d6, "checkout", "main")
gitTest(t, work, d6, "merge", "--no-ff", "-m", "merge feature/with-slash", "feature/with-slash")
gitTest(t, work, d6, "tag", "-a", "-m", "release 1.0.0", "v1.0.0")
// bare clone into reposRoot/~alice/demo
if err := os.MkdirAll(filepath.Join(reposRoot, "~"+fxOwner), 0o755); err != nil {
t.Fatal(err)
}
bare := filepath.Join(reposRoot, "~"+fxOwner, fxName)
gitTest(t, work, d6, "clone", "--bare", work, bare)
return reposRoot
}
// fxRev resolves a revision inside the bare fixture repo to its full SHA. It is
// a test helper for landmark navigation (e.g. "main^1~1" for the rename
// commit); it is independent of the code under test.
func fxRev(t *testing.T, reposRoot, rev string) string {
t.Helper()
return fxRevIn(t, reposRoot, fxName, rev)
}
// openFixture opens the fixture repo through the code under test.
func openFixture(t *testing.T, reposRoot string) *Repo {
t.Helper()
repo, err := Open(reposRoot, fxOwner, fxName)
if err != nil {
t.Fatalf("Open: %v", err)
}
return repo
}