feat: add Git worktree compatibility (PR #478)
Adds comprehensive Git worktree support for beads issue tracking: Core changes: - New internal/git/gitdir.go package for worktree detection - GetGitDir() returns proper .git location (main repo, not worktree) - Updated all hooks to use git.GetGitDir() instead of local helper - BeadsDir() now prioritizes main repository's .beads directory Features: - Hooks auto-install in main repo when run from worktree - Shared .beads directory across all worktrees - Config option no-install-hooks to disable auto-install - New bd worktree subcommand for diagnostics Documentation: - New docs/WORKTREES.md with setup instructions - Updated CHANGELOG.md and AGENT_INSTRUCTIONS.md Testing: - Updated tests to use exported git.GetGitDir() - Added worktree detection tests Co-authored-by: Claude <noreply@anthropic.com> Closes: #478
This commit is contained in:
230
cmd/bd/reset.go
230
cmd/bd/reset.go
@@ -1,230 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/beads/internal/beads"
|
||||
"github.com/steveyegge/beads/internal/reset"
|
||||
)
|
||||
|
||||
var resetCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Reset beads to a clean starting state",
|
||||
Long: `Reset beads to a clean starting state by clearing .beads/ and reinitializing.
|
||||
|
||||
This command is useful when:
|
||||
- Your beads workspace is in an invalid state after an update
|
||||
- You want to start fresh with issue tracking
|
||||
- bd doctor cannot automatically fix problems
|
||||
|
||||
RESET MODES:
|
||||
|
||||
Soft Reset (default):
|
||||
- Kills all daemons
|
||||
- Clears .beads/ directory
|
||||
- Reinitializes with bd init
|
||||
- Git history is unchanged
|
||||
|
||||
Hard Reset (--hard):
|
||||
- Same as soft reset, plus:
|
||||
- Removes .beads/ files from git (git rm)
|
||||
- Creates a commit removing the old state
|
||||
- Creates a commit with fresh initialized state
|
||||
|
||||
OPTIONS:
|
||||
|
||||
--backup Create .beads-backup-{timestamp}/ before clearing
|
||||
--dry-run Preview what would happen without making changes
|
||||
--force Skip confirmation prompt
|
||||
--hard Include git operations (git rm + commit)
|
||||
--skip-init Don't reinitialize after clearing (leaves .beads/ empty)
|
||||
--verbose Show detailed progress
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
bd reset # Reset with confirmation prompt
|
||||
bd reset --backup # Reset with backup first
|
||||
bd reset --dry-run # Preview the impact
|
||||
bd reset --hard # Reset including git history
|
||||
bd reset --force # Reset without confirmation`,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
hard, _ := cmd.Flags().GetBool("hard")
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
backup, _ := cmd.Flags().GetBool("backup")
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
skipInit, _ := cmd.Flags().GetBool("skip-init")
|
||||
verbose, _ := cmd.Flags().GetBool("verbose")
|
||||
|
||||
// Color helpers
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
cyan := color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
// Validate state
|
||||
if err := reset.ValidateState(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s %v\n", red("Error:"), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get impact summary
|
||||
impact, err := reset.CountImpact()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Failed to analyze workspace: %v\n", red("Error:"), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Show impact summary
|
||||
fmt.Printf("\n%s Reset Impact Summary\n", yellow("⚠"))
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
|
||||
totalIssues := impact.IssueCount - impact.TombstoneCount
|
||||
if totalIssues > 0 {
|
||||
fmt.Printf(" Issues to delete: %s\n", cyan(fmt.Sprintf("%d", totalIssues)))
|
||||
fmt.Printf(" - Open: %d\n", impact.OpenCount)
|
||||
fmt.Printf(" - Closed: %d\n", impact.ClosedCount)
|
||||
} else {
|
||||
fmt.Printf(" Issues to delete: %s\n", cyan("0"))
|
||||
}
|
||||
|
||||
if impact.TombstoneCount > 0 {
|
||||
fmt.Printf(" Tombstones to delete: %s\n", cyan(fmt.Sprintf("%d", impact.TombstoneCount)))
|
||||
}
|
||||
|
||||
if impact.HasUncommitted {
|
||||
fmt.Printf(" %s Uncommitted changes in .beads/ will be lost\n", yellow("⚠"))
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Show what will happen
|
||||
fmt.Printf("Actions:\n")
|
||||
if backup {
|
||||
fmt.Printf(" 1. Create backup (.beads-backup-{timestamp}/)\n")
|
||||
}
|
||||
fmt.Printf(" %s. Kill all daemons\n", actionNumber(backup, 1))
|
||||
if hard {
|
||||
fmt.Printf(" %s. Remove .beads/ from git index and commit\n", actionNumber(backup, 2))
|
||||
}
|
||||
fmt.Printf(" %s. Delete .beads/ directory\n", actionNumber(backup, hardOffset(hard, 2)))
|
||||
if !skipInit {
|
||||
fmt.Printf(" %s. Reinitialize workspace (bd init)\n", actionNumber(backup, hardOffset(hard, 3)))
|
||||
if hard {
|
||||
fmt.Printf(" %s. Commit fresh state to git\n", actionNumber(backup, hardOffset(hard, 4)))
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
|
||||
// Dry run - stop here
|
||||
if dryRun {
|
||||
fmt.Printf("%s This was a dry run. No changes were made.\n\n", cyan("ℹ"))
|
||||
return
|
||||
}
|
||||
|
||||
// Confirmation prompt (unless --force)
|
||||
if !force {
|
||||
fmt.Printf("%s This will permanently delete all issues. Continue? [y/N]: ", yellow("Warning:"))
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Failed to read response: %v\n", red("Error:"), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Printf("Reset canceled.\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Execute reset
|
||||
if verbose {
|
||||
fmt.Printf("\n%s Starting reset...\n", cyan("→"))
|
||||
}
|
||||
|
||||
opts := reset.ResetOptions{
|
||||
Hard: hard,
|
||||
Backup: backup,
|
||||
DryRun: false, // Already handled above
|
||||
SkipInit: skipInit,
|
||||
}
|
||||
|
||||
result, err := reset.Reset(opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Reset failed: %v\n", red("Error:"), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Handle --hard mode: commit fresh state after reinit
|
||||
if hard && !skipInit {
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir != "" {
|
||||
commitMsg := "Initialize fresh beads workspace\n\nCreated new .beads/ directory after reset."
|
||||
if err := reset.GitAddAndCommit(beadsDir, commitMsg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s Failed to commit fresh state: %v\n", yellow("Warning:"), err)
|
||||
fmt.Fprintf(os.Stderr, "You may need to manually run: git add .beads && git commit -m \"Fresh beads state\"\n")
|
||||
} else if verbose {
|
||||
fmt.Printf(" %s Committed fresh state to git\n", green("✓"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show results
|
||||
fmt.Printf("\n%s Reset complete!\n\n", green("✓"))
|
||||
|
||||
if result.BackupPath != "" {
|
||||
fmt.Printf(" Backup created: %s\n", cyan(result.BackupPath))
|
||||
}
|
||||
|
||||
if result.DaemonsKilled > 0 {
|
||||
fmt.Printf(" Daemons stopped: %d\n", result.DaemonsKilled)
|
||||
}
|
||||
|
||||
if result.IssuesDeleted > 0 || result.TombstonesDeleted > 0 {
|
||||
fmt.Printf(" Issues deleted: %d\n", result.IssuesDeleted)
|
||||
if result.TombstonesDeleted > 0 {
|
||||
fmt.Printf(" Tombstones deleted: %d\n", result.TombstonesDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
if !skipInit {
|
||||
fmt.Printf("\n Workspace reinitialized. Run %s to get started.\n", cyan("bd quickstart"))
|
||||
} else {
|
||||
fmt.Printf("\n .beads/ directory has been cleared. Run %s to reinitialize.\n", cyan("bd init"))
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
},
|
||||
}
|
||||
|
||||
// actionNumber returns the step number accounting for backup offset
|
||||
func actionNumber(hasBackup bool, step int) string {
|
||||
if hasBackup {
|
||||
return fmt.Sprintf("%d", step+1)
|
||||
}
|
||||
return fmt.Sprintf("%d", step)
|
||||
}
|
||||
|
||||
// hardOffset adjusts step number for --hard mode which adds extra steps
|
||||
func hardOffset(isHard bool, step int) int {
|
||||
if isHard {
|
||||
return step + 1
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
func init() {
|
||||
resetCmd.Flags().Bool("hard", false, "Include git operations (git rm + commit)")
|
||||
resetCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
|
||||
resetCmd.Flags().Bool("backup", false, "Create backup before reset")
|
||||
resetCmd.Flags().Bool("dry-run", false, "Preview what would happen without making changes")
|
||||
resetCmd.Flags().Bool("skip-init", false, "Don't reinitialize after clearing")
|
||||
resetCmd.Flags().BoolP("verbose", "v", false, "Show detailed progress")
|
||||
rootCmd.AddCommand(resetCmd)
|
||||
}
|
||||
Reference in New Issue
Block a user