fix: prevent sparse checkout config from leaking to main repo (GH#886)

Use git sparse-checkout command instead of manually setting
core.sparseCheckout config. The sparse-checkout command properly
scopes the setting to the worktree via extensions.worktreeConfig,
avoiding the confusing sparse checkout message in git status.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/fang
2026-01-04 13:52:14 -08:00
committed by Steve Yegge
parent b789b99537
commit fbc93e3de2
2 changed files with 22 additions and 55 deletions

View File

@@ -389,40 +389,24 @@ func (wm *WorktreeManager) branchExists(branch string) bool {
}
// configureSparseCheckout sets up sparse checkout to only include .beads/
// Uses `git sparse-checkout` command which properly scopes the config to the
// worktree, avoiding GH#886 where core.sparseCheckout leaked to main repo.
func (wm *WorktreeManager) configureSparseCheckout(worktreePath string) error {
// Get the actual git directory (for worktrees, .git is a file)
gitFile := filepath.Join(worktreePath, ".git")
gitContent, err := os.ReadFile(gitFile) // #nosec G304 - controlled path
// Initialize sparse checkout in non-cone mode (supports glob patterns)
// This uses extensions.worktreeConfig to scope sparseCheckout to this worktree only
initCmd := exec.Command("git", "sparse-checkout", "init", "--no-cone")
initCmd.Dir = worktreePath
output, err := initCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to read .git file: %w", err)
return fmt.Errorf("failed to init sparse checkout: %w\nOutput: %s", err, string(output))
}
// Parse "gitdir: /path/to/git/dir"
gitDirLine := strings.TrimSpace(string(gitContent))
if !strings.HasPrefix(gitDirLine, "gitdir: ") {
return fmt.Errorf("invalid .git file format: %s", gitDirLine)
}
gitDir := strings.TrimPrefix(gitDirLine, "gitdir: ")
// Enable sparse checkout config
cmd := exec.Command("git", "config", "core.sparseCheckout", "true")
cmd.Dir = worktreePath
output, err := cmd.CombinedOutput()
// Set sparse checkout to only include .beads/
setCmd := exec.Command("git", "sparse-checkout", "set", "/.beads/")
setCmd.Dir = worktreePath
output, err = setCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to enable sparse checkout: %w\nOutput: %s", err, string(output))
}
// Create info directory if it doesn't exist
infoDir := filepath.Join(gitDir, "info")
if err := os.MkdirAll(infoDir, 0750); err != nil {
return fmt.Errorf("failed to create info directory: %w", err)
}
// Write sparse-checkout file to include only .beads/
sparseFile := filepath.Join(infoDir, "sparse-checkout")
sparseContent := ".beads/*\n"
if err := os.WriteFile(sparseFile, []byte(sparseContent), 0644); err != nil { // #nosec G306 - sparse-checkout config file needs standard permissions
return fmt.Errorf("failed to write sparse-checkout file: %w", err)
return fmt.Errorf("failed to set sparse checkout patterns: %w\nOutput: %s", err, string(output))
}
return nil
@@ -445,34 +429,16 @@ func NormalizeBeadsRelPath(relPath string) string {
// verifySparseCheckout checks if sparse checkout is configured correctly
func (wm *WorktreeManager) verifySparseCheckout(worktreePath string) error {
// Check if sparse-checkout file exists and contains .beads
sparseFile := filepath.Join(worktreePath, ".git", "info", "sparse-checkout")
// For worktrees, .git is a file pointing to the actual git dir
// We need to read the actual git directory location
gitFile := filepath.Join(worktreePath, ".git")
gitContent, err := os.ReadFile(gitFile) // #nosec G304 - controlled path
// Use git sparse-checkout list to verify configuration
cmd := exec.Command("git", "sparse-checkout", "list")
cmd.Dir = worktreePath
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to read .git file: %w", err)
}
// Parse "gitdir: /path/to/git/dir"
gitDirLine := strings.TrimSpace(string(gitContent))
if !strings.HasPrefix(gitDirLine, "gitdir: ") {
return fmt.Errorf("invalid .git file format")
}
gitDir := strings.TrimPrefix(gitDirLine, "gitdir: ")
// Sparse checkout file is in the git directory
sparseFile = filepath.Join(gitDir, "info", "sparse-checkout")
data, err := os.ReadFile(sparseFile) // #nosec G304 - controlled path
if err != nil {
return fmt.Errorf("sparse-checkout file not found: %w", err)
return fmt.Errorf("failed to list sparse checkout patterns: %w\nOutput: %s", err, string(output))
}
// Verify it contains .beads
if !strings.Contains(string(data), ".beads") {
if !strings.Contains(string(output), ".beads") {
return fmt.Errorf("sparse-checkout does not include .beads")
}

View File

@@ -547,8 +547,9 @@ func TestVerifySparseCheckoutErrors(t *testing.T) {
if err == nil {
t.Error("verifySparseCheckout should fail with invalid .git file format")
}
if !strings.Contains(err.Error(), "invalid .git file format") {
t.Errorf("Expected 'invalid .git file format' error, got: %v", err)
// git sparse-checkout list will fail when .git file is invalid
if !strings.Contains(err.Error(), "failed to list sparse checkout patterns") {
t.Errorf("Expected 'failed to list sparse checkout patterns' error, got: %v", err)
}
})
}