Files
beads/cmd/bd/orphans.go
Steve Yegge abd3feb761 fix(lint): check error return from provider.Close() (#1250)
Two lint errors introduced in recent commits:
- cmd/bd/doctor/git.go:896 - unchecked provider.Close()
- cmd/bd/orphans.go:120 - unchecked provider.Close()

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 21:11:33 -08:00

169 lines
5.2 KiB
Go

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/storage"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
)
// doctorFindOrphanedIssues is the function used to find orphaned issues.
// It accepts a git path and an IssueProvider for flexibility (cross-repo, mock testing).
var doctorFindOrphanedIssues = doctor.FindOrphanedIssues
var closeIssueRunner = func(issueID string) error {
cmd := exec.Command("bd", "close", issueID, "--reason", "Implemented")
return cmd.Run()
}
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"`
}
// getIssueProvider returns an IssueProvider based on the current configuration.
// If --db flag is set, it creates a provider from that database path.
// Otherwise, it uses the global store (already opened in PersistentPreRun).
func getIssueProvider() (types.IssueProvider, func(), error) {
// If --db flag is set and we have a dbPath, create a provider from that path
if dbPath != "" {
provider, err := storage.NewLocalProvider(dbPath)
if err != nil {
return nil, nil, fmt.Errorf("failed to open database at %s: %w", dbPath, err)
}
return provider, func() { _ = provider.Close() }, nil
}
// Use the global store (already opened by PersistentPreRun)
if store != nil {
provider := storage.NewStorageProvider(store)
return provider, func() {}, nil // No cleanup needed for global store
}
return nil, nil, fmt.Errorf("no database available")
}
// findOrphanedIssues wraps the shared doctor package function and converts to output format.
// It respects the --db flag for cross-repo orphan detection.
func findOrphanedIssues(path string) ([]orphanIssueOutput, error) {
provider, cleanup, err := getIssueProvider()
if err != nil {
return nil, fmt.Errorf("unable to find orphaned issues: %w", err)
}
defer cleanup()
orphans, err := doctorFindOrphanedIssues(path, provider)
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 {
return closeIssueRunner(issueID)
}
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)
}