Skip to content

Commit d7f7cab

Browse files
committed
swtich to nearest surviving branch after modify
After `gh stack modify` applies changes, the user may end up on an orphaned branch that is no longer part of the stack — for example, if their checked-out branch was dropped, folded into another branch, or renamed. Previously, the code blindly restored the original branch regardless of whether it still existed in the stack. Add a `resolveCheckoutBranch` helper that inspects the modify plan and the post-modify stack to determine the best branch to check out: 1. Still in stack → keep the original branch (no-op) 2. Renamed → check out the new name 3. Folded down → check out the fold target (branch below) 4. Folded up → check out the fold target (branch above) 5. Dropped → check out the nearest surviving neighbor (prefer above, fall back to below) 6. Fallback → topmost branch in the stack Both `ApplyPlan` and `ContinueApply` (the `--continue` path) now use this helper instead of unconditionally restoring the original branch. When the resolved branch differs from the original, a message is printed so the user knows they've been switched. The resolution uses the pre-modify snapshot (already persisted in the state file) to determine original adjacency, so it works correctly even when multiple branches are removed in the same operation. Includes 12 new tests: - 9 unit tests for resolveCheckoutBranch covering all action types, edge cases (topmost dropped, multiple drops, empty stack), and the fallback path - 3 integration tests verifying ApplyPlan checks out the correct branch after drop, fold-down, and rename operations
1 parent 4f79dfe commit d7f7cab

2 files changed

Lines changed: 406 additions & 4 deletions

File tree

internal/modify/apply.go

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,13 @@ func ApplyPlan(
495495
result.MovedBranches++
496496
}
497497

498-
// Restore original branch
499-
_ = git.CheckoutBranch(currentBranch)
498+
// Check out the best branch — the original if it's still in the stack,
499+
// otherwise the nearest surviving branch.
500+
targetBranch := resolveCheckoutBranch(currentBranch, plan, snapshot, s)
501+
_ = git.CheckoutBranch(targetBranch)
502+
if targetBranch != currentBranch {
503+
cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, currentBranch)
504+
}
500505

501506
// Update base SHAs
502507
updateBaseSHAs(s)
@@ -520,6 +525,103 @@ func ApplyPlan(
520525
return result, nil, nil
521526
}
522527

528+
// resolveCheckoutBranch determines which branch to check out after a modify
529+
// operation completes. If the user's original branch was dropped, folded, or
530+
// renamed, this returns the most appropriate surviving branch.
531+
func resolveCheckoutBranch(originalBranch string, plan []Action, snapshot Snapshot, s *stack.Stack) string {
532+
// Check if the original branch is still in the stack — quick exit.
533+
if s.IndexOf(originalBranch) >= 0 {
534+
return originalBranch
535+
}
536+
537+
// Scan the plan for an action that targeted the original branch.
538+
for _, a := range plan {
539+
if a.Branch != originalBranch {
540+
continue
541+
}
542+
543+
switch a.Type {
544+
case "rename":
545+
if a.NewName != "" && s.IndexOf(a.NewName) >= 0 {
546+
return a.NewName
547+
}
548+
549+
case "fold_down":
550+
// Fold-down merges into the branch below in the original order.
551+
if target := adjacentSnapshotBranch(snapshot, originalBranch, -1); target != "" {
552+
if s.IndexOf(target) >= 0 {
553+
return target
554+
}
555+
}
556+
557+
case "fold_up":
558+
// Fold-up merges into the branch above in the original order.
559+
if target := adjacentSnapshotBranch(snapshot, originalBranch, +1); target != "" {
560+
if s.IndexOf(target) >= 0 {
561+
return target
562+
}
563+
}
564+
565+
case "drop":
566+
// Prefer the branch that was directly above in the original order,
567+
// then fall back to the one below.
568+
if above := nearestSurvivingBranch(snapshot, originalBranch, s); above != "" {
569+
return above
570+
}
571+
}
572+
}
573+
574+
// Fallback: topmost branch in the stack.
575+
if len(s.Branches) > 0 {
576+
return s.Branches[len(s.Branches)-1].Branch
577+
}
578+
return originalBranch
579+
}
580+
581+
// adjacentSnapshotBranch returns the branch adjacent to target in the snapshot.
582+
// direction -1 means below (toward trunk), +1 means above (away from trunk).
583+
func adjacentSnapshotBranch(snapshot Snapshot, target string, direction int) string {
584+
for i, bs := range snapshot.Branches {
585+
if bs.Name == target {
586+
adj := i + direction
587+
if adj >= 0 && adj < len(snapshot.Branches) {
588+
return snapshot.Branches[adj].Name
589+
}
590+
return ""
591+
}
592+
}
593+
return ""
594+
}
595+
596+
// nearestSurvivingBranch finds the closest branch to the dropped branch that
597+
// still exists in the stack. Prefers the branch above (higher index), then below.
598+
func nearestSurvivingBranch(snapshot Snapshot, dropped string, s *stack.Stack) string {
599+
pos := -1
600+
for i, bs := range snapshot.Branches {
601+
if bs.Name == dropped {
602+
pos = i
603+
break
604+
}
605+
}
606+
if pos < 0 {
607+
return ""
608+
}
609+
610+
// Search above first (higher indices = away from trunk)
611+
for i := pos + 1; i < len(snapshot.Branches); i++ {
612+
if s.IndexOf(snapshot.Branches[i].Name) >= 0 {
613+
return snapshot.Branches[i].Name
614+
}
615+
}
616+
// Then below (lower indices = toward trunk)
617+
for i := pos - 1; i >= 0; i-- {
618+
if s.IndexOf(snapshot.Branches[i].Name) >= 0 {
619+
return snapshot.Branches[i].Name
620+
}
621+
}
622+
return ""
623+
}
624+
523625
// ContinueApply resumes a modify operation after the user resolves a rebase conflict.
524626
// It finishes the in-progress git rebase, then continues the cascading rebase for
525627
// remaining branches stored in the state file.
@@ -657,9 +759,13 @@ func ContinueApply(
657759
cfg.Successf("Rebased %s onto %s", branchName, newBase)
658760
}
659761

660-
// All rebases done — restore original branch
762+
// All rebases done — check out the best branch
661763
if state.OriginalBranch != "" {
662-
_ = git.CheckoutBranch(state.OriginalBranch)
764+
targetBranch := resolveCheckoutBranch(state.OriginalBranch, state.Plan, state.Snapshot, s)
765+
_ = git.CheckoutBranch(targetBranch)
766+
if targetBranch != state.OriginalBranch {
767+
cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, state.OriginalBranch)
768+
}
663769
}
664770

665771
// Update base SHAs

0 commit comments

Comments
 (0)