//! `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; 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 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 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 = 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"), }) }) .collect(); print_json(&Value::Array(trimmed)) } else { for c in &comments { let user = c .get("user") .and_then(|u| u.get("login")) .and_then(Value::as_str) .unwrap_or("?"); let ts = c.get("created_at").and_then(Value::as_str).unwrap_or("?"); let body = c.get("body").and_then(Value::as_str).unwrap_or(""); println!("**{user} @ {ts}**: {body}"); println!(); } 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()) }