Add --id flag to bd list for filtering by specific issue IDs
- Add --id flag accepting comma-separated IDs - Implements ID filtering at CLI, RPC, and storage layers - Normalizes IDs (trim, dedupe, remove empty) like labels - Guards against excessive ID lists (max 1000) - Works with other filters (status, priority, etc.) Closes bd-200 Amp-Thread-ID: https://ampcode.com/threads/T-377464f2-1e7f-46f9-b23e-1e3cfd611061 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -45,41 +45,48 @@ var listCmd = &cobra.Command{
|
|||||||
labels, _ := cmd.Flags().GetStringSlice("label")
|
labels, _ := cmd.Flags().GetStringSlice("label")
|
||||||
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
|
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
|
||||||
titleSearch, _ := cmd.Flags().GetString("title")
|
titleSearch, _ := cmd.Flags().GetString("title")
|
||||||
|
idFilter, _ := cmd.Flags().GetString("id")
|
||||||
|
|
||||||
// Normalize labels: trim, dedupe, remove empty
|
// Normalize labels: trim, dedupe, remove empty
|
||||||
labels = normalizeLabels(labels)
|
labels = normalizeLabels(labels)
|
||||||
labelsAny = normalizeLabels(labelsAny)
|
labelsAny = normalizeLabels(labelsAny)
|
||||||
|
|
||||||
filter := types.IssueFilter{
|
filter := types.IssueFilter{
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
}
|
}
|
||||||
if status != "" && status != "all" {
|
if status != "" && status != "all" {
|
||||||
s := types.Status(status)
|
s := types.Status(status)
|
||||||
filter.Status = &s
|
filter.Status = &s
|
||||||
}
|
}
|
||||||
// Use Changed() to properly handle P0 (priority=0)
|
// Use Changed() to properly handle P0 (priority=0)
|
||||||
if cmd.Flags().Changed("priority") {
|
if cmd.Flags().Changed("priority") {
|
||||||
priority, _ := cmd.Flags().GetInt("priority")
|
priority, _ := cmd.Flags().GetInt("priority")
|
||||||
filter.Priority = &priority
|
filter.Priority = &priority
|
||||||
}
|
}
|
||||||
if assignee != "" {
|
if assignee != "" {
|
||||||
filter.Assignee = &assignee
|
filter.Assignee = &assignee
|
||||||
}
|
}
|
||||||
if issueType != "" {
|
if issueType != "" {
|
||||||
t := types.IssueType(issueType)
|
t := types.IssueType(issueType)
|
||||||
filter.IssueType = &t
|
filter.IssueType = &t
|
||||||
}
|
}
|
||||||
if len(labels) > 0 {
|
if len(labels) > 0 {
|
||||||
filter.Labels = labels
|
filter.Labels = labels
|
||||||
}
|
}
|
||||||
if len(labelsAny) > 0 {
|
if len(labelsAny) > 0 {
|
||||||
filter.LabelsAny = labelsAny
|
filter.LabelsAny = labelsAny
|
||||||
}
|
}
|
||||||
if titleSearch != "" {
|
if titleSearch != "" {
|
||||||
filter.TitleSearch = titleSearch
|
filter.TitleSearch = titleSearch
|
||||||
}
|
}
|
||||||
|
if idFilter != "" {
|
||||||
|
ids := normalizeLabels(strings.Split(idFilter, ","))
|
||||||
|
if len(ids) > 0 {
|
||||||
|
filter.IDs = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
listArgs := &rpc.ListArgs{
|
listArgs := &rpc.ListArgs{
|
||||||
Status: status,
|
Status: status,
|
||||||
@@ -99,10 +106,13 @@ var listCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
// Forward title search via Query field (searches title/description/id)
|
// Forward title search via Query field (searches title/description/id)
|
||||||
if titleSearch != "" {
|
if titleSearch != "" {
|
||||||
listArgs.Query = titleSearch
|
listArgs.Query = titleSearch
|
||||||
}
|
}
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
listArgs.IDs = filter.IDs
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := daemonClient.List(listArgs)
|
resp, err := daemonClient.List(listArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -197,6 +207,7 @@ func init() {
|
|||||||
listCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL). Can combine with --label-any")
|
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().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().String("title", "", "Filter by title text (case-insensitive substring match)")
|
||||||
|
listCmd.Flags().String("id", "", "Filter by specific issue IDs (comma-separated, e.g., bd-1,bd-5,bd-10)")
|
||||||
listCmd.Flags().IntP("limit", "n", 0, "Limit results")
|
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")
|
listCmd.Flags().String("format", "", "Output format: 'digraph' (for golang.org/x/tools/cmd/digraph), 'dot' (Graphviz), or Go template")
|
||||||
rootCmd.AddCommand(listCmd)
|
rootCmd.AddCommand(listCmd)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ type ListArgs struct {
|
|||||||
Label string `json:"label,omitempty"` // Deprecated: use Labels
|
Label string `json:"label,omitempty"` // Deprecated: use Labels
|
||||||
Labels []string `json:"labels,omitempty"` // AND semantics
|
Labels []string `json:"labels,omitempty"` // AND semantics
|
||||||
LabelsAny []string `json:"labels_any,omitempty"` // OR semantics
|
LabelsAny []string `json:"labels_any,omitempty"` // OR semantics
|
||||||
|
IDs []string `json:"ids,omitempty"` // Filter by specific issue IDs
|
||||||
Limit int `json:"limit,omitempty"`
|
Limit int `json:"limit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -940,6 +940,21 @@ func (s *Server) handleList(req *Request) Response {
|
|||||||
if len(labelsAny) > 0 {
|
if len(labelsAny) > 0 {
|
||||||
filter.LabelsAny = labelsAny
|
filter.LabelsAny = labelsAny
|
||||||
}
|
}
|
||||||
|
if len(listArgs.IDs) > 0 {
|
||||||
|
ids := normalizeLabels(listArgs.IDs)
|
||||||
|
if len(ids) > 0 {
|
||||||
|
filter.IDs = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard against excessive ID lists to avoid SQLite parameter limits
|
||||||
|
const maxIDs = 1000
|
||||||
|
if len(filter.IDs) > maxIDs {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("--id flag supports at most %d issue IDs, got %d", maxIDs, len(filter.IDs)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx := s.reqCtx(req)
|
ctx := s.reqCtx(req)
|
||||||
issues, err := store.SearchIssues(ctx, listArgs.Query, filter)
|
issues, err := store.SearchIssues(ctx, listArgs.Query, filter)
|
||||||
|
|||||||
@@ -1705,6 +1705,16 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
|
|||||||
whereClauses = append(whereClauses, fmt.Sprintf("id IN (SELECT issue_id FROM labels WHERE label IN (%s))", strings.Join(placeholders, ", ")))
|
whereClauses = append(whereClauses, fmt.Sprintf("id IN (SELECT issue_id FROM labels WHERE label IN (%s))", strings.Join(placeholders, ", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID filtering: match specific issue IDs
|
||||||
|
if len(filter.IDs) > 0 {
|
||||||
|
placeholders := make([]string, len(filter.IDs))
|
||||||
|
for i, id := range filter.IDs {
|
||||||
|
placeholders[i] = "?"
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
whereSQL := ""
|
whereSQL := ""
|
||||||
if len(whereClauses) > 0 {
|
if len(whereClauses) > 0 {
|
||||||
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
|
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ type IssueFilter struct {
|
|||||||
Labels []string // AND semantics: issue must have ALL these labels
|
Labels []string // AND semantics: issue must have ALL these labels
|
||||||
LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels
|
LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels
|
||||||
TitleSearch string
|
TitleSearch string
|
||||||
|
IDs []string // Filter by specific issue IDs
|
||||||
Limit int
|
Limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user