hive-forge: comments --tail N flag (#694 part 3) — last N comments in chronological order

This commit is contained in:
damocles 2026-05-31 14:05:09 +02:00 committed by mara
commit 5f4494c239

View file

@ -1,6 +1,17 @@
//! `comments <number> [--limit N]` — list all comments on an issue
//! or PR. Closes the curl-fallback gap (#418). Use the global
//! `--json` flag for JSON output (#421).
//! `comments <number> [--limit N | --tail N]` — list comments on an
//! issue or PR. Closes the curl-fallback gap (#418); `--tail`
//! closes the third of the four #694 gaps (paging-for-long-threads
//! awkwardness).
//!
//! - `--limit N` (default 50, Forgejo's cap) returns the first N
//! comments — same shape this verb has always had.
//! - `--tail N` returns the *last* N comments in chronological
//! order. Internally pages through every comment on the issue,
//! then slices the tail. Use this for "what was the conclusion
//! on this long thread?" without scrolling through the whole
//! history.
//!
//! Use the global `--json` flag for JSON output (#421).
use anyhow::Result;
use clap::Args as ClapArgs;
@ -9,42 +20,52 @@ use serde_json::{Value, json};
use crate::client::Client;
use crate::verbs::print_json;
/// Hard cap on `--tail` pagination so a confused caller can't
/// accidentally paginate forever on a runaway-comment thread.
/// 20 pages × Forgejo's 50-per-page cap = 1000 comments which
/// covers every issue in the hyperhive repo today with headroom.
/// Tune up if a future thread breaches it (the truncated tail
/// will surface as a smaller-than-expected slice).
const TAIL_MAX_PAGES: u32 = 20;
#[derive(ClapArgs)]
pub struct Args {
/// Issue or PR number.
number: u64,
/// Page size (Forgejo caps at 50 by default).
#[arg(long, default_value_t = 50)]
/// Page size for the head-of-thread shape (Forgejo caps at 50).
/// Mutually exclusive with `--tail`.
#[arg(long, default_value_t = 50, conflicts_with = "tail")]
limit: u64,
/// Return the last `N` comments in chronological order. Pages
/// through every comment on the issue under the hood, then
/// slices the tail. Mutually exclusive with `--limit`.
#[arg(long)]
tail: Option<usize>,
}
pub fn run(client: &Client, args: Args) -> Result<()> {
let repo = client.repo();
let v = client.get_json(&format!(
"/repos/{repo}/issues/{}/comments?limit={}",
args.number, args.limit
))?;
let comments = match args.tail {
Some(n) => fetch_tail(client, &repo, args.number, n)?,
None => fetch_head(client, &repo, args.number, args.limit)?,
};
if client.json_mode() {
let trimmed: Vec<Value> = v
.as_array()
.map(|a| {
a.iter()
.map(|c| {
json!({
"id": c.get("id"),
"user": c.get("user").and_then(|u| u.get("login")),
"created_at": c.get("created_at"),
"updated_at": c.get("updated_at"),
"body": c.get("body"),
"url": c.get("html_url"),
})
})
.collect()
let trimmed: Vec<Value> = comments
.iter()
.map(|c| {
json!({
"id": c.get("id"),
"user": c.get("user").and_then(|u| u.get("login")),
"created_at": c.get("created_at"),
"updated_at": c.get("updated_at"),
"body": c.get("body"),
"url": c.get("html_url"),
})
})
.unwrap_or_default();
.collect();
print_json(&Value::Array(trimmed))
} else {
for c in v.as_array().cloned().unwrap_or_default() {
for c in &comments {
let user = c
.get("user")
.and_then(|u| u.get("login"))
@ -58,3 +79,27 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
Ok(())
}
}
/// Fetch the first page's worth of comments (existing behaviour).
fn fetch_head(client: &Client, repo: &str, number: u64, limit: u64) -> Result<Vec<Value>> {
let v = client.get_json(&format!(
"/repos/{repo}/issues/{number}/comments?limit={limit}"
))?;
Ok(v.as_array().cloned().unwrap_or_default())
}
/// Fetch every comment via paginated GET, then return the last `n`
/// items in chronological order. Forgejo orders
/// `/issues/<n>/comments` oldest-first so the trailing slice maps
/// directly to "most recent N".
fn fetch_tail(client: &Client, repo: &str, number: u64, n: usize) -> Result<Vec<Value>> {
let all = client.get_json_all(
&format!("/repos/{repo}/issues/{number}/comments?limit=50"),
TAIL_MAX_PAGES,
)?;
let total = all.len();
if total <= n {
return Ok(all);
}
Ok(all.into_iter().skip(total - n).collect())
}