ref:9f0bcbf9d6300959b4ac13bdc8705ea456989f94

feat(pr): add --base, --draft, --ready to `pr edit` (#315)

`anvil pr edit` could only change title/body. Add: - `--base <branch>` to repoint the base branch (sends base_branch) - `--draft` / `--ready` to toggle is_draft (mutually exclusive via clap) The server's PR update endpoint now accepts these fields. Updated the empty-payload hint and added parse tests for the new flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SHA: 9f0bcbf9d6300959b4ac13bdc8705ea456989f94
Author: CI <ci@anvil.test>
Date: 2026-06-06 23:17
Parents: e110357
1 files changed +103 -3
Type
src/commands/pr.rs +103 −3
@@ -65,7 +65,7 @@
#[arg(long)]
repo: Option<String>,
},
/// Edit a pull request's title or body
/// Edit a pull request's title, body, base branch, or draft status
Edit {
/// PR number
number: u32,
@@ -78,6 +78,15 @@
/// New body
#[arg(long)]
body: Option<String>,
/// Repoint the base branch (the branch the PR merges into)
#[arg(long)]
base: Option<String>,
/// Mark the PR as a draft
#[arg(long)]
draft: bool,
/// Mark the PR as ready for review (clears draft)
#[arg(long, conflicts_with = "draft")]
ready: bool,
},
/// Merge a pull request
Merge {
@@ -197,7 +206,21 @@
repo,
title,
body,
base,
draft,
ready,
} => edit(repo.as_deref(), number, title.as_deref(), body.as_deref()).await,
} => {
edit(
repo.as_deref(),
number,
title.as_deref(),
body.as_deref(),
base.as_deref(),
draft,
ready,
)
.await
}
PrCommand::Close { number, repo } => close(repo.as_deref(), number).await,
PrCommand::Reopen { number, repo } => reopen(repo.as_deref(), number).await,
PrCommand::Merge {
@@ -430,11 +453,15 @@
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn edit(
repo: Option<&str>,
number: u32,
title: Option<&str>,
body: Option<&str>,
base: Option<&str>,
draft: bool,
ready: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
@@ -445,9 +472,19 @@
}
if let Some(b) = body {
payload.insert("body".into(), serde_json::json!(b));
}
if let Some(base) = base {
payload.insert("base_branch".into(), serde_json::json!(base));
}
// --draft and --ready are mutually exclusive (enforced by clap); each maps
// to an explicit is_draft boolean.
if draft {
payload.insert("is_draft".into(), serde_json::json!(true));
} else if ready {
payload.insert("is_draft".into(), serde_json::json!(false));
}
if payload.is_empty() {
output::warn("Nothing to update — specify --title or --body");
output::warn("Nothing to update — specify --title, --body, --base, --draft, or --ready");
return Ok(());
}
@@ -460,5 +497,8 @@
.await?;
output::success(&format!("Updated PR #{number}"));
if let Some(base) = base {
output::detail("Base", base);
}
Ok(())
}
@@ -725,5 +765,65 @@
}
_ => panic!("expected Checkout"),
}
}
#[test]
fn edit_parses_title_and_body() {
match parse(&["edit", "42", "--title", "New", "--body", "Desc"]) {
PrCommand::Edit {
number,
title,
body,
base,
draft,
ready,
..
} => {
assert_eq!(number, 42);
assert_eq!(title.as_deref(), Some("New"));
assert_eq!(body.as_deref(), Some("Desc"));
assert!(base.is_none());
assert!(!draft);
assert!(!ready);
}
_ => panic!("expected Edit"),
}
}
#[test]
fn edit_parses_base_repoint() {
match parse(&["edit", "42", "--base", "release"]) {
PrCommand::Edit { base, .. } => {
assert_eq!(base.as_deref(), Some("release"));
}
_ => panic!("expected Edit"),
}
}
#[test]
fn edit_parses_draft_flag() {
match parse(&["edit", "42", "--draft"]) {
PrCommand::Edit { draft, ready, .. } => {
assert!(draft);
assert!(!ready);
}
_ => panic!("expected Edit"),
}
}
#[test]
fn edit_parses_ready_flag() {
match parse(&["edit", "42", "--ready"]) {
PrCommand::Edit { draft, ready, .. } => {
assert!(!draft);
assert!(ready);
}
_ => panic!("expected Edit"),
}
}
#[test]
fn edit_rejects_draft_and_ready_together() {
assert!(PrCli::try_parse_from(["edit", "42", "--draft", "--ready"]).is_err());
}
}