Skip to content

fix: open a worktree on a branch that already exists - #335

Merged
omartelo merged 1 commit into
mainfrom
fix/worktree-on-existing-branch
Aug 21, 2026
Merged

omartelo merged 1 commit into
mainfrom
fix/worktree-on-existing-branch

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

Fixes #334.

The bug

CreateWorktree built every call as git worktree add -b <name>, and -b means create the branch. git refuses a name it already knows, so opening a worktree on a branch that existed failed with "A branch or worktree with that name already exists" — even with the branch free, checked out nowhere.

The reporter's case is the one that has no way out: the branch was cut from a freshly fetched origin/main, pushed, and linked to a GitHub issue. Deleting it to get past lich was not an option.

There were three states and only two had an answer:

Branch Before Now
Does not exist created with -b unchanged
Exists, holds a checkout that checkout is opened (spawn.resolveCheckout) unchanged
Exists, free fatal checked out as it stands

The fix

The guard is in project.CreateWorktree, not in spawn.resolveCheckout, because the New worktree dialog calls project.CreateWorktree over RPC directly and never passes through spawn. All three surfaces — the dialog, lich open --worktree, and the open_session MCP tool — route through this one function.

Naming a branch is naming the work on it. That is already how spawn.resolveCheckout reads a branch holding a worktree; this extends the same reading to a branch that merely exists. The base goes unused in that case: a base says where a branch starts, and that one already started.

A second hole the fix exposed

Reusing an existing branch makes a git failure reachable that CreateWorktree could never hit before: the branch is held by another checkout. git reworded that refusal in 2.36 —

  • until 2.36: fatal: 'x' is already checked out at '...'
  • since 2.36: fatal: 'x' is already used by worktree at '...'

giterror.go only knew the old one, so on any current git this fell through to the useless "git could not complete the operation." Verified against git 2.55 locally. Both wordings are matched now, and giterror_test.go pins each.

Contract change, named

TestCreateWorktreeErrors carried {"duplicate branch", "taken", "already exists"} — a test that documented the bug as contract. Per hard invariant #2 this is the "the contract changed" case, not a test bent to green: the expectation moved to TestCreateWorktreeExistingBranch, and the error table now pins what git still refuses (a branch held elsewhere).

Test plan

  • TestCreateWorktreeExistingBranch replicates the report verbatim — prepared branch plus --base origin/main with baseIsRemote=true. It plants a commit reachable only from the prepared branch and rewinds main, so one HEAD assertion proves both that the branch was reused and that the base was ignored. Fails on main with the reported error.
  • gofmt -l . clean, go vet ./... clean
  • go test ./... — 1738 passed, 32 packages
  • -count=50 on the new test, no flake
  • Cross-compile loop green for linux, darwin, windows

Also updated

docs/cli.md, the --worktree/--base flag help, and the open_session parameter descriptions — all four promised a worktree "created off --base", which is no longer the whole truth. CHANGELOG.md [Unreleased] › Fixed.

No docs/ceilings.md entry: there is no provider gap, all five reach the same spawn.Open.

🤖 Generated with Claude Code

Every worktree creation went to git as `worktree add -b`, which creates the
branch — so a branch that was already there failed with "A branch or worktree
with that name already exists", and the only way through was to delete it
first. A branch prepared before the session cannot afford that: cut from a
freshly fetched remote, pushed, and linked to a GitHub issue.

An existing branch is now checked out as it stands, on the New worktree dialog,
`lich open --worktree` and the `open_session` tool alike — all three route
through project.CreateWorktree, and the GUI reaches it without passing spawn's
resolveCheckout, so the guard belongs here. Naming a branch is naming the work
on it, which is already how spawn.resolveCheckout reads a branch that holds a
worktree. The base drops out with it: it says where a branch starts, and that
one already started.

That opens a path CreateWorktree could not reach before — a branch held by
another checkout — and git reworded that refusal in 2.36, from "is already
checked out at" to "is already used by worktree at". Only the old wording was
mapped, so modern git fell through to "git could not complete the operation".
Both are matched now; both are live, depending on the distribution's git.

TestCreateWorktreeErrors covered the old refusal as contract. The contract
changed, so that case moved to TestCreateWorktreeExistingBranch and the table
now pins what git still refuses.

Fixes #334
@omartelo
omartelo merged commit 7fb988c into main Aug 21, 2026
4 checks passed
@omartelo
omartelo deleted the fix/worktree-on-existing-branch branch August 21, 2026 03: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.

lich open --worktree fails when the target branch already exists locally

1 participant