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 //! `comments <number> [--limit N | --tail N]` — list comments on an
//! or PR. Closes the curl-fallback gap (#418). Use the global //! issue or PR. Closes the curl-fallback gap (#418); `--tail`
//! `--json` flag for JSON output (#421). //! 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 anyhow::Result;
use clap::Args as ClapArgs; use clap::Args as ClapArgs;
@ -9,42 +20,52 @@ use serde_json::{Value, json};
use crate::client::Client; use crate::client::Client;
use crate::verbs::print_json; 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)] #[derive(ClapArgs)]
pub struct Args { pub struct Args {
/// Issue or PR number. /// Issue or PR number.
number: u64, number: u64,
/// Page size (Forgejo caps at 50 by default). /// Page size for the head-of-thread shape (Forgejo caps at 50).
#[arg(long, default_value_t = 50)] /// Mutually exclusive with `--tail`.
#[arg(long, default_value_t = 50, conflicts_with = "tail")]
limit: u64, 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<()> { pub fn run(client: &Client, args: Args) -> Result<()> {
let repo = client.repo(); let repo = client.repo();
let v = client.get_json(&format!( let comments = match args.tail {
"/repos/{repo}/issues/{}/comments?limit={}", Some(n) => fetch_tail(client, &repo, args.number, n)?,
args.number, args.limit None => fetch_head(client, &repo, args.number, args.limit)?,
))?; };
if client.json_mode() { if client.json_mode() {
let trimmed: Vec<Value> = v let trimmed: Vec<Value> = comments
.as_array() .iter()
.map(|a| { .map(|c| {
a.iter() json!({
.map(|c| { "id": c.get("id"),
json!({ "user": c.get("user").and_then(|u| u.get("login")),
"id": c.get("id"), "created_at": c.get("created_at"),
"user": c.get("user").and_then(|u| u.get("login")), "updated_at": c.get("updated_at"),
"created_at": c.get("created_at"), "body": c.get("body"),
"updated_at": c.get("updated_at"), "url": c.get("html_url"),
"body": c.get("body"), })
"url": c.get("html_url"),
})
})
.collect()
}) })
.unwrap_or_default(); .collect();
print_json(&Value::Array(trimmed)) print_json(&Value::Array(trimmed))
} else { } else {
for c in v.as_array().cloned().unwrap_or_default() { for c in &comments {
let user = c let user = c
.get("user") .get("user")
.and_then(|u| u.get("login")) .and_then(|u| u.get("login"))
@ -58,3 +79,27 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
Ok(()) 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())
}