hyperhive/hive-forge/src/verbs/comments.rs

105 lines
3.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `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;
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<usize>,
}
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<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"),
})
})
.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<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())
}