From dfcbb7d3affd7aee0882db021d2aa4c06f6fb149 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 23 Nov 2025 22:54:05 -0800 Subject: [PATCH] Optimize sync_test.go: eliminate redundant git init calls - Add setupGitRepo(), setupGitRepoWithBranch(), and setupMinimalGitRepo() helpers - Refactor 19 test functions to use shared git repo setup - Reduces duplicate git initialization boilerplate by ~300 lines - All tests pass with improved maintainability Related to bd-ktng --- .beads/beads.jsonl | 2 +- cmd/bd/sync_test.go | 254 ++++++------------------------------- cmd/bd/test_wait_helper.go | 121 ++++++++++++++++++ 3 files changed, 161 insertions(+), 216 deletions(-) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index fca5fe20..8db878d3 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -41,7 +41,7 @@ {"id":"bd-hdt","content_hash":"8e6cf1653ef2ea583b39a421b3d708763ab7c042d6cd494e77202a92af0a7398","title":"Implement auto-merge functionality in duplicates command","description":"The duplicates.go file has a TODO at line 95 to implement the performMerge function for automatic duplicate merging. Currently it just prints a warning message. This would automate the merge process instead of just suggesting commands.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-21T18:55:02.828619-05:00","updated_at":"2025-11-21T18:55:02.828619-05:00","source_repo":"."} {"id":"bd-j3zt","content_hash":"531ad51101f41375a93d66b8d22105ce7c4913261db78b662bb759e802bc01e2","title":"Fix mypy errors in beads-mcp","description":"Running `mypy .` in `integrations/beads-mcp` reports 287 errors. These should be addressed to improve type safety and code quality.","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-20T18:53:28.557708-05:00","updated_at":"2025-11-20T18:53:28.557708-05:00","source_repo":"."} {"id":"bd-koab","content_hash":"1b5e9a3a60f61472698af52ab6b2cbe46c839660900e7da3b562d9c3d5c608f6","title":"Import should continue on FOREIGN KEY constraint violations from deletions","description":"# Problem\n\nWhen importing JSONL after a merge that includes deletions, we may encounter FOREIGN KEY constraint violations if:\n- Issue A was deleted in one branch\n- Issue B (that depends on A) was modified in another branch \n- The merge keeps the deletion of A and the modification of B\n- Import tries to import B with a dependency/reference to deleted A\n\nCurrently import fails completely on such constraint violations, requiring manual intervention.\n\n# Solution\n\nAdd IsForeignKeyConstraintError() helper similar to IsUniqueConstraintError()\n\nUpdate import code to:\n1. Detect FOREIGN KEY constraint violations\n2. Log a warning with the issue ID and constraint\n3. Continue importing remaining issues\n4. Report summary of skipped issues at the end\n\n# Implementation Notes\n\n- Add to internal/storage/sqlite/util.go\n- Pattern: strings.Contains(err.Error(), \"FOREIGN KEY constraint failed\")\n- Update importer to handle these errors gracefully\n- Keep track of skipped issues for summary reporting","notes":"## Progress\n\nAdded IsForeignKeyConstraintError() helper function:\n- Located in internal/storage/sqlite/util.go \n- Detects both uppercase and lowercase variants\n- Full test coverage added to util_test.go\n- Tests pass ✓\n\n## Next Steps\n\nWhen FK constraint error is reproduced:\n1. Update importer.go to use IsForeignKeyConstraintError()\n2. Log warning with issue ID and constraint details\n3. Track skipped issues in Result struct\n4. Continue import instead of failing\n5. Report skipped issues in summary\n\nThe helper is ready to use when you encounter the actual constraint violation.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-23T21:37:02.811665-08:00","updated_at":"2025-11-23T21:37:50.739917-08:00","source_repo":"."} -{"id":"bd-ktng","content_hash":"0a09f3e1549a70817f23aa57444811aaf18683ff9336944ff6e8c277ac5684b4","title":"Optimize CLI test suite - eliminate redundant git init calls","description":"Current: Each of 13 CLI tests calls git init (31s total). Solution: Use single test binary built once in init(), skip git operations where possible, or use mock filesystem.","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-04T11:23:13.660276-08:00","updated_at":"2025-11-04T11:23:13.660276-08:00","source_repo":"."} +{"id":"bd-ktng","content_hash":"b303c08fd8c8f4d34bc245e3dfa7898e232a63e7c8d635da1d89f3ccd206cba4","title":"Optimize CLI test suite - eliminate redundant git init calls","description":"Current: Each of 13 CLI tests calls git init (31s total). Solution: Use single test binary built once in init(), skip git operations where possible, or use mock filesystem.","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-11-04T11:23:13.660276-08:00","updated_at":"2025-11-23T22:45:34.031792-08:00","source_repo":"."} {"id":"bd-l954","content_hash":"263dd2111cf0353b307f2e47489aa42ecf607e49b1316b54a6497cad9d3722b0","title":"Performance Testing Framework","description":"Add comprehensive performance testing for beads focusing on optimization guidance and validating 10K+ database scale. Uses standard Go tooling, follows existing patterns, minimal complexity.\n\nComponents:\n- Benchmark suite for critical operations at 10K-20K scale\n- Fixture generator for realistic test data (epic hierarchies, cross-links)\n- User diagnostics via bd doctor --perf\n- Always-on profiling integration\n\nGoals:\n- Identify bottlenecks for optimization work\n- Validate performance at 10K+ issue scale\n- Enable users to collect diagnostics for bug reports\n- Support both SQLite and JSONL import paths","status":"open","priority":2,"issue_type":"epic","created_at":"2025-11-13T22:22:11.203467-08:00","updated_at":"2025-11-13T22:22:11.203467-08:00","source_repo":"."} {"id":"bd-m0w","content_hash":"e8641e225f1d4cf13fbd97c4a83046e3597df180d3ee134125e4a35abc6941cd","title":"Add test coverage for internal/validation package","description":"","design":"Validation package has 1 test file. Critical for data integrity. Target: 80% coverage","acceptance_criteria":"- At least 4 test files\n- Package coverage \u003e= 80%\n- Tests cover all validation rules","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T21:21:24.129559-05:00","updated_at":"2025-11-20T21:21:24.129559-05:00","source_repo":".","dependencies":[{"issue_id":"bd-m0w","depends_on_id":"bd-ge7","type":"blocks","created_at":"2025-11-20T21:21:31.350477-05:00","created_by":"daemon"}]} {"id":"bd-m7ge","content_hash":"bb08f2bcbbdd2e392733d92bff2e46a51000337ac019d306dd6a2983916873c4","title":"Add .beads/README.md during 'bd init' for project documentation and promotion","description":"When 'bd init' is run, automatically generate a .beads/README.md file that:\n\n1. Briefly explains what Beads is (AI-native issue tracking that lives in your repo)\n2. Links to the main repository: https://github.com/steveyegge/beads\n3. Provides a quick reference of essential commands:\n - bd create: Create new issues\n - bd list: View all issues\n - bd update: Modify issue status/details\n - bd show: View issue details\n - bd sync: Sync with git remote\n4. Highlights key benefits for AI coding agents and developers\n5. Encourages developers to try it out\n\nThe README should be enthusiastic and compelling to get open source contributors excited about using Beads for their AI-assisted development workflows.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-16T22:32:50.478681-08:00","updated_at":"2025-11-16T22:32:58.492868-08:00","source_repo":"."} diff --git a/cmd/bd/sync_test.go b/cmd/bd/sync_test.go index bf4ff212..db0fc848 100644 --- a/cmd/bd/sync_test.go +++ b/cmd/bd/sync_test.go @@ -32,20 +32,8 @@ func TestIsGitRepo_NotInGitRepo(t *testing.T) { } func TestGitHasUpstream_NoUpstream(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a fresh git repo without upstream - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Should not have upstream if gitHasUpstream() { @@ -55,23 +43,10 @@ func TestGitHasUpstream_NoUpstream(t *testing.T) { func TestGitHasChanges_NoFile(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) + _, cleanup := setupGitRepo(t) + defer cleanup() - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create and commit a file - testFile := filepath.Join(tmpDir, "test.txt") - os.WriteFile(testFile, []byte("original"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() - - // Check - should have no changes + // Check - should have no changes (test.txt was committed by setupGitRepo) hasChanges, err := gitHasChanges(ctx, "test.txt") if err != nil { t.Fatalf("gitHasChanges() error = %v", err) @@ -83,23 +58,11 @@ func TestGitHasChanges_NoFile(t *testing.T) { func TestGitHasChanges_ModifiedFile(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create and commit a file - testFile := filepath.Join(tmpDir, "test.txt") - os.WriteFile(testFile, []byte("original"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + tmpDir, cleanup := setupGitRepo(t) + defer cleanup() // Modify the file + testFile := filepath.Join(tmpDir, "test.txt") os.WriteFile(testFile, []byte("modified"), 0644) // Check - should have changes @@ -113,20 +76,8 @@ func TestGitHasChanges_ModifiedFile(t *testing.T) { } func TestGitHasUnmergedPaths_CleanRepo(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Should not have unmerged paths hasUnmerged, err := gitHasUnmergedPaths() @@ -140,23 +91,11 @@ func TestGitHasUnmergedPaths_CleanRepo(t *testing.T) { func TestGitCommit_Success(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("initial.txt", []byte("initial"), 0644) - exec.Command("git", "add", "initial.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Create a new file - testFile := "test.txt" + testFile := "new.txt" os.WriteFile(testFile, []byte("content"), 0644) // Commit the file @@ -177,23 +116,11 @@ func TestGitCommit_Success(t *testing.T) { func TestGitCommit_AutoMessage(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("initial.txt", []byte("initial"), 0644) - exec.Command("git", "add", "initial.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Create a new file - testFile := "test.txt" + testFile := "new.txt" os.WriteFile(testFile, []byte("content"), 0644) // Commit with auto-generated message (empty string) @@ -275,20 +202,8 @@ not valid json func TestGetCurrentBranch(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Get current branch branch, err := getCurrentBranch(ctx) @@ -304,20 +219,8 @@ func TestGetCurrentBranch(t *testing.T) { func TestMergeSyncBranch_NoSyncBranchConfigured(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Try to merge without sync.branch configured (or database) err := mergeSyncBranch(ctx, false) @@ -332,20 +235,8 @@ func TestMergeSyncBranch_NoSyncBranchConfigured(t *testing.T) { func TestMergeSyncBranch_OnSyncBranch(t *testing.T) { ctx := context.Background() - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit on main - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + tmpDir, cleanup := setupGitRepo(t) + defer cleanup() // Create sync branch exec.Command("git", "checkout", "-b", "beads-metadata").Run() @@ -363,20 +254,8 @@ func TestMergeSyncBranch_OnSyncBranch(t *testing.T) { } func TestMergeSyncBranch_DirtyWorkingTree(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Create uncommitted changes os.WriteFile("test.txt", []byte("modified"), 0644) @@ -443,20 +322,8 @@ func TestGetSyncBranch_EnvOverridesDB(t *testing.T) { } func TestIsInRebase_NotInRebase(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Should not be in rebase if isInRebase() { @@ -465,20 +332,8 @@ func TestIsInRebase_NotInRebase(t *testing.T) { } func TestIsInRebase_InRebase(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + tmpDir, cleanup := setupGitRepo(t) + defer cleanup() // Simulate rebase by creating rebase-merge directory os.MkdirAll(filepath.Join(tmpDir, ".git", "rebase-merge"), 0755) @@ -490,13 +345,8 @@ func TestIsInRebase_InRebase(t *testing.T) { } func TestIsInRebase_InRebaseApply(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() + tmpDir, cleanup := setupMinimalGitRepo(t) + defer cleanup() // Simulate non-interactive rebase by creating rebase-apply directory os.MkdirAll(filepath.Join(tmpDir, ".git", "rebase-apply"), 0755) @@ -508,20 +358,8 @@ func TestIsInRebase_InRebaseApply(t *testing.T) { } func TestHasJSONLConflict_NoConflict(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit - os.WriteFile("test.txt", []byte("test"), 0644) - exec.Command("git", "add", "test.txt").Run() - exec.Command("git", "commit", "-m", "initial").Run() + _, cleanup := setupGitRepo(t) + defer cleanup() // Should not have JSONL conflict if hasJSONLConflict() { @@ -530,22 +368,15 @@ func TestHasJSONLConflict_NoConflict(t *testing.T) { } func TestHasJSONLConflict_OnlyJSONLConflict(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) + tmpDir, cleanup := setupGitRepoWithBranch(t, "main") + defer cleanup() - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init", "-b", "main").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() - - // Create initial commit + // Create initial commit with beads.jsonl beadsDir := filepath.Join(tmpDir, ".beads") os.MkdirAll(beadsDir, 0755) os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"original"}`), 0644) exec.Command("git", "add", ".").Run() - exec.Command("git", "commit", "-m", "initial").Run() + exec.Command("git", "commit", "-m", "add beads.jsonl").Run() // Create a second commit on main (modify same issue) os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"main-version"}`), 0644) @@ -568,15 +399,8 @@ func TestHasJSONLConflict_OnlyJSONLConflict(t *testing.T) { } func TestHasJSONLConflict_MultipleConflicts(t *testing.T) { - tmpDir := t.TempDir() - originalWd, _ := os.Getwd() - defer os.Chdir(originalWd) - - // Create a git repo - os.Chdir(tmpDir) - exec.Command("git", "init", "-b", "main").Run() - exec.Command("git", "config", "user.email", "test@test.com").Run() - exec.Command("git", "config", "user.name", "Test User").Run() + tmpDir, cleanup := setupGitRepoWithBranch(t, "main") + defer cleanup() // Create initial commit with beads.jsonl and another file beadsDir := filepath.Join(tmpDir, ".beads") @@ -584,7 +408,7 @@ func TestHasJSONLConflict_MultipleConflicts(t *testing.T) { os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"original"}`), 0644) os.WriteFile("other.txt", []byte("line1\nline2\nline3"), 0644) exec.Command("git", "add", ".").Run() - exec.Command("git", "commit", "-m", "initial").Run() + exec.Command("git", "commit", "-m", "add initial files").Run() // Create a second commit on main (modify both files) os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"main-version"}`), 0644) diff --git a/cmd/bd/test_wait_helper.go b/cmd/bd/test_wait_helper.go index c64131bb..347d58d7 100644 --- a/cmd/bd/test_wait_helper.go +++ b/cmd/bd/test_wait_helper.go @@ -1,6 +1,8 @@ package main import ( + "os" + "os/exec" "testing" "time" ) @@ -18,3 +20,122 @@ func waitFor(t *testing.T, timeout, poll time.Duration, pred func() bool) { } t.Fatalf("condition not met within %v", timeout) } + +// setupGitRepo creates a temporary git repository and returns its path and cleanup function. +// The repo is initialized with git config and an initial commit. +// The current directory is changed to the new repo. +func setupGitRepo(t *testing.T) (repoPath string, cleanup func()) { + t.Helper() + + tmpDir := t.TempDir() + originalWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + + // Initialize git repo + if err := exec.Command("git", "init").Run(); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to init git repo: %v", err) + } + + // Configure git + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit + if err := os.WriteFile("test.txt", []byte("test"), 0644); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to write test file: %v", err) + } + exec.Command("git", "add", "test.txt").Run() + if err := exec.Command("git", "commit", "-m", "initial").Run(); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to create initial commit: %v", err) + } + + cleanup = func() { + os.Chdir(originalWd) + } + + return tmpDir, cleanup +} + +// setupGitRepoWithBranch creates a git repo and checks out a specific branch. +// Use this when tests need a specific branch name (e.g., "main"). +func setupGitRepoWithBranch(t *testing.T, branch string) (repoPath string, cleanup func()) { + t.Helper() + + tmpDir := t.TempDir() + originalWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + + // Initialize git repo with specific branch + if err := exec.Command("git", "init", "-b", branch).Run(); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to init git repo: %v", err) + } + + // Configure git + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit + if err := os.WriteFile("test.txt", []byte("test"), 0644); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to write test file: %v", err) + } + exec.Command("git", "add", "test.txt").Run() + if err := exec.Command("git", "commit", "-m", "initial").Run(); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to create initial commit: %v", err) + } + + cleanup = func() { + os.Chdir(originalWd) + } + + return tmpDir, cleanup +} + +// setupMinimalGitRepo creates a git repo without an initial commit. +// Use this when tests need to control the initial state more precisely. +func setupMinimalGitRepo(t *testing.T) (repoPath string, cleanup func()) { + t.Helper() + + tmpDir := t.TempDir() + originalWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("failed to change to temp directory: %v", err) + } + + // Initialize git repo + if err := exec.Command("git", "init").Run(); err != nil { + os.Chdir(originalWd) + t.Fatalf("failed to init git repo: %v", err) + } + + // Configure git + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + cleanup = func() { + os.Chdir(originalWd) + } + + return tmpDir, cleanup +}