feat(compact): add --dolt flag for Dolt garbage collection
Add `bd compact --dolt` command to run Dolt garbage collection on .beads/dolt directory. This helps reclaim disk space when using the Dolt backend, where auto-commit per mutation causes commit history to grow over time. Features: - Runs `dolt gc` in the .beads/dolt directory - Shows disk space before/after with bytes freed - Supports --dry-run to preview without running GC - Supports --json for machine-readable output - Helpful error messages for missing dolt directory or command Closes: hq-ew1mbr.14 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
gastown/crew/dennis
parent
ab5f507c66
commit
ca24c17af8
@@ -71,6 +71,7 @@ func init() {
|
||||
compactAliasCmd.Flags().StringVar(&compactSummary, "summary", "", "Path to summary file (use '-' for stdin)")
|
||||
compactAliasCmd.Flags().StringVar(&compactActor, "actor", "agent", "Actor name for audit trail")
|
||||
compactAliasCmd.Flags().IntVar(&compactLimit, "limit", 0, "Limit number of candidates (0 = no limit)")
|
||||
compactAliasCmd.Flags().BoolVar(&compactDolt, "dolt", false, "Dolt mode: run Dolt garbage collection on .beads/dolt")
|
||||
|
||||
// Reset alias flags - these read from cmd.Flags() in the Run function
|
||||
resetAliasCmd.Flags().Bool("force", false, "Actually perform the reset (required)")
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/compact"
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
)
|
||||
@@ -30,6 +33,7 @@ var (
|
||||
compactActor string
|
||||
compactLimit int
|
||||
compactOlderThan int
|
||||
compactDolt bool
|
||||
)
|
||||
|
||||
var compactCmd = &cobra.Command{
|
||||
@@ -45,6 +49,7 @@ Modes:
|
||||
- Analyze: Export candidates for agent review (no API key needed)
|
||||
- Apply: Accept agent-provided summary (no API key needed)
|
||||
- Auto: AI-powered compaction (requires ANTHROPIC_API_KEY, legacy)
|
||||
- Dolt: Run Dolt garbage collection (for Dolt-backend repositories)
|
||||
|
||||
Tiers:
|
||||
- Tier 1: Semantic compression (30 days closed, 70% reduction)
|
||||
@@ -60,6 +65,13 @@ Tombstone Cleanup:
|
||||
removes any tombstone that no open issues depend on, regardless of age.
|
||||
Also cleans stale deps from closed issues to tombstones.
|
||||
|
||||
Dolt Garbage Collection:
|
||||
With auto-commit per mutation, Dolt commit history grows over time. Use
|
||||
--dolt to run Dolt garbage collection and reclaim disk space.
|
||||
|
||||
--dolt: Run Dolt GC on .beads/dolt directory to free disk space.
|
||||
This removes unreachable commits and compacts storage.
|
||||
|
||||
Examples:
|
||||
# Age-based pruning
|
||||
bd compact --prune # Remove tombstones older than 30 days
|
||||
@@ -70,6 +82,10 @@ Examples:
|
||||
bd compact --purge-tombstones --dry-run # Preview what would be purged
|
||||
bd compact --purge-tombstones # Remove tombstones with no open deps
|
||||
|
||||
# Dolt garbage collection
|
||||
bd compact --dolt # Run Dolt GC
|
||||
bd compact --dolt --dry-run # Preview without running GC
|
||||
|
||||
# Agent-driven workflow (recommended)
|
||||
bd compact --analyze --json # Get candidates with full content
|
||||
bd compact --apply --id bd-42 --summary summary.txt
|
||||
@@ -84,8 +100,8 @@ Examples:
|
||||
bd compact --stats # Show statistics
|
||||
`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
// Compact modifies data unless --stats or --analyze or --dry-run
|
||||
if !compactStats && !compactAnalyze && !compactDryRun {
|
||||
// Compact modifies data unless --stats or --analyze or --dry-run or --dolt with --dry-run
|
||||
if !compactStats && !compactAnalyze && !compactDryRun && !(compactDolt && compactDryRun) {
|
||||
CheckReadonly("compact")
|
||||
}
|
||||
ctx := rootCtx
|
||||
@@ -105,6 +121,12 @@ Examples:
|
||||
return
|
||||
}
|
||||
|
||||
// Handle dolt GC mode
|
||||
if compactDolt {
|
||||
runCompactDolt()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle prune mode (standalone tombstone pruning by age)
|
||||
if compactPrune {
|
||||
runCompactPrune()
|
||||
@@ -766,6 +788,134 @@ func runCompactApply(ctx context.Context, store *sqlite.SQLiteStorage) {
|
||||
markDirtyAndScheduleFlush()
|
||||
}
|
||||
|
||||
// runCompactDolt runs Dolt garbage collection on the .beads/dolt directory
|
||||
func runCompactDolt() {
|
||||
start := time.Now()
|
||||
|
||||
// Find beads directory
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: could not find .beads directory\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check for dolt directory
|
||||
doltPath := filepath.Join(beadsDir, "dolt")
|
||||
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Error: Dolt directory not found at %s\n", doltPath)
|
||||
fmt.Fprintf(os.Stderr, "Hint: --dolt flag is only for repositories using the Dolt backend\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if dolt command is available
|
||||
if _, err := exec.LookPath("dolt"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: dolt command not found in PATH\n")
|
||||
fmt.Fprintf(os.Stderr, "Hint: install Dolt from https://github.com/dolthub/dolt\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get size before GC
|
||||
sizeBefore, err := getDirSize(doltPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not calculate directory size: %v\n", err)
|
||||
sizeBefore = 0
|
||||
}
|
||||
|
||||
if compactDryRun {
|
||||
if jsonOutput {
|
||||
output := map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"dolt_path": doltPath,
|
||||
"size_before": sizeBefore,
|
||||
"size_display": formatBytes(sizeBefore),
|
||||
}
|
||||
outputJSON(output)
|
||||
return
|
||||
}
|
||||
fmt.Printf("DRY RUN - Dolt garbage collection\n\n")
|
||||
fmt.Printf("Dolt directory: %s\n", doltPath)
|
||||
fmt.Printf("Current size: %s\n", formatBytes(sizeBefore))
|
||||
fmt.Printf("\nRun without --dry-run to perform garbage collection.\n")
|
||||
return
|
||||
}
|
||||
|
||||
if !jsonOutput {
|
||||
fmt.Printf("Running Dolt garbage collection...\n")
|
||||
}
|
||||
|
||||
// Run dolt gc
|
||||
cmd := exec.Command("dolt", "gc") // #nosec G204 -- fixed command, no user input
|
||||
cmd.Dir = doltPath
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: dolt gc failed: %v\n", err)
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Output: %s\n", string(output))
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get size after GC
|
||||
sizeAfter, err := getDirSize(doltPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not calculate directory size after GC: %v\n", err)
|
||||
sizeAfter = 0
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
freed := sizeBefore - sizeAfter
|
||||
if freed < 0 {
|
||||
freed = 0 // GC may not always reduce size
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
result := map[string]interface{}{
|
||||
"success": true,
|
||||
"dolt_path": doltPath,
|
||||
"size_before": sizeBefore,
|
||||
"size_after": sizeAfter,
|
||||
"freed_bytes": freed,
|
||||
"freed_display": formatBytes(freed),
|
||||
"elapsed_ms": elapsed.Milliseconds(),
|
||||
}
|
||||
outputJSON(result)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Dolt garbage collection complete\n")
|
||||
fmt.Printf(" %s → %s (freed %s)\n", formatBytes(sizeBefore), formatBytes(sizeAfter), formatBytes(freed))
|
||||
fmt.Printf(" Time: %v\n", elapsed)
|
||||
}
|
||||
|
||||
// getDirSize calculates the total size of a directory recursively
|
||||
func getDirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return size, err
|
||||
}
|
||||
|
||||
// formatBytes formats a byte count as a human-readable string
|
||||
func formatBytes(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
func init() {
|
||||
compactCmd.Flags().BoolVar(&compactDryRun, "dry-run", false, "Preview without compacting")
|
||||
compactCmd.Flags().IntVar(&compactTier, "tier", 1, "Compaction tier (1 or 2)")
|
||||
@@ -787,6 +937,7 @@ func init() {
|
||||
compactCmd.Flags().StringVar(&compactSummary, "summary", "", "Path to summary file (use '-' for stdin)")
|
||||
compactCmd.Flags().StringVar(&compactActor, "actor", "agent", "Actor name for audit trail")
|
||||
compactCmd.Flags().IntVar(&compactLimit, "limit", 0, "Limit number of candidates (0 = no limit)")
|
||||
compactCmd.Flags().BoolVar(&compactDolt, "dolt", false, "Dolt mode: run Dolt garbage collection on .beads/dolt")
|
||||
|
||||
// Note: compactCmd is added to adminCmd in admin.go
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user