Skip to main content

bb_cli/commands/
pr_retarget.rs

1//! `bb pr retarget`: point an open pull request at a different destination branch.
2//!
3//! The api only lets the destination move — a pull request's source branch is
4//! fixed for its lifetime — so there is no flag for the other side. The update
5//! rides the same `PUT` that edits a pull request's title, which is why the
6//! existing title is read back and resent: a `PUT` without it is rejected.
7
8use crate::api::models::PullRequest;
9use crate::commands::pr::Ctx;
10use crate::error::{BbError, Result};
11use crate::output::{self, Format};
12use serde::Serialize;
13
14#[derive(Serialize)]
15struct RetargetRow {
16    id: u64,
17    title: String,
18    source: String,
19    destination: String,
20    url: String,
21}
22
23impl RetargetRow {
24    fn from(pr: &PullRequest) -> Self {
25        Self {
26            id: pr.id,
27            title: pr.title.clone().unwrap_or_default(),
28            source: pr.source_branch().to_string(),
29            destination: pr.destination_branch().to_string(),
30            url: pr.html_url().to_string(),
31        }
32    }
33}
34
35pub async fn run(ctx: &Ctx, id: u64, to: &str) -> Result<()> {
36    let path = ctx.path(&format!("/pullrequests/{id}"));
37    let pr: PullRequest = ctx.client.get_json(&path).await?;
38
39    // A closed pull request answers the `PUT` with an unhelpful 400, so the
40    // state is checked here where the message can name what is wrong.
41    let state = pr.state.as_deref().unwrap_or("UNKNOWN");
42    if !state.eq_ignore_ascii_case("OPEN") {
43        return Err(BbError::Config(format!(
44            "pull request #{id} is {state}, and only an open one can be retargeted"
45        )));
46    }
47
48    let from = pr.destination_branch().to_string();
49    if from == to {
50        let row = RetargetRow::from(&pr);
51        match ctx.format {
52            Format::Json => output::print_json(&row)?,
53            Format::Human => output::info(&format!(
54                "pull request #{id} already targets {to} — nothing to do"
55            )),
56        }
57        return Ok(());
58    }
59
60    let title = pr.title.clone().unwrap_or_default();
61    let body = serde_json::json!({
62        "title": title,
63        "destination": { "branch": { "name": to } },
64    });
65    let updated: PullRequest = ctx.client.put_json(&path, &body).await?;
66
67    let row = RetargetRow::from(&updated);
68    match ctx.format {
69        Format::Json => output::print_json(&row)?,
70        Format::Human => {
71            output::success(&format!("pull request #{id} retargeted {from} → {to}"));
72            output::warn(
73                "the diff was recomputed; inline comments on the old base may now read as outdated",
74            );
75        }
76    }
77    Ok(())
78}