shithub: furgit

Download patch

ref: f762271dbfc121eaac7eb59c0beb01620a09118f
parent: 8e320c9ca634e6b2431f9442b7d5191864735ae4
author: Runxi Yu <me@runxiyu.org>
date: Fri Jan 30 12:10:01 EST 2026

test: Make gitCmd accept an stdin []byte

--- a/commitgraph_read_test.go
+++ b/commitgraph_read_test.go
@@ -28,12 +28,12 @@
 		if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
 			t.Fatalf("write %s: %v", name, err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("commit %d", i))
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("commit %d", i))
 	}
 
-	gitCmd(t, repoPath, "repack", "-a", "-d")
-	gitCmd(t, repoPath, "commit-graph", "write", "--reachable", "--changed-paths")
+	gitCmd(t, repoPath, nil, "repack", "-a", "-d")
+	gitCmd(t, repoPath, nil, "commit-graph", "write", "--reachable", "--changed-paths")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -46,7 +46,7 @@
 		t.Fatalf("CommitGraph failed: %v", err)
 	}
 
-	revList := gitCmd(t, repoPath, "rev-list", "--all")
+	revList := gitCmd(t, repoPath, nil, "rev-list", "--all")
 	commits := strings.Fields(revList)
 	if len(commits) == 0 {
 		t.Fatal("no commits found")
@@ -67,7 +67,7 @@
 			t.Fatalf("CommitAt failed: %v", err)
 		}
 
-		treeStr := string(gitCmd(t, repoPath, "show", "-s", "--format=%T", oidStr))
+		treeStr := string(gitCmd(t, repoPath, nil, "show", "-s", "--format=%T", oidStr))
 		tree, err := repo.ParseHash(treeStr)
 		if err != nil {
 			t.Fatalf("ParseHash tree failed: %v", err)
@@ -76,7 +76,7 @@
 			t.Fatalf("tree mismatch for %s: got %s want %s", oidStr, cc.Tree.String(), tree.String())
 		}
 
-		parentStr := string(gitCmd(t, repoPath, "show", "-s", "--format=%P", oidStr))
+		parentStr := string(gitCmd(t, repoPath, nil, "show", "-s", "--format=%P", oidStr))
 		parents := strings.Fields(parentStr)
 		if len(parents) != len(cc.Parents) {
 			t.Fatalf("parent count mismatch for %s: got %d want %d", oidStr, len(cc.Parents), len(parents))
@@ -95,7 +95,7 @@
 			}
 		}
 
-		ctStr := gitCmd(t, repoPath, "show", "-s", "--format=%ct", oidStr)
+		ctStr := gitCmd(t, repoPath, nil, "show", "-s", "--format=%ct", oidStr)
 		ct, err := strconv.ParseInt(ctStr, 10, 64)
 		if err != nil {
 			t.Fatalf("parse commit time: %v", err)
@@ -104,7 +104,7 @@
 			t.Fatalf("commit time mismatch for %s: got %d want %d", oidStr, cc.CommitTime, ct)
 		}
 
