bd cleanup: Remove 493 closed issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-20 20:39:29 -05:00
5 changed files with 62 additions and 21 deletions

View File

@@ -12,23 +12,34 @@ import (
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
) )
// isJSONLNewer checks if JSONL file is newer than database file.
// Returns true if JSONL is newer, false otherwise.
func isJSONLNewer(jsonlPath string) bool {
jsonlInfo, jsonlStatErr := os.Stat(jsonlPath)
if jsonlStatErr != nil {
return false
}
beadsDir := filepath.Dir(jsonlPath)
dbPath := filepath.Join(beadsDir, "beads.db")
dbInfo, dbStatErr := os.Stat(dbPath)
if dbStatErr != nil {
return false
}
return jsonlInfo.ModTime().After(dbInfo.ModTime())
}
// validatePreExport performs integrity checks before exporting database to JSONL. // validatePreExport performs integrity checks before exporting database to JSONL.
// Returns error if critical issues found that would cause data loss. // Returns error if critical issues found that would cause data loss.
func validatePreExport(ctx context.Context, store storage.Storage, jsonlPath string) error { func validatePreExport(ctx context.Context, store storage.Storage, jsonlPath string) error {
// Check if JSONL is newer than database - if so, must import first // Check if JSONL is newer than database - if so, must import first
jsonlInfo, jsonlStatErr := os.Stat(jsonlPath) if isJSONLNewer(jsonlPath) {
if jsonlStatErr == nil { return fmt.Errorf("refusing to export: JSONL is newer than database (import first to avoid data loss)")
beadsDir := filepath.Dir(jsonlPath)
dbPath := filepath.Join(beadsDir, "beads.db")
dbInfo, dbStatErr := os.Stat(dbPath)
if dbStatErr == nil {
// If JSONL is newer, refuse export - caller must import first
if jsonlInfo.ModTime().After(dbInfo.ModTime()) {
return fmt.Errorf("refusing to export: JSONL is newer than database (import first to avoid data loss)")
}
}
} }
jsonlInfo, jsonlStatErr := os.Stat(jsonlPath)
// Get database issue count (fast path with COUNT(*) if available) // Get database issue count (fast path with COUNT(*) if available)
dbCount, err := countDBIssuesFast(ctx, store) dbCount, err := countDBIssuesFast(ctx, store)
if err != nil { if err != nil {

View File

@@ -339,18 +339,19 @@ var listCmd = &cobra.Command{
} }
if jsonOutput { if jsonOutput {
// Populate labels for JSON output // Get labels and dependency counts in bulk (single query instead of N queries)
for _, issue := range issues {
issue.Labels, _ = store.GetLabels(ctx, issue.ID)
}
// Get dependency counts in bulk (single query instead of N queries)
issueIDs := make([]string, len(issues)) issueIDs := make([]string, len(issues))
for i, issue := range issues { for i, issue := range issues {
issueIDs[i] = issue.ID issueIDs[i] = issue.ID
} }
labelsMap, _ := store.GetLabelsForIssues(ctx, issueIDs)
depCounts, _ := store.GetDependencyCounts(ctx, issueIDs) depCounts, _ := store.GetDependencyCounts(ctx, issueIDs)
// Populate labels for JSON output
for _, issue := range issues {
issue.Labels = labelsMap[issue.ID]
}
// Build response with counts // Build response with counts
issuesWithCounts := make([]*types.IssueWithCounts, len(issues)) issuesWithCounts := make([]*types.IssueWithCounts, len(issues))
for i, issue := range issues { for i, issue := range issues {
@@ -368,12 +369,18 @@ var listCmd = &cobra.Command{
return return
} }
// Load labels in bulk for display
issueIDs := make([]string, len(issues))
for i, issue := range issues {
issueIDs[i] = issue.ID
}
labelsMap, _ := store.GetLabelsForIssues(ctx, issueIDs)
if longFormat { if longFormat {
// Long format: multi-line with details // Long format: multi-line with details
fmt.Printf("\nFound %d issues:\n\n", len(issues)) fmt.Printf("\nFound %d issues:\n\n", len(issues))
for _, issue := range issues { for _, issue := range issues {
// Load labels for display labels := labelsMap[issue.ID]
labels, _ := store.GetLabels(ctx, issue.ID)
fmt.Printf("%s [P%d] [%s] %s\n", issue.ID, issue.Priority, issue.IssueType, issue.Status) fmt.Printf("%s [P%d] [%s] %s\n", issue.ID, issue.Priority, issue.IssueType, issue.Status)
fmt.Printf(" %s\n", issue.Title) fmt.Printf(" %s\n", issue.Title)
@@ -388,8 +395,7 @@ var listCmd = &cobra.Command{
} else { } else {
// Compact format: one line per issue // Compact format: one line per issue
for _, issue := range issues { for _, issue := range issues {
// Load labels for display labels := labelsMap[issue.ID]
labels, _ := store.GetLabels(ctx, issue.ID)
labelsStr := "" labelsStr := ""
if len(labels) > 0 { if len(labels) > 0 {

View File

@@ -126,6 +126,16 @@ Use --merge to merge the sync branch back to main branch.`,
if dryRun { if dryRun {
fmt.Println("→ [DRY RUN] Would export pending changes to JSONL") fmt.Println("→ [DRY RUN] Would export pending changes to JSONL")
} else { } else {
// Smart conflict resolution: if JSONL is newer, auto-import first
if isJSONLNewer(jsonlPath) {
fmt.Println("→ JSONL is newer than database, importing first...")
if err := importFromJSONL(ctx, jsonlPath, renameOnImport); err != nil {
fmt.Fprintf(os.Stderr, "Error auto-importing: %v\n", err)
os.Exit(1)
}
fmt.Println("✓ Auto-import complete")
}
// Pre-export integrity checks // Pre-export integrity checks
if err := ensureStoreActive(); err == nil && store != nil { if err := ensureStoreActive(); err == nil && store != nil {
if err := validatePreExport(ctx, store, jsonlPath); err != nil { if err := validatePreExport(ctx, store, jsonlPath); err != nil {

View File

@@ -791,6 +791,19 @@ func (m *MemoryStorage) GetLabels(ctx context.Context, issueID string) ([]string
return m.labels[issueID], nil return m.labels[issueID], nil
} }
func (m *MemoryStorage) GetLabelsForIssues(ctx context.Context, issueIDs []string) (map[string][]string, error) {
m.mu.RLock()
defer m.mu.RUnlock()
result := make(map[string][]string)
for _, issueID := range issueIDs {
if labels, exists := m.labels[issueID]; exists {
result[issueID] = labels
}
}
return result, nil
}
func (m *MemoryStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error) { func (m *MemoryStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()

View File

@@ -35,6 +35,7 @@ type Storage interface {
AddLabel(ctx context.Context, issueID, label, actor string) error AddLabel(ctx context.Context, issueID, label, actor string) error
RemoveLabel(ctx context.Context, issueID, label, actor string) error RemoveLabel(ctx context.Context, issueID, label, actor string) error
GetLabels(ctx context.Context, issueID string) ([]string, error) GetLabels(ctx context.Context, issueID string) ([]string, error)
GetLabelsForIssues(ctx context.Context, issueIDs []string) (map[string][]string, error)
GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error) GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error)
// Ready Work & Blocking // Ready Work & Blocking