Skip to content

fix(git): detached HEAD, tag-shadowed branches, submodule worktrees and wide PR titles - #679

Open
eric-engberg wants to merge 4 commits into
sirmalloc:mainfrom
eric-engberg:fix/git-widget-edge-cases
Open

eric-engberg wants to merge 4 commits into
sirmalloc:mainfrom
eric-engberg:fix/git-widget-edge-cases

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

  • Git Branch on a detached HEAD (a tag checkout, a rebase in progress, a bisect) shows the short commit in parentheses, e.g. ⎇ (0449a12), instead of ⎇ no git. With hide-no-git on, it no longer disappears. The repo link points at that commit.
  • Git Branch when a tag has the branch's name shows ⎇ release instead of ⎇ heads/release, and links to …/tree/release instead of …/tree/heads/release.
  • Git Worktree in a submodule or a --separate-git-dir repo shows 𖠰 main instead of 𖠰 no git.
  • Git PR titles are cut by terminal columns and whole characters: an emoji at the cut no longer turns into �, and a CJK title is cut to the same 30 columns as any other instead of taking 59.

Why

  • Detached HEAD: Git Branch reads the branch with git symbolic-ref --short HEAD, which fails when HEAD isn't on a branch:
    $ git symbolic-ref --short HEAD
    fatal: ref HEAD is not a symbolic ref
    $ git rev-parse --short HEAD
    0449a12
    
    The widget treated that failure as "not a repository", though the Git SHA widget in the same line shows the commit.
  • Branch and tag with the same name: --short disambiguates the name when a tag has it too, so it isn't a branch name any more:
    $ git switch -c release && git tag release
    $ git symbolic-ref --short HEAD
    heads/release
    
  • Submodules and separate git dirs: Git Worktree told the main worktree from linked ones by the git dir's path: .git at the end means main, …/worktrees/<name> means linked, anything else means "no git". A submodule's git dir is .git/modules/<name>, and a --separate-git-dir repo's is wherever it was put:
    $ cd app/vendor/lib && git rev-parse --git-dir
    /…/app/.git/modules/vendor/lib
    $ cd sep && git rev-parse --git-dir
    /…/sep.git
    
    The path pattern could also misread a submodule of a repo kept under a directory named worktrees, e.g. ~/worktrees/app/.git/modules/lib showed as app/.git/modules/lib.
  • PR titles: truncateTitle used title.slice(0, 29), which counts UTF-16 code units. Update the release workflow 🚀 for tagged builds kept the first half of the rocket's surrogate pair, which prints as U+FFFD. 修复状态栏在窄终端下的显示问题并且添加更多的测试用例以及文档说明 kept 29 characters, 58 columns wide.

How

  • Git Branch reads the full ref with git symbolic-ref HEAD and strips refs/heads/, which always leaves the plain branch name. (git branch --show-current would also work, but needs git 2.22.) When there's no branch, it falls back to git rev-parse --short HEAD, the lookup Git SHA already makes, so the two share the git cache entry. no git (or hiding it) now only happens outside a work tree or when both lookups fail. A normal branch renders and links exactly as before. The detached link uses /tree/<sha>, which GitHub and GitLab both resolve.
  • Git Worktree asks for git rev-parse --git-dir --git-common-dir in the one call it already made. A linked worktree's git dir is always <common dir>/worktrees/<name>, so that's the only shape read as linked, and the name is what follows. Any other git dir is the main worktree's. Linked worktrees keep their names, including those of bare repos, submodules and separate-git-dir repos. In a subdirectory of the main worktree, git prints the git dir absolute and the common dir relative (../../.git), and that still reads as main. Git older than 2.5, which has no --git-common-dir or linked worktrees, reads as main too.
  • Git PR truncates with truncateStyledText, the display-cluster and width logic the renderer already uses for max widths, to 29 columns, then adds the same single …. Titles of plain ASCII come out exactly as before.

Demo

The status line piped from sample repositories built for the demo (fake remotes, PR data from a pre-filled cache, nothing fetched), with Git Branch, Git Worktree and Git PR on one line: a tag checkout, a branch with a tag of the same name, a submodule, and two long PR titles.

Powerline: before

Git widget edge cases before the fix, Powerline mode

Powerline: after

Git widget edge cases after the fix, Powerline mode

Plain: before

Git widget edge cases before the fix, plain mode

Plain: after

Git widget edge cases after the fix, plain mode

Testing

  • New tests, each failing on main first: Git Branch on a detached HEAD (text, hide-no-git, link) and with a tag of the same name; Git Worktree in a submodule, a submodule under a worktrees directory, and a separate git dir; Git PR titles with an emoji at the cut and in CJK. Also added: main from a subdirectory of the main worktree, and a CJK title that fits. The existing Git Branch and Git Worktree tests now mock git's real output for the new commands.
  • bun test: 2792 pass, 0 fail. bun run lint passes.
  • The changed test files under Node (Vitest): 69 pass (59 on main).
  • Real repositories under Bun: a tag checkout, a branch shadowed by a tag, a submodule, a separate git dir, the main worktree and a subdirectory of it, linked worktrees (with subdirectories) of a normal repo, a bare repo, a submodule and a separate-git-dir repo. Only the detached, shadowed-branch, submodule and separate-git-dir outputs change.
  • Built CLI under Bun 1.4.2 and Node 26.10.0, piping a payload for each of the five demo repos: all runtimes give identical output in plain and Powerline modes, and each differs from main only as shown above. The baseline line is unchanged, and the TUI opens and exits cleanly under both.

On a detached HEAD (a tag checkout, a rebase in progress, a bisect),
`git symbolic-ref` fails, so Git Branch showed "no git" (or hid itself
with hide-no-git) inside a perfectly good work tree. It now shows the
short commit in parentheses, as git prompts do, e.g. "⎇ (0449a12)", and
the repo link points at that commit.

When a tag has the same name as the current branch, `symbolic-ref
--short` disambiguates the name to "heads/<branch>", so the widget
showed "⎇ heads/release" and linked to .../tree/heads/release. It now
reads the full ref and strips "refs/heads/", which is always the plain
branch name.
…rate git dirs

Git Worktree decided "main worktree" by the git dir's path: ".git" at
the end, else a ".../worktrees/<name>" pattern. A submodule's git dir is
.git/modules/<name> and a --separate-git-dir repo's is wherever it was
put (e.g. /x/repo.git), so both showed "no git". The path pattern could
also misread a repo kept under a directory named "worktrees".

It now asks git for both the git dir and the common dir in one call. A
linked worktree's git dir is always <common dir>/worktrees/<name>; any
other git dir belongs to the main worktree.
PR titles were cut with title.slice(0, 29), counting UTF-16 code units.
An emoji at the cut lost its second half and showed as U+FFFD ("�"), and
a CJK title passed the 30-unit limit while taking 59 columns, since each
character is two columns wide.

Truncation now goes through the same display-cluster and width logic
the renderer uses for max widths, keeping the 30-column limit and the
single "…" ellipsis.
A plain replaceAll says what the line does without a global regex.
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.

1 participant