SIGN IN SIGN UP

fix: [ENG-2835] checkout from unborn HEAD to fetched ref (#656)

* fix: [ENG-2835] checkout from unborn HEAD to fetched ref

The checkout snapshot resolved the source branch via git.listFiles before
delegating, which throws NotFoundError when HEAD is on an unborn branch
(post-init, pre-commit). The handler then mapped the error to "no commits
yet", masking the real cause and blocking the normal init + fetch + checkout
flow against a remote-only ref.

Resolve the source ref first; on unresolved (unborn) source, skip the
listFiles snapshot and pass undefined into guardStagedConflicts. The guard
now accepts a missing source — every staged blob is treated as new — and
resolves the target via a small DWIM helper so a real conflict still trips
on a remote-only target.

* refactor: [ENG-2835] address PR review on checkout guard

- Extract `tryResolveRef` helper to replace inline `.catch(() => {})` (one
  call site, three previous duplications). Lint rule
  `unicorn/no-useless-undefined` blocks `.catch(() => undefined)`, so the
  helper is the lint-clean way to land the explicit-undefined semantics.
- Thread `sourceOid` through `guardStagedConflicts` instead of re-resolving
  the source branch inside the helper. One fewer fs round-trip per checkout.
- `resolveCheckoutRef` now refuses on multi-remote ambiguity (collect all
  matches, return undefined when more than one), matching native git's DWIM
  refuse-on-ambiguity instead of first-wins.
- Annotate `guardStagedConflicts`'s early return on missing target OID so
  the trade-off (resolver could disagree with `git.checkout`'s internal
  resolution) is documented in-source.
- Drop the now-redundant `sourceBranchResolves` flag; use truthy `sourceOid`
  directly. Update the gate comment to mention detached HEAD explicitly.
- Add an integration test asserting the guard now also trips on
  detached-HEAD + staged-conflict (previously skipped, leaving the
  documented data-loss bug unprotected — fixed as a side effect of the
  ENG-2835 gate change).
- Hoist test cleanup into an `extraTmpDirs` array drained in `afterEach`,
  so tmp dirs no longer leak when an assertion mid-test throws.

* test: [ENG-2835] explain detached-HEAD filesystem-layer write

isomorphic-git's `git.checkout({ref: sha})` creates a local branch from the
SHA instead of detaching HEAD, and the library exposes no `--detach` mode.
A filesystem write to `.git/HEAD` is the only way to reach detached state
for the staged-conflict test. Document that in-test so the next reader does
not wonder whether the API was tried first.

* fix: [ENG-2835] disambiguate NotFoundError by branch name, not commit count

The checkout-handler catch block previously used `log({depth: 1}).length === 0`
to decide between BRANCH_NOT_FOUND and NO_COMMITS. That proxy was added in
4dd3ffd69e (ENG-1656) to make `brv vc init && brv vc checkout main` (current
branch is unborn) say "no commits yet" instead of "branch not found".

After ENG-2835 the proxy collapses two distinct cases into the same wrong
answer when HEAD is unborn:
  (a) user typoed the target name (e.g. `checkout zorqplex_nonexistent_4242`)
      → message should be BRANCH_NOT_FOUND with the `-b` hint.
  (b) user asked to checkout the branch they are already on, but it is unborn
      (`init && checkout main`)
      → message should be NO_COMMITS with the add/commit/pull hint.

Both cases produce zero commits, so commit-count cannot separate them. The
target-name vs current-branch comparison can: case (a) differs, case (b)
matches. Switch the disambiguator to `data.branch === previousBranch`.
Detached HEAD (`previousBranch === undefined`) falls through to (a) which is
correct — checking out a typoed ref from a detached state is still
target-not-found.

Two unit tests added:
- `should throw BRANCH_NOT_FOUND (not NO_COMMITS) when target ref missing
  from unborn HEAD` — the user-reported `zorqplex_nonexistent_4242` flow.
- `should throw NO_COMMITS when checking out the current unborn branch by
  name` — the ENG-1656 flow this handler was originally written to protect.
B
bao-byterover committed
12ce3d078be65a131201b8486faf7eb175fbe95d
Parent: 0a274b3
Committed by GitHub <noreply@github.com> on 5/18/2026, 3:30:50 AM