Skip to content

Branches()/Tags() report "heads/v1" / "tags/v1" when a branch and a tag share a name, so the name can't be passed back to git #137

Description

@matt-edmondson

What's wrong

Branch and tag names are read from git's %(refname:short) and %(upstream:short) (GitIntegration/Parsing/GitOutputFormats.cs, ForEachRefFormat and ForEachTagFormat). With core.warnAmbiguousRefs on, which is git's default, :short does not always strip the refs/heads/ or refs/tags/ prefix. If the shortest name would be ambiguous, git keeps part of the prefix:

$ git branch v1 && git tag v1
$ git for-each-ref --format='%(refname) %(refname:short)' refs/heads refs/tags
refs/heads/master master
refs/heads/v1 heads/v1
refs/tags/v1 tags/v1

(Verified with git 2.43.)

GitBranchParser takes Name from that field (fields[1]), and Upstream from fields[3]. GitTagParser takes Name from fields[0].

Failure scenario

In a repository with both a branch v1 and a tag v1 (a common "release branch plus release tag" pattern):

  • Branches() reports a branch named heads/v1, and Tags() reports a tag named tags/v1.
  • Status().Branch still says v1, so the current branch doesn't match its own Branches() entry.
  • Passing the reported name back to git fails. git branch --delete heads/v1 gives error: branch 'heads/v1' not found, and deleting tag tags/v1 fails the same way.

The same thing happens whenever a local branch is named like a remote-tracking ref. For example, a local branch origin/main makes the remote-tracking branch, and every branch's Upstream, come back as remotes/origin/main.

Suggested fix

Derive names from the full refname and don't rely on :short:

  • Branches and tags: %(refname:lstrip=2) (strips refs/heads/ or refs/tags/).
  • Remote-tracking branches: %(refname:lstrip=2) also gives origin/main for refs/remotes/origin/main. The parser already knows which kind it has from the full refname in field 0.
  • Upstream: emit %(upstream) and strip refs/heads/ or refs/remotes/ in the parser, or use %(upstream:lstrip=2).

Acceptance criteria

  • A test repository with a branch and a tag both named v1: Branches() contains v1 and Tags() contains v1, with no heads/ or tags/ prefix.
  • A local branch named origin/main alongside refs/remotes/origin/main: the upstream and the remote-tracking branch are both reported as origin/main.

Activity

  1. matt-edmondson commented on Sep 27, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Category: Bug
    • Priority: Medium. It only triggers on ambiguous names, but "release branch v1 plus tag v1" is a common pattern. When it triggers, the reported names can't be passed back to git, and Upstream is also affected.
    • Area / suggested assignee: GitIntegration/Parsing/GitOutputFormats.cs (ForEachRefFormat, ForEachTagFormat), plus GitBranchParser and GitTagParser. Owner: @matt-edmondson
    • Duplicates / in progress: None found. Open PR Read quoted and " b/"-containing paths in Patch() headers #129 (quoted paths in Patch() headers) is in the parsing layer but doesn't touch ref names.
    • Next step: Switch to %(refname:lstrip=2) and %(upstream:lstrip=2), or strip the prefix from the full refname in the parser. Add the v1 branch+tag test and the local origin/main test.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

readyFully specified; implement as written

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions