diff --git a/hive-forge/src/verbs/pr_reviews.rs b/hive-forge/src/verbs/pr_reviews.rs index f9eff091..e2ab8086 100644 --- a/hive-forge/src/verbs/pr_reviews.rs +++ b/hive-forge/src/verbs/pr_reviews.rs @@ -32,8 +32,6 @@ pub struct Args { } pub fn run(client: &Client, args: Args) -> Result<()> { - let repo = client.repo(); - let event = if args.approve { Some("APPROVED") } else if args.request_changes { @@ -45,114 +43,119 @@ pub fn run(client: &Client, args: Args) -> Result<()> { }; if let Some(ev) = event { - // Submit a review. - let payload = json!({ - "event": ev, - "body": args.body.unwrap_or_default(), - }); - let v = client.post_json( - &format!("/repos/{repo}/pulls/{}/reviews", args.number), - &payload, - )?; - // Print a compact summary rather than the full review blob. - let summary = json!({ - "id": v.get("id"), - "state": v.get("state"), - "user": v.get("user").and_then(|u| u.get("login")), - }); - print_json(&summary) + submit_review(client, args.number, ev, args.body) } else { if args.body.is_some() { bail!("--body requires one of --approve / --request-changes / --comment"); } - // List mode: fetch reviews, then fetch inline comments for each review - // so the full review content is visible without curl fallbacks. - let v = - client.get_json(&format!("/repos/{repo}/pulls/{}/reviews", args.number))?; - let reviews = v.as_array().cloned().unwrap_or_default(); + list_reviews(client, args.number) + } +} - if client.json_mode() { - let trimmed: Vec = reviews - .iter() - .map(|r| { - let id = r.get("id").and_then(Value::as_u64).unwrap_or(0); - let inline = if id > 0 { - client - .get_json(&format!( - "/repos/{repo}/pulls/{}/reviews/{id}/comments", - args.number - )) - .ok() - .and_then(|v| v.as_array().cloned()) - .map(|comments| { - comments - .iter() - .map(|c| { - json!({ - "id": c.get("id"), - "path": c.get("path"), - "line": c.get("line"), - "body": c.get("body"), - }) - }) - .collect::>() +/// Submit a review event (APPROVED / REQUEST_CHANGES / COMMENT) and print +/// a compact summary of the created review. +fn submit_review( + client: &Client, + number: u64, + event: &str, + body: Option, +) -> Result<()> { + let repo = client.repo(); + let payload = json!({ + "event": event, + "body": body.unwrap_or_default(), + }); + let v = client.post_json( + &format!("/repos/{repo}/pulls/{number}/reviews"), + &payload, + )?; + print_json(&json!({ + "id": v.get("id"), + "state": v.get("state"), + "user": v.get("user").and_then(|u| u.get("login")), + })) +} + +/// Fetch inline diff comments for a single review. Returns an empty vec on +/// any error (missing review, network failure) so callers can degrade +/// gracefully. +fn fetch_inline_comments(client: &Client, repo: &str, pr: u64, review_id: u64) -> Vec { + client + .get_json(&format!("/repos/{repo}/pulls/{pr}/reviews/{review_id}/comments")) + .ok() + .and_then(|v| v.as_array().cloned()) + .unwrap_or_default() +} + +/// List all reviews for a PR, including inline diff comments per review. +fn list_reviews(client: &Client, number: u64) -> Result<()> { + let repo = client.repo(); + let v = client.get_json(&format!("/repos/{repo}/pulls/{number}/reviews"))?; + let reviews = v.as_array().cloned().unwrap_or_default(); + + if client.json_mode() { + let trimmed: Vec = reviews + .iter() + .map(|r| { + let id = r.get("id").and_then(Value::as_u64).unwrap_or(0); + let inline: Vec = if id > 0 { + fetch_inline_comments(client, repo, number, id) + .iter() + .map(|c| { + json!({ + "id": c.get("id"), + "path": c.get("path"), + "line": c.get("line"), + "body": c.get("body"), }) - .unwrap_or_default() - } else { - vec![] - }; - json!({ - "id": r.get("id"), - "state": r.get("state"), - "user": r.get("user").and_then(|u| u.get("login")), - "body": r.get("body"), - "comments_count": r.get("comments_count"), - "comments": inline, - }) + }) + .collect() + } else { + vec![] + }; + json!({ + "id": r.get("id"), + "state": r.get("state"), + "user": r.get("user").and_then(|u| u.get("login")), + "body": r.get("body"), + "comments_count": r.get("comments_count"), + "comments": inline, }) - .collect(); - print_json(&Value::Array(trimmed)) - } else { - if reviews.is_empty() { - println!("(no reviews)"); - return Ok(()); + }) + .collect(); + print_json(&Value::Array(trimmed)) + } else { + if reviews.is_empty() { + println!("(no reviews)"); + return Ok(()); + } + for r in &reviews { + let id = r.get("id").and_then(Value::as_u64).unwrap_or(0); + let user = r + .get("user") + .and_then(|u| u.get("login")) + .and_then(Value::as_str) + .unwrap_or("?"); + let state = r.get("state").and_then(Value::as_str).unwrap_or("?"); + let body = r.get("body").and_then(Value::as_str).unwrap_or("").trim(); + println!("### review by {user} ({state})"); + if !body.is_empty() { + println!("{body}"); } - for r in &reviews { - let id = r.get("id").and_then(Value::as_u64).unwrap_or(0); - let user = r - .get("user") - .and_then(|u| u.get("login")) - .and_then(Value::as_str) - .unwrap_or("?"); - let state = r.get("state").and_then(Value::as_str).unwrap_or("?"); - let body = r.get("body").and_then(Value::as_str).unwrap_or("").trim(); - println!("### review by {user} ({state})"); - if !body.is_empty() { - println!("{body}"); - } - // Fetch and print inline comments for this review. - if id > 0 { - if let Ok(ic) = client.get_json(&format!( - "/repos/{repo}/pulls/{}/reviews/{id}/comments", - args.number - )) { - let inline = ic.as_array().cloned().unwrap_or_default(); - for c in &inline { - let path = c.get("path").and_then(Value::as_str).unwrap_or("?"); - let line = c - .get("line") - .and_then(Value::as_u64) - .map(|n| n.to_string()) - .unwrap_or_else(|| "?".to_string()); - let cbody = - c.get("body").and_then(Value::as_str).unwrap_or("").trim(); - println!(" [{path}:{line}] {cbody}"); - } - } + if id > 0 { + for c in &fetch_inline_comments(client, repo, number, id) { + let path = c.get("path").and_then(Value::as_str).unwrap_or("?"); + let line = c + .get("line") + .and_then(Value::as_u64) + .map(|n| n.to_string()) + .unwrap_or_else(|| "?".to_string()); + let cbody = c.get("body").and_then(Value::as_str).unwrap_or("").trim(); + println!(" [{path}:{line}] {cbody}"); } - println!(); } - Ok(()) + println!(); } + Ok(()) } }