From 54b8122f6610f15901f4310c4baefae91ff94057 Mon Sep 17 00:00:00 2001 From: damocles Date: Tue, 13 Oct 2026 14:58:43 +0200 Subject: [PATCH] repo-search: warn when a result count hits --limit, may be truncated --- docs/tools/forge.md | 8 +++++ hive-forge/src/verbs/repo_search.rs | 49 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/docs/tools/forge.md b/docs/tools/forge.md index efdefd7d..62d7a81f 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -341,4 +341,12 @@ to discover valid label names before triaging or to audit the label set. reports the real total from the response's `X-Total-Count` (`… 50 of 187 shown … 137 more`), so trust the trailer, not the row count, when deciding whether you've seen everything. +- `repo-search` has no `X-Total-Count` to fall back on, so it can only + warn, not quantify: a result count that exactly equals `--limit` + prints a stderr warning that more may exist, same trust-the-trailer + principle as `list` above but without a real number behind it. It + also can't tell "doesn't exist" from "you lack permission to see + it" — both render as an absent row with no signal either way — and + it matches repo **name** only, not owner, so there's no way to + enumerate an org's repos through this verb. diff --git a/hive-forge/src/verbs/repo_search.rs b/hive-forge/src/verbs/repo_search.rs index 5ad952c8..68fb2372 100644 --- a/hive-forge/src/verbs/repo_search.rs +++ b/hive-forge/src/verbs/repo_search.rs @@ -7,6 +7,14 @@ //! //! Human output: one `owner/repo description` line per hit. `--json` //! emits the full `SearchResults.data` array as raw JSON. +//! +//! A result count that exactly equals `--limit` is a hit on the cap, not +//! necessarily the whole answer — the forge may be holding back more (it +//! also silently drops repos the caller lacks permission to see, with no +//! distinguishing signal at all). Either way we warn on stderr rather than +//! let a capped list read as complete; stdout — and `--json` stdout +//! especially — stays exactly the raw result set, so a script piping it +//! doesn't have to parse prose out of its own data. use anyhow::Result; use clap::Args as ClapArgs; @@ -65,6 +73,9 @@ pub fn run(client: &Client, args: Args) -> Result<()> { .send()?; let repos = results.data.unwrap_or_default(); + if let Some(msg) = truncation_warning(repos.len() as u64, args.limit) { + eprintln!("{msg}"); + } if client.json_mode() { return print_json(&serde_json::to_value(&repos)?); } @@ -83,3 +94,41 @@ pub fn run(client: &Client, args: Args) -> Result<()> { } Ok(()) } + +/// A result count that exactly equals `--limit` is a hit on the cap, not +/// necessarily the whole answer — see the module doc. Returns the stderr +/// warning to print, or `None` when the count came in under the cap (a +/// short result *is* the complete answer, same reasoning `list`'s own +/// `trailer()` uses for its full-page heuristic). +fn truncation_warning(count: u64, limit: u64) -> Option { + (count == limit).then(|| { + format!( + "warning: got exactly --limit ({limit}) results — the forge may be holding back \ + more (and silently drops any repo you lack permission to see, with no signal \ + either way); raise --limit or narrow --query rather than trusting this as a \ + complete list" + ) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn warns_when_count_hits_the_cap() { + assert!(truncation_warning(30, 30).is_some()); + } + + #[test] + fn stays_quiet_under_the_cap() { + assert_eq!(truncation_warning(9, 30), None); + } + + #[test] + fn stays_quiet_on_zero_results() { + // An empty result needs "(no results)" (printed separately in + // `run`), not a truncation warning implying more might exist. + assert_eq!(truncation_warning(0, 30), None); + } +}