Skip to content

fix: resolve BranchOnto target from git ref, not stored state#1151

Closed
ed-irl wants to merge 1 commit into
ed-irl/sync-survivingancestor-loopfrom
branch-onto-resolve-from-ref
Closed

fix: resolve BranchOnto target from git ref, not stored state#1151
ed-irl wants to merge 1 commit into
ed-irl/sync-survivingancestor-loopfrom
branch-onto-resolve-from-ref

Conversation

@ed-irl

@ed-irl ed-irl commented May 12, 2026

Copy link
Copy Markdown
Collaborator

When BranchOnto moved a non-trunk target, it used the target branch's
stored Head hash to rebase the moved branch. This stored value could
be stale within a single transaction — for example, when branch onto
restacks upstack branches after moving the target, each subsequent
rebase saw the pre-move hash rather than the branch's actual ref.

Resolve the target by peeling its git ref instead, which always
reflects the current state. The tracked-branch check is preserved for
non-trunk targets, but the hash itself now comes from the ref.

This issue is similar to #1150 - the gs bm command will introduce the possibility of iterative changes to the stack structure during command execution, so there's a need to re-read refs during command execution.

I also had some issues remembering why exactly we needed this. Claude had this somewhat more verbose explanation:

This is a defensive consistency fix in BranchOnto. Before: the target hash was resolved from the git ref when the target was trunk, but from LookupBranch(...).Head when non-trunk. Even though both paths ultimately route through PeelToCommit, the two-source pattern made BranchOnto's behavior sensitive to small state/ref desynchronization windows that show up in chained BranchOnto calls (stack edit, delete-with-upstack-reparent, gs bm's cleanup loop).

The fix collapses both to PeelToCommit and uses LookupBranch only as a tracking-check for non-trunk targets. The intent is that BranchOnto's rebase target is always exactly what git currently says — no transient stored-state path can produce a stale hash.

This surfaced while developing gs branch merge, which calls BranchOnto indirectly through gs branch delete for upstack reparenting after each merge in the plan. The corruption symptom there was hard to reproduce in isolation because it requires a partial-stack merge whose surviving base isn't trunk.

[skip changelog]: bug fix only

@ed-irl

ed-irl commented May 12, 2026

Copy link
Copy Markdown
Collaborator Author

@ed-irl ed-irl requested a review from abhinav May 12, 2026 12:55
@ed-irl ed-irl added skip changelog PRs that don't need a changelog. labels May 12, 2026
@ed-irl ed-irl force-pushed the sync-survivingancestor-loop branch from a40644a to 979d0cd Compare May 12, 2026 13:48
@ed-irl ed-irl changed the title fix: resolve BranchOnto target from git ref, not stored state feat: add branch merge and ci merge-guard commands May 12, 2026
@ed-irl ed-irl force-pushed the branch-onto-resolve-from-ref branch from 265d671 to 3e389f6 Compare May 12, 2026 13:49
@ed-irl ed-irl changed the title feat: add branch merge and ci merge-guard commands fix: resolve BranchOnto target from git ref, not stored state May 12, 2026
@ed-irl ed-irl force-pushed the branch-onto-resolve-from-ref branch from 3e389f6 to 0d9161c Compare May 12, 2026 15:11
@abhinav abhinav force-pushed the sync-survivingancestor-loop branch from 979d0cd to 3d9ac92 Compare May 17, 2026 21:43
BranchOnto resolved the target hash from the stored state
(LookupBranch.Head) for non-trunk branches, but used
PeelToCommit (current git ref) for trunk. This asymmetry
could cause stale target hashes when a prior operation in
the same transaction modified the target branch's git ref
without updating the stored state.

Use PeelToCommit consistently for all target branches.
@abhinav abhinav force-pushed the branch-onto-resolve-from-ref branch from 0d9161c to 19e5015 Compare May 17, 2026 21:51

@abhinav abhinav left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is right. The premise in the PR message is false.
LookupBranch does not use a recorded Head commit hash. Only the base hash uses recorded values:

We load storage information and current PeelToCommit:

func (s *Service) LookupBranch(ctx context.Context, name string) (*LookupBranchResponse, error) {
resp, storeErr := s.store.LookupBranch(ctx, name)
head, gitErr := s.repo.PeelToCommit(ctx, name)

Then we put that directly in the response:

out := &LookupBranchResponse{
Base: resp.Base,
BaseHash: resp.BaseHash,
UpstreamBranch: resp.UpstreamBranch,
Head: head,

So I think this might've been a speculative fix for something else that went wrong?

@ed-irl ed-irl deleted the branch ed-irl/sync-survivingancestor-loop May 18, 2026 11:44
@ed-irl ed-irl closed this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip changelog PRs that don't need a changelog.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants