From 5f4494c2397ce4d82844b1d87effce7b548af267 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 14:05:09 +0200 Subject: [PATCH] =?UTF-8?q?hive-forge:=20comments=20--tail=20N=20flag=20(#?= =?UTF-8?q?694=20part=203)=20=E2=80=94=20last=20N=20comments=20in=20chrono?= =?UTF-8?q?logical=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hive-forge/src/verbs/comments.rs | 97 +++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/hive-forge/src/verbs/comments.rs b/hive-forge/src/verbs/comments.rs index ad9aecb8..7a2d72ec 100644 --- a/hive-forge/src/verbs/comments.rs +++ b/hive-forge/src/verbs/comments.rs @@ -1,6 +1,17 @@ -//! `comments [--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 [--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, } 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 = 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 = 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> { + 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//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> { + 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()) +}