bd sync: 2025-12-23 13:49:07
This commit is contained in:
239
cmd/bd/doctor.go
239
cmd/bd/doctor.go
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -52,7 +53,6 @@ var (
|
||||
doctorInteractive bool // bd-3xl: per-fix confirmation mode
|
||||
doctorDryRun bool // bd-a5z: preview fixes without applying
|
||||
doctorOutput string // bd-9cc: export diagnostics to file
|
||||
doctorVerbose bool // bd-4qfb: show all checks including passed
|
||||
perfMode bool
|
||||
checkHealthMode bool
|
||||
)
|
||||
@@ -422,10 +422,6 @@ func applyFixList(path string, fixes []doctorCheck) {
|
||||
// No auto-fix: compaction requires agent review
|
||||
fmt.Printf(" ⚠ Run 'bd compact --analyze' to review candidates\n")
|
||||
continue
|
||||
case "Large Database":
|
||||
// No auto-fix: pruning deletes data, must be user-controlled
|
||||
fmt.Printf(" ⚠ Run 'bd cleanup --older-than 90' to prune old closed issues\n")
|
||||
continue
|
||||
default:
|
||||
fmt.Printf(" ⚠ No automatic fix available for %s\n", check.Name)
|
||||
fmt.Printf(" Manual fix: %s\n", check.Fix)
|
||||
@@ -821,12 +817,6 @@ func runDiagnostics(path string) doctorResult {
|
||||
result.Checks = append(result.Checks, compactionCheck)
|
||||
// Info only, not a warning - compaction requires human review
|
||||
|
||||
// Check 29: Database size (pruning suggestion)
|
||||
// Note: This check has no auto-fix - pruning is destructive and user-controlled
|
||||
sizeCheck := convertDoctorCheck(doctor.CheckDatabaseSize(path))
|
||||
result.Checks = append(result.Checks, sizeCheck)
|
||||
// Don't fail overall check for size warning, just inform
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -868,118 +858,136 @@ func exportDiagnostics(result doctorResult, outputPath string) error {
|
||||
}
|
||||
|
||||
func printDiagnostics(result doctorResult) {
|
||||
// Count checks by status and collect into categories
|
||||
var passCount, warnCount, failCount int
|
||||
var errors, warnings []doctorCheck
|
||||
passedByCategory := make(map[string][]doctorCheck)
|
||||
|
||||
for _, check := range result.Checks {
|
||||
switch check.Status {
|
||||
case statusOK:
|
||||
passCount++
|
||||
cat := check.Category
|
||||
if cat == "" {
|
||||
cat = "Other"
|
||||
}
|
||||
passedByCategory[cat] = append(passedByCategory[cat], check)
|
||||
case statusWarning:
|
||||
warnCount++
|
||||
warnings = append(warnings, check)
|
||||
case statusError:
|
||||
failCount++
|
||||
errors = append(errors, check)
|
||||
}
|
||||
}
|
||||
|
||||
// Print header with version and summary at TOP
|
||||
// Print header with version
|
||||
fmt.Printf("\nbd doctor v%s\n\n", result.CLIVersion)
|
||||
fmt.Printf("Summary: %d checks passed, %d warnings, %d errors\n", passCount, warnCount, failCount)
|
||||
|
||||
// Print errors section (always shown if any)
|
||||
if failCount > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
fmt.Printf("%s Errors (%d)\n", ui.RenderFailIcon(), failCount)
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
fmt.Println()
|
||||
// Group checks by category
|
||||
checksByCategory := make(map[string][]doctorCheck)
|
||||
for _, check := range result.Checks {
|
||||
cat := check.Category
|
||||
if cat == "" {
|
||||
cat = "Other"
|
||||
}
|
||||
checksByCategory[cat] = append(checksByCategory[cat], check)
|
||||
}
|
||||
|
||||
for _, check := range errors {
|
||||
fmt.Printf("[%s] %s\n", check.Name, check.Message)
|
||||
// Track counts
|
||||
var passCount, warnCount, failCount int
|
||||
var warnings []doctorCheck
|
||||
|
||||
// Print checks by category in defined order
|
||||
for _, category := range doctor.CategoryOrder {
|
||||
checks, exists := checksByCategory[category]
|
||||
if !exists || len(checks) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Print category header
|
||||
fmt.Println(ui.RenderCategory(category))
|
||||
|
||||
// Print each check in this category
|
||||
for _, check := range checks {
|
||||
// Determine status icon
|
||||
var statusIcon string
|
||||
switch check.Status {
|
||||
case statusOK:
|
||||
statusIcon = ui.RenderPassIcon()
|
||||
passCount++
|
||||
case statusWarning:
|
||||
statusIcon = ui.RenderWarnIcon()
|
||||
warnCount++
|
||||
warnings = append(warnings, check)
|
||||
case statusError:
|
||||
statusIcon = ui.RenderFailIcon()
|
||||
failCount++
|
||||
warnings = append(warnings, check)
|
||||
}
|
||||
|
||||
// Print check line: icon + name + message
|
||||
fmt.Printf(" %s %s", statusIcon, check.Name)
|
||||
if check.Message != "" {
|
||||
fmt.Printf("%s", ui.RenderMuted(" "+check.Message))
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// Print detail if present (indented)
|
||||
if check.Detail != "" {
|
||||
fmt.Printf(" %s\n", check.Detail)
|
||||
fmt.Printf(" %s%s\n", ui.MutedStyle.Render(ui.TreeLast), ui.RenderMuted(check.Detail))
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Print any checks without a category
|
||||
if otherChecks, exists := checksByCategory["Other"]; exists && len(otherChecks) > 0 {
|
||||
fmt.Println(ui.RenderCategory("Other"))
|
||||
for _, check := range otherChecks {
|
||||
var statusIcon string
|
||||
switch check.Status {
|
||||
case statusOK:
|
||||
statusIcon = ui.RenderPassIcon()
|
||||
passCount++
|
||||
case statusWarning:
|
||||
statusIcon = ui.RenderWarnIcon()
|
||||
warnCount++
|
||||
warnings = append(warnings, check)
|
||||
case statusError:
|
||||
statusIcon = ui.RenderFailIcon()
|
||||
failCount++
|
||||
warnings = append(warnings, check)
|
||||
}
|
||||
fmt.Printf(" %s %s", statusIcon, check.Name)
|
||||
if check.Message != "" {
|
||||
fmt.Printf("%s", ui.RenderMuted(" "+check.Message))
|
||||
}
|
||||
fmt.Println()
|
||||
if check.Detail != "" {
|
||||
fmt.Printf(" %s%s\n", ui.MutedStyle.Render(ui.TreeLast), ui.RenderMuted(check.Detail))
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Print summary line
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
summary := fmt.Sprintf("%s %d passed %s %d warnings %s %d failed",
|
||||
ui.RenderPassIcon(), passCount,
|
||||
ui.RenderWarnIcon(), warnCount,
|
||||
ui.RenderFailIcon(), failCount,
|
||||
)
|
||||
fmt.Println(summary)
|
||||
|
||||
// Print warnings/errors section with fixes
|
||||
if len(warnings) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println(ui.RenderWarn(ui.IconWarn + " WARNINGS"))
|
||||
|
||||
// Sort by severity: errors first, then warnings
|
||||
slices.SortStableFunc(warnings, func(a, b doctorCheck) int {
|
||||
// Errors (statusError) come before warnings (statusWarning)
|
||||
if a.Status == statusError && b.Status != statusError {
|
||||
return -1
|
||||
}
|
||||
if a.Status != statusError && b.Status == statusError {
|
||||
return 1
|
||||
}
|
||||
return 0 // maintain original order within same severity
|
||||
})
|
||||
|
||||
for i, check := range warnings {
|
||||
// Show numbered items with icon and color based on status
|
||||
// Errors get entire line in red, warnings just the number in yellow
|
||||
line := fmt.Sprintf("%s: %s", check.Name, check.Message)
|
||||
if check.Status == statusError {
|
||||
fmt.Printf(" %s %s %s\n", ui.RenderFailIcon(), ui.RenderFail(fmt.Sprintf("%d.", i+1)), ui.RenderFail(line))
|
||||
} else {
|
||||
fmt.Printf(" %s %s %s\n", ui.RenderWarnIcon(), ui.RenderWarn(fmt.Sprintf("%d.", i+1)), line)
|
||||
}
|
||||
if check.Fix != "" {
|
||||
fmt.Printf(" Fix: %s\n", check.Fix)
|
||||
fmt.Printf(" %s%s\n", ui.MutedStyle.Render(ui.TreeLast), check.Fix)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// Print warnings section (always shown if any)
|
||||
if warnCount > 0 {
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
fmt.Printf("%s Warnings (%d)\n", ui.RenderWarnIcon(), warnCount)
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
fmt.Println()
|
||||
|
||||
for _, check := range warnings {
|
||||
fmt.Printf("[%s] %s\n", check.Name, check.Message)
|
||||
if check.Detail != "" {
|
||||
fmt.Printf(" %s\n", check.Detail)
|
||||
}
|
||||
if check.Fix != "" {
|
||||
fmt.Printf(" Fix: %s\n", check.Fix)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// Print passed section
|
||||
if passCount > 0 {
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
if doctorVerbose {
|
||||
// Verbose mode: show all passed checks grouped by category
|
||||
fmt.Printf("%s Passed (%d)\n", ui.RenderPassIcon(), passCount)
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
fmt.Println()
|
||||
|
||||
for _, category := range doctor.CategoryOrder {
|
||||
checks, exists := passedByCategory[category]
|
||||
if !exists || len(checks) == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" %s\n", category)
|
||||
for _, check := range checks {
|
||||
fmt.Printf(" %s %s", ui.RenderPassIcon(), check.Name)
|
||||
if check.Message != "" {
|
||||
fmt.Printf(" %s", ui.RenderMuted(check.Message))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Print "Other" category if exists
|
||||
if otherChecks, exists := passedByCategory["Other"]; exists && len(otherChecks) > 0 {
|
||||
fmt.Printf(" %s\n", "Other")
|
||||
for _, check := range otherChecks {
|
||||
fmt.Printf(" %s %s", ui.RenderPassIcon(), check.Name)
|
||||
if check.Message != "" {
|
||||
fmt.Printf(" %s", ui.RenderMuted(check.Message))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
} else {
|
||||
// Default mode: collapsed summary
|
||||
fmt.Printf("%s Passed (%d) %s\n", ui.RenderPassIcon(), passCount, ui.RenderMuted("[use --verbose to show details]"))
|
||||
fmt.Println(ui.RenderSeparator())
|
||||
}
|
||||
}
|
||||
|
||||
// Final status message
|
||||
if failCount == 0 && warnCount == 0 {
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s\n", ui.RenderPass("✓ All checks passed"))
|
||||
}
|
||||
@@ -990,5 +998,4 @@ func init() {
|
||||
doctorCmd.Flags().BoolVar(&perfMode, "perf", false, "Run performance diagnostics and generate CPU profile")
|
||||
doctorCmd.Flags().BoolVar(&checkHealthMode, "check-health", false, "Quick health check for git hooks (silent on success)")
|
||||
doctorCmd.Flags().StringVarP(&doctorOutput, "output", "o", "", "Export diagnostics to JSON file (bd-9cc)")
|
||||
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show all checks including passed (bd-4qfb)")
|
||||
}
|
||||
|
||||
@@ -620,92 +620,3 @@ func isNoDbModeConfigured(beadsDir string) bool {
|
||||
|
||||
return cfg.NoDb
|
||||
}
|
||||
|
||||
// CheckDatabaseSize warns when the database has accumulated many closed issues.
|
||||
// This is purely informational - pruning is NEVER auto-fixed because it
|
||||
// permanently deletes data. Users must explicitly run 'bd cleanup' to prune.
|
||||
//
|
||||
// Config: doctor.suggest_pruning_issue_count (default: 5000, 0 = disabled)
|
||||
//
|
||||
// DESIGN NOTE: This check intentionally has NO auto-fix. Unlike other doctor
|
||||
// checks that fix configuration or sync issues, pruning is destructive and
|
||||
// irreversible. The user must make an explicit decision to delete their
|
||||
// closed issue history. We only provide guidance, never action.
|
||||
func CheckDatabaseSize(path string) DoctorCheck {
|
||||
beadsDir := filepath.Join(path, ".beads")
|
||||
|
||||
// Get database path
|
||||
var dbPath string
|
||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||
dbPath = cfg.DatabasePath(beadsDir)
|
||||
} else {
|
||||
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
||||
}
|
||||
|
||||
// If no database, skip this check
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Large Database",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no database)",
|
||||
}
|
||||
}
|
||||
|
||||
// Read threshold from config (default 5000, 0 = disabled)
|
||||
threshold := 5000
|
||||
db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro&_pragma=busy_timeout(30000)")
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Large Database",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (unable to open database)",
|
||||
}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Check for custom threshold in config table
|
||||
var thresholdStr string
|
||||
err = db.QueryRow("SELECT value FROM config WHERE key = ?", "doctor.suggest_pruning_issue_count").Scan(&thresholdStr)
|
||||
if err == nil {
|
||||
if _, err := fmt.Sscanf(thresholdStr, "%d", &threshold); err != nil {
|
||||
threshold = 5000 // Reset to default on parse error
|
||||
}
|
||||
}
|
||||
|
||||
// If disabled, return OK
|
||||
if threshold == 0 {
|
||||
return DoctorCheck{
|
||||
Name: "Large Database",
|
||||
Status: StatusOK,
|
||||
Message: "Check disabled (threshold = 0)",
|
||||
}
|
||||
}
|
||||
|
||||
// Count closed issues
|
||||
var closedCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM issues WHERE status = 'closed'").Scan(&closedCount)
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Large Database",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (unable to count issues)",
|
||||
}
|
||||
}
|
||||
|
||||
// Check against threshold
|
||||
if closedCount > threshold {
|
||||
return DoctorCheck{
|
||||
Name: "Large Database",
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d closed issues (threshold: %d)", closedCount, threshold),
|
||||
Detail: "Large number of closed issues may impact performance",
|
||||
Fix: "Consider running 'bd cleanup --older-than 90' to prune old closed issues",
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Large Database",
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("%d closed issues (threshold: %d)", closedCount, threshold),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +145,6 @@ func CheckSyncBranchHookCompatibility(path string) DoctorCheck {
|
||||
Status: StatusWarning,
|
||||
Message: "Pre-push hook is not a bd hook",
|
||||
Detail: "Cannot verify sync-branch compatibility with custom hooks",
|
||||
Fix: "Either run 'bd hooks install --force' to use bd hooks,\n" +
|
||||
" or ensure your custom hook skips validation when pushing to sync-branch",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ func CheckLegacyJSONLFilename(repoPath string) DoctorCheck {
|
||||
Detail: "Having multiple JSONL files can cause sync and merge conflicts.\n" +
|
||||
" Only one JSONL file should be used per repository.",
|
||||
Fix: "Determine which file is current and remove the others:\n" +
|
||||
" 1. Check .beads/metadata.json for 'jsonl_export' setting\n" +
|
||||
" 1. Check 'bd stats' to see which file is being used\n" +
|
||||
" 2. Verify with 'git log .beads/*.jsonl' to see commit history\n" +
|
||||
" 3. Remove the unused file(s): git rm .beads/<unused>.jsonl\n" +
|
||||
" 4. Commit the change",
|
||||
|
||||
104
cmd/bd/search.go
104
cmd/bd/search.go
@@ -26,14 +26,9 @@ Examples:
|
||||
bd search "database" --label backend --limit 10
|
||||
bd search --query "performance" --assignee alice
|
||||
bd search "bd-5q" # Search by partial ID
|
||||
bd search "security" --priority 1 # Exact priority match
|
||||
bd search "security" --priority-min 0 --priority-max 2 # Priority range
|
||||
bd search "security" --priority-min 0 --priority-max 2
|
||||
bd search "bug" --created-after 2025-01-01
|
||||
bd search "refactor" --updated-after 2025-01-01 --priority-min 1
|
||||
bd search "bug" --desc-contains "authentication" # Search in description
|
||||
bd search "" --empty-description # Issues without description
|
||||
bd search "" --no-assignee # Unassigned issues
|
||||
bd search "" --no-labels # Issues without labels
|
||||
bd search "bug" --sort priority
|
||||
bd search "task" --sort created --reverse`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@@ -46,31 +41,9 @@ Examples:
|
||||
query = queryFlag
|
||||
}
|
||||
|
||||
// Check if any filter flags are set (allows empty query with filters)
|
||||
hasFilters := cmd.Flags().Changed("status") ||
|
||||
cmd.Flags().Changed("priority") ||
|
||||
cmd.Flags().Changed("assignee") ||
|
||||
cmd.Flags().Changed("type") ||
|
||||
cmd.Flags().Changed("label") ||
|
||||
cmd.Flags().Changed("label-any") ||
|
||||
cmd.Flags().Changed("created-after") ||
|
||||
cmd.Flags().Changed("created-before") ||
|
||||
cmd.Flags().Changed("updated-after") ||
|
||||
cmd.Flags().Changed("updated-before") ||
|
||||
cmd.Flags().Changed("closed-after") ||
|
||||
cmd.Flags().Changed("closed-before") ||
|
||||
cmd.Flags().Changed("priority-min") ||
|
||||
cmd.Flags().Changed("priority-max") ||
|
||||
cmd.Flags().Changed("title-contains") ||
|
||||
cmd.Flags().Changed("desc-contains") ||
|
||||
cmd.Flags().Changed("notes-contains") ||
|
||||
cmd.Flags().Changed("empty-description") ||
|
||||
cmd.Flags().Changed("no-assignee") ||
|
||||
cmd.Flags().Changed("no-labels")
|
||||
|
||||
// If no query and no filters provided, show help
|
||||
if query == "" && !hasFilters {
|
||||
fmt.Fprintf(os.Stderr, "Error: search query or filter is required\n")
|
||||
// If no query provided, show help
|
||||
if query == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: search query is required\n")
|
||||
if err := cmd.Help(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error displaying help: %v\n", err)
|
||||
}
|
||||
@@ -88,11 +61,6 @@ Examples:
|
||||
sortBy, _ := cmd.Flags().GetString("sort")
|
||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
||||
|
||||
// Pattern matching flags
|
||||
titleContains, _ := cmd.Flags().GetString("title-contains")
|
||||
descContains, _ := cmd.Flags().GetString("desc-contains")
|
||||
notesContains, _ := cmd.Flags().GetString("notes-contains")
|
||||
|
||||
// Date range flags
|
||||
createdAfter, _ := cmd.Flags().GetString("created-after")
|
||||
createdBefore, _ := cmd.Flags().GetString("created-before")
|
||||
@@ -101,11 +69,6 @@ Examples:
|
||||
closedAfter, _ := cmd.Flags().GetString("closed-after")
|
||||
closedBefore, _ := cmd.Flags().GetString("closed-before")
|
||||
|
||||
// Empty/null check flags
|
||||
emptyDesc, _ := cmd.Flags().GetBool("empty-description")
|
||||
noAssignee, _ := cmd.Flags().GetBool("no-assignee")
|
||||
noLabels, _ := cmd.Flags().GetBool("no-labels")
|
||||
|
||||
// Priority range flags
|
||||
priorityMinStr, _ := cmd.Flags().GetString("priority-min")
|
||||
priorityMaxStr, _ := cmd.Flags().GetString("priority-max")
|
||||
@@ -141,39 +104,6 @@ Examples:
|
||||
filter.LabelsAny = labelsAny
|
||||
}
|
||||
|
||||
// Exact priority match (use Changed() to properly handle P0)
|
||||
if cmd.Flags().Changed("priority") {
|
||||
priorityStr, _ := cmd.Flags().GetString("priority")
|
||||
priority, err := validation.ValidatePriority(priorityStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
filter.Priority = &priority
|
||||
}
|
||||
|
||||
// Pattern matching
|
||||
if titleContains != "" {
|
||||
filter.TitleContains = titleContains
|
||||
}
|
||||
if descContains != "" {
|
||||
filter.DescriptionContains = descContains
|
||||
}
|
||||
if notesContains != "" {
|
||||
filter.NotesContains = notesContains
|
||||
}
|
||||
|
||||
// Empty/null checks
|
||||
if emptyDesc {
|
||||
filter.EmptyDescription = true
|
||||
}
|
||||
if noAssignee {
|
||||
filter.NoAssignee = true
|
||||
}
|
||||
if noLabels {
|
||||
filter.NoLabels = true
|
||||
}
|
||||
|
||||
// Date ranges
|
||||
if createdAfter != "" {
|
||||
t, err := parseTimeFlag(createdAfter)
|
||||
@@ -270,21 +200,6 @@ Examples:
|
||||
listArgs.LabelsAny = labelsAny
|
||||
}
|
||||
|
||||
// Exact priority match
|
||||
if filter.Priority != nil {
|
||||
listArgs.Priority = filter.Priority
|
||||
}
|
||||
|
||||
// Pattern matching
|
||||
listArgs.TitleContains = titleContains
|
||||
listArgs.DescriptionContains = descContains
|
||||
listArgs.NotesContains = notesContains
|
||||
|
||||
// Empty/null checks
|
||||
listArgs.EmptyDescription = filter.EmptyDescription
|
||||
listArgs.NoAssignee = filter.NoAssignee
|
||||
listArgs.NoLabels = filter.NoLabels
|
||||
|
||||
// Date ranges
|
||||
if filter.CreatedAfter != nil {
|
||||
listArgs.CreatedAfter = filter.CreatedAfter.Format(time.RFC3339)
|
||||
@@ -457,7 +372,6 @@ func outputSearchResults(issues []*types.Issue, query string, longFormat bool) {
|
||||
func init() {
|
||||
searchCmd.Flags().String("query", "", "Search query (alternative to positional argument)")
|
||||
searchCmd.Flags().StringP("status", "s", "", "Filter by status (open, in_progress, blocked, deferred, closed)")
|
||||
registerPriorityFlag(searchCmd, "")
|
||||
searchCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
|
||||
searchCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore, merge-request, molecule, gate)")
|
||||
searchCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL)")
|
||||
@@ -467,11 +381,6 @@ func init() {
|
||||
searchCmd.Flags().String("sort", "", "Sort by field: priority, created, updated, closed, status, id, title, type, assignee")
|
||||
searchCmd.Flags().BoolP("reverse", "r", false, "Reverse sort order")
|
||||
|
||||
// Pattern matching flags
|
||||
searchCmd.Flags().String("title-contains", "", "Filter by title substring (case-insensitive)")
|
||||
searchCmd.Flags().String("desc-contains", "", "Filter by description substring (case-insensitive)")
|
||||
searchCmd.Flags().String("notes-contains", "", "Filter by notes substring (case-insensitive)")
|
||||
|
||||
// Date range flags
|
||||
searchCmd.Flags().String("created-after", "", "Filter issues created after date (YYYY-MM-DD or RFC3339)")
|
||||
searchCmd.Flags().String("created-before", "", "Filter issues created before date (YYYY-MM-DD or RFC3339)")
|
||||
@@ -480,11 +389,6 @@ func init() {
|
||||
searchCmd.Flags().String("closed-after", "", "Filter issues closed after date (YYYY-MM-DD or RFC3339)")
|
||||
searchCmd.Flags().String("closed-before", "", "Filter issues closed before date (YYYY-MM-DD or RFC3339)")
|
||||
|
||||
// Empty/null check flags
|
||||
searchCmd.Flags().Bool("empty-description", false, "Filter issues with empty or missing description")
|
||||
searchCmd.Flags().Bool("no-assignee", false, "Filter issues with no assignee")
|
||||
searchCmd.Flags().Bool("no-labels", false, "Filter issues with no labels")
|
||||
|
||||
// Priority range flags
|
||||
searchCmd.Flags().String("priority-min", "", "Filter by minimum priority (inclusive, 0-4 or P0-P4)")
|
||||
searchCmd.Flags().String("priority-max", "", "Filter by maximum priority (inclusive, 0-4 or P0-P4)")
|
||||
|
||||
@@ -972,10 +972,6 @@ var closeCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CheckReadonly("close")
|
||||
reason, _ := cmd.Flags().GetString("reason")
|
||||
// Check --resolution alias if --reason not provided
|
||||
if reason == "" {
|
||||
reason, _ = cmd.Flags().GetString("resolution")
|
||||
}
|
||||
if reason == "" {
|
||||
reason = "Closed"
|
||||
}
|
||||
@@ -1057,8 +1053,6 @@ var closeCmd = &cobra.Command{
|
||||
if hookRunner != nil {
|
||||
hookRunner.Run(hooks.EventClose, &issue)
|
||||
}
|
||||
// Run config-based close hooks (bd-g4b4)
|
||||
hooks.RunConfigCloseHooks(ctx, &issue)
|
||||
if jsonOutput {
|
||||
closedIssues = append(closedIssues, &issue)
|
||||
}
|
||||
@@ -1111,12 +1105,8 @@ var closeCmd = &cobra.Command{
|
||||
|
||||
// Run close hook (bd-kwro.8)
|
||||
closedIssue, _ := store.GetIssue(ctx, id)
|
||||
if closedIssue != nil {
|
||||
if hookRunner != nil {
|
||||
hookRunner.Run(hooks.EventClose, closedIssue)
|
||||
}
|
||||
// Run config-based close hooks (bd-g4b4)
|
||||
hooks.RunConfigCloseHooks(ctx, closedIssue)
|
||||
if closedIssue != nil && hookRunner != nil {
|
||||
hookRunner.Run(hooks.EventClose, closedIssue)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
@@ -1421,8 +1411,6 @@ func init() {
|
||||
rootCmd.AddCommand(editCmd)
|
||||
|
||||
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
||||
closeCmd.Flags().String("resolution", "", "Alias for --reason (Jira CLI convention)")
|
||||
_ = closeCmd.Flags().MarkHidden("resolution") // Hidden alias for agent/CLI ergonomics
|
||||
closeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
closeCmd.Flags().BoolP("force", "f", false, "Force close pinned issues")
|
||||
closeCmd.Flags().Bool("continue", false, "Auto-advance to next step in molecule")
|
||||
|
||||
16
cmd/bd/testdata/close_resolution_alias.txt
vendored
16
cmd/bd/testdata/close_resolution_alias.txt
vendored
@@ -1,16 +0,0 @@
|
||||
# Test bd close --resolution alias (GH#721)
|
||||
# Jira CLI convention: --resolution instead of --reason
|
||||
bd init --prefix test
|
||||
|
||||
# Create issue
|
||||
bd create 'Issue to close with resolution'
|
||||
cp stdout issue.txt
|
||||
exec sh -c 'grep -oE "test-[a-z0-9]+" issue.txt > issue_id.txt'
|
||||
|
||||
# Close using --resolution alias
|
||||
exec sh -c 'bd close $(cat issue_id.txt) --resolution "Fixed via resolution alias"'
|
||||
stdout 'Closed test-'
|
||||
|
||||
# Verify close_reason is set correctly
|
||||
exec sh -c 'bd show $(cat issue_id.txt) --json'
|
||||
stdout 'Fixed via resolution alias'
|
||||
Reference in New Issue
Block a user