Skip to content

fix(git): find the PR and the tracking remote when a tag or branch has the same short name - #774

Merged
sirmalloc merged 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/git-pr-tag-shadowing
Oct 30, 2026
Merged

sirmalloc merged 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/git-pr-tag-shadowing

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

Git PR/MR and Git CI Status now find the PR for a branch that has a tag of the same name, for example a release branch release-1.4 next to the tag release-1.4. Before, when the lookup had to fall back to origin, as in a fork where gh resolves to the upstream repository, they showed (no PR) (and CI -) for a branch with an open PR. GitLab merge requests through glab had the same problem.

The Git Upstream widgets (Owner, Repo, Owner/Repo) have the same flaw in their tracking-branch fallback, so it is fixed here too. In a clone with no upstream remote, they showed no upstream when a local branch or tag was named origin/<branch>.

Why

When a short ref name is ambiguous, git's short form adds a prefix. Git Branch hit this in #679. Two other places read the short form.

Git PR/MR and Git CI Status. getCurrentBranch in git-review-cache.ts ran git symbolic-ref --short HEAD. The first lookup is the CLI's own gh pr view (or glab mr view). If that finds nothing, a second lookup is pinned to origin: gh pr view --repo <origin> -- <branch> (or glab mr view --repo <origin> -- <branch>), with the branch from getCurrentBranch. With a tag named like the branch, git prints heads/<branch>, so the pinned lookup asks for a PR on a branch called heads/<branch> and finds none.

In a scratch repo with git 2.50.1, on branch feature with a tag feature:

$ git symbolic-ref --short HEAD
heads/feature
$ git symbolic-ref HEAD
refs/heads/feature

The Git PR/MR demo setup below, piped through main's built CLI under Bun and Node, with a stand-in gh that logs its calls, gives this. Each call runs twice: with a CI widget on the line, gh is asked again without the CI fields after any failure, a retry this change doesn't touch.

gh pr view  → no PR (resolved to the upstream repo)  (x2)
gh pr view --repo https://github.com/me/proj -- heads/release-1.4  → no PR for branch "heads/release-1.4"  (x2)

Git Upstream. getTrackingRemoteName in git-remote.ts ran git rev-parse --abbrev-ref --symbolic-full-name @{upstream} and matched the result against the remote names (origin/…). With a local branch or tag named origin/feature, git prints remotes/origin/feature, which matches no remote:

$ git rev-parse --abbrev-ref --symbolic-full-name @{upstream}
remotes/origin/feature
$ git rev-parse --symbolic-full-name @{upstream}
refs/remotes/origin/feature

The Git Upstream demo repository below has only an origin remote. Piped through main, it renders ⎇ feature | me/proj without the local origin/feature branch, and ⎇ feature | no upstream with it. A tag named origin/feature gives no upstream too.

No other call under src/ reads a short ref name. rev-parse --short HEAD gives a commit hash.

How

  • getCurrentBranch reads the full ref with git symbolic-ref HEAD and strips refs/heads/, the way Git Branch does since fix(git): detached HEAD, tag-shadowed branches, submodule worktrees and wide PR titles #679. The pinned gh and glab lookups now pass the plain branch name. The PR cache entry is keyed by that name too, so a (no PR) cached under the old heads/ name is not reused.
  • getTrackingRemoteName reads git rev-parse --symbolic-full-name @{upstream}. It returns no remote unless the ref starts with refs/remotes/, and strips that prefix before matching remote names as before. A branch that tracks a local branch (refs/heads/…) has no tracking remote.
  • Tests:
    • The git-review-cache.test.ts harness answers symbolic-ref as git does: the full ref, or with --short the name, which becomes heads/<name> once a tag of that name exists (addTag).
    • Two new tests check that the pinned gh and glab lookups get the plain name when a tag shadows the branch.
    • In git-remote.test.ts, the three tracking-remote tests now return full refs, as the new command does. A new test checks the command and that refs/remotes/origin/feature resolves to origin.

Demo

Git PR/MR on a tag-shadowed branch. A scratch fork with origin me/proj and upstream acme/proj. The checked-out branch release-1.4 has a tag of the same name. A stand-in gh comes first on PATH, so nothing reaches GitHub. Its own repo resolution picks upstream and finds no PR. Only --repo https://github.com/me/proj -- release-1.4 answers, with PR #42. Render 1 starts the background refresh. The helper then waits for the refresh and lists the gh calls it made, and render 2 shows the cached result. The (x2) is the retry without the CI fields described above.

