Skip to content

sync: retarget upstack PRs after deleting merged branches#1073

Closed
ed-irl wants to merge 1 commit into
abhinav:mainfrom
irl-llc:gs/sync-retarget
Closed

sync: retarget upstack PRs after deleting merged branches#1073
ed-irl wants to merge 1 commit into
abhinav:mainfrom
irl-llc:gs/sync-retarget

Conversation

@ed-irl

@ed-irl ed-irl commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes a "bug" I discovered while testing my branch merge implementation where syncs could cause weird state that would mess up future merges.

  • After repo sync deletes merged branches, surviving upstack PRs are retargeted on the forge so their base matches the updated local state
  • Walks the base chain to find the nearest surviving ancestor, falling back to trunk for cycles or missing entries
  • Covers multi-level deletions (e.g., A and B merged, C retargets to main)

[skip changelog]: bug fix only

Add cycle detection to survivingAncestor so that if deleted branches
form a cycle in their base references, the function falls back to
trunk instead of looping forever.
@ed-irl ed-irl changed the title fix(sync): prevent infinite loop in survivingAncestor on cyclic bases sync: retarget upstack PRs after deleting merged branches Mar 25, 2026
@abhinav

abhinav commented Apr 6, 2026

Copy link
Copy Markdown
Owner

Thanks, @ed-irl!
So just to clarify/confirm the behavior:
git-spice already retargets upstack branches when their bases are deleted, but it does so entirely locally.
The focus of this change is to specifically also update the forge?
The next submit for those branches would also update the base branch in the forge.
I would appreciate some more context on what issue we're running into here.
(Context on my hesitance: repo sync is currently read-only for the forge. This would add a write, so I want to make sure we have a good reason to.)

@abhinav abhinav force-pushed the main branch 2 times, most recently from e2b9bf5 to 02c7c2d Compare April 20, 2026 01:20
@ed-irl

ed-irl commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #1150

@abhinav

abhinav commented May 17, 2026

Copy link
Copy Markdown
Owner

Thanks, the explanation in #1150 is helpful.
I think you forgot to press close, so I'll close in favor of #1150.

@abhinav abhinav closed this May 17, 2026
abhinav added a commit that referenced this pull request May 23, 2026
When `gs repo sync` deletes branches whose PRs were merged,
upstack PRs on the forge are left targeting deleted branches.
This causes confusion on platforms like GitHub, where the PR
base no longer matches the local stack state.

This change retargets surviving upstack PRs on the forge to
their nearest surviving ancestor (or trunk if none exists)
after sync deletes merged branches. Cyclic base chains fall
back to trunk to avoid infinite loops.

Covered by unit tests for candidate collection and ancestor
resolution, plus test scripts exercising single-merge and
deep-stack scenarios end-to-end.

Regarding this question on #1073:
> So just to clarify/confirm the behavior:
> git-spice already retargets upstack branches when their bases are
deleted, but it does so entirely locally.
> The focus of this change is to specifically also update the forge?
> The next submit for those branches would also update the base branch
in the forge.
> I would appreciate some more context on what issue we're running into
here.
> (Context on my hesitance: repo sync is currently read-only for the
forge. This would add a write, so I want to make sure we have a good
reason to.)

The issue encountered is branch corruption when adding the `gs bm`
command - which I will post later today on this stack. Prior to the
merge command, the forge itself drove merge events and the sync/submit
cycle pulled merge changes and drove repointing based on merges that
happened elsewhere. When I added the `gs bm` command the flow was
flipped, and the stack would become corrupted if you merged anything
besides the bottom branch. I'll post a ref to that PR when I post it. It
took me a minute to remember what had been happening and I needed claude
to help me figure it out again 😂 . It provided this somewhat more
verbose explanation:

> Before gs branch merge, gs read forge merge state and reconciled local
— that's the read-only pattern you're describing, and repo sync was fine
being one-way (forge → local). The next submit falling back to fix the
forge base was fine because nothing else gs did was authoritative over
forge merges.
> 
> gs branch merge (in a separate PR in this stack) breaks that
assumption. It writes merges to the forge and then reacts to those
merges by mutating local state (deletes branches, reparents upstack).
That makes the forge↔local invariant load-bearing inside gs bm, not just
at "next submit" time. Concretely, a stack of A → B → C where A was
squash-merged externally and you then run gs bm C will silently bring
A's pre-squash commits back into main via B's merge, because B's remote
branch still contains them and gs has no way to know B should be rebased
first.
>
> PR #1073 is the other half of that contract: when repo sync rebases B
locally, also retarget B's forge PR base so the forge and local agree on
what B's parent is. Without it, gs is making local decisions (rebases,
reparents) that the forge isn't told about, and the next forge-driven
operation (review, or another gs bm) sees an inconsistent world.

---------

Co-authored-by: Abhinav Gupta <mail@abhinavg.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants