Merge branch 'main' of https://github.com/steveyegge/beads
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
# Issue prefix for this repository (used by bd init)
|
# Issue prefix for this repository (used by bd init)
|
||||||
# If not set, bd init will auto-detect from directory name
|
# If not set, bd init will auto-detect from directory name
|
||||||
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
||||||
# issue-prefix: ""
|
issue-prefix: "bd"
|
||||||
|
|
||||||
# Use no-db mode: load from JSONL, no SQLite, write back after each command
|
# Use no-db mode: load from JSONL, no SQLite, write back after each command
|
||||||
# When true, bd will use .beads/issues.jsonl as the source of truth
|
# When true, bd will use .beads/issues.jsonl as the source of truth
|
||||||
|
|||||||
161
cmd/bd/clean.go
Normal file
161
cmd/bd/clean.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cleanCmd = &cobra.Command{
|
||||||
|
Use: "clean",
|
||||||
|
Short: "Clean up temporary beads artifacts",
|
||||||
|
Long: `Delete temporary beads artifacts to clean up after git operations.
|
||||||
|
|
||||||
|
This removes temporary files created during git merges and conflicts from the
|
||||||
|
.beads directory.
|
||||||
|
|
||||||
|
Files removed:
|
||||||
|
- 3-way merge snapshots (beads.base.jsonl, beads.left.jsonl, beads.right.jsonl)
|
||||||
|
- Merge metadata (*.meta.json)
|
||||||
|
- Git merge driver temp files (*.json[0-9], *.jsonl[0-9])
|
||||||
|
|
||||||
|
Files preserved:
|
||||||
|
- beads.jsonl (source of truth)
|
||||||
|
- beads.db (SQLite database)
|
||||||
|
- metadata.json
|
||||||
|
- config.yaml
|
||||||
|
- All daemon files
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
Clean up temporary files:
|
||||||
|
bd clean
|
||||||
|
|
||||||
|
Preview what would be deleted:
|
||||||
|
bd clean --dry-run`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
|
|
||||||
|
// Find beads directory
|
||||||
|
beadsDir := findBeadsDir()
|
||||||
|
if beadsDir == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: .beads directory not found\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read patterns from .beads/.gitignore (only merge artifacts section)
|
||||||
|
cleanPatterns, err := readMergeArtifactPatterns(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading .gitignore: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect files to delete
|
||||||
|
var filesToDelete []string
|
||||||
|
for _, pattern := range cleanPatterns {
|
||||||
|
matches, err := filepath.Glob(filepath.Join(beadsDir, pattern))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: error matching pattern %s: %v\n", pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filesToDelete = append(filesToDelete, matches...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filesToDelete) == 0 {
|
||||||
|
fmt.Println("Nothing to clean - all artifacts already removed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just run by default, no --force needed
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Println(color.YellowString("DRY RUN - no changes will be made"))
|
||||||
|
}
|
||||||
|
fmt.Printf("Found %d file(s) to clean:\n", len(filesToDelete))
|
||||||
|
for _, file := range filesToDelete {
|
||||||
|
relPath, err := filepath.Rel(beadsDir, file)
|
||||||
|
if err != nil {
|
||||||
|
relPath = file
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s\n", relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually delete the files
|
||||||
|
deletedCount := 0
|
||||||
|
errorCount := 0
|
||||||
|
for _, file := range filesToDelete {
|
||||||
|
if err := os.Remove(file); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
relPath, _ := filepath.Rel(beadsDir, file)
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to delete %s: %v\n", relPath, err)
|
||||||
|
errorCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deletedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nDeleted %d file(s)", deletedCount)
|
||||||
|
if errorCount > 0 {
|
||||||
|
fmt.Printf(" (%d error(s))", errorCount)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// readMergeArtifactPatterns reads the .beads/.gitignore file and extracts
|
||||||
|
// patterns from the "Merge artifacts" section
|
||||||
|
func readMergeArtifactPatterns(beadsDir string) ([]string, error) {
|
||||||
|
gitignorePath := filepath.Join(beadsDir, ".gitignore")
|
||||||
|
file, err := os.Open(gitignorePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open .gitignore: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var patterns []string
|
||||||
|
inMergeSection := false
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Look for the merge artifacts section
|
||||||
|
if strings.Contains(line, "Merge artifacts") {
|
||||||
|
inMergeSection = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop at the next section (starts with #)
|
||||||
|
if inMergeSection && strings.HasPrefix(line, "#") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect patterns from merge section
|
||||||
|
if inMergeSection && line != "" && !strings.HasPrefix(line, "#") {
|
||||||
|
// Skip negation patterns (starting with !)
|
||||||
|
if !strings.HasPrefix(line, "!") {
|
||||||
|
patterns = append(patterns, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading .gitignore: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cleanCmd.Flags().Bool("dry-run", false, "Preview what would be deleted without making changes")
|
||||||
|
rootCmd.AddCommand(cleanCmd)
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/beads/internal/merge"
|
"github.com/steveyegge/beads/internal/merge"
|
||||||
@@ -45,6 +48,11 @@ Vendored into bd with permission.`,
|
|||||||
leftPath := args[2]
|
leftPath := args[2]
|
||||||
rightPath := args[3]
|
rightPath := args[3]
|
||||||
|
|
||||||
|
// Ensure cleanup runs after merge completes
|
||||||
|
defer func() {
|
||||||
|
cleanupMergeArtifacts(outputPath, debugMerge)
|
||||||
|
}()
|
||||||
|
|
||||||
err := merge.Merge3Way(outputPath, basePath, leftPath, rightPath, debugMerge)
|
err := merge.Merge3Way(outputPath, basePath, leftPath, rightPath, debugMerge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if error is due to conflicts
|
// Check if error is due to conflicts
|
||||||
@@ -63,6 +71,61 @@ Vendored into bd with permission.`,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanupMergeArtifacts(outputPath string, debug bool) {
|
||||||
|
// Determine the .beads directory from the output path
|
||||||
|
// outputPath is typically .beads/beads.jsonl
|
||||||
|
beadsDir := filepath.Dir(outputPath)
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "=== CLEANUP ===\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Cleaning up artifacts in: %s\n", beadsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Find and remove any files with "backup" in the name
|
||||||
|
entries, err := os.ReadDir(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to read directory for cleanup: %v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(entry.Name()), "backup") {
|
||||||
|
fullPath := filepath.Join(beadsDir, entry.Name())
|
||||||
|
|
||||||
|
// Try to git rm if tracked
|
||||||
|
gitRmCmd := exec.Command("git", "rm", "-f", "--quiet", fullPath)
|
||||||
|
gitRmCmd.Dir = filepath.Dir(beadsDir)
|
||||||
|
_ = gitRmCmd.Run() // Ignore errors, file may not be tracked
|
||||||
|
|
||||||
|
// Also remove from filesystem if git rm didn't work
|
||||||
|
if err := os.Remove(fullPath); err == nil {
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "Removed backup file: %s\n", entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Run git clean -f in .beads/ directory to remove untracked files
|
||||||
|
cleanCmd := exec.Command("git", "clean", "-f")
|
||||||
|
cleanCmd.Dir = beadsDir
|
||||||
|
if debug {
|
||||||
|
cleanCmd.Stderr = os.Stderr
|
||||||
|
cleanCmd.Stdout = os.Stderr
|
||||||
|
fmt.Fprintf(os.Stderr, "Running: git clean -f in %s\n", beadsDir)
|
||||||
|
}
|
||||||
|
_ = cleanCmd.Run() // Ignore errors, git clean may fail in some contexts
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "Cleanup complete\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
mergeCmd.Flags().BoolVar(&debugMerge, "debug", false, "Enable debug output to stderr")
|
mergeCmd.Flags().BoolVar(&debugMerge, "debug", false, "Enable debug output to stderr")
|
||||||
rootCmd.AddCommand(mergeCmd)
|
rootCmd.AddCommand(mergeCmd)
|
||||||
|
|||||||
Reference in New Issue
Block a user