Skip to content

swtich to nearest surviving branch after modify#105

Open
skarim wants to merge 1 commit into
skarim/add-existing-branchfrom
skarim/switch-after-modify
Open

swtich to nearest surviving branch after modify#105
skarim wants to merge 1 commit into
skarim/add-existing-branchfrom
skarim/switch-after-modify

Conversation

@skarim
Copy link
Copy Markdown
Collaborator

@skarim skarim commented May 22, 2026

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

Stack created with GitHub Stacks CLIGive Feedback 💬

@skarim skarim force-pushed the skarim/add-existing-branch branch from 1fdd772 to d522c91 Compare May 22, 2026 16:27
@skarim skarim force-pushed the skarim/switch-after-modify branch from f7e7a80 to f633f33 Compare May 22, 2026 16:27
@skarim skarim marked this pull request as ready for review May 23, 2026 21:42
Copilot AI review requested due to automatic review settings May 23, 2026 21:42
Copy link
Copy Markdown
Contributor

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 gh stack modify UX by choosing a sensible post-modify checkout branch when the user’s original branch is no longer part of the stack (dropped/folded/renamed), instead of always restoring the original branch.

Changes:

  • Add resolveCheckoutBranch (plus snapshot-neighbor helpers) to determine the best branch to check out after modify.
  • Use the resolver in both ApplyPlan and ContinueApply, and print a message when switching away from the original branch.
  • Add unit tests for branch resolution and integration tests asserting final checkout behavior for drop/fold-down/rename scenarios.
Show a summary per file
File Description
internal/modify/apply.go Adds resolver logic and uses it in ApplyPlan/ContinueApply to avoid landing on an orphaned branch.
internal/modify/apply_test.go Adds unit + integration tests covering post-modify checkout resolution.

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)

internal/modify/apply.go:535

  • resolveCheckoutBranch treats only stack.Branches as “in stack”; if the user started on the trunk branch (e.g. "main"), s.IndexOf returns -1 and the function may incorrectly switch them to the topmost branch. Use Stack.Contains (which includes trunk) for the quick-exit check.
	// Check if the original branch is still in the stack — quick exit.
	if s.IndexOf(originalBranch) >= 0 {
		return originalBranch
	}

internal/modify/apply.go:571

  • Branch resolution for fold/drop uses snapshot neighbor names and checks them directly against the post-modify stack. If a neighboring branch was renamed in the same modify operation (e.g. fold B down into A while also renaming A→new-A), the neighbor lookup will miss and resolution can fall back to an unrelated branch. Consider applying rename actions from the plan to candidate neighbor names before checking s.IndexOf / returning the target.
		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
			}
		}
  • Files reviewed: 2/2 changed files
  • Comments generated: 3

Comment thread internal/modify/apply.go
Comment on lines +498 to +504
// 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)
}
Comment thread internal/modify/apply.go
Comment on lines +762 to +768
// 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)
}
Comment on lines +1348 to +1361
// ─── 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)
}
@skarim skarim force-pushed the skarim/switch-after-modify branch from f633f33 to d7f7cab Compare May 23, 2026 22:05
@skarim skarim force-pushed the skarim/add-existing-branch branch 2 times, most recently from 4f79dfe to 3ac2962 Compare May 23, 2026 22:33
@skarim skarim force-pushed the skarim/switch-after-modify branch from d7f7cab to 2609d7f Compare May 23, 2026 22:33
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
@skarim skarim force-pushed the skarim/switch-after-modify branch from 2609d7f to 12decbf Compare May 24, 2026 00:36
@skarim skarim force-pushed the skarim/add-existing-branch branch from 3ac2962 to ae84c7d Compare May 24, 2026 00:36
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