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