-		changed := gitCmd(t, repoPath, "diff-tree", "--no-commit-id", "--name-only", "-r", "--root", oidStr)
+		changed := gitCmd(t, repoPath, nil, "diff-tree", "--no-commit-id", "--name-only", "-r", "--root", oidStr)
 		for _, path := range strings.Fields(changed) {
 			if path == "" {
 				continue
@@ -126,42 +126,42 @@
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
 
-	gitCmd(t, workDir, "init")
+	gitCmd(t, workDir, nil, "init")
 
 	if err := os.WriteFile(filepath.Join(workDir, "base.txt"), []byte("base\n"), 0o644); err != nil {
 		t.Fatalf("write base: %v", err)
 	}
-	gitCmd(t, workDir, "add", ".")
-	gitCmd(t, workDir, "commit", "-m", "base")
+	gitCmd(t, workDir, nil, "add", ".")
+	gitCmd(t, workDir, nil, "commit", "-m", "base")
 
-	gitCmd(t, workDir, "checkout", "-b", "branch1")
+	gitCmd(t, workDir, nil, "checkout", "-b", "branch1")
 	if err := os.WriteFile(filepath.Join(workDir, "b1.txt"), []byte("b1\n"), 0o644); err != nil {
 		t.Fatalf("write b1: %v", err)
 	}
-	gitCmd(t, workDir, "add", ".")
-	gitCmd(t, workDir, "commit", "-m", "b1")
+	gitCmd(t, workDir, nil, "add", ".")
+	gitCmd(t, workDir, nil, "commit", "-m", "b1")
 
-	gitCmd(t, workDir, "checkout", "master")
-	gitCmd(t, workDir, "checkout", "-b", "branch2")
+	gitCmd(t, workDir, nil, "checkout", "master")
+	gitCmd(t, workDir, nil, "checkout", "-b", "branch2")
 	if err := os.WriteFile(filepath.Join(workDir, "b2.txt"), []byte("b2\n"), 0o644); err != nil {
 		t.Fatalf("write b2: %v", err)
 	}
-	gitCmd(t, workDir, "add", ".")
-	gitCmd(t, workDir, "commit", "-m", "b2")
+	gitCmd(t, workDir, nil, "add", ".")
+	gitCmd(t, workDir, nil, "commit", "-m", "b2")
 
-	gitCmd(t, workDir, "checkout", "master")
-	gitCmd(t, workDir, "checkout", "-b", "branch3")
+	gitCmd(t, workDir, nil, "checkout", "master")
+	gitCmd(t, workDir, nil, "checkout", "-b", "branch3")
 	if err := os.WriteFile(filepath.Join(workDir, "b3.txt"), []byte("b3\n"), 0o644); err != nil {
 		t.Fatalf("write b3: %v", err)
 	}
-	gitCmd(t, workDir, "add", ".")
-	gitCmd(t, workDir, "commit", "-m", "b3")
+	gitCmd(t, workDir, nil, "add", ".")
+	gitCmd(t, workDir, nil, "commit", "-m", "b3")
 
-	gitCmd(t, workDir, "checkout", "master")
-	gitCmd(t, workDir, "merge", "--no-ff", "-m", "octopus", "branch1", "branch2", "branch3")
+	gitCmd(t, workDir, nil, "checkout", "master")
+	gitCmd(t, workDir, nil, "merge", "--no-ff", "-m", "octopus", "branch1", "branch2", "branch3")
 
-	gitCmd(t, workDir, "repack", "-a", "-d")
-	gitCmd(t, workDir, "commit-graph", "write", "--reachable")
+	gitCmd(t, workDir, nil, "repack", "-a", "-d")
+	gitCmd(t, workDir, nil, "commit-graph", "write", "--reachable")
 
 	repo, err := OpenRepository(filepath.Join(workDir, ".git"))
 	if err != nil {
@@ -174,7 +174,7 @@
 		t.Fatalf("CommitGraph failed: %v", err)
 	}
 
-	mergeHash := gitCmd(t, workDir, "rev-parse", "HEAD")
+	mergeHash := gitCmd(t, workDir, nil, "rev-parse", "HEAD")
 	id, _ := repo.ParseHash(mergeHash)
 	pos, ok := cg.CommitPosition(id)
 	if !ok {
@@ -184,7 +184,7 @@
 	if err != nil {
 		t.Fatalf("CommitAt failed: %v", err)
 	}
-	parentStr := gitCmd(t, workDir, "show", "-s", "--format=%P", mergeHash)
+	parentStr := gitCmd(t, workDir, nil, "show", "-s", "--format=%P", mergeHash)
 	parents := strings.Fields(parentStr)
 	if len(cc.Parents) != len(parents) {
 		t.Fatalf("octopus parent mismatch: got %d want %d", len(cc.Parents), len(parents))
@@ -203,12 +203,12 @@
 		if err := os.WriteFile(path, []byte(fmt.Sprintf("v%d\n", i)), 0o644); err != nil {
 			t.Fatalf("write %s: %v", path, err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("commit %d", i))
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("commit %d", i))
 	}
 
-	gitCmd(t, repoPath, "repack", "-a", "-d")
-	gitCmd(t, repoPath, "commit-graph", "write", "--reachable", "--changed-paths", "--split=no-merge")
+	gitCmd(t, repoPath, nil, "repack", "-a", "-d")
+	gitCmd(t, repoPath, nil, "commit-graph", "write", "--reachable", "--changed-paths", "--split=no-merge")
 
 	// more commits needed to get a split chain with base layer
 	for i := 5; i < 8; i++ {
@@ -216,11 +216,11 @@
 		if err := os.WriteFile(path, []byte(fmt.Sprintf("v%d\n", i)), 0o644); err != nil {
 			t.Fatalf("write %s: %v", path, err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("commit %d", i))
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("commit %d", i))
 	}
-	gitCmd(t, repoPath, "repack", "-a", "-d")
-	gitCmd(t, repoPath, "commit-graph", "write", "--reachable", "--changed-paths", "--split=no-merge", "--append")
+	gitCmd(t, repoPath, nil, "repack", "-a", "-d")
+	gitCmd(t, repoPath, nil, "commit-graph", "write", "--reachable", "--changed-paths", "--split=no-merge", "--append")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -242,7 +242,7 @@
 	if cg.numCommitsInBase == 0 {
 		t.Fatalf("expected non-zero base commit count")
 	}
-	revList := gitCmd(t, repoPath, "rev-list", "--max-parents=0", "HEAD")
+	revList := gitCmd(t, repoPath, nil, "rev-list", "--max-parents=0", "HEAD")
 	id, _ := repo.ParseHash(revList)
 	if _, ok := cg.CommitPosition(id); !ok {
 		t.Fatalf("base commit not found in commit-graph chain")
--- a/difftrees_test.go
+++ b/difftrees_test.go
@@ -22,26 +22,26 @@
 	writeTestFile(t, filepath.Join(workDir, "treeB", "legacy.txt"), "legacy root\n")
 	writeTestFile(t, filepath.Join(workDir, "treeB", "sub", "retired.txt"), "retired\n")
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	baseTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	baseTreeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	writeTestFile(t, filepath.Join(workDir, "README.md"), "updated readme\n")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-f", "dir/file_a.txt")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "rm", "-f", "dir/file_a.txt")
 	writeTestFile(t, filepath.Join(workDir, "dir", "nested", "file_b.txt"), "beta v2\n")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-f", "dir/nested/deeper/old.txt")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "rm", "-f", "dir/nested/deeper/old.txt")
 	writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "new.txt"), "new branch entry\n")
 	writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "branch", "info.md"), "branch info\n")
 	writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "branch", "subbranch", "leaf.txt"), "leaf data\n")
 	writeTestFile(t, filepath.Join(workDir, "dir", "nested", "deeper", "branch", "subbranch", "deep", "final.txt"), "final artifact\n")
 	writeTestFile(t, filepath.Join(workDir, "dir", "newchild.txt"), "brand new sibling\n")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-r", "-f", "treeB")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "rm", "-r", "-f", "treeB")
 	writeTestFile(t, filepath.Join(workDir, "features", "alpha", "README.md"), "alpha docs\n")
 	writeTestFile(t, filepath.Join(workDir, "features", "alpha", "beta", "gamma.txt"), "gamma payload\n")
 	writeTestFile(t, filepath.Join(workDir, "modules", "v2", "core", "main.go"), "package core\n")
 	writeTestFile(t, filepath.Join(workDir, "root_addition.txt"), "root level file\n")
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	updatedTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	updatedTreeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -108,16 +108,16 @@
 	writeTestFile(t, filepath.Join(workDir, "old_dir", "sub1", "legacy.txt"), "legacy path\n")
 	writeTestFile(t, filepath.Join(workDir, "old_dir", "sub1", "nested", "end.txt"), "legacy end\n")
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	originalTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	originalTreeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "rm", "-r", "-f", "old_dir")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "rm", "-r", "-f", "old_dir")
 	writeTestFile(t, filepath.Join(workDir, "fresh", "alpha", "beta", "new.txt"), "brand new directory\n")
 	writeTestFile(t, filepath.Join(workDir, "fresh", "alpha", "docs", "note.md"), "docs note\n")
 	writeTestFile(t, filepath.Join(workDir, "fresh", "alpha", "beta", "gamma", "delta.txt"), "delta payload\n")
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	nextTreeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	nextTreeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
--- a/hybrid_test.go
+++ b/hybrid_test.go
@@ -29,8 +29,8 @@
 		t.Fatalf("failed to create deep.txt: %v", err)
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -83,8 +83,8 @@
 		t.Fatalf("failed to create symlink: %v", err)
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -130,9 +130,9 @@
 		if err != nil {
 			t.Fatalf("failed to create %s: %v", filename, err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("Commit %d", i))
-		commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("Commit %d", i))
+		commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 		commits = append(commits, commitHash)
 	}
 
@@ -185,13 +185,13 @@
 	if err != nil {
 		t.Fatalf("failed to create file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Tagged commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Tagged commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	tags := []string{"v1.0.0", "v1.0.1", "v1.1.0", "v2.0.0"}
 	for _, tagName := range tags {
-		gitCmd(t, repoPath, "tag", "-a", "-m", fmt.Sprintf("Release %s", tagName), tagName, commitHash)
+		gitCmd(t, repoPath, nil, "tag", "-a", "-m", fmt.Sprintf("Release %s", tagName), tagName, commitHash)
 	}
 
 	repo, err := OpenRepository(repoPath)
@@ -203,7 +203,7 @@
 	}()
 
 	for _, tagName := range tags {
-		tagHash := gitCmd(t, repoPath, "rev-parse", tagName)
+		tagHash := gitCmd(t, repoPath, nil, "rev-parse", tagName)
 		hash, _ := repo.ParseHash(tagHash)
 		obj, err := repo.ReadObject(hash)
 		if err != nil {
@@ -231,7 +231,7 @@
 	repoPath, cleanup := setupTestRepo(t)
 	defer cleanup()
 
-	gitCmd(t, repoPath, "config", "gc.auto", "0")
+	gitCmd(t, repoPath, nil, "config", "gc.auto", "0")
 
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
@@ -241,12 +241,12 @@
 		if err != nil {
 			t.Fatalf("failed to create file%d.txt: %v", i, err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("Commit %d", i))
-		gitCmd(t, repoPath, "repack", "-d")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", fmt.Sprintf("Commit %d", i))
+		gitCmd(t, repoPath, nil, "repack", "-d")
 	}
 
-	gitCmd(t, repoPath, "repack", "-a", "-d")
+	gitCmd(t, repoPath, nil, "repack", "-a", "-d")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -256,7 +256,7 @@
 		_ = repo.Close()
 	}()
 
-	headHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	headHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 	hash, _ := repo.ParseHash(headHash)
 
 	obj, err := repo.ReadObject(hash)
--- a/obj_commit_test.go
+++ b/obj_commit_test.go
@@ -87,9 +87,9 @@
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Test commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Test commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -135,24 +135,24 @@
 	if err != nil {
 		t.Fatalf("failed to write file1.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "First commit")
-	parent1Hash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "First commit")
+	parent1Hash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	err = os.WriteFile(filepath.Join(workDir, "file2.txt"), []byte("content2"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write file2.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Second commit")
-	parent2Hash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Second commit")
+	parent2Hash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	err = os.WriteFile(filepath.Join(workDir, "file3.txt"), []byte("content3"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write file3.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	mergeCommitData := fmt.Sprintf("tree %s\nparent %s\nparent %s\nauthor Test Author <test@example.org> 1234567890 +0000\ncommitter Test Committer <committer@example.org> 1234567890 +0000\n\nMerge commit\n",
 		treeHash, parent1Hash, parent2Hash)
--- a/obj_tag_test.go
+++ b/obj_tag_test.go
@@ -19,9 +19,9 @@
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Tagged commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Tagged commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -90,12 +90,12 @@
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Commit for tag")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Commit for tag")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "tag", "-a", "-m", "Tag message", "v1.0.0", commitHash)
-	tagHash := gitCmd(t, repoPath, "rev-parse", "v1.0.0")
+	gitCmd(t, repoPath, nil, "tag", "-a", "-m", "Tag message", "v1.0.0", commitHash)
+	tagHash := gitCmd(t, repoPath, nil, "rev-parse", "v1.0.0")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -136,9 +136,9 @@
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
--- a/obj_tree_test.go
+++ b/obj_tree_test.go
@@ -39,7 +39,7 @@
 		t.Errorf("git type: got %q, want %q", gitType, "tree")
 	}
 
-	gitLsTree := gitCmd(t, repoPath, "ls-tree", treeHash.String())
+	gitLsTree := gitCmd(t, repoPath, nil, "ls-tree", treeHash.String())
 	if !strings.Contains(gitLsTree, "file.txt") {
 		t.Errorf("git ls-tree doesn't contain file.txt: %s", gitLsTree)
 	}
@@ -68,8 +68,8 @@
 		t.Fatalf("failed to write c.txt: %v", err)
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -124,8 +124,8 @@
 		t.Fatalf("failed to write c.txt: %v", err)
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -175,8 +175,8 @@
 		t.Fatalf("failed to write dir/nested.txt: %v", err)
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -233,7 +233,7 @@
 	repoPath, cleanup := setupTestRepo(t)
 	defer cleanup()
 
-	gitCmd(t, repoPath, "config", "gc.auto", "0")
+	gitCmd(t, repoPath, nil, "config", "gc.auto", "0")
 
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
@@ -248,8 +248,8 @@
 		}
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	treeHash := gitCmd(t, repoPath, "--work-tree="+workDir, "write-tree")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	treeHash := gitCmd(t, repoPath, nil, "--work-tree="+workDir, "write-tree")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -265,7 +265,7 @@
 		t.Errorf("tree entries: got %d, want %d", len(tree.Entries), numFiles)
 	}
 
-	gitCount := gitCmd(t, repoPath, "ls-tree", treeHash)
+	gitCount := gitCmd(t, repoPath, nil, "ls-tree", treeHash)
 	gitLines := strings.Count(gitCount, "\n") + 1
 	if len(tree.Entries) != gitLines {
 		t.Errorf("furgit found %d entries, git found %d", len(tree.Entries), gitLines)
--- a/packed_read_test.go
+++ b/packed_read_test.go
@@ -13,7 +13,7 @@
 	repoPath, cleanup := setupTestRepo(t)
 	defer cleanup()
 
-	gitCmd(t, repoPath, "config", "gc.auto", "0")
+	gitCmd(t, repoPath, nil, "config", "gc.auto", "0")
 
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
@@ -27,11 +27,11 @@
 		t.Fatalf("failed to write file2.txt: %v", err)
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Test commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Test commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "repack", "-a", "-d")
+	gitCmd(t, repoPath, nil, "repack", "-a", "-d")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -64,7 +64,7 @@
 		t.Errorf("tree entries: got %d, want 2", len(tree.Entries))
 	}
 
-	gitLsTree := gitCmd(t, repoPath, "ls-tree", commit.Tree.String())
+	gitLsTree := gitCmd(t, repoPath, nil, "ls-tree", commit.Tree.String())
 	for _, entry := range tree.Entries {
 		if !strings.Contains(gitLsTree, string(entry.Name)) {
 			t.Errorf("git ls-tree doesn't contain %s", entry.Name)
@@ -80,7 +80,7 @@
 	repoPath, cleanup := setupTestRepo(t)
 	defer cleanup()
 
-	gitCmd(t, repoPath, "config", "gc.auto", "0")
+	gitCmd(t, repoPath, nil, "config", "gc.auto", "0")
 
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
@@ -95,11 +95,11 @@
 		}
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Large commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Large commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "repack", "-a", "-d")
+	gitCmd(t, repoPath, nil, "repack", "-a", "-d")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -118,7 +118,7 @@
 		t.Errorf("tree entries: got %d, want %d", len(tree.Entries), numFiles)
 	}
 
-	gitCount := gitCmd(t, repoPath, "ls-tree", commit.Tree.String())
+	gitCount := gitCmd(t, repoPath, nil, "ls-tree", commit.Tree.String())
 	gitLines := strings.Count(gitCount, "\n") + 1
 	if len(tree.Entries) != gitLines {
 		t.Errorf("furgit found %d entries, git found %d", len(tree.Entries), gitLines)
--- a/packed_write_test.go
+++ b/packed_write_test.go
@@ -6,7 +6,6 @@
 	"encoding/binary"
 	"fmt"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -109,9 +108,9 @@
 		}
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Test commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Test commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	commitBody := gitCatFile(t, repoPath, "commit", commitHash)
 	lines := bytes.Split(commitBody, []byte{'\n'})
@@ -120,7 +119,7 @@
 	}
 	treeHash := strings.TrimSpace(string(bytes.TrimPrefix(lines[0], []byte("tree "))))
 
-	lsTree := gitCmd(t, repoPath, "ls-tree", "-r", treeHash)
+	lsTree := gitCmd(t, repoPath, nil, "ls-tree", "-r", treeHash)
 	var blobHashes []string
 	for _, line := range strings.Split(lsTree, "\n") {
 		if line == "" {
@@ -177,18 +176,9 @@
 		t.Fatalf("pack stream invalid: %v", err)
 	}
 
-	cmd := exec.Command("git", "index-pack", "-o", idxPath, packPath)
-	cmd.Dir = repoPath
-	cmd.Env = append(os.Environ(),
-		"GIT_CONFIG_GLOBAL=/dev/null",
-		"GIT_CONFIG_SYSTEM=/dev/null",
-	)
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		t.Fatalf("git index-pack failed: %v\n%s", err, output)
-	}
+	_ = gitCmd(t, repoPath, nil, "index-pack", "-o", idxPath, packPath)
 
