//! Pull requests: list, read, comment, review and merge.
//!
//! Merging is included for the same reason the phone has it — it's the PR
//! action you most often want without opening a browser. It enforces the
//! repository's review rules; `--override-rules` is accepted from repo
//! admins and recorded as an override, exactly as the web UI does it.
use anyhow::Result;
use crate::cli::PullCommand;
use crate::commands::{Ctx, read_body};
use crate::output::{
Table, Tone, ago, author_of, bold, heading, indented, labels, note, paint, state_cell, truncate,
};
pub async fn run(ctx: &Ctx, command: &PullCommand) -> Result<()> {
let repo = ctx.repo()?;
match command {
PullCommand::List {
state,
search,
limit,
} => {
let res = ctx
.api
.pulls(&repo, Some(state.as_param()), search.as_deref(), *limit)
.await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
if res.pulls.is_empty() {
let qualifier = if state.as_param() == "all" {
String::new()
} else {
format!("{} ", state.as_param())
};
note(&format!("No {qualifier}pull requests in {repo}."));
return Ok(());
}
let mut table = Table::new(["#", "STATE", "TITLE", "BRANCH", "AUTHOR", "UPDATED"]);
for pull in &res.pulls {
table.row([
format!("#{}", pull.number),
state_cell(&pull.state),
truncate(pull.title.as_deref().unwrap_or("(untitled)"), 48),
truncate(
&branches(pull.source_ref.as_deref(), pull.target_ref.as_deref()),
30,
),
author_of(pull.author.as_ref()),
ago(pull.updated_at.as_deref().or(pull.inserted_at.as_deref())),
]);
}
table.print();
Ok(())
}
PullCommand::Show {
number,
no_comments,
} => {
let res = ctx.api.pull(&repo, *number).await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
let pull = &res.pull;
heading(&format!(
"#{} {}",
pull.number,
pull.title.as_deref().unwrap_or("(untitled)")
));
println!(
"{} · {} · by {} {}",
state_cell(&pull.state),
branches(pull.source_ref.as_deref(), pull.target_ref.as_deref()),
author_of(pull.author.as_ref()),
ago(pull.inserted_at.as_deref())
);
if !pull.labels.is_empty() {
println!("labels: {}", labels(&pull.labels));
}
if let Some(merged_at) = &pull.merged_at {
println!(
"merged by {} {}",
author_of(pull.merged_by.as_ref()),
ago(Some(merged_at))
);
}
if let Some(body) = pull.body.as_deref().filter(|b| !b.trim().is_empty()) {
println!();
indented(body, " ");
}
if !res.reviews.is_empty() {
println!("\n{}", bold("Reviews"));
for review in &res.reviews {
let state = review.state.as_deref().unwrap_or("commented");
let tone = match state {
"approved" => Tone::Good,
"changes_requested" => Tone::Bad,
_ => Tone::Muted,
};
println!(
" {} {} {}",
paint(state, tone),
author_of(review.reviewer.as_ref()),
ago(review.inserted_at.as_deref())
);
if let Some(body) = review.body.as_deref().filter(|b| !b.trim().is_empty()) {
indented(body, " ");
}
}
}
if !res.review_requests.is_empty() {
let waiting: Vec<String> = res
.review_requests
.iter()
.map(|r| author_of(r.reviewer.as_ref()))
.collect();
println!("\nawaiting review from {}", waiting.join(", "));
}
if !res.merge_blockers.is_empty() {
println!("\n{}", bold("Merge blockers"));
for blocker in &res.merge_blockers {
println!(" {} {blocker}", paint("✘", Tone::Bad));
}
} else if pull.state == "open" {
let mergeable = match pull.mergeable {
Some(true) => paint("no conflicts", Tone::Good),
Some(false) => paint("conflicts with the target branch", Tone::Bad),
None => paint("mergeability not computed yet", Tone::Muted),
};
println!("\n{mergeable}");
}
if !no_comments {
if !res.comments.is_empty() {
println!("\n{}", bold(&format!("{} comment(s)", res.comments.len())));
for comment in &res.comments {
println!();
println!(
"{} {}",
bold(&author_of(comment.author.as_ref())),
ago(comment.inserted_at.as_deref())
);
let body = if comment.deleted {
"(deleted)"
} else {
comment.body.as_deref().unwrap_or("")
};
indented(body, " ");
}
}
// Line comments only make sense next to the file and line
// they're pinned to, so they're listed apart from the
// discussion rather than interleaved into it.
if !res.line_comments.is_empty() {
println!(
"\n{}",
bold(&format!("{} line comment(s)", res.line_comments.len()))
);
for comment in &res.line_comments {
println!();
println!(
"{} {}:{} — {}",
bold(&author_of(comment.author.as_ref())),
comment.file_path.as_deref().unwrap_or("?"),
comment
.line
.map(|l| l.to_string())
.unwrap_or_else(|| "?".into()),
ago(comment.inserted_at.as_deref())
);
indented(comment.body.as_deref().unwrap_or(""), " ");
}
}
}
note(&format!(
"\n{}",
ctx.repo_web_url(&repo, &format!("/pulls/{}", pull.number))
));
let mut cant: Vec<&str> = Vec::new();
if !res.can_comment {
cant.push("comment");
}
if !res.can_review {
cant.push("review");
}
if !res.can_merge && pull.state == "open" {
cant.push("merge");
}
if !cant.is_empty() {
note(&format!("(you can't {} this one)", cant.join(" or ")));
}
Ok(())
}
PullCommand::Comment { number, body } => {
let text = read_body(body.body.as_deref(), body.body_file.as_deref(), true)?
.expect("required body");
let res = ctx.api.comment_on_pull(&repo, *number, text.trim()).await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
println!("Commented on pull request #{number}.");
Ok(())
}
PullCommand::Review {
number,
state,
body,
} => {
let text = read_body(body.body.as_deref(), body.body_file.as_deref(), false)?;
let res = ctx
.api
.review(
&repo,
*number,
state.as_param(),
text.as_deref().map(str::trim).filter(|t| !t.is_empty()),
)
.await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
println!(
"Review recorded on #{number}: {}.",
state.as_param().replace('_', " ")
);
Ok(())
}
PullCommand::Merge {
number,
message,
override_rules,
} => {
let res = ctx
.api
.merge(&repo, *number, message.as_deref(), *override_rules)
.await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
println!(
"{} pull request #{number}{}.",
paint("Merged", Tone::Good),
if *override_rules {
" (review rules overridden)"
} else {
""
}
);
Ok(())
}
}
}
fn branches(source: Option<&str>, target: Option<&str>) -> String {
let short = |r: Option<&str>| {
r.map(|r| {
r.strip_prefix("refs/heads/")
.or_else(|| r.strip_prefix("refs/tags/"))
.unwrap_or(r)
.to_string()
})
.unwrap_or_else(|| "?".into())
};
format!("{} → {}", short(source), short(target))
}
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
9.9 KiB · text
History
1294137