Skip to content

fix: don't create a local branch named HEAD - #3014

Open
VXNCXNX wants to merge 1 commit into
gitui-org:masterfrom
VXNCXNX:fix/checkout-remote-head
Open

fix: don't create a local branch named HEAD#3014
VXNCXNX wants to merge 1 commit into
gitui-org:masterfrom
VXNCXNX:fix/checkout-remote-head

Conversation

@VXNCXNX

@VXNCXNXVXNCXNX commented Aug 14, 2026

Copy link
Copy Markdown

This Pull Request fixes/closes#2681.

It changes the following:

  • checkout_remote_branch resolves a symbolic remote ref before deriving the local branch name, so checking out origin/HEAD no longer creates a local branch literally named HEAD.
  • When the derived local branch already exists, it is reused only if it points at the same commit; a diverged local branch keeps the pre-existing error rather than being silently substituted.

The problem

origin/HEAD is a symbolic ref pointing at the remote's default branch, but checkout_remote_branch derived the local name from the ref text. So checking it out produced a local branch literally named HEAD:

assertion `left == right` failed
left: "HEAD"
right: "master"

That's from a test against a real fixture: a bare remote, a clone with refs/remotes/origin/HEAD, then checkout_remote_branch on it.

The fix

Resolve the symref first, then derive the name from what it actually points at:

let remote_ref = repo.find_reference(&branch.reference)?.resolve()?;let remote_name = bytes2string(remote_ref.shorthand_bytes())?;

origin/HEAD becomes origin/master, so the local name is master and no HEAD branch is created. That matches what git checkout origin/HEAD gives you.

The part that needed a second pass

Resolving alone isn't enough: the branch origin/HEAD points at is the one checked out at clone time, so the local master already exists and repo.branch() fails with "a reference with that name already exists".

My first version handled that by falling back to the existing local branch whenever one was found. A review caught that this is a silent-wrong-commit bug, and I confirmed it:

local foo @ A, origin/foo @ B, then check out origin/foo:
with the naive fallback: Ok(()) HEAD -> foo @ A <- wrong commit, no message
before the patch: Err("a reference with that name already exists")

The user asks for origin/foo and silently lands on their stale local foo. The pre-existing error is confusing, but it's honest, and it does reach the user (InternalEvent::ShowErrorMsg).

So the fallback is now conditional on the existing local branch pointing at the same commit:

.filter(|local| local.get().target() == Some(commit.id()))

which is exactly the origin/HEAD case and nothing else. Diverged local branches keep the old error.

Tests

Two, both against real fixtures using the existing repo_init_bare / repo_clone / push_branch helpers:

  • test_checkout_remote_head, checking out origin/HEAD lands on master and creates no branch named HEAD. Verified load-bearing: with the production change reverted it fails with the assertion quoted above.
  • test_checkout_remote_branch_diverged_local, pins that a diverged local branch is not silently substituted.
cargo test -p asyncgit --features vendor-openssl branch 31 passed; 0 failed
cargo fmt --check clean
cargo clippy --all-targets 0 hits in the changed file

(asyncgit needs --features vendor-openssl in my environment for lack of system OpenSSL.)

I followed the checklist:

  • I added unittests (two, in asyncgit/src/sync/branch/mod.rs, listed above)
  • I ran make check without errors (fmt and clippy clean; cargo nextest run --workspace is 319/320, the one failure being git2-hooks tests::test_pre_commit_py, which needs a python binary and fails identically on a clean master checkout here. sort and deny need tombi and cargo-deny, which are not installed in this environment)
  • I tested the overall application (verified at the asyncgit layer with real git fixtures rather than by driving the TUI)
  • I added an appropriate item to the changelog (under ## Unreleased)

Disclosure: written with AI assistance (Claude Code). I reproduced the issue, ran the change and the verification myself.

origin/HEAD is a symbolic ref pointing at the remote's default branch, but checkout_remote_branch derived the local name from the ref text, so it created and checked out a branch literally called HEAD. Resolve the symref first and check out its target. Its local counterpart usually already exists from the clone, so switch to it, but only when it points at the same commit, since a diverged local branch is not what was asked for. Fixesgitui-org#2681.
Sign up for freeto 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.

Fetching HEAD creates a local HEAD branch

1 participant

@VXNCXNX