Implements the bd reset command for GitHub issue #479: - CLI command with flags: --hard, --force, --backup, --dry-run, --skip-init, --verbose - Impact summary showing issues/tombstones to be deleted - Confirmation prompt (skippable with --force) - Colored output for better UX - Unit tests for reset.go and git.go - Fix: use --force flag in git rm to handle staged files Part of epic bd-aydr. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
package reset
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// GitState represents the current state of the git repository
|
|
type GitState struct {
|
|
IsRepo bool // Is this a git repository?
|
|
IsDirty bool // Are there uncommitted changes?
|
|
IsDetached bool // Is HEAD detached?
|
|
Branch string // Current branch name (empty if detached)
|
|
}
|
|
|
|
// CheckGitState detects the current git repository state
|
|
func CheckGitState(beadsDir string) (*GitState, error) {
|
|
state := &GitState{}
|
|
|
|
// Check if we're in a git repository
|
|
cmd := exec.Command("git", "rev-parse", "--git-dir")
|
|
if err := cmd.Run(); err != nil {
|
|
// Not a git repo - this is OK, we'll skip git operations gracefully
|
|
state.IsRepo = false
|
|
return state, nil
|
|
}
|
|
state.IsRepo = true
|
|
|
|
// Check if there are uncommitted changes specifically in .beads/
|
|
// (not the entire repo, just the beads directory)
|
|
cmd = exec.Command("git", "status", "--porcelain", "--", beadsDir)
|
|
var statusOut bytes.Buffer
|
|
cmd.Stdout = &statusOut
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, fmt.Errorf("failed to check git status: %w", err)
|
|
}
|
|
state.IsDirty = len(strings.TrimSpace(statusOut.String())) > 0
|
|
|
|
// Check if HEAD is detached and get current branch
|
|
cmd = exec.Command("git", "symbolic-ref", "-q", "HEAD")
|
|
var branchOut bytes.Buffer
|
|
cmd.Stdout = &branchOut
|
|
err := cmd.Run()
|
|
|
|
if err != nil {
|
|
// symbolic-ref fails on detached HEAD
|
|
state.IsDetached = true
|
|
state.Branch = ""
|
|
} else {
|
|
state.IsDetached = false
|
|
// Extract branch name from refs/heads/branch-name
|
|
fullRef := strings.TrimSpace(branchOut.String())
|
|
state.Branch = strings.TrimPrefix(fullRef, "refs/heads/")
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
// GitRemoveBeads uses git rm to remove the JSONL files from the index
|
|
// This prepares for a reset by staging the removal of beads files
|
|
func GitRemoveBeads(beadsDir string) error {
|
|
// Find all JSONL files in the beads directory
|
|
// We support both canonical (issues.jsonl) and legacy (beads.jsonl) names
|
|
jsonlFiles := []string{
|
|
filepath.Join(beadsDir, "issues.jsonl"),
|
|
filepath.Join(beadsDir, "beads.jsonl"),
|
|
}
|
|
|
|
// Try to remove each file (git rm ignores non-existent files with --ignore-unmatch)
|
|
// Use --force to handle files with staged changes
|
|
for _, file := range jsonlFiles {
|
|
cmd := exec.Command("git", "rm", "--ignore-unmatch", "--quiet", "--force", file)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to git rm %s: %w\nstderr: %s", file, err, stderr.String())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GitCommitReset creates a commit with the removal of beads files
|
|
// Returns nil without error if there's nothing to commit
|
|
func GitCommitReset(message string) error {
|
|
// First check if there are any staged changes
|
|
cmd := exec.Command("git", "diff", "--cached", "--quiet")
|
|
if err := cmd.Run(); err == nil {
|
|
// Exit code 0 means no staged changes - nothing to commit
|
|
return nil
|
|
}
|
|
// Exit code 1 means there are staged changes - proceed with commit
|
|
|
|
cmd = exec.Command("git", "commit", "-m", message)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to commit reset: %w\nstderr: %s", err, stderr.String())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GitAddAndCommit stages the beads directory and creates a commit with fresh state
|
|
func GitAddAndCommit(beadsDir, message string) error {
|
|
// Add the entire beads directory (this will pick up the fresh JSONL)
|
|
cmd := exec.Command("git", "add", beadsDir)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to git add %s: %w\nstderr: %s", beadsDir, err, stderr.String())
|
|
}
|
|
|
|
// Create the commit
|
|
cmd = exec.Command("git", "commit", "-m", message)
|
|
stderr.Reset()
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to commit fresh state: %w\nstderr: %s", err, stderr.String())
|
|
}
|
|
|
|
return nil
|
|
}
|