feat: Add 'bd orphans' command with DRY refactoring of orphan detection

Implements the 'bd orphans' command to identify issues referenced in commits
but still open in the database, and refactors to eliminate code duplication.

- Creates cmd/bd/orphans.go with Cobra command structure
- Identifies orphaned issues (referenced in git commits but still open/in_progress)
- Supports multiple output formats (human, JSON, detailed)
- Auto-close with --fix flag

DRY refactoring:
- Extracted FindOrphanedIssues() to cmd/bd/doctor/git.go as shared core logic
- Moved OrphanIssue type to cmd/bd/doctor/types.go
- Refactored CheckOrphanedIssues() to use FindOrphanedIssues()

Cherry-picked from PR #767

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
matt wilkie
2025-12-27 17:23:45 -08:00
committed by Steve Yegge
parent d71601d98e
commit 99f5e50a32
4 changed files with 374 additions and 87 deletions

129
cmd/bd/orphans.go Normal file
View File

@@ -0,0 +1,129 @@
package main
import (
"fmt"
"os"
"os/exec"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/ui"
)
var orphansCmd = &cobra.Command{
Use: "orphans",
Short: "Identify orphaned issues (referenced in commits but still open)",
Long: `Identify orphaned issues - issues that are referenced in commit messages but remain open or in_progress in the database.
This helps identify work that has been implemented but not formally closed.
Examples:
bd orphans # Show orphaned issues
bd orphans --json # Machine-readable output
bd orphans --details # Show full commit information
bd orphans --fix # Close orphaned issues with confirmation`,
Run: func(cmd *cobra.Command, args []string) {
path := "."
orphans, err := findOrphanedIssues(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fix, _ := cmd.Flags().GetBool("fix")
details, _ := cmd.Flags().GetBool("details")
if jsonOutput {
outputJSON(orphans)
return
}
if len(orphans) == 0 {
fmt.Printf("%s No orphaned issues found\n", ui.RenderPass("✓"))
return
}
fmt.Printf("\n%s Found %d orphaned issue(s):\n\n", ui.RenderWarn("⚠"), len(orphans))
// Sort by issue ID for consistent output
sort.Slice(orphans, func(i, j int) bool {
return orphans[i].IssueID < orphans[j].IssueID
})
for i, orphan := range orphans {
fmt.Printf("%d. %s: %s\n", i+1, ui.RenderID(orphan.IssueID), orphan.Title)
fmt.Printf(" Status: %s\n", orphan.Status)
if details && orphan.LatestCommit != "" {
fmt.Printf(" Latest commit: %s - %s\n", orphan.LatestCommit, orphan.LatestCommitMessage)
}
}
if fix {
fmt.Println()
fmt.Printf("This will close %d orphaned issue(s). Continue? (Y/n): ", len(orphans))
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "" && response != "y" && response != "yes" {
fmt.Println("Canceled.")
return
}
// Close orphaned issues
closedCount := 0
for _, orphan := range orphans {
err := closeIssue(orphan.IssueID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", orphan.IssueID, err)
} else {
fmt.Printf("✓ Closed %s\n", orphan.IssueID)
closedCount++
}
}
fmt.Printf("\nClosed %d issue(s)\n", closedCount)
}
},
}
// orphanIssueOutput is the JSON output format for orphaned issues
type orphanIssueOutput struct {
IssueID string `json:"issue_id"`
Title string `json:"title"`
Status string `json:"status"`
LatestCommit string `json:"latest_commit,omitempty"`
LatestCommitMessage string `json:"latest_commit_message,omitempty"`
}
// findOrphanedIssues wraps the shared doctor package function and converts to output format
func findOrphanedIssues(path string) ([]orphanIssueOutput, error) {
orphans, err := doctor.FindOrphanedIssues(path)
if err != nil {
return nil, fmt.Errorf("unable to find orphaned issues: %w", err)
}
var output []orphanIssueOutput
for _, orphan := range orphans {
output = append(output, orphanIssueOutput{
IssueID: orphan.IssueID,
Title: orphan.Title,
Status: orphan.Status,
LatestCommit: orphan.LatestCommit,
LatestCommitMessage: orphan.LatestCommitMessage,
})
}
return output, nil
}
// closeIssue closes an issue using bd close
func closeIssue(issueID string) error {
cmd := exec.Command("bd", "close", issueID, "--reason", "Implemented")
return cmd.Run()
}
func init() {
orphansCmd.Flags().BoolP("fix", "f", false, "Close orphaned issues with confirmation")
orphansCmd.Flags().Bool("details", false, "Show full commit information")
rootCmd.AddCommand(orphansCmd)
}