//! `pr-reviews ` — list reviews, or submit one via `--approve` / //! `--request-changes` / `--comment`. use anyhow::{Result, bail}; use clap::Args as ClapArgs; use serde_json::{Value, json}; use crate::client::Client; use crate::verbs::print_json; #[derive(ClapArgs)] pub struct Args { /// PR number. number: u64, /// Approve the PR (submit an APPROVED review). #[arg(long, conflicts_with_all = ["request_changes", "comment"])] approve: bool, /// Request changes on the PR (submit a REQUEST_CHANGES review). #[arg(long, conflicts_with_all = ["approve", "comment"])] request_changes: bool, /// Leave a comment review (submit a COMMENT review). #[arg(long, conflicts_with_all = ["approve", "request_changes"])] comment: bool, /// Optional body / message for the review (used with --approve, /// --request-changes, or --comment). #[arg(long, short = 'm')] body: Option, } pub fn run(client: &Client, args: Args) -> Result<()> { let repo = client.repo(); let event = if args.approve { Some("APPROVED") } else if args.request_changes { Some("REQUEST_CHANGES") } else if args.comment { Some("COMMENT") } else { None }; 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) } 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(); 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::>() }) .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(); 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}"); } // 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}"); } } } println!(); } Ok(()) } } }