105 lines
3.8 KiB
Rust
105 lines
3.8 KiB
Rust
//! `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())
|
||
}
|