diff --git a/internal/cli/app.go b/internal/cli/app.go index 880fb4e..08c6f0d 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -252,8 +252,10 @@ func (a *App) runThreads(ctx context.Context, args []string) error { fs := flag.NewFlagSet("threads", flag.ContinueOnError) fs.SetOutput(io.Discard) includeClosed := fs.Bool("include-closed", false, "include locally closed rows") + numbersRaw := fs.String("numbers", "", "comma-separated issue or pull request numbers") + limitRaw := fs.String("limit", "", "maximum thread rows") jsonOut := fs.Bool("json", false, "write JSON output") - if err := fs.Parse(normalizeCommandArgs(args, nil)); err != nil { + if err := fs.Parse(normalizeCommandArgs(args, map[string]bool{"numbers": true, "limit": true})); err != nil { return usageErr(err) } a.applyCommandJSON(*jsonOut) @@ -264,6 +266,14 @@ func (a *App) runThreads(ctx context.Context, args []string) error { if err != nil { return usageErr(err) } + numbers, err := parseOptionalPositiveIntList(*numbersRaw) + if err != nil { + return usageErr(err) + } + limit, err := parseOptionalPositiveInt(*limitRaw) + if err != nil { + return usageErr(err) + } cfg, err := config.Load(a.configPath) if err != nil { @@ -279,7 +289,12 @@ func (a *App) runThreads(ctx context.Context, args []string) error { if err != nil { return err } - threads, err := st.ListThreads(ctx, repo.ID, *includeClosed) + threads, err := st.ListThreadsFiltered(ctx, store.ThreadListOptions{ + RepoID: repo.ID, + IncludeClosed: *includeClosed, + Numbers: numbers, + Limit: limit, + }) if err != nil { return err } @@ -482,6 +497,22 @@ func parseOptionalPositiveInt(value string) (int, error) { return parsed, nil } +func parseOptionalPositiveIntList(value string) ([]int, error) { + if strings.TrimSpace(value) == "" { + return nil, nil + } + parts := strings.Split(value, ",") + out := make([]int, 0, len(parts)) + for _, part := range parts { + parsed, err := parseOptionalPositiveInt(strings.TrimSpace(part)) + if err != nil { + return nil, err + } + out = append(out, parsed) + } + return out, nil +} + func (a *App) writeOutput(title string, payload any, allowLog bool) error { switch a.format { case FormatJSON: diff --git a/internal/store/repositories_test.go b/internal/store/repositories_test.go index 82738a0..b543fc1 100644 --- a/internal/store/repositories_test.go +++ b/internal/store/repositories_test.go @@ -53,4 +53,11 @@ func TestUpsertRepositoryAndThread(t *testing.T) { if len(rows) != 1 || rows[0].Title != "download stalls" { t.Fatalf("unexpected rows: %#v", rows) } + filtered, err := st.ListThreadsFiltered(ctx, ThreadListOptions{RepoID: repoID, Numbers: []int{1}, Limit: 1}) + if err != nil { + t.Fatalf("filtered threads: %v", err) + } + if len(filtered) != 1 || filtered[0].Number != 1 { + t.Fatalf("unexpected filtered rows: %#v", filtered) + } } diff --git a/internal/store/threads.go b/internal/store/threads.go index 2d6712e..c62720d 100644 --- a/internal/store/threads.go +++ b/internal/store/threads.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strings" ) type Thread struct { @@ -76,10 +77,35 @@ func (s *Store) UpsertThread(ctx context.Context, thread Thread) (int64, error) } func (s *Store) ListThreads(ctx context.Context, repoID int64, includeClosed bool) ([]Thread, error) { + return s.ListThreadsFiltered(ctx, ThreadListOptions{RepoID: repoID, IncludeClosed: includeClosed}) +} + +type ThreadListOptions struct { + RepoID int64 + IncludeClosed bool + Numbers []int + Limit int +} + +func (s *Store) ListThreadsFiltered(ctx context.Context, options ThreadListOptions) ([]Thread, error) { where := `repo_id = ?` - if !includeClosed { + args := []any{options.RepoID} + if !options.IncludeClosed { where += ` and closed_at_local is null` } + if len(options.Numbers) > 0 { + placeholders := make([]string, 0, len(options.Numbers)) + for _, number := range options.Numbers { + placeholders = append(placeholders, "?") + args = append(args, number) + } + where += ` and number in (` + strings.Join(placeholders, ",") + `)` + } + limitSQL := `` + if options.Limit > 0 { + limitSQL = ` limit ?` + args = append(args, options.Limit) + } rows, err := s.db.QueryContext(ctx, ` select id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url, labels_json, assignees_json, raw_json, content_hash, is_draft, @@ -87,8 +113,7 @@ func (s *Store) ListThreads(ctx context.Context, repoID int64, includeClosed boo first_pulled_at, last_pulled_at, updated_at, closed_at_local, close_reason_local from threads where `+where+` - order by number - `, repoID) + order by number`+limitSQL, args...) if err != nil { return nil, fmt.Errorf("list threads: %w", err) }