Files
orca/src/shared/composer-branch-selection.ts
T
Neil ae77156ab2 feat(worktree): reuse an existing branch when creating a worktree (#5181) (#5781)
* feat(worktree): reuse an existing branch when creating a worktree (#5181)

Adds an explicit "Reuse this branch" checkbox to the new-worktree composer.
When an existing local branch is selected in the smart name field, the
checkbox (default on) checks that branch out instead of creating a new
branch from it — and the choice now survives renaming the worktree folder.

Previously, reusing an existing branch only worked implicitly: it required
keeping the auto-filled worktree name equal to the branch, and editing the
name silently reverted to "create a new branch". There was no discoverable,
durable control — the gap the issue reports.

The fix is renderer-only: picking an existing local branch already sets
baseBranch === branchNameOverride, so the existing backend reuse path
(`git worktree add <path> <branch>`, preserveBranchOnDelete) checks the
branch out correctly across local/SSH/runtime. The checkbox pins that
override via the existing branchNameOverridePreservesNameEdits flag so the
worktree folder can be named independently while the branch is reused;
unchecking creates a fresh branch from the selected ref as base.

- New pure helper resolveComposerBranchReuse (local-vs-remote + default).
- Checkbox hidden for remote-only refs and non-branch sources.
- Tests: pure helper, card render/toggle, and a backend integration test
  proving a renamed folder still reuses the exact branch.

* fix(worktree): refine reuse-branch checkbox (placement, label, eligibility)

Address review feedback on the "Reuse branch" control:

- Move the checkbox directly under the branch (smart) selection instead of the
  Advanced "Name" field — the worktree folder name can legitimately differ from
  the branch, so the choice belongs next to the branch pick and is now visible
  without expanding Advanced.
- Rename the label "Reuse this branch" -> "Reuse branch".
- Make eligibility dynamic: reuse is impossible when the branch is already
  checked out in another worktree (git allows a branch in only one worktree), so
  the checkbox is now hidden in that case (it was already hidden for remote-only
  refs). When a busy branch is picked, the override is no longer pinned to it, so
  creation cleanly falls back to a new branch from that ref as base instead of a
  silently-suffixed branch.

Adds isBranchCheckedOutInWorktrees (pure, unit-tested) and threads the repo's
worktree branch list from the store into the selection logic.

* feat(worktree): animate the reuse-branch row's show/hide

Keep the "Reuse branch" row mounted and collapse it with a grid-rows
transition (same pattern as the Advanced drawer) instead of conditionally
rendering it, so the create-worktree dialog grows and shrinks smoothly when the
option appears/disappears as the selected branch changes. The checkbox is taken
out of the tab order while collapsed.

* refactor(worktree): address review nits on reuse-branch control

From a multi-dimensional review of the branch (all findings low severity):

- Reset reuse state (reuseEligibleBranch, reuseSelectedBranch,
  branchNameOverridePreservesNameEdits) on repo and project switches, matching
  the other reset paths — avoids carrying stale branch-scoped state.
- Disable the reuse checkbox while collapsed so no focusable control lives
  inside the aria-hidden row.
- Extract the busy-branch override decision into a pure resolveComposerReuseOverride
  helper and unit-test it (busy local branch drops the override; remote-only ref
  keeps it) — pins the "no suffixed-branch collision" guarantee.
- Note the worktreesByRepo visibility limitation near the busy-branch check
  (a branch busy only in a hidden external worktree is caught by the backend).
- Strengthen tests: reuse-checkbox toggle in both directions, empty worktree
  list case.
2026-06-19 02:47:05 -07:00

114 lines
3.8 KiB
TypeScript

export type ComposerBranchSelection = {
baseBranch: string
branchNameOverride: string | undefined
branchAutoName: string
name: string | undefined
lastAutoName: string | undefined
}
export function resolveComposerBranchSelection(args: {
refName: string
localBranchName: string
currentName: string
lastAutoName: string
}): ComposerBranchSelection {
const trimmedCurrentName = args.currentName.trim()
const shouldAutoName =
!trimmedCurrentName ||
args.currentName === args.lastAutoName ||
args.localBranchName.startsWith(trimmedCurrentName) ||
args.refName.startsWith(trimmedCurrentName)
if (!shouldAutoName) {
return {
baseBranch: args.refName,
branchNameOverride: undefined,
branchAutoName: '',
name: undefined,
lastAutoName: undefined
}
}
return {
baseBranch: args.refName,
branchNameOverride: args.localBranchName,
branchAutoName: args.localBranchName,
name: args.localBranchName,
lastAutoName: args.localBranchName
}
}
/**
* True when `branchName` is already checked out in one of the given worktree
* branch refs (which may be `refs/heads/foo` or short `foo`). Git refuses to
* check out a branch in two worktrees, so such a branch cannot be reused.
*/
export function isBranchCheckedOutInWorktrees(
branchName: string,
worktreeBranches: readonly string[]
): boolean {
return worktreeBranches.some((ref) => ref.replace(/^refs\/heads\//, '') === branchName)
}
/**
* Issue #5181: decide whether a picked branch row is an existing LOCAL branch
* that can be reused (checked out) instead of branched off, and whether reuse
* should default ON.
*
* Reuse is only possible for a LOCAL branch (ref === local name; remote-only
* refs carry an `origin/`-style prefix) that is NOT already checked out in
* another worktree — git allows a branch in only one worktree at a time. Reuse
* defaults ON only when the worktree name was auto-derived from the branch (the
* selection produced a branch-name override); a user who typed a custom
* worktree name first is branching off the ref, so reuse stays OFF unless they
* opt in.
*/
export function resolveComposerBranchReuse(args: {
refName: string
localBranchName: string
selectionProducedOverride: boolean
branchCheckedOutElsewhere: boolean
}): { reuseEligibleBranch: string | null; defaultReuse: boolean } {
const reuseEligibleBranch =
args.refName === args.localBranchName && !args.branchCheckedOutElsewhere
? args.localBranchName
: null
return {
reuseEligibleBranch,
defaultReuse: reuseEligibleBranch !== null && args.selectionProducedOverride
}
}
/**
* Issue #5181: the branch-name override to apply for a picked branch. A local
* branch already checked out in another worktree can't be reused, so it must
* NOT be pinned as the override — pinning it would collide and silently produce
* a suffixed branch. In that case fall back to letting the worktree name derive
* a fresh branch from the selected ref as base; otherwise use the selection's
* override unchanged.
*/
export function resolveComposerReuseOverride(args: {
refName: string
localBranchName: string
branchNameOverride: string | undefined
branchCheckedOutElsewhere: boolean
}): string | undefined {
if (args.branchCheckedOutElsewhere && args.refName === args.localBranchName) {
return undefined
}
return args.branchNameOverride
}
export function resolveComposerBranchNameOverrideForCreate(args: {
branchNameOverride: string | undefined
branchAutoName: string
workspaceName: string
preserveWorkspaceNameEdits: boolean
}): string | undefined {
if (!args.branchNameOverride) {
return undefined
}
if (args.preserveWorkspaceNameEdits) {
return args.branchNameOverride
}
return args.workspaceName === args.branchAutoName ? args.branchNameOverride : undefined
}