Git Upstream with a ref named like the tracking branch. A clone with only origin (https://github.com/me/proj.git). Branch feature tracks origin/feature, and a local branch is also named origin/feature. The demo prints both forms of the tracking ref, then renders Git Branch and Git Upstream Owner/Repo.

Powerline

Git PR/MR, before:

Git PR/MR on a tag-shadowed branch before the fix, Powerline

Git PR/MR, after:

Git PR/MR on a tag-shadowed branch after the fix, Powerline

Git Upstream, before:

Git Upstream with a ref named like the tracking branch before the fix, Powerline

Git Upstream, after:

Git Upstream with a ref named like the tracking branch after the fix, Powerline

Plain

Git PR/MR, before:

Git PR/MR on a tag-shadowed branch before the fix, Plain

Git PR/MR, after:

Git PR/MR on a tag-shadowed branch after the fix, Plain

Git Upstream, before:

Git Upstream with a ref named like the tracking branch before the fix, Plain

Git Upstream, after:

Git Upstream with a ref named like the tracking branch after the fix, Plain

Testing

  • On main as of [low] perf(claude-status): hold a lock while the status page fetch is in flight #635 and [medium] perf(git-review): key the PR cache through the cached runGit #639 (62a371b): merges cleanly, bun run lint is clean, and bun test gives 3706 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matches main in plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly.
  • bun test: 3171 pass, 0 fail across 178 files (main: 3168 pass, 0 fail). There are 3 new tests.
  • Against main's source, run one at a time, 5 tests fail: the 3 new ones, and 2 of the 3 existing tracking-remote tests, whose mocked git output is now a full ref. The third (returns null when the tracking remote cannot be resolved) passes either way. Running both files whole against main's source fails 15 tests: those 5 plus 10 later tests in git-remote.test.ts, because each failing test leaves its queued mock outputs to the tests after it.
  • bun run lint: clean.
  • Node 26.10.0, git-review-cache.test.ts and git-remote.test.ts through vitest: 81 pass (main: 78 pass).
  • runtime-check.sh with the Git PR/MR demo config and a payload in the demo repo, with the stand-in gh first on PATH:
    • Plain and Powerline renders agree across Bun 1.4.2 and Node 26.10.0.
    • The plain renders match main: both show (no PR), because a render returns at once on a cache miss and starts the lookup in the background.
    • The Powerline renders, which run next, differ from main, as intended: they pick up that background lookup's result. On this branch they show PR #42 OPEN Backport the login fix ✓2, and on main they show (no PR).
    • The TUI smoke test passes on both runtimes.
  • End to end with the demo setups, Bun 1.4.2 and Node 26.10.0 (built dist):
    • main shows (no PR) and no upstream.
    • This branch shows PR #42 OPEN Backport the login fix ✓2 and me/proj.

git's short ref names add a prefix when the short name is ambiguous.
With a tag named like the current branch, `git symbolic-ref --short
HEAD` prints "heads/<branch>". Git PR and Git CI Status pass that name
to the lookup pinned to origin (`gh pr view --repo <origin> -- <branch>`,
and the same for glab), which then asks for a PR on a branch called
"heads/<branch>" and finds none. In a fork, where gh's own resolution
picks the upstream repository, that pinned lookup is the one that finds
the PR, so the widgets showed "(no PR)" for a branch that has one.

The tracking-remote fallback of the Git Upstream widgets has the same
flaw: `git rev-parse --abbrev-ref --symbolic-full-name @{upstream}`
prints "remotes/origin/<branch>" when a local branch or tag is named
"origin/<branch>", which matches no remote, so the widgets showed
"no upstream".

Both now read the full ref and strip "refs/heads/" or "refs/remotes/",
as Git Branch already does for the branch name.
@sirmalloc
sirmalloc merged commit e9b7cc0 into sirmalloc:main Oct 30, 2026
3 checks passed
sirmalloc pushed a commit that referenced this pull request Nov 5, 2026
…791)

With a tag named like the current branch, `git symbolic-ref --short HEAD`
prints "heads/<branch>". The refresh child keys the PR cache by the full
ref with "refs/heads/" stripped since #774, but the render's lookup
through the cached runGit (#639) still used --short. The two then hash
to different cache files: the child writes the PR to one, Git PR and Git
CI Status read the other, and the widgets show "(no PR)" on every render
while a refresh runs again each time its lock goes stale. The child also
never releases the lock it was handed, since that lock belongs to the
other cache file.

The render's lookup now reads `symbolic-ref HEAD` and strips the prefix
the same way, which is also the command Git Branch already runs, so the
cached runGit answers both.

Co-authored-by: effectustasi <216187407+effectustasi@users.noreply.github.com>
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.

2 participants