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

@VXNCXNX VXNCXNX commented Aug 14, 2026

Copy link
Copy Markdown

Fixes #2681.

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.)

CHANGELOG entry added under ## Unreleased.

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. Fixes gitui-org#2681.
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.

Fetching HEAD creates a local HEAD branch

1 participant