fix(beads): SetupRedirect preserves tracked files (gt-fj0ol)
Previously, SetupRedirect used os.RemoveAll() which deleted all files in .beads/ including tracked files like formulas/, README.md, config.yaml. Now cleanBeadsRuntimeFiles() selectively removes only gitignored runtime files (*.db, daemon.*, issues.jsonl, etc.) while preserving tracked content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,47 @@ func resolveBeadsDirWithDepth(beadsDir string, maxDepth int) string {
|
|||||||
return resolveBeadsDirWithDepth(resolved, maxDepth-1)
|
return resolveBeadsDirWithDepth(resolved, maxDepth-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanBeadsRuntimeFiles removes gitignored runtime files from a .beads directory
|
||||||
|
// while preserving tracked files (formulas/, README.md, config.yaml, .gitignore).
|
||||||
|
// This is safe to call even if the directory doesn't exist.
|
||||||
|
func cleanBeadsRuntimeFiles(beadsDir string) error {
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
return nil // Nothing to clean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime files/patterns that are gitignored and safe to remove
|
||||||
|
runtimePatterns := []string{
|
||||||
|
// SQLite databases
|
||||||
|
"*.db", "*.db-*", "*.db?*",
|
||||||
|
// Daemon runtime
|
||||||
|
"daemon.lock", "daemon.log", "daemon.pid", "bd.sock",
|
||||||
|
// Sync state
|
||||||
|
"sync-state.json", "last-touched", "metadata.json",
|
||||||
|
// Version tracking
|
||||||
|
".local_version",
|
||||||
|
// Redirect file (we're about to recreate it)
|
||||||
|
"redirect",
|
||||||
|
// Merge artifacts
|
||||||
|
"beads.base.*", "beads.left.*", "beads.right.*",
|
||||||
|
// JSONL files (tracked but will be redirected, safe to remove in worktrees)
|
||||||
|
"issues.jsonl", "interactions.jsonl",
|
||||||
|
// Runtime directories
|
||||||
|
"mq",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range runtimePatterns {
|
||||||
|
matches, err := filepath.Glob(filepath.Join(beadsDir, pattern))
|
||||||
|
if err != nil {
|
||||||
|
continue // Invalid pattern, skip
|
||||||
|
}
|
||||||
|
for _, match := range matches {
|
||||||
|
os.RemoveAll(match) // Best effort, ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetupRedirect creates a .beads/redirect file for a worktree to point to the rig's shared beads.
|
// SetupRedirect creates a .beads/redirect file for a worktree to point to the rig's shared beads.
|
||||||
// This is used by crew, polecats, and refinery worktrees to share the rig's beads database.
|
// This is used by crew, polecats, and refinery worktrees to share the rig's beads database.
|
||||||
//
|
//
|
||||||
@@ -119,7 +160,7 @@ func resolveBeadsDirWithDepth(beadsDir string, maxDepth int) string {
|
|||||||
//
|
//
|
||||||
// The function:
|
// The function:
|
||||||
// 1. Computes the relative path from worktree to rig-level .beads
|
// 1. Computes the relative path from worktree to rig-level .beads
|
||||||
// 2. Cleans up any existing .beads/ contents (from tracked branches)
|
// 2. Cleans up runtime files (preserving tracked files like formulas/)
|
||||||
// 3. Creates the redirect file
|
// 3. Creates the redirect file
|
||||||
//
|
//
|
||||||
// Safety: This function refuses to create redirects in the canonical beads location
|
// Safety: This function refuses to create redirects in the canonical beads location
|
||||||
@@ -149,15 +190,13 @@ func SetupRedirect(townRoot, worktreePath string) error {
|
|||||||
return fmt.Errorf("no rig .beads found at %s", rigBeadsPath)
|
return fmt.Errorf("no rig .beads found at %s", rigBeadsPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any existing .beads/ contents from the branch
|
// Clean up runtime files in .beads/ but preserve tracked files (formulas/, README.md, etc.)
|
||||||
worktreeBeadsDir := filepath.Join(worktreePath, ".beads")
|
worktreeBeadsDir := filepath.Join(worktreePath, ".beads")
|
||||||
if _, err := os.Stat(worktreeBeadsDir); err == nil {
|
if err := cleanBeadsRuntimeFiles(worktreeBeadsDir); err != nil {
|
||||||
if err := os.RemoveAll(worktreeBeadsDir); err != nil {
|
return fmt.Errorf("cleaning runtime files: %w", err)
|
||||||
return fmt.Errorf("cleaning existing .beads dir: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create .beads directory
|
// Create .beads directory if it doesn't exist
|
||||||
if err := os.MkdirAll(worktreeBeadsDir, 0755); err != nil {
|
if err := os.MkdirAll(worktreeBeadsDir, 0755); err != nil {
|
||||||
return fmt.Errorf("creating .beads dir: %w", err)
|
return fmt.Errorf("creating .beads dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1644,7 +1644,7 @@ func TestSetupRedirect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cleans existing tracked beads from worktree", func(t *testing.T) {
|
t.Run("cleans runtime files but preserves tracked files", func(t *testing.T) {
|
||||||
townRoot := t.TempDir()
|
townRoot := t.TempDir()
|
||||||
rigRoot := filepath.Join(townRoot, "testrig")
|
rigRoot := filepath.Join(townRoot, "testrig")
|
||||||
rigBeads := filepath.Join(rigRoot, ".beads")
|
rigBeads := filepath.Join(rigRoot, ".beads")
|
||||||
@@ -1654,27 +1654,43 @@ func TestSetupRedirect(t *testing.T) {
|
|||||||
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
||||||
t.Fatalf("mkdir rig beads: %v", err)
|
t.Fatalf("mkdir rig beads: %v", err)
|
||||||
}
|
}
|
||||||
// Simulate worktree with tracked .beads (has database files)
|
// Simulate worktree with both runtime and tracked files
|
||||||
if err := os.MkdirAll(crewBeads, 0755); err != nil {
|
if err := os.MkdirAll(crewBeads, 0755); err != nil {
|
||||||
t.Fatalf("mkdir crew beads: %v", err)
|
t.Fatalf("mkdir crew beads: %v", err)
|
||||||
}
|
}
|
||||||
|
// Runtime files (should be removed)
|
||||||
if err := os.WriteFile(filepath.Join(crewBeads, "beads.db"), []byte("fake db"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(crewBeads, "beads.db"), []byte("fake db"), 0644); err != nil {
|
||||||
t.Fatalf("write fake db: %v", err)
|
t.Fatalf("write fake db: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(crewBeads, "issues.jsonl"), []byte("{}"), 0644); err != nil {
|
||||||
|
t.Fatalf("write issues.jsonl: %v", err)
|
||||||
|
}
|
||||||
|
// Tracked files (should be preserved)
|
||||||
if err := os.WriteFile(filepath.Join(crewBeads, "config.yaml"), []byte("prefix: test"), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(crewBeads, "config.yaml"), []byte("prefix: test"), 0644); err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(crewBeads, "README.md"), []byte("# Beads"), 0644); err != nil {
|
||||||
|
t.Fatalf("write README: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := SetupRedirect(townRoot, crewPath); err != nil {
|
if err := SetupRedirect(townRoot, crewPath); err != nil {
|
||||||
t.Fatalf("SetupRedirect failed: %v", err)
|
t.Fatalf("SetupRedirect failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify old files were cleaned up
|
// Verify runtime files were cleaned up
|
||||||
if _, err := os.Stat(filepath.Join(crewBeads, "beads.db")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(crewBeads, "beads.db")); !os.IsNotExist(err) {
|
||||||
t.Error("beads.db should have been removed")
|
t.Error("beads.db should have been removed")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Join(crewBeads, "config.yaml")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(crewBeads, "issues.jsonl")); !os.IsNotExist(err) {
|
||||||
t.Error("config.yaml should have been removed")
|
t.Error("issues.jsonl should have been removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tracked files were preserved
|
||||||
|
if _, err := os.Stat(filepath.Join(crewBeads, "config.yaml")); err != nil {
|
||||||
|
t.Errorf("config.yaml should have been preserved: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(crewBeads, "README.md")); err != nil {
|
||||||
|
t.Errorf("README.md should have been preserved: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify redirect was created
|
// Verify redirect was created
|
||||||
|
|||||||
Reference in New Issue
Block a user