Watch
0
0
Fork
You've already forked hyperhive
0

hive-forge: page past the forge's 50-row clamp; show list --json truncation

The forge clamps page_size to 50. branches, pr reviews, the review
bodies merged into `comments <pr>`, and view's comment list each sent
one request, so anything past the first page was dropped silently.
labels::repo_labels did loop, but asked for 100/page and stopped on
`len < 100`, so a clamped full page of 50 ended the walk.

Add verbs::all_pages: requests 50/page until a short page, capped at
50 pages with a stderr warning when the cap is hit. Use it in all of
the above; repo-labels now calls labels::repo_labels instead of its
own copy of the loop.

list --json now prints the same "N of M shown" stderr trailer as the
pretty output; its paging and stdout shape are unchanged.

Closes #5051
This commit is contained in:
atlas 2026-10-09 11:44:03 +02:00 • committed by mara
commit 0e5c61d12c
9 changed files with 164 additions and 90 deletions

View file

@ -120,9 +120,99 @@ pub(crate) fn clamp_limit(requested: u64) -> (u64, bool) {
(limit, limit < requested)
}
/// [`PAGE_SIZE`] as the `u32` the generated client's `.page_size()` takes.
const PAGE_SIZE_U32: u32 = 50;
/// Runaway cap for [`all_pages`]: at most `MAX_PAGES * PAGE_SIZE` rows.
const MAX_PAGES: u32 = 50;
/// Every row of a paged forge listing. `fetch(page, page_size)` sends one
/// request; the walk stops at the first page shorter than `page_size`.
/// The page size is the forge's clamp, not a larger request: a clamped
/// full page would read as short and end the walk after one page.
/// Stopping at [`MAX_PAGES`] prints a warning on stderr, so a capped
/// result is never presented as the whole population.
pub(crate) fn all_pages<T>(fetch: impl FnMut(u32, u32) -> Result<Vec<T>>) -> Result<Vec<T>> {
let (rows, capped) = walk_pages(MAX_PAGES, fetch)?;
if capped {
eprintln!(
"hive-forge: stopped after {MAX_PAGES} pages ({} rows); the result may be incomplete",
rows.len()
);
}
Ok(rows)
}
/// [`all_pages`]' loop, with the cap as a parameter. Returns the rows and
/// whether the walk hit `max_pages` without seeing a short page.
fn walk_pages<T>(
max_pages: u32,
mut fetch: impl FnMut(u32, u32) -> Result<Vec<T>>,
) -> Result<(Vec<T>, bool)> {
let mut rows = Vec::new();
for page in 1..=max_pages {
let batch = fetch(page, PAGE_SIZE_U32)?;
let short = batch.len() < PAGE_SIZE_U32 as usize;
rows.extend(batch);
if short {
return Ok((rows, false));
}
}
Ok((rows, true))
}
#[cfg(test)]
mod page_limit_tests {
use super::{MAX_LIMIT, PAGE_SIZE, clamp_limit};
use anyhow::Result;
use super::{MAX_LIMIT, PAGE_SIZE, PAGE_SIZE_U32, clamp_limit, walk_pages};
/// A fake listing of `total` rows, served `page_size` at a time.
fn listing(total: usize) -> impl FnMut(u32, u32) -> Result<Vec<usize>> {
move |page, size| {
let (page, size) = (page as usize, size as usize);
Ok(((page - 1) * size..(page * size).min(total)).collect())
}
}
#[test]
fn page_size_u32_matches_page_size() {
assert_eq!(u64::from(PAGE_SIZE_U32), PAGE_SIZE);
}
#[test]
fn walk_pages_collects_every_row_past_the_first_page() {
let (rows, capped) = walk_pages(50, listing(86)).unwrap();
assert_eq!(rows, (0..86).collect::<Vec<_>>());
assert!(!capped);
}
#[test]
fn walk_pages_stops_on_the_empty_page_after_an_exact_multiple() {
let mut calls = 0;
let mut inner = listing(100);
let (rows, capped) = walk_pages(50, |p, s| {
calls += 1;
inner(p, s)
})
.unwrap();
assert_eq!(rows.len(), 100);
assert_eq!(calls, 3);
assert!(!capped);
}
#[test]
fn walk_pages_reports_hitting_the_cap() {
let (rows, capped) = walk_pages(2, listing(500)).unwrap();
assert_eq!(rows.len(), 100);
assert!(capped);
}
#[test]
fn walk_pages_propagates_a_fetch_error() {
let err = walk_pages::<usize>(5, |_, _| anyhow::bail!("boom")).unwrap_err();
assert_eq!(err.to_string(), "boom");
}
#[test]
fn clamp_limit_passes_small_requests_through() {