feat: add date, priority, and content filters to bd search (bd-au0.5)
Add parity with bd list for filtering: - Exact priority match with --priority/-p - Pattern matching: --title-contains, --desc-contains, --notes-contains - Empty/null checks: --empty-description, --no-assignee, --no-labels - Allow empty query when filters are provided (e.g., bd search --no-assignee) All filters work in both direct and daemon mode via RPC. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
104
cmd/bd/search.go
104
cmd/bd/search.go
@@ -26,9 +26,14 @@ Examples:
|
|||||||
bd search "database" --label backend --limit 10
|
bd search "database" --label backend --limit 10
|
||||||
bd search --query "performance" --assignee alice
|
bd search --query "performance" --assignee alice
|
||||||
bd search "bd-5q" # Search by partial ID
|
bd search "bd-5q" # Search by partial ID
|
||||||
bd search "security" --priority-min 0 --priority-max 2
|
bd search "security" --priority 1 # Exact priority match
|
||||||
|
bd search "security" --priority-min 0 --priority-max 2 # Priority range
|
||||||
bd search "bug" --created-after 2025-01-01
|
bd search "bug" --created-after 2025-01-01
|
||||||
bd search "refactor" --updated-after 2025-01-01 --priority-min 1
|
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 "bug" --sort priority
|
||||||
bd search "task" --sort created --reverse`,
|
bd search "task" --sort created --reverse`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
@@ -41,9 +46,31 @@ Examples:
|
|||||||
query = queryFlag
|
query = queryFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no query provided, show help
|
// Check if any filter flags are set (allows empty query with filters)
|
||||||
if query == "" {
|
hasFilters := cmd.Flags().Changed("status") ||
|
||||||
fmt.Fprintf(os.Stderr, "Error: search query is required\n")
|
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 err := cmd.Help(); err != nil {
|
if err := cmd.Help(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error displaying help: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error displaying help: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -61,6 +88,11 @@ Examples:
|
|||||||
sortBy, _ := cmd.Flags().GetString("sort")
|
sortBy, _ := cmd.Flags().GetString("sort")
|
||||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
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
|
// Date range flags
|
||||||
createdAfter, _ := cmd.Flags().GetString("created-after")
|
createdAfter, _ := cmd.Flags().GetString("created-after")
|
||||||
createdBefore, _ := cmd.Flags().GetString("created-before")
|
createdBefore, _ := cmd.Flags().GetString("created-before")
|
||||||
@@ -69,6 +101,11 @@ Examples:
|
|||||||
closedAfter, _ := cmd.Flags().GetString("closed-after")
|
closedAfter, _ := cmd.Flags().GetString("closed-after")
|
||||||
closedBefore, _ := cmd.Flags().GetString("closed-before")
|
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
|
// Priority range flags
|
||||||
priorityMinStr, _ := cmd.Flags().GetString("priority-min")
|
priorityMinStr, _ := cmd.Flags().GetString("priority-min")
|
||||||
priorityMaxStr, _ := cmd.Flags().GetString("priority-max")
|
priorityMaxStr, _ := cmd.Flags().GetString("priority-max")
|
||||||
@@ -104,6 +141,39 @@ Examples:
|
|||||||
filter.LabelsAny = labelsAny
|
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
|
// Date ranges
|
||||||
if createdAfter != "" {
|
if createdAfter != "" {
|
||||||
t, err := parseTimeFlag(createdAfter)
|
t, err := parseTimeFlag(createdAfter)
|
||||||
@@ -200,6 +270,21 @@ Examples:
|
|||||||
listArgs.LabelsAny = labelsAny
|
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
|
// Date ranges
|
||||||
if filter.CreatedAfter != nil {
|
if filter.CreatedAfter != nil {
|
||||||
listArgs.CreatedAfter = filter.CreatedAfter.Format(time.RFC3339)
|
listArgs.CreatedAfter = filter.CreatedAfter.Format(time.RFC3339)
|
||||||
@@ -372,6 +457,7 @@ func outputSearchResults(issues []*types.Issue, query string, longFormat bool) {
|
|||||||
func init() {
|
func init() {
|
||||||
searchCmd.Flags().String("query", "", "Search query (alternative to positional argument)")
|
searchCmd.Flags().String("query", "", "Search query (alternative to positional argument)")
|
||||||
searchCmd.Flags().StringP("status", "s", "", "Filter by status (open, in_progress, blocked, deferred, closed)")
|
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("assignee", "a", "", "Filter by assignee")
|
||||||
searchCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore, merge-request, molecule, gate)")
|
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)")
|
searchCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL)")
|
||||||
@@ -381,6 +467,11 @@ func init() {
|
|||||||
searchCmd.Flags().String("sort", "", "Sort by field: priority, created, updated, closed, status, id, title, type, assignee")
|
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")
|
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
|
// Date range flags
|
||||||
searchCmd.Flags().String("created-after", "", "Filter issues created after date (YYYY-MM-DD or RFC3339)")
|
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)")
|
searchCmd.Flags().String("created-before", "", "Filter issues created before date (YYYY-MM-DD or RFC3339)")
|
||||||
@@ -389,6 +480,11 @@ func init() {
|
|||||||
searchCmd.Flags().String("closed-after", "", "Filter issues closed after date (YYYY-MM-DD or RFC3339)")
|
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)")
|
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
|
// Priority range flags
|
||||||
searchCmd.Flags().String("priority-min", "", "Filter by minimum priority (inclusive, 0-4 or P0-P4)")
|
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)")
|
searchCmd.Flags().String("priority-max", "", "Filter by maximum priority (inclusive, 0-4 or P0-P4)")
|
||||||
|
|||||||
Reference in New Issue
Block a user