Skip to content

fix --onto rebase for merged branches#43

Open
skarim wants to merge 3 commits intoskarim/fix-sync-deletedfrom
skarim/fix-onto-rebase
Open

fix --onto rebase for merged branches#43
skarim wants to merge 3 commits intoskarim/fix-sync-deletedfrom
skarim/fix-onto-rebase

Conversation

@skarim
Copy link
Copy Markdown
Collaborator

@skarim skarim commented Apr 16, 2026

Fix --onto rebase for merged branches (fixes #31)

When a branch in the stack is merged on remote, subsequent rebase and sync operations need to use git rebase --onto to transplant the next branch onto trunk. Fixes for this in a couple of areas:

  • Deleted merged branch: The merged branch's ref no longer exists locally, so originalRefs had no entry and ontoOldBase was empty — causing git rebase --onto main "" b2 to fail. Fixed by backfilling originalRefs from the persisted BranchRef.Head SHA in the stack file.
  • --upstack flag: When using --upstack, the rebase loop starts at the current branch, so it never iterates over merged branches below it and never sets needsOnto. Fixed by checking the immediate predecessor first.
  • Stale refs on repeated runs: After the first rebase handles a merged branch, the old base SHA becomes stale (no longer an ancestor of the target). A second run would replay already-applied commits, causing spurious conflicts. Fixed by adding an IsAncestor guard that falls back to merge-base when the old base is stale.

Copilot AI review requested due to automatic review settings April 16, 2026 14:58
@skarim skarim changed the title skarim/fix onto rebase fix --onto rebase for merged branches Apr 16, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves how gh stack sync and gh stack rebase compute git rebase --onto arguments when merged branches are present—especially when merged branches have been deleted locally—and adds coverage for “stale onto old base” scenarios to avoid replaying already-applied commits.

Changes:

  • Backfill originalRefs for merged branches deleted locally using the stack file’s stored BranchRef.Head so --onto rebases have a valid oldBase.
  • Detect stale ontoOldBase (no longer an ancestor) and fall back to merge-base(newBase, branch) for the divergence point.
  • Add/adjust tests covering stale ontoOldBase, --upstack with merged predecessor branches, and merged-branch-deleted scenarios.
Show a summary per file
File Description
cmd/sync.go Backfills originalRefs for deleted merged branches and adds stale ontoOldBasemerge-base fallback during cascade rebase.
cmd/rebase.go Same backfill + stale-base fallback, plus pre-seeding --onto state for --upstack when a merged branch is immediately below the range.
cmd/sync_test.go Adds a stale ontoOldBase test and strengthens merged-branch-deleted expectations using stored Head.
cmd/rebase_test.go Adds stale ontoOldBase test, --upstack merged-predecessor coverage, and updates merged-branch-deleted test to validate stored Head usage.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (2)

cmd/sync.go:207

  • The stale ontoOldBase fallback currently runs only when IsAncestor returns (false, nil), and it silently ignores errors from both IsAncestor and MergeBase. If ontoOldBase is empty (e.g., merged branch deleted locally and no stored Head) or either git call errors, actualOldBase can remain empty/stale and RebaseOnto will fail with a misleading “Conflict detected” path. Consider explicitly handling ontoOldBase == "" (compute merge-base(newBase, branch) or return a targeted error), and surface errors from IsAncestor/MergeBase as warnings or failures rather than silently proceeding.
				// If ontoOldBase is stale (not an ancestor of the branch), the
				// branch was already rebased past it (e.g. by a previous run).
				// Fall back to merge-base(newBase, branch) to avoid replaying
				// already-applied commits.
				actualOldBase := ontoOldBase
				if isAnc, err := git.IsAncestor(ontoOldBase, br.Branch); err == nil && !isAnc {
					if mb, err := git.MergeBase(newBase, br.Branch); err == nil {
						actualOldBase = mb
					}
				}

cmd/rebase.go:257

  • Similar to sync: the stale ontoOldBase detection ignores errors and doesn’t guard against an empty ontoOldBase. If ontoOldBase is empty/stale and IsAncestor errors (or MergeBase fails), actualOldBase can remain invalid and RebaseOnto will fail, surfacing as a generic conflict flow. Prefer treating empty ontoOldBase as a first-class case (compute merge-base(newBase, branch) or error), and propagate/emit warnings when IsAncestor or MergeBase fail so users aren’t left with unexplained behavior.
			// If ontoOldBase is stale (not an ancestor of the branch), the
			// branch was already rebased past it (e.g. by a previous run).
			// Fall back to merge-base(newBase, branch) which gives the correct
			// divergence point and avoids replaying already-applied commits.
			actualOldBase := ontoOldBase
			if isAnc, err := git.IsAncestor(ontoOldBase, br.Branch); err == nil && !isAnc {
				if mb, err := git.MergeBase(newBase, br.Branch); err == nil {
					actualOldBase = mb
				}
			}
  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment thread cmd/sync.go
Comment thread cmd/rebase.go
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