handle having to unstack to change base - #86
Merged
slinder1 merged 1 commit intoAug 11, 2026
Conversation
slinder1
changed the base branch from
main
to
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
August 11, 2026 15:19
Owner
Author
🛠️ Initial changes (click to expand):diff --git b/src/gh.rs a/src/gh.rs
@@ -217,6 +217,37 @@ fn rest_api_empty(method: &str, endpoint: String, body: Option<&[u64]>) -> Resul
Ok(())
}
+fn unstack(endpoint: String) -> Result<()> {
+ let mut cmd = gh();
+ cmd.args([
+ "api",
+ endpoint.as_str(),
+ "--method",
+ "POST",
+ "--header",
+ "Accept: application/vnd.github+json",
+ "--header",
+ "X-GitHub-Api-Version: 2026-03-10",
+ "--include",
+ ]);
+ if env::get().dry_run() {
+ eprintln!("would-exec: {:?}", cmd);
+ return Ok(());
+ }
+ let output = exec!(env::get(), cmd);
+ let status = String::from_utf8_lossy(output.stdout.as_ref())
+ .lines()
+ .next()
+ .and_then(|line| line.split_whitespace().nth(1))
+ .and_then(|status| status.parse::<u16>().ok())
+ .context("gh api unstack response did not contain an HTTP status")?;
+ match status {
+ 204 => Ok(()),
+ 200 => bail!("unstacking {endpoint} left pull requests in the stack"),
+ _ => bail!("unexpected HTTP status from unstacking {endpoint}: {status}"),
+ }
+}
+
fn graphql_search<T, K, F>(gql_query: &str, search_query: &str, key: F) -> Result<Vec<T>>
where
T: DeserializeOwned,
@@ -310,11 +341,16 @@ impl Pr {
Ok(())
}
- pub fn set_base(&self, base: &str) -> Result<()> {
+ pub fn set_base(&mut self, base: &str) -> Result<()> {
let mut cmd = gh();
let args = self.args_for("edit", [format!("--base={base}")])?;
cmd.args(args);
- exec!(env::get(), dry_return = (), cmd);
+ if env::get().dry_run() {
+ eprintln!("would-exec: {:?}", cmd);
+ } else {
+ exec!(env::get(), cmd);
+ }
+ self.base_ref_name = base.to_owned();
Ok(())
}
@@ -496,7 +532,7 @@ pub fn reconcile_stack(prs: &[&Pr]) -> Result<()> {
rest_api_empty("POST", endpoint("/add"), Some(additions))?;
}
} else {
- rest_api_empty("DELETE", endpoint("/remove"), Some(&existing))?;
+ unstack(endpoint("/unstack"))?;
rest_api_empty(
"POST",
format!("repos/{owner}/{name}/stacks"),
@@ -506,6 +542,43 @@ pub fn reconcile_stack(prs: &[&Pr]) -> Result<()> {
Ok(())
}
+pub fn remove_stack(prs: &[&Pr]) -> Result<()> {
+ let (owner, name) = repo_name()?;
+ let stack_numbers: HashSet<u64> = prs
+ .iter()
+ .filter_map(|pr| pr.stack.as_ref().map(|stack| stack.number))
+ .collect();
+ if stack_numbers.len() > 1 {
+ bail!("local prs belong to multiple GitHub stacks: {stack_numbers:?}");
+ }
+ let Some(stack_number) = stack_numbers.iter().next().copied() else {
+ return Ok(());
+ };
+ let endpoint = format!("repos/{owner}/{name}/stacks/{stack_number}");
+ let stack = rest_stack(endpoint.clone())?;
+ let unmerged: Vec<u64> = stack
+ .pull_requests
+ .iter()
+ .filter(|pr| pr.merged_at.is_none())
+ .map(|pr| pr.number)
+ .collect();
+ let local_count = prs
+ .iter()
+ .filter(|pr| {
+ pr.stack
+ .as_ref()
+ .is_some_and(|stack| stack.number == stack_number)
+ })
+ .count();
+ if local_count != unmerged.len() {
+ bail!("GitHub stack {stack_number} contains unmerged prs outside the local stack");
+ }
+ if !unmerged.is_empty() {
+ unstack(format!("{endpoint}/unstack"))?;
+ }
+ Ok(())
+}
+
pub fn prs_by_change_id() -> Result<HashMap<String, Pr>> {
let mut by_id = HashMap::new();
for pr in prs()? {
diff --git b/src/praddle.rs a/src/praddle.rs
@@ -66,9 +66,25 @@ fn push(args: &cli::Push) -> Result<()> {
});
}
let has_cycles = detect_cycles(&any_changes);
+ if has_cycles {
+ let stacked_prs: Vec<&Pr> = any_changes
+ .iter()
+ .filter_map(|any_change| match any_change {
+ AnyChange::Change(change) => Some(&change.pr),
+ AnyChange::LocalChange(_) => None,
+ })
+ .collect();
+ gh::remove_stack(&stacked_prs).context("could not remove pr stack for rebase")?;
+ for any_change in any_changes.iter_mut() {
+ if let AnyChange::Change(change) = any_change {
+ change.pr.stack = None;
+ change.pr.stack_entry = None;
+ }
+ }
+ }
if has_cycles || args.draft {
any_changes
- .par_iter()
+ .par_iter_mut()
.filter_map(|ac| {
if let AnyChange::Change(c) = ac {
Some(c)
@@ -108,7 +124,7 @@ fn push(args: &cli::Push) -> Result<()> {
// FIXME: Should try to restore the original branch contents if we fail from this point on. It
// would be at least an attempt at being "atomic" about the push, and it would mean we don't
// lose the interdiff in a future re-run.
- let changes = any_changes
+ let mut changes = any_changes
.into_par_iter()
.map(|any_change| {
let change = match any_change {
@@ -123,17 +139,22 @@ fn push(args: &cli::Push) -> Result<()> {
})
.collect::<Result<Vec<_>>>()
.context("could not create new prs")?;
- changes
- .par_iter()
+ let bases: Vec<String> = changes
+ .iter()
.enumerate()
- .map(|(i, c)| {
- let parents = &changes[i + 1..];
- let base = parents
+ .map(|(i, _)| {
+ changes[i + 1..]
.iter()
.next()
.map(|p| p.local_change.remote_branch())
- .unwrap_or_else(|| env.base_branch().to_owned());
- if c.pr.base_ref_name != base {
+ .unwrap_or_else(|| env.base_branch().to_owned())
+ })
+ .collect();
+ changes
+ .par_iter_mut()
+ .zip(bases.par_iter())
+ .map(|(c, base)| {
+ if c.pr.base_ref_name != base.as_str() {
if c.pr.stack.is_some() {
bail!(
"cannot retarget pr {} while it is part of a stack",
|
slinder1
marked this pull request as ready for review
August 11, 2026 15:19
slinder1
marked this pull request as draft
August 11, 2026 15:28
slinder1
changed the base branch from
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
to
main
August 11, 2026 15:29
slinder1
force-pushed
the
users/slinder1/I8c0390e5375d7f66a86313042e52e039230d89e8
branch
from
August 11, 2026 15:29
2d248f8 to
9a64c3c
Compare
slinder1
changed the base branch from
main
to
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
August 11, 2026 15:29
slinder1
marked this pull request as ready for review
August 11, 2026 15:29
slinder1
marked this pull request as draft
August 11, 2026 16:05
slinder1
changed the base branch from
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
to
main
August 11, 2026 16:05
slinder1
changed the base branch from
main
to
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
August 11, 2026 16:05
slinder1
marked this pull request as ready for review
August 11, 2026 16:05
slinder1
force-pushed
the
users/slinder1/I8c0390e5375d7f66a86313042e52e039230d89e8
branch
from
August 11, 2026 16:09
9a64c3c to
28bda3a
Compare
slinder1
marked this pull request as draft
August 11, 2026 16:29
slinder1
changed the base branch from
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
to
main
August 11, 2026 16:29
slinder1
force-pushed
the
users/slinder1/I8c0390e5375d7f66a86313042e52e039230d89e8
branch
from
August 11, 2026 16:29
28bda3a to
c11ed81
Compare
slinder1
changed the base branch from
main
to
users/slinder1/Idc736a3b078ba2982cb38652ba2246c504a660c5
August 11, 2026 16:29
slinder1
marked this pull request as ready for review
August 11, 2026 16:30
slinder1
force-pushed
the
users/slinder1/I8c0390e5375d7f66a86313042e52e039230d89e8
branch
2 times, most recently
from
August 11, 2026 17:09
869beaf to
94bd1d6
Compare
Change-Id: I8c0390e5375d7f66a86313042e52e039230d89e8
slinder1
force-pushed
the
users/slinder1/I8c0390e5375d7f66a86313042e52e039230d89e8
branch
from
August 11, 2026 17:20
94bd1d6 to
232c423
Compare
slinder1
deleted the
users/slinder1/I8c0390e5375d7f66a86313042e52e039230d89e8
branch
August 11, 2026 17:24
slinder1
added a commit
that referenced
this pull request
Aug 12, 2026
Change-Id: I8c0390e5375d7f66a86313042e52e039230d89e8 Assisted-by: opencode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change-Id: I8c0390e5375d7f66a86313042e52e039230d89e8