@@ -76,6 +76,46 @@
#[arg(long, default_value = "merge_commit")]
strategy: String,
},
/// Submit a review on a pull request
Review {
/// PR number
number: u32,
/// Review action: approve, request_changes, comment
#[arg(long)]
action: String,
/// Review body/comment
#[arg(long, default_value = "")]
body: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// List reviews on a pull request
Reviews {
/// PR number
number: u32,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Add an inline comment on a pull request
#[command(name = "comment")]
CommentCode {
/// PR number
number: u32,
/// File path to comment on
#[arg(long)]
file: String,
/// Line number
#[arg(long)]
line: u32,
/// Comment body
#[arg(long)]
body: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
}
#[derive(Debug, Deserialize)]
@@ -120,5 +160,8 @@
repo,
strategy,
} => merge(repo.as_deref(), number, &strategy).await,
PrCommand::Review { number, action, body, repo } => submit_review(repo.as_deref(), number, &action, &body).await,
PrCommand::Reviews { number, repo } => list_reviews(repo.as_deref(), number).await,
PrCommand::CommentCode { number, file, line, body, repo } => add_pr_comment(repo.as_deref(), number, &file, line, &body).await,
}
}
@@ -330,5 +373,48 @@
);
}
Ok(())
}
async fn submit_review(repo: Option<&str>, number: u32, action: &str, body: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let _resp: serde_json::Value = client
.post(&format!("/{org}/{name}/pulls/{number}/reviews"), &serde_json::json!({"state": action, "body": body}))
.await?;
let action_display = match action {
"approve" | "approved" => "Approved",
"request_changes" | "changes_requested" => "Requested changes on",
_ => "Commented on",
};
output::success(&format!("{action_display} PR #{number}"));
Ok(())
}
async fn list_reviews(repo: Option<&str>, number: u32) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client.get(&format!("/{org}/{name}/pulls/{number}/reviews")).await?;
let reviews: Vec<serde_json::Value> = resp.get("reviews").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
output::header(&format!("Reviews on PR #{number}"));
let rows: Vec<Vec<String>> = reviews.iter().map(|r| {
vec![
r.get("state").and_then(|v| v.as_str()).map(|s| output::colorize_status(s)).unwrap_or_default(),
r.pointer("/author/email").and_then(|v| v.as_str()).unwrap_or("unknown").to_string(),
r.get("body").and_then(|v| v.as_str()).unwrap_or("").lines().next().unwrap_or("").to_string(),
r.get("inserted_at").and_then(|v| v.as_str()).map(|t| output::format_time(t)).unwrap_or_default(),
]
}).collect();
output::print_table(&["STATE", "AUTHOR", "BODY", "DATE"], &rows);
Ok(())
}
async fn add_pr_comment(repo: Option<&str>, number: u32, file: &str, line: u32, body: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let _resp: serde_json::Value = client
.post(&format!("/{org}/{name}/pulls/{number}/comments"), &serde_json::json!({"file_path": file, "line_number": line, "body": body}))
.await?;
output::success(&format!("Added comment on {file}:{line} in PR #{number}"));
Ok(())
}