The forge's issue-comments endpoint takes no page or limit and always returns the whole thread. #5055 wrapped view's fetch in all_pages, which never saw a short page on a thread of 50+ comments and fetched the same list 50 times: `issue view 3782` printed 3000 comments instead of 60. `comments --limit K` sent K as a page size the forge ignored, so it returned the whole thread. view and pr status's last_comment now send one request. comments fetches the thread once and cuts the --limit / --tail window client-side; the X-Total-Count probe and the computed-page tail walk are gone, and the count of comments outside the window is the length of the list. --since is unchanged. tests/comments_no_paging.rs runs the binary against a loopback mock forge whose comments route ignores page and limit, as Forgejo's does.
94 lines
2.9 KiB
Rust
94 lines
2.9 KiB
Rust
//! `view <number> [repo]` — dump title + body + all comments as
|
|
//! markdown.
|
|
|
|
use anyhow::Result;
|
|
use clap::Args as ClapArgs;
|
|
use forgejo_api::structs::{IssueGetCommentsQuery, StateType};
|
|
|
|
use crate::client::{Client, index};
|
|
use crate::notify;
|
|
use crate::verbs::{attachment_line, issue_reactions, reaction_summary, refuse_json};
|
|
|
|
#[derive(ClapArgs)]
|
|
pub struct Args {
|
|
/// Issue or PR number.
|
|
pub(crate) number: u64,
|
|
}
|
|
|
|
pub fn run(client: &Client, args: Args) -> Result<()> {
|
|
refuse_json(
|
|
client.json_mode(),
|
|
"view",
|
|
"use `issue show`/`pr show` for structured fields",
|
|
)?;
|
|
let repo = client.repo()?;
|
|
// Reading the thread clears its unread notification so the
|
|
// read-before-comment guard (in `comment`) lets a reply through.
|
|
notify::mark_read_best_effort(client, repo, args.number);
|
|
let (owner, name) = client.owner_repo()?;
|
|
let issue = client
|
|
.api()
|
|
.issue_get_issue(owner, name, index(args.number)?)
|
|
.send()?;
|
|
let title = issue.title.as_deref().unwrap_or("");
|
|
let body = issue.body.as_deref().unwrap_or("");
|
|
let state = match issue.state {
|
|
Some(StateType::Open) => "open",
|
|
Some(StateType::Closed) => "closed",
|
|
None => "?",
|
|
};
|
|
let user = issue
|
|
.user
|
|
.as_ref()
|
|
.and_then(|u| u.login.as_deref())
|
|
.unwrap_or("?");
|
|
let kind = if issue.pull_request.is_some() {
|
|
"PR"
|
|
} else {
|
|
"issue"
|
|
};
|
|
println!("# {kind} #{} [{state}] by {user}", args.number);
|
|
println!("{title}");
|
|
if !body.is_empty() {
|
|
println!();
|
|
println!("{body}");
|
|
}
|
|
for a in issue.assets.as_deref().unwrap_or_default() {
|
|
if let Some(line) = attachment_line(a) {
|
|
println!("{line}");
|
|
}
|
|
}
|
|
let reactions = issue_reactions(client, owner, name, args.number)?;
|
|
if let Some(summary) = reaction_summary(&reactions) {
|
|
println!("[reactions: {summary}]");
|
|
}
|
|
let idx = index(args.number)?;
|
|
// One request: this endpoint takes no page or limit and returns the
|
|
// whole thread, so a paging loop would fetch it again on every page.
|
|
let (_, comments) = client
|
|
.api()
|
|
.issue_get_comments(owner, name, idx, IssueGetCommentsQuery::default())
|
|
.send()?;
|
|
if !comments.is_empty() {
|
|
println!();
|
|
println!("---");
|
|
println!("## Comments ({})", comments.len());
|
|
println!();
|
|
for c in &comments {
|
|
let cu = c
|
|
.user
|
|
.as_ref()
|
|
.and_then(|u| u.login.as_deref())
|
|
.unwrap_or("?");
|
|
let cb = c.body.as_deref().unwrap_or("");
|
|
println!("**{cu}**: {cb}");
|
|
for a in c.assets.as_deref().unwrap_or_default() {
|
|
if let Some(line) = attachment_line(a) {
|
|
println!("{line}");
|
|
}
|
|
}
|
|
println!();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|