Add label filtering to bd list with AND/OR semantics

- Add --label flag for AND filtering (must have ALL labels)
- Add --label-any flag for OR filtering (must have AT LEAST ONE label)
- Add normalizeLabels() helper to trim, dedupe, and clean inputs
- Fix RPC title filtering parity bug (forward via Query field)
- Add comprehensive tests for label filtering including combined AND+OR
- Update documentation in README and CHANGELOG
- Improve flag help text to clarify combined semantics

Closes bd-161
This commit is contained in:
Steve Yegge
2025-10-19 23:03:02 -07:00
parent f1ec927a7c
commit 422c102f46
9 changed files with 346 additions and 181 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"text/template"
"github.com/spf13/cobra"
@@ -14,6 +15,24 @@ import (
"github.com/steveyegge/beads/internal/types"
)
// normalizeLabels trims whitespace, removes empty strings, and deduplicates labels
func normalizeLabels(ss []string) []string {
seen := make(map[string]struct{})
out := make([]string, 0, len(ss))
for _, s := range ss {
s = strings.TrimSpace(s)
if s == "" {
continue
}
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
return out
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List issues",
@@ -24,8 +43,13 @@ var listCmd = &cobra.Command{
limit, _ := cmd.Flags().GetInt("limit")
formatStr, _ := cmd.Flags().GetString("format")
labels, _ := cmd.Flags().GetStringSlice("label")
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
titleSearch, _ := cmd.Flags().GetString("title")
// Normalize labels: trim, dedupe, remove empty
labels = normalizeLabels(labels)
labelsAny = normalizeLabels(labelsAny)
filter := types.IssueFilter{
Limit: limit,
}
@@ -48,6 +72,9 @@ var listCmd = &cobra.Command{
if len(labels) > 0 {
filter.Labels = labels
}
if len(labelsAny) > 0 {
filter.LabelsAny = labelsAny
}
if titleSearch != "" {
filter.TitleSearch = titleSearch
}
@@ -65,7 +92,14 @@ var listCmd = &cobra.Command{
listArgs.Priority = &priority
}
if len(labels) > 0 {
listArgs.Label = labels[0] // TODO: daemon protocol needs to support multiple labels
listArgs.Labels = labels
}
if len(labelsAny) > 0 {
listArgs.LabelsAny = labelsAny
}
// Forward title search via Query field (searches title/description/id)
if titleSearch != "" {
listArgs.Query = titleSearch
}
resp, err := daemonClient.List(listArgs)
@@ -148,7 +182,8 @@ func init() {
listCmd.Flags().IntP("priority", "p", 0, "Filter by priority (0-4: 0=critical, 1=high, 2=medium, 3=low, 4=backlog)")
listCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
listCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore)")
listCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (comma-separated, must have ALL labels)")
listCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL). Can combine with --label-any")
listCmd.Flags().StringSlice("label-any", []string{}, "Filter by labels (OR: must have AT LEAST ONE). Can combine with --label")
listCmd.Flags().String("title", "", "Filter by title text (case-insensitive substring match)")
listCmd.Flags().IntP("limit", "n", 0, "Limit results")
listCmd.Flags().String("format", "", "Output format: 'digraph' (for golang.org/x/tools/cmd/digraph), 'dot' (Graphviz), or Go template")