Add sparse checkout to exclude Claude context files from source repos
Excludes all Claude Code context files to prevent source repo instructions from interfering with Gas Town agent configuration: - .claude/ : settings, rules, agents, commands - CLAUDE.md : primary context file - CLAUDE.local.md: personal context file - .mcp.json : MCP server configuration Legacy configurations (only excluding .claude/) are detected and upgraded by gt doctor --fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/git"
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SparseCheckoutCheck verifies that git clones/worktrees have sparse checkout configured
|
// SparseCheckoutCheck verifies that git clones/worktrees have sparse checkout configured
|
||||||
// to exclude .claude/ from source repos. This ensures source repo settings don't override
|
// to exclude Claude Code context files from source repos. This ensures source repo settings
|
||||||
// Gas Town agent settings.
|
// and instructions don't override Gas Town agent configuration.
|
||||||
|
// Excluded files: .claude/, CLAUDE.md, CLAUDE.local.md, .mcp.json
|
||||||
type SparseCheckoutCheck struct {
|
type SparseCheckoutCheck struct {
|
||||||
FixableCheck
|
FixableCheck
|
||||||
rigPath string
|
rigPath string
|
||||||
@@ -23,7 +25,7 @@ func NewSparseCheckoutCheck() *SparseCheckoutCheck {
|
|||||||
FixableCheck: FixableCheck{
|
FixableCheck: FixableCheck{
|
||||||
BaseCheck: BaseCheck{
|
BaseCheck: BaseCheck{
|
||||||
CheckName: "sparse-checkout",
|
CheckName: "sparse-checkout",
|
||||||
CheckDescription: "Verify sparse checkout is configured to exclude .claude/",
|
CheckDescription: "Verify sparse checkout excludes Claude context files (.claude/, CLAUDE.md, etc.)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ func (c *SparseCheckoutCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
return &CheckResult{
|
return &CheckResult{
|
||||||
Name: c.Name(),
|
Name: c.Name(),
|
||||||
Status: StatusOK,
|
Status: StatusOK,
|
||||||
Message: "All repos have sparse checkout configured to exclude .claude/",
|
Message: "All repos have sparse checkout configured to exclude Claude context files",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +109,22 @@ func (c *SparseCheckoutCheck) Run(ctx *CheckContext) *CheckResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix configures sparse checkout for affected repos to exclude .claude/.
|
// Fix configures sparse checkout for affected repos to exclude Claude context files.
|
||||||
func (c *SparseCheckoutCheck) Fix(ctx *CheckContext) error {
|
func (c *SparseCheckoutCheck) Fix(ctx *CheckContext) error {
|
||||||
for _, repoPath := range c.affectedRepos {
|
for _, repoPath := range c.affectedRepos {
|
||||||
if err := git.ConfigureSparseCheckout(repoPath); err != nil {
|
if err := git.ConfigureSparseCheckout(repoPath); err != nil {
|
||||||
relPath, _ := filepath.Rel(c.rigPath, repoPath)
|
relPath, _ := filepath.Rel(c.rigPath, repoPath)
|
||||||
return fmt.Errorf("failed to configure sparse checkout for %s: %w", relPath, err)
|
return fmt.Errorf("failed to configure sparse checkout for %s: %w", relPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if any excluded files remain (untracked or modified files won't be removed by git read-tree)
|
||||||
|
if remaining := git.CheckExcludedFilesExist(repoPath); len(remaining) > 0 {
|
||||||
|
relPath, _ := filepath.Rel(c.rigPath, repoPath)
|
||||||
|
return fmt.Errorf("sparse checkout configured for %s but these files still exist: %s\n"+
|
||||||
|
"These files are untracked or modified and were not removed by git.\n"+
|
||||||
|
"Please manually remove or revert these files in %s",
|
||||||
|
relPath, strings.Join(remaining, ", "), repoPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,3 +345,309 @@ func TestSparseCheckoutCheck_NonGitDirSkipped(t *testing.T) {
|
|||||||
t.Errorf("expected StatusOK when no git repos, got %v", result.Status)
|
t.Errorf("expected StatusOK when no git repos, got %v", result.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_VerifiesAllPatterns(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
// Configure sparse checkout using our function
|
||||||
|
if err := git.ConfigureSparseCheckout(mayorRig); err != nil {
|
||||||
|
t.Fatalf("ConfigureSparseCheckout failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the sparse-checkout file and verify all patterns are present
|
||||||
|
sparseFile := filepath.Join(mayorRig, ".git", "info", "sparse-checkout")
|
||||||
|
content, err := os.ReadFile(sparseFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sparse-checkout file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentStr := string(content)
|
||||||
|
|
||||||
|
// Verify all required patterns are present
|
||||||
|
requiredPatterns := []string{
|
||||||
|
"!/.claude/", // Settings, rules, agents, commands
|
||||||
|
"!/CLAUDE.md", // Primary context file
|
||||||
|
"!/CLAUDE.local.md", // Personal context file
|
||||||
|
"!/.mcp.json", // MCP server configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range requiredPatterns {
|
||||||
|
if !strings.Contains(contentStr, pattern) {
|
||||||
|
t.Errorf("sparse-checkout file missing pattern %q, got:\n%s", pattern, contentStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_LegacyPatternNotSufficient(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
// Manually configure sparse checkout with only legacy .claude/ pattern (missing CLAUDE.md)
|
||||||
|
cmd := exec.Command("git", "config", "core.sparseCheckout", "true")
|
||||||
|
cmd.Dir = mayorRig
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("git config failed: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
sparseFile := filepath.Join(mayorRig, ".git", "info", "sparse-checkout")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(sparseFile), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Only include legacy pattern, missing CLAUDE.md
|
||||||
|
if err := os.WriteFile(sparseFile, []byte("/*\n!.claude/\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewSparseCheckoutCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir, RigName: rigName}
|
||||||
|
|
||||||
|
result := check.Run(ctx)
|
||||||
|
|
||||||
|
// Should fail because CLAUDE.md pattern is missing
|
||||||
|
if result.Status != StatusError {
|
||||||
|
t.Errorf("expected StatusError for legacy-only pattern, got %v", result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_FixUpgradesLegacyPatterns(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo with legacy sparse checkout (only .claude/)
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "config", "core.sparseCheckout", "true")
|
||||||
|
cmd.Dir = mayorRig
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("git config failed: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
sparseFile := filepath.Join(mayorRig, ".git", "info", "sparse-checkout")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(sparseFile), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(sparseFile, []byte("/*\n!.claude/\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewSparseCheckoutCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir, RigName: rigName}
|
||||||
|
|
||||||
|
// Verify fix is needed
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusError {
|
||||||
|
t.Fatalf("expected StatusError before fix, got %v", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply fix
|
||||||
|
if err := check.Fix(ctx); err != nil {
|
||||||
|
t.Fatalf("Fix failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all patterns are now present
|
||||||
|
content, err := os.ReadFile(sparseFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sparse-checkout file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentStr := string(content)
|
||||||
|
requiredPatterns := []string{"!/.claude/", "!/CLAUDE.md", "!/CLAUDE.local.md", "!/.mcp.json"}
|
||||||
|
for _, pattern := range requiredPatterns {
|
||||||
|
if !strings.Contains(contentStr, pattern) {
|
||||||
|
t.Errorf("after fix, sparse-checkout file missing pattern %q", pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify check now passes
|
||||||
|
result = check.Run(ctx)
|
||||||
|
if result.Status != StatusOK {
|
||||||
|
t.Errorf("expected StatusOK after fix, got %v", result.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_FixFailsWithUntrackedCLAUDEMD(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo without sparse checkout
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
// Create untracked CLAUDE.md (not added to git)
|
||||||
|
claudeFile := filepath.Join(mayorRig, "CLAUDE.md")
|
||||||
|
if err := os.WriteFile(claudeFile, []byte("# Untracked context\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewSparseCheckoutCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir, RigName: rigName}
|
||||||
|
|
||||||
|
// Verify fix is needed
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusError {
|
||||||
|
t.Fatalf("expected StatusError before fix, got %v", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix should fail because CLAUDE.md is untracked and won't be removed
|
||||||
|
err := check.Fix(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected Fix to return error for untracked CLAUDE.md, but it succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify error message is helpful
|
||||||
|
if !strings.Contains(err.Error(), "CLAUDE.md") {
|
||||||
|
t.Errorf("expected error to mention CLAUDE.md, got: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "untracked or modified") {
|
||||||
|
t.Errorf("expected error to explain files are untracked/modified, got: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "manually remove") {
|
||||||
|
t.Errorf("expected error to mention manual removal, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_FixFailsWithUntrackedClaudeDir(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo without sparse checkout
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
// Create untracked .claude/ directory (not added to git)
|
||||||
|
claudeDir := filepath.Join(mayorRig, ".claude")
|
||||||
|
if err := os.MkdirAll(claudeDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(claudeDir, "settings.json"), []byte("{}"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewSparseCheckoutCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir, RigName: rigName}
|
||||||
|
|
||||||
|
// Verify fix is needed
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusError {
|
||||||
|
t.Fatalf("expected StatusError before fix, got %v", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix should fail because .claude/ is untracked and won't be removed
|
||||||
|
err := check.Fix(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected Fix to return error for untracked .claude/, but it succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify error message mentions .claude
|
||||||
|
if !strings.Contains(err.Error(), ".claude") {
|
||||||
|
t.Errorf("expected error to mention .claude, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_FixFailsWithModifiedCLAUDEMD(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo without sparse checkout
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
// Add and commit CLAUDE.md to the repo
|
||||||
|
claudeFile := filepath.Join(mayorRig, "CLAUDE.md")
|
||||||
|
if err := os.WriteFile(claudeFile, []byte("# Original context\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("git", "add", "CLAUDE.md")
|
||||||
|
cmd.Dir = mayorRig
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("git add failed: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("git", "commit", "-m", "Add CLAUDE.md")
|
||||||
|
cmd.Dir = mayorRig
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("git commit failed: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now modify CLAUDE.md without committing (making it "dirty")
|
||||||
|
if err := os.WriteFile(claudeFile, []byte("# Modified context - local changes\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewSparseCheckoutCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir, RigName: rigName}
|
||||||
|
|
||||||
|
// Verify fix is needed
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusError {
|
||||||
|
t.Fatalf("expected StatusError before fix, got %v", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix should fail because CLAUDE.md is modified and git won't remove it
|
||||||
|
err := check.Fix(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected Fix to return error for modified CLAUDE.md, but it succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify error message is helpful
|
||||||
|
if !strings.Contains(err.Error(), "CLAUDE.md") {
|
||||||
|
t.Errorf("expected error to mention CLAUDE.md, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSparseCheckoutCheck_FixFailsWithMultipleProblems(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
rigName := "testrig"
|
||||||
|
rigDir := filepath.Join(tmpDir, rigName)
|
||||||
|
|
||||||
|
// Create git repo without sparse checkout
|
||||||
|
mayorRig := filepath.Join(rigDir, "mayor", "rig")
|
||||||
|
initGitRepo(t, mayorRig)
|
||||||
|
|
||||||
|
// Create multiple untracked context files
|
||||||
|
if err := os.WriteFile(filepath.Join(mayorRig, "CLAUDE.md"), []byte("# Context\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(mayorRig, ".mcp.json"), []byte("{}"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check := NewSparseCheckoutCheck()
|
||||||
|
ctx := &CheckContext{TownRoot: tmpDir, RigName: rigName}
|
||||||
|
|
||||||
|
// Verify fix is needed
|
||||||
|
result := check.Run(ctx)
|
||||||
|
if result.Status != StatusError {
|
||||||
|
t.Fatalf("expected StatusError before fix, got %v", result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix should fail and list multiple files
|
||||||
|
err := check.Fix(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected Fix to return error for multiple untracked files, but it succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify error mentions both files
|
||||||
|
errStr := err.Error()
|
||||||
|
if !strings.Contains(errStr, "CLAUDE.md") {
|
||||||
|
t.Errorf("expected error to mention CLAUDE.md, got: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(errStr, ".mcp.json") {
|
||||||
|
t.Errorf("expected error to mention .mcp.json, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+48
-6
@@ -635,12 +635,19 @@ func ConfigureSparseCheckout(repoPath string) error {
|
|||||||
|
|
||||||
// Write patterns directly to sparse-checkout file
|
// Write patterns directly to sparse-checkout file
|
||||||
// (git sparse-checkout set --stdin escapes the ! character incorrectly)
|
// (git sparse-checkout set --stdin escapes the ! character incorrectly)
|
||||||
|
// Exclude all Claude Code context files to prevent source repo instructions
|
||||||
|
// from interfering with Gas Town agent context:
|
||||||
|
// - .claude/ : settings, rules, agents, commands
|
||||||
|
// - CLAUDE.md : primary context file
|
||||||
|
// - CLAUDE.local.md : personal context file
|
||||||
|
// - .mcp.json : MCP server configuration
|
||||||
infoDir := filepath.Join(gitDir, "info")
|
infoDir := filepath.Join(gitDir, "info")
|
||||||
if err := os.MkdirAll(infoDir, 0755); err != nil {
|
if err := os.MkdirAll(infoDir, 0755); err != nil {
|
||||||
return fmt.Errorf("creating info dir: %w", err)
|
return fmt.Errorf("creating info dir: %w", err)
|
||||||
}
|
}
|
||||||
sparseFile := filepath.Join(infoDir, "sparse-checkout")
|
sparseFile := filepath.Join(infoDir, "sparse-checkout")
|
||||||
if err := os.WriteFile(sparseFile, []byte("/*\n!.claude/\n"), 0644); err != nil {
|
sparsePatterns := "/*\n!/.claude/\n!/CLAUDE.md\n!/CLAUDE.local.md\n!/.mcp.json\n"
|
||||||
|
if err := os.WriteFile(sparseFile, []byte(sparsePatterns), 0644); err != nil {
|
||||||
return fmt.Errorf("writing sparse-checkout: %w", err)
|
return fmt.Errorf("writing sparse-checkout: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,10 +669,33 @@ func ConfigureSparseCheckout(repoPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExcludedContextFiles lists all Claude context files that should be excluded by sparse checkout.
|
||||||
|
var ExcludedContextFiles = []string{
|
||||||
|
".claude",
|
||||||
|
"CLAUDE.md",
|
||||||
|
"CLAUDE.local.md",
|
||||||
|
".mcp.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckExcludedFilesExist checks if any Claude context files still exist in the repo
|
||||||
|
// after sparse checkout was configured. These files should have been removed by
|
||||||
|
// git read-tree, but may remain if they were untracked or modified.
|
||||||
|
// Returns a list of files that still exist and should be manually removed.
|
||||||
|
func CheckExcludedFilesExist(repoPath string) []string {
|
||||||
|
var remaining []string
|
||||||
|
for _, file := range ExcludedContextFiles {
|
||||||
|
path := filepath.Join(repoPath, file)
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
remaining = append(remaining, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remaining
|
||||||
|
}
|
||||||
|
|
||||||
// IsSparseCheckoutConfigured checks if sparse checkout is enabled and configured
|
// IsSparseCheckoutConfigured checks if sparse checkout is enabled and configured
|
||||||
// to exclude .claude/ for a given repo/worktree.
|
// to exclude Claude Code context files for a given repo/worktree.
|
||||||
// Returns true only if both core.sparseCheckout is true AND the sparse-checkout
|
// Returns true only if both core.sparseCheckout is true AND the sparse-checkout
|
||||||
// file contains the !.claude/ exclusion pattern.
|
// file contains all required exclusion patterns.
|
||||||
func IsSparseCheckoutConfigured(repoPath string) bool {
|
func IsSparseCheckoutConfigured(repoPath string) bool {
|
||||||
// Check if core.sparseCheckout is true
|
// Check if core.sparseCheckout is true
|
||||||
cmd := exec.Command("git", "-C", repoPath, "config", "core.sparseCheckout")
|
cmd := exec.Command("git", "-C", repoPath, "config", "core.sparseCheckout")
|
||||||
@@ -685,15 +715,27 @@ func IsSparseCheckoutConfigured(repoPath string) bool {
|
|||||||
gitDir = filepath.Join(repoPath, gitDir)
|
gitDir = filepath.Join(repoPath, gitDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if sparse-checkout file exists and excludes .claude/
|
// Check if sparse-checkout file exists and excludes Claude context files
|
||||||
sparseFile := filepath.Join(gitDir, "info", "sparse-checkout")
|
sparseFile := filepath.Join(gitDir, "info", "sparse-checkout")
|
||||||
content, err := os.ReadFile(sparseFile)
|
content, err := os.ReadFile(sparseFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for our exclusion pattern
|
// Check for all required exclusion patterns
|
||||||
return strings.Contains(string(content), "!.claude/")
|
contentStr := string(content)
|
||||||
|
requiredPatterns := []string{
|
||||||
|
"!/.claude/", // or legacy "!.claude/"
|
||||||
|
"!/CLAUDE.md", // or legacy without leading slash
|
||||||
|
}
|
||||||
|
for _, pattern := range requiredPatterns {
|
||||||
|
// Accept both with and without leading slash for backwards compatibility
|
||||||
|
legacyPattern := strings.TrimPrefix(pattern, "/")
|
||||||
|
if !strings.Contains(contentStr, pattern) && !strings.Contains(contentStr, legacyPattern) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorktreeRemove removes a worktree.
|
// WorktreeRemove removes a worktree.
|
||||||
|
|||||||
Reference in New Issue
Block a user