From f9fbd253ee24ef4fdde3057d2cb1a88a47d28bc0 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Fri, 22 May 2026 11:10:55 -0400 Subject: [PATCH 1/3] swtich to nearest surviving branch after modify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/modify/apply.go | 114 ++++++++++++- internal/modify/apply_test.go | 296 ++++++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+), 4 deletions(-) diff --git a/internal/modify/apply.go b/internal/modify/apply.go index 6dfe940..adcd7e9 100644 --- a/internal/modify/apply.go +++ b/internal/modify/apply.go @@ -495,8 +495,13 @@ func ApplyPlan( result.MovedBranches++ } - // Restore original branch - _ = git.CheckoutBranch(currentBranch) + // Check out the best branch — the original if it's still in the stack, + // otherwise the nearest surviving branch. + targetBranch := resolveCheckoutBranch(currentBranch, plan, snapshot, s) + _ = git.CheckoutBranch(targetBranch) + if targetBranch != currentBranch { + cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, currentBranch) + } // Update base SHAs updateBaseSHAs(s) @@ -520,6 +525,103 @@ func ApplyPlan( return result, nil, nil } +// resolveCheckoutBranch determines which branch to check out after a modify +// operation completes. If the user's original branch was dropped, folded, or +// renamed, this returns the most appropriate surviving branch. +func resolveCheckoutBranch(originalBranch string, plan []Action, snapshot Snapshot, s *stack.Stack) string { + // Check if the original branch is still in the stack — quick exit. + if s.IndexOf(originalBranch) >= 0 { + return originalBranch + } + + // Scan the plan for an action that targeted the original branch. + for _, a := range plan { + if a.Branch != originalBranch { + continue + } + + switch a.Type { + case "rename": + if a.NewName != "" && s.IndexOf(a.NewName) >= 0 { + return a.NewName + } + + case "fold_down": + // Fold-down merges into the branch below in the original order. + if target := adjacentSnapshotBranch(snapshot, originalBranch, -1); target != "" { + if s.IndexOf(target) >= 0 { + return target + } + } + + case "fold_up": + // Fold-up merges into the branch above in the original order. + if target := adjacentSnapshotBranch(snapshot, originalBranch, +1); target != "" { + if s.IndexOf(target) >= 0 { + return target + } + } + + case "drop": + // Prefer the branch that was directly above in the original order, + // then fall back to the one below. + if above := nearestSurvivingBranch(snapshot, originalBranch, s); above != "" { + return above + } + } + } + + // Fallback: topmost branch in the stack. + if len(s.Branches) > 0 { + return s.Branches[len(s.Branches)-1].Branch + } + return originalBranch +} + +// adjacentSnapshotBranch returns the branch adjacent to target in the snapshot. +// direction -1 means below (toward trunk), +1 means above (away from trunk). +func adjacentSnapshotBranch(snapshot Snapshot, target string, direction int) string { + for i, bs := range snapshot.Branches { + if bs.Name == target { + adj := i + direction + if adj >= 0 && adj < len(snapshot.Branches) { + return snapshot.Branches[adj].Name + } + return "" + } + } + return "" +} + +// nearestSurvivingBranch finds the closest branch to the dropped branch that +// still exists in the stack. Prefers the branch above (higher index), then below. +func nearestSurvivingBranch(snapshot Snapshot, dropped string, s *stack.Stack) string { + pos := -1 + for i, bs := range snapshot.Branches { + if bs.Name == dropped { + pos = i + break + } + } + if pos < 0 { + return "" + } + + // Search above first (higher indices = away from trunk) + for i := pos + 1; i < len(snapshot.Branches); i++ { + if s.IndexOf(snapshot.Branches[i].Name) >= 0 { + return snapshot.Branches[i].Name + } + } + // Then below (lower indices = toward trunk) + for i := pos - 1; i >= 0; i-- { + if s.IndexOf(snapshot.Branches[i].Name) >= 0 { + return snapshot.Branches[i].Name + } + } + return "" +} + // ContinueApply resumes a modify operation after the user resolves a rebase conflict. // It finishes the in-progress git rebase, then continues the cascading rebase for // remaining branches stored in the state file. @@ -657,9 +759,13 @@ func ContinueApply( cfg.Successf("Rebased %s onto %s", branchName, newBase) } - // All rebases done — restore original branch + // All rebases done — check out the best branch if state.OriginalBranch != "" { - _ = git.CheckoutBranch(state.OriginalBranch) + targetBranch := resolveCheckoutBranch(state.OriginalBranch, state.Plan, state.Snapshot, s) + _ = git.CheckoutBranch(targetBranch) + if targetBranch != state.OriginalBranch { + cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, state.OriginalBranch) + } } // Update base SHAs diff --git a/internal/modify/apply_test.go b/internal/modify/apply_test.go index d98b75b..b3bf1b9 100644 --- a/internal/modify/apply_test.go +++ b/internal/modify/apply_test.go @@ -1344,3 +1344,299 @@ func TestApplyPlan_ClearsStateForLocalStack(t *testing.T) { // Local stack (no ID) should clear the state file assert.False(t, StateExists(gitDir)) } + +// ─── resolveCheckoutBranch ────────────────────────────────────────────────── + +func TestResolveCheckoutBranch_StillInStack(t *testing.T) { + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "B"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{{Name: "A", Position: 0}, {Name: "B", Position: 1}}, + } + + result := resolveCheckoutBranch("A", nil, snapshot, s) + assert.Equal(t, "A", result) +} + +func TestResolveCheckoutBranch_Renamed(t *testing.T) { + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "new-A"}, {Branch: "B"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{{Name: "A", Position: 0}, {Name: "B", Position: 1}}, + } + plan := []Action{{Type: "rename", Branch: "A", NewName: "new-A"}} + + result := resolveCheckoutBranch("A", plan, snapshot, s) + assert.Equal(t, "new-A", result) +} + +func TestResolveCheckoutBranch_FoldDown(t *testing.T) { + // B is folded down into A. After fold, stack has [A, C]. + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "C"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + }, + } + plan := []Action{{Type: "fold_down", Branch: "B"}} + + result := resolveCheckoutBranch("B", plan, snapshot, s) + assert.Equal(t, "A", result) +} + +func TestResolveCheckoutBranch_FoldUp(t *testing.T) { + // B is folded up into C. After fold, stack has [A, C]. + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "C"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + }, + } + plan := []Action{{Type: "fold_up", Branch: "B"}} + + result := resolveCheckoutBranch("B", plan, snapshot, s) + assert.Equal(t, "C", result) +} + +func TestResolveCheckoutBranch_Dropped_HasAbove(t *testing.T) { + // B is dropped. Stack has [A, C]. Should pick C (above B). + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "C"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + }, + } + plan := []Action{{Type: "drop", Branch: "B"}} + + result := resolveCheckoutBranch("B", plan, snapshot, s) + assert.Equal(t, "C", result) +} + +func TestResolveCheckoutBranch_Dropped_TopBranch(t *testing.T) { + // C (topmost) is dropped. Stack has [A, B]. Should pick B (below C). + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "B"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + }, + } + plan := []Action{{Type: "drop", Branch: "C"}} + + result := resolveCheckoutBranch("C", plan, snapshot, s) + assert.Equal(t, "B", result) +} + +func TestResolveCheckoutBranch_Dropped_MultipleDropped(t *testing.T) { + // B and C both dropped. Stack has [A, D]. Original on B → should pick D (nearest above). + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "D"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + {Name: "D", Position: 3}, + }, + } + plan := []Action{ + {Type: "drop", Branch: "B"}, + {Type: "drop", Branch: "C"}, + } + + result := resolveCheckoutBranch("B", plan, snapshot, s) + assert.Equal(t, "D", result) +} + +func TestResolveCheckoutBranch_Fallback_EmptyStack(t *testing.T) { + // All branches removed — falls back to original (no crash). + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{{Name: "A", Position: 0}}, + } + plan := []Action{{Type: "drop", Branch: "A"}} + + result := resolveCheckoutBranch("A", plan, snapshot, s) + // No surviving branches → returns original as last resort + assert.Equal(t, "A", result) +} + +func TestResolveCheckoutBranch_Fallback_TopBranch(t *testing.T) { + // Original branch not in plan and not in stack → fallback to topmost. + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "X"}, {Branch: "Y"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{{Name: "A", Position: 0}}, + } + + result := resolveCheckoutBranch("A", nil, snapshot, s) + assert.Equal(t, "Y", result) +} + +// ─── ApplyPlan: Checkout behavior after drop ──────────────────────────────── + +func TestApplyPlan_Drop_ChecksOutNearestBranch(t *testing.T) { + s := stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{ + {Branch: "A"}, + {Branch: "B"}, + {Branch: "C"}, + }, + } + + gitDir := t.TempDir() + sf := writeTestStackFile(t, gitDir, s) + + branchSHAs := map[string]string{ + "main": "sha-main", + "A": "sha-A", + "B": "sha-B", + "C": "sha-C", + } + + var lastCheckout string + mock := newApplyMock(gitDir, branchSHAs) + mock.CheckoutBranchFn = func(name string) error { + lastCheckout = name + return nil + } + + restore := git.SetOps(mock) + defer restore() + + cfg, _, _ := config.NewTestConfig() + defer cfg.Out.Close() + defer cfg.Err.Close() + + // Drop B, user was on B + nodes := makeNodes(&sf.Stacks[0]) + nodes[1].PendingAction = &modifyview.PendingAction{Type: modifyview.ActionDrop} + nodes[1].Removed = true + + _, _, err := ApplyPlan(cfg, gitDir, &sf.Stacks[0], sf, nodes, "B", noopUpdateBaseSHAs) + require.NoError(t, err) + + // Should check out C (branch above B), not B + assert.Equal(t, "C", lastCheckout) +} + +func TestApplyPlan_FoldDown_ChecksOutTarget(t *testing.T) { + s := stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{ + {Branch: "A"}, + {Branch: "B"}, + }, + } + + gitDir := t.TempDir() + sf := writeTestStackFile(t, gitDir, s) + + branchSHAs := map[string]string{ + "main": "sha-main", + "A": "sha-A", + "B": "sha-B", + } + + var lastCheckout string + mock := newApplyMock(gitDir, branchSHAs) + mock.CheckoutBranchFn = func(name string) error { + lastCheckout = name + return nil + } + mock.LogRangeFn = func(base, head string) ([]git.CommitInfo, error) { + return []git.CommitInfo{{SHA: "commit-1"}}, nil + } + + restore := git.SetOps(mock) + defer restore() + + cfg, _, _ := config.NewTestConfig() + defer cfg.Out.Close() + defer cfg.Err.Close() + + // Fold B down into A, user was on B + nodes := makeNodes(&sf.Stacks[0]) + nodes[1].PendingAction = &modifyview.PendingAction{Type: modifyview.ActionFoldDown} + nodes[1].Removed = true + + _, _, err := ApplyPlan(cfg, gitDir, &sf.Stacks[0], sf, nodes, "B", noopUpdateBaseSHAs) + require.NoError(t, err) + + // Should check out A (fold target), not B + assert.Equal(t, "A", lastCheckout) +} + +func TestApplyPlan_Rename_ChecksOutNewName(t *testing.T) { + s := stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{ + {Branch: "A"}, + {Branch: "B"}, + }, + } + + gitDir := t.TempDir() + sf := writeTestStackFile(t, gitDir, s) + + branchSHAs := map[string]string{ + "main": "sha-main", + "A": "sha-A", + "B": "sha-B", + } + + var lastCheckout string + mock := newApplyMock(gitDir, branchSHAs) + mock.CheckoutBranchFn = func(name string) error { + lastCheckout = name + return nil + } + + restore := git.SetOps(mock) + defer restore() + + cfg, _, _ := config.NewTestConfig() + defer cfg.Out.Close() + defer cfg.Err.Close() + + // Rename A to new-A, user was on A + nodes := makeNodes(&sf.Stacks[0]) + nodes[0].PendingAction = &modifyview.PendingAction{Type: modifyview.ActionRename, NewName: "new-A"} + + _, _, err := ApplyPlan(cfg, gitDir, &sf.Stacks[0], sf, nodes, "A", noopUpdateBaseSHAs) + require.NoError(t, err) + + // Should check out new-A, not A + assert.Equal(t, "new-A", lastCheckout) +} From eb39abc03926399993ede9bda02e13af72025e44 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Sat, 23 May 2026 22:04:41 -0400 Subject: [PATCH 2/3] handle CheckoutBranch errors --- internal/modify/apply.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/modify/apply.go b/internal/modify/apply.go index adcd7e9..ef9e164 100644 --- a/internal/modify/apply.go +++ b/internal/modify/apply.go @@ -498,10 +498,11 @@ func ApplyPlan( // Check out the best branch — the original if it's still in the stack, // otherwise the nearest surviving branch. targetBranch := resolveCheckoutBranch(currentBranch, plan, snapshot, s) - _ = git.CheckoutBranch(targetBranch) + if err := git.CheckoutBranch(targetBranch); err == nil { if targetBranch != currentBranch { cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, currentBranch) } + } // Update base SHAs updateBaseSHAs(s) @@ -762,9 +763,10 @@ func ContinueApply( // All rebases done — check out the best branch if state.OriginalBranch != "" { targetBranch := resolveCheckoutBranch(state.OriginalBranch, state.Plan, state.Snapshot, s) - _ = git.CheckoutBranch(targetBranch) + if err := git.CheckoutBranch(targetBranch); err == nil { if targetBranch != state.OriginalBranch { cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, state.OriginalBranch) + } } } From b4d6ebe2a9616d47845a5a79bb353ed54ad6902f Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Sat, 23 May 2026 22:06:58 -0400 Subject: [PATCH 3/3] handle renamed branches --- internal/modify/apply.go | 55 +++++++++++++++++++++++++---------- internal/modify/apply_test.go | 46 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/internal/modify/apply.go b/internal/modify/apply.go index ef9e164..b39ecd4 100644 --- a/internal/modify/apply.go +++ b/internal/modify/apply.go @@ -499,9 +499,9 @@ func ApplyPlan( // otherwise the nearest surviving branch. targetBranch := resolveCheckoutBranch(currentBranch, plan, snapshot, s) if err := git.CheckoutBranch(targetBranch); err == nil { - if targetBranch != currentBranch { - cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, currentBranch) - } + if targetBranch != currentBranch { + cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, currentBranch) + } } // Update base SHAs @@ -535,6 +535,24 @@ func resolveCheckoutBranch(originalBranch string, plan []Action, snapshot Snapsh return originalBranch } + // Build a rename map (old name → new name) so we can translate snapshot + // neighbor names that may have been renamed in the same modify operation. + renames := make(map[string]string) + for _, a := range plan { + if a.Type == "rename" && a.NewName != "" { + renames[a.Branch] = a.NewName + } + } + + // resolvedName returns the post-rename name for a branch, or the + // original name if it wasn't renamed. + resolvedName := func(name string) string { + if newName, ok := renames[name]; ok { + return newName + } + return name + } + // Scan the plan for an action that targeted the original branch. for _, a := range plan { if a.Branch != originalBranch { @@ -550,24 +568,26 @@ func resolveCheckoutBranch(originalBranch string, plan []Action, snapshot Snapsh case "fold_down": // Fold-down merges into the branch below in the original order. if target := adjacentSnapshotBranch(snapshot, originalBranch, -1); target != "" { - if s.IndexOf(target) >= 0 { - return target + resolved := resolvedName(target) + if s.IndexOf(resolved) >= 0 { + return resolved } } case "fold_up": // Fold-up merges into the branch above in the original order. if target := adjacentSnapshotBranch(snapshot, originalBranch, +1); target != "" { - if s.IndexOf(target) >= 0 { - return target + resolved := resolvedName(target) + if s.IndexOf(resolved) >= 0 { + return resolved } } case "drop": // Prefer the branch that was directly above in the original order, // then fall back to the one below. - if above := nearestSurvivingBranch(snapshot, originalBranch, s); above != "" { - return above + if nearest := nearestSurvivingBranch(snapshot, originalBranch, s, resolvedName); nearest != "" { + return nearest } } } @@ -596,7 +616,8 @@ func adjacentSnapshotBranch(snapshot Snapshot, target string, direction int) str // nearestSurvivingBranch finds the closest branch to the dropped branch that // still exists in the stack. Prefers the branch above (higher index), then below. -func nearestSurvivingBranch(snapshot Snapshot, dropped string, s *stack.Stack) string { +// resolvedName translates snapshot names through any renames from the same operation. +func nearestSurvivingBranch(snapshot Snapshot, dropped string, s *stack.Stack, resolvedName func(string) string) string { pos := -1 for i, bs := range snapshot.Branches { if bs.Name == dropped { @@ -610,14 +631,16 @@ func nearestSurvivingBranch(snapshot Snapshot, dropped string, s *stack.Stack) s // Search above first (higher indices = away from trunk) for i := pos + 1; i < len(snapshot.Branches); i++ { - if s.IndexOf(snapshot.Branches[i].Name) >= 0 { - return snapshot.Branches[i].Name + name := resolvedName(snapshot.Branches[i].Name) + if s.IndexOf(name) >= 0 { + return name } } // Then below (lower indices = toward trunk) for i := pos - 1; i >= 0; i-- { - if s.IndexOf(snapshot.Branches[i].Name) >= 0 { - return snapshot.Branches[i].Name + name := resolvedName(snapshot.Branches[i].Name) + if s.IndexOf(name) >= 0 { + return name } } return "" @@ -764,8 +787,8 @@ func ContinueApply( if state.OriginalBranch != "" { targetBranch := resolveCheckoutBranch(state.OriginalBranch, state.Plan, state.Snapshot, s) if err := git.CheckoutBranch(targetBranch); err == nil { - if targetBranch != state.OriginalBranch { - cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, state.OriginalBranch) + if targetBranch != state.OriginalBranch { + cfg.Printf("Switched to %s (original branch %s is no longer in the stack)", targetBranch, state.OriginalBranch) } } } diff --git a/internal/modify/apply_test.go b/internal/modify/apply_test.go index b3bf1b9..084bc92 100644 --- a/internal/modify/apply_test.go +++ b/internal/modify/apply_test.go @@ -1503,6 +1503,52 @@ func TestResolveCheckoutBranch_Fallback_TopBranch(t *testing.T) { assert.Equal(t, "Y", result) } +func TestResolveCheckoutBranch_FoldDown_TargetRenamed(t *testing.T) { + // B is folded down into A, and A is renamed to new-A in the same operation. + // After apply, stack has [new-A, C]. + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "new-A"}, {Branch: "C"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + }, + } + plan := []Action{ + {Type: "rename", Branch: "A", NewName: "new-A"}, + {Type: "fold_down", Branch: "B"}, + } + + result := resolveCheckoutBranch("B", plan, snapshot, s) + assert.Equal(t, "new-A", result) +} + +func TestResolveCheckoutBranch_Dropped_NeighborRenamed(t *testing.T) { + // B is dropped, and C (above) is renamed to new-C in the same operation. + // After apply, stack has [A, new-C]. + s := &stack.Stack{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "A"}, {Branch: "new-C"}}, + } + snapshot := Snapshot{ + Branches: []BranchSnapshot{ + {Name: "A", Position: 0}, + {Name: "B", Position: 1}, + {Name: "C", Position: 2}, + }, + } + plan := []Action{ + {Type: "rename", Branch: "C", NewName: "new-C"}, + {Type: "drop", Branch: "B"}, + } + + result := resolveCheckoutBranch("B", plan, snapshot, s) + assert.Equal(t, "new-C", result) +} + // ─── ApplyPlan: Checkout behavior after drop ──────────────────────────────── func TestApplyPlan_Drop_ChecksOutNearestBranch(t *testing.T) {