-	verifyOut := gitCmd(t, repoPath, "verify-pack", "-v", idxPath)
+	verifyOut := gitCmd(t, repoPath, nil, "verify-pack", "-v", idxPath)
 	seen := make(map[string]struct{})
 	for _, line := range strings.Split(verifyOut, "\n") {
 		if strings.TrimSpace(line) == "" {
@@ -215,10 +205,10 @@
 		}
 	}
 	for _, oid := range expectedOids {
-		_ = gitCmd(t, repoPath, "cat-file", "-p", oid)
+		_ = gitCmd(t, repoPath, nil, "cat-file", "-p", oid)
 	}
 
-	_ = gitCmd(t, repoPath, "fsck", "--full", "--strict")
+	_ = gitCmd(t, repoPath, nil, "fsck", "--full", "--strict")
 }
 
 func TestPackWriteDeltas(t *testing.T) {
@@ -243,9 +233,9 @@
 		}
 	}
 
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "Delta commit")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "Delta commit")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	commitBody := gitCatFile(t, repoPath, "commit", commitHash)
 	lines := bytes.Split(commitBody, []byte{'\n'})
@@ -254,7 +244,7 @@
 	}
 	treeHash := strings.TrimSpace(string(bytes.TrimPrefix(lines[0], []byte("tree "))))
 
-	lsTree := gitCmd(t, repoPath, "ls-tree", "-r", treeHash)
+	lsTree := gitCmd(t, repoPath, nil, "ls-tree", "-r", treeHash)
 	var blobHashes []string
 	for _, line := range strings.Split(lsTree, "\n") {
 		if line == "" {
@@ -314,18 +304,9 @@
 		t.Fatalf("pack stream invalid: %v", err)
 	}
 
-	cmd := exec.Command("git", "index-pack", "-o", idxPath, packPath)
-	cmd.Dir = repoPath
-	cmd.Env = append(os.Environ(),
-		"GIT_CONFIG_GLOBAL=/dev/null",
-		"GIT_CONFIG_SYSTEM=/dev/null",
-	)
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		t.Fatalf("git index-pack failed: %v\n%s", err, output)
-	}
+	_ = gitCmd(t, repoPath, nil, "index-pack", "-o", idxPath, packPath)
 
-	verifyOut := gitCmd(t, repoPath, "verify-pack", "-v", idxPath)
+	verifyOut := gitCmd(t, repoPath, nil, "verify-pack", "-v", idxPath)
 	seen := make(map[string]struct{})
 	for _, line := range strings.Split(verifyOut, "\n") {
 		if strings.TrimSpace(line) == "" {
@@ -352,10 +333,10 @@
 		}
 	}
 	for _, oid := range expectedOids {
-		_ = gitCmd(t, repoPath, "cat-file", "-p", oid)
+		_ = gitCmd(t, repoPath, nil, "cat-file", "-p", oid)
 	}
 
-	_ = gitCmd(t, repoPath, "fsck", "--full", "--strict")
+	_ = gitCmd(t, repoPath, nil, "fsck", "--full", "--strict")
 }
 
 func TestPackWriteThinPackReachable(t *testing.T) {
@@ -369,9 +350,9 @@
 	if err := os.WriteFile(filepath.Join(workDir, "file.txt"), base, 0o644); err != nil {
 		t.Fatalf("write base file: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "base")
-	haveHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "base")
+	haveHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	mod := append([]byte(nil), base...)
 	mod[1024] = 'B'
@@ -378,9 +359,9 @@
 	if err := os.WriteFile(filepath.Join(workDir, "file.txt"), mod, 0o644); err != nil {
 		t.Fatalf("write mod file: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "target")
-	wantHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "target")
+	wantHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -423,20 +404,10 @@
 	_ = os.Remove(packPath)
 	_ = os.Remove(idxPath)
 
-	cmd := exec.Command("git", "index-pack", "--stdin", "--fix-thin", "-o", idxPath, packPath)
-	cmd.Dir = repoPath
-	cmd.Env = append(os.Environ(),
-		"GIT_CONFIG_GLOBAL=/dev/null",
-		"GIT_CONFIG_SYSTEM=/dev/null",
-	)
-	cmd.Stdin = bytes.NewReader(buf.Bytes())
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		t.Fatalf("git index-pack --fix-thin failed: %v\n%s", err, output)
-	}
+	_ = gitCmd(t, repoPath, buf.Bytes(), "index-pack", "--stdin", "--fix-thin", "-o", idxPath, packPath)
 
-	_ = gitCmd(t, repoPath, "cat-file", "-p", wantHash)
-	_ = gitCmd(t, repoPath, "fsck", "--full", "--strict")
+	_ = gitCmd(t, repoPath, nil, "cat-file", "-p", wantHash)
+	_ = gitCmd(t, repoPath, nil, "fsck", "--full", "--strict")
 
 	_ = os.Remove(packPath)
 	_ = os.Remove(idxPath)
--- a/reachability_test.go
+++ b/reachability_test.go
@@ -20,9 +20,9 @@
 		if err := os.WriteFile(path, []byte{byte('a' + i), '\n'}, 0o644); err != nil {
 			t.Fatalf("write file: %v", err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit")
-		commits = append(commits, gitCmd(t, repoPath, "rev-parse", "HEAD"))
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit")
+		commits = append(commits, gitCmd(t, repoPath, nil, "rev-parse", "HEAD"))
 	}
 
 	repo, err := OpenRepository(repoPath)
@@ -97,8 +97,8 @@
 	if err := os.WriteFile(filepath.Join(workDir, "dir", "file2.txt"), []byte("two\n"), 0o644); err != nil {
 		t.Fatalf("write file2: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -106,7 +106,7 @@
 	}
 	defer func() { _ = repo.Close() }()
 
-	head := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	head := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 	wantID, _ := repo.ParseHash(head)
 	walk, err := repo.ReachableObjects(ReachabilityQuery{
 		Wants: []Hash{wantID},
@@ -124,9 +124,9 @@
 		t.Fatalf("Reachability walk error: %v", err)
 	}
 
-	treeStr := gitCmd(t, repoPath, "show", "-s", "--format=%T", head)
+	treeStr := gitCmd(t, repoPath, nil, "show", "-s", "--format=%T", head)
 	treeID, _ := repo.ParseHash(treeStr)
-	lsTree := gitCmd(t, repoPath, "ls-tree", "-r", treeStr)
+	lsTree := gitCmd(t, repoPath, nil, "ls-tree", "-r", treeStr)
 	fields := strings.Fields(lsTree)
 	if len(fields) < 3 {
 		t.Fatalf("unexpected ls-tree output: %q", lsTree)
@@ -157,9 +157,9 @@
 		if err := os.WriteFile(path, []byte{byte('a' + i), '\n'}, 0o644); err != nil {
 			t.Fatalf("write file: %v", err)
 		}
-		gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-		gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit")
-		commits = append(commits, gitCmd(t, repoPath, "rev-parse", "HEAD"))
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+		gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit")
+		commits = append(commits, gitCmd(t, repoPath, nil, "rev-parse", "HEAD"))
 	}
 
 	repo, err := OpenRepository(repoPath)
--- a/refs_test.go
+++ b/refs_test.go
@@ -18,10 +18,10 @@
 	if err != nil {
 		t.Fatalf("Failed to write test.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "test")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commitHash)
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "test")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commitHash)
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -42,7 +42,7 @@
 		t.Errorf("resolved hash: got %s, want %s", resolved.Hash, hashObj)
 	}
 
-	gitRevParse := gitCmd(t, repoPath, "rev-parse", "refs/heads/main")
+	gitRevParse := gitCmd(t, repoPath, nil, "rev-parse", "refs/heads/main")
 	if resolved.Hash.String() != gitRevParse {
 		t.Errorf("furgit resolved %s, git resolved %s", resolved.Hash, gitRevParse)
 	}
@@ -64,11 +64,11 @@
 	if err != nil {
 		t.Fatalf("failed to write test.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "test")
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commitHash)
-	gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "test")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commitHash)
+	gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -89,7 +89,7 @@
 		t.Errorf("HEAD symbolic ref: got %q, want %q", ref.Ref, "refs/heads/main")
 	}
 
-	gitSymRef := gitCmd(t, repoPath, "symbolic-ref", "HEAD")
+	gitSymRef := gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD")
 	if ref.Ref != gitSymRef {
 		t.Errorf("furgit resolved %v, git resolved %s", ref.Ref, gitSymRef)
 	}
@@ -106,23 +106,23 @@
 	if err != nil {
 		t.Fatalf("failed to write test.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit1")
-	commit1Hash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit1")
+	commit1Hash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	err = os.WriteFile(filepath.Join(workDir, "test2.txt"), []byte("content2"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write test2.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "commit2")
-	commit2Hash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "commit2")
+	commit2Hash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "update-ref", "refs/heads/branch1", commit1Hash)
-	gitCmd(t, repoPath, "update-ref", "refs/heads/branch2", commit2Hash)
-	gitCmd(t, repoPath, "update-ref", "refs/tags/v1.0", commit1Hash)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/branch1", commit1Hash)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/branch2", commit2Hash)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/tags/v1.0", commit1Hash)
 
-	gitCmd(t, repoPath, "pack-refs", "--all")
+	gitCmd(t, repoPath, nil, "pack-refs", "--all")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -141,7 +141,7 @@
 		t.Errorf("branch1: got %s, want %s", resolved1.Hash, hash1)
 	}
 
-	gitResolved1 := gitCmd(t, repoPath, "rev-parse", "refs/heads/branch1")
+	gitResolved1 := gitCmd(t, repoPath, nil, "rev-parse", "refs/heads/branch1")
 	if resolved1.Hash.String() != gitResolved1 {
 		t.Errorf("furgit resolved %s, git resolved %s", resolved1.Hash, gitResolved1)
 	}
@@ -175,14 +175,14 @@
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "init")
-	commit := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "init")
+	commit := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	// Create two layers of symbolic refs
-	gitCmd(t, repoPath, "symbolic-ref", "refs/heads/level1", "refs/heads/level2")
-	gitCmd(t, repoPath, "symbolic-ref", "refs/heads/level2", "refs/heads/main")
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit)
+	gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/level1", "refs/heads/level2")
+	gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/level2", "refs/heads/main")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit)
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -215,8 +215,8 @@
 	}
 	defer func() { _ = repo.Close() }()
 
-	gitCmd(t, repoPath, "symbolic-ref", "refs/heads/A", "refs/heads/B")
-	gitCmd(t, repoPath, "symbolic-ref", "refs/heads/B", "refs/heads/A")
+	gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/A", "refs/heads/B")
+	gitCmd(t, repoPath, nil, "symbolic-ref", "refs/heads/B", "refs/heads/A")
 
 	_, err = repo.ResolveRefFully("refs/heads/A")
 	if err == nil {
@@ -239,10 +239,10 @@
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "init")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "init")
 
-	commitHash := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	commitHash := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -287,28 +287,28 @@
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
 
-	gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+	gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main")
 
 	err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c1")
-	commit1 := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c1")
+	commit1 := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit1)
-	gitCmd(t, repoPath, "update-ref", "refs/heads/feature", commit1)
-	gitCmd(t, repoPath, "pack-refs", "--all", "--prune")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit1)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/feature", commit1)
+	gitCmd(t, repoPath, nil, "pack-refs", "--all", "--prune")
 
 	err = os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("two"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c2")
-	commit2 := gitCmd(t, repoPath, "rev-parse", "HEAD")
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit2)
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c2")
+	commit2 := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit2)
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -360,19 +360,19 @@
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
 
-	gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+	gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main")
 
 	err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c1")
-	commit1 := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c1")
+	commit1 := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit1)
-	gitCmd(t, repoPath, "update-ref", "refs/heads/feature", commit1)
-	gitCmd(t, repoPath, "pack-refs", "--all", "--prune")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit1)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/feature", commit1)
+	gitCmd(t, repoPath, nil, "pack-refs", "--all", "--prune")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
@@ -404,21 +404,21 @@
 	workDir, cleanupWork := setupWorkDir(t)
 	defer cleanupWork()
 
-	gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+	gitCmd(t, repoPath, nil, "symbolic-ref", "HEAD", "refs/heads/main")
 
 	err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644)
 	if err != nil {
 		t.Fatalf("failed to write file.txt: %v", err)
 	}
-	gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
-	gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c1")
-	commit := gitCmd(t, repoPath, "rev-parse", "HEAD")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "add", ".")
+	gitCmd(t, repoPath, nil, "--work-tree="+workDir, "commit", "-m", "c1")
+	commit := gitCmd(t, repoPath, nil, "rev-parse", "HEAD")
 
-	gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit)
-	gitCmd(t, repoPath, "update-ref", "refs/heads/feature/one", commit)
-	gitCmd(t, repoPath, "update-ref", "refs/notes/review", commit)
-	gitCmd(t, repoPath, "update-ref", "refs/tags/v1", commit)
-	gitCmd(t, repoPath, "pack-refs", "--all", "--prune")
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/main", commit)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/heads/feature/one", commit)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/notes/review", commit)
+	gitCmd(t, repoPath, nil, "update-ref", "refs/tags/v1", commit)
+	gitCmd(t, repoPath, nil, "pack-refs", "--all", "--prune")
 
 	repo, err := OpenRepository(repoPath)
 	if err != nil {
--- a/testutil_test.go
+++ b/testutil_test.go
@@ -17,10 +17,13 @@
 	return workDir, func() { _ = os.RemoveAll(workDir) }
 }
 
-func gitCmd(t *testing.T, dir string, args ...string) string {
+func gitCmd(t *testing.T, dir string, stdin []byte, args ...string) string {
 	t.Helper()
 	cmd := exec.Command("git", args...)
 	cmd.Dir = dir
+	if stdin != nil {
+		cmd.Stdin = bytes.NewReader(stdin)
+	}
 	cmd.Env = append(os.Environ(),
 		"GIT_CONFIG_GLOBAL=/dev/null",
 		"GIT_CONFIG_SYSTEM=/dev/null",
--