Skip to content

fix(git-branch): read a jj repo's detached HEAD as no branch - #705

Open
eric-engberg wants to merge 2 commits into
sirmalloc:mainfrom
eric-engberg:fix/git-branch-jj
Open

eric-engberg wants to merge 2 commits into
sirmalloc:mainfrom
eric-engberg:fix/git-branch-jj

Conversation

@eric-engberg

@eric-engberg eric-engberg commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Relates to #517: it undoes the part of that issue's problem that showing detached commits (#679) added. Git Changes, which the issue is about, is unchanged; #499 covers that.

What

In a colocated jj repo, Git Branch goes back to what it showed before #679: ⎇ no git, or nothing with "hide no git". Any other git repo's detached HEAD (rebase, bisect, tag checkout) still shows its commit, ⎇ (7d897a5). That includes a git repo nested inside a jj workspace, such as a submodule of a colocated repo or a clone under a jj-managed home directory.

Why

jj keeps git's HEAD detached in a colocated repo, on the working copy's parent. Since #679, Git Branch shows a detached HEAD's commit, so it now shows one in every jj repo: ⎇ (4227868). That commit is jj's bookkeeping, not something the user checked out. #517's reporter pairs the Git widgets with the jj widgets and hides the Git ones outside git repos. Git Branch used to hide in jj repos for them, and since #679 it no longer does.

Checked with jj 0.46.0: after jj git init --colocate and a commit, git symbolic-ref HEAD fails ("ref HEAD is not a symbolic ref"), and git rev-parse --short HEAD gives the commit the widget then printed.

How

On a detached HEAD, GitBranch.render checks that the repo is a colocated jj repo before looking up the commit. Two things must hold: jj root succeeds (isInsideJjRepo, the same check the jj widgets use), and git's top-level directory (git rev-parse --show-toplevel) holds a .jj directory. jj root alone isn't enough. jj finds its workspace by walking up to the nearest .jj and doesn't stop at a git repo's root, so it also succeeds in a git repo nested anywhere under a jj workspace, whose HEAD jj doesn't manage. In a colocated repo the ref is null, which takes the existing "no git" path.

The checks only run on a detached HEAD, so a checked-out branch costs nothing more, and the top-level lookup only runs once jj root has succeeded. Outside a jj workspace, or without jj installed, jj root fails and the commit shows as before. A second workspace from jj workspace add gets its own git worktree with a .jj at its top level, so it reads as no branch like the first. isInsideJjRepo itself is unchanged, so the jj widgets aren't affected.

Demo

Piped renders in a colocated jj repo (bookmark demo), then in a plain git repo on a detached HEAD. Each has Git Branch as is on line 1, and with "hide no git" on line 2, next to Jj Bookmarks.

Powerline

Before (main) After
Git Branch showing jj's detached commit, Powerline Git Branch reading the jj repo as no git, Powerline

Plain

Before (main) After
Git Branch showing jj's detached commit, plain Git Branch reading the jj repo as no git, plain

Update

These are piped renders against fake scratch repos built with jj 0.46.0, each on a detached HEAD: a colocated jj repo, a git submodule of that repo, and a plain git repo under a jj workspace that isn't colocated. In each group, line 1 shows Git Branch as is and line 2 shows it with "hide no git". Before this fix, both nested repos show "⎇ no git" or hide the widget. After it, they show their commits (973d5d1, 1642305), and the colocated repo still shows no branch, as before.

Powerline, before and after:

Before, Powerline

After, Powerline

Plain, before and after:

Before, Plain

After, Plain

Testing

  • GitBranch.test.ts:

    • A colocated jj repo's detached HEAD renders ⎇ no git, or nothing with "hide no git", after exactly four commands: the work-tree probe, git symbolic-ref HEAD, jj root and git rev-parse --show-toplevel (no commit lookup). The test points them at a temp directory with a real .jj in it.
    • A git repo nested in that jj workspace (jj root gives the workspace, the top level is code/app inside it) still renders its detached commit, ⎇ (ddfb2de), with "hide no git" on.
    • The two detached-HEAD tests from fix(git): detached HEAD, tag-shadowed branches, submodule worktrees and wide PR titles #679 now answer the jj root call with "no jj repo" and still get the commit and its link.
    • The empty-lookups test gets an answer for the new call.
  • bun test: 3115 pass, 0 fail. bun run lint passes. Under Node (Vitest), GitBranch.test.ts passes 29 of 29.

  • Built CLI with real jj 0.46.0, piped renders on a detached HEAD, Git Branch as is / with "hide no git". Bun 1.4.2 and Node 26.10.0 print the same in every case. "First commit" is this PR's first commit, which checked jj root alone.

    Repo First commit Now
    Colocated jj repo (jj git init --colocate, then a commit) ⎇ no git / hidden ⎇ no git / hidden
    Second workspace (jj workspace add) ⎇ no git / hidden ⎇ no git / hidden
    Git submodule of a colocated repo ⎇ no git / hidden ⎇ (87ae600) / ⎇ (87ae600)
    Plain git repo nested in a non-colocated jj workspace ⎇ no git / hidden ⎇ (8baf8cc) / ⎇ (8baf8cc)
    Plain git repo, no jj above ⎇ (8baf8cc) / ⎇ (8baf8cc) ⎇ (8baf8cc) / ⎇ (8baf8cc)
  • Baseline configs (plain and Powerline, example payload), built CLI: identical to main under both runtimes, and the TUI opens and exits under both.

Since Git Branch started showing a detached HEAD's commit, it shows one
in every colocated jj repo: jj keeps git's HEAD detached on the working
copy's parent, so the widget printed "⎇ (4227868)" where it used to
show "no git", or hide with "hide no git". That commit is jj's
bookkeeping, not something the user checked out, and users who pair Git
Branch with the jj widgets and hide it outside git repos saw it come
back in every jj repo.

On a detached HEAD, check whether the directory is in a jj repo first.
If it is, there's no branch: "no git", or hidden, as before. A plain git
repo's detached HEAD (rebase, bisect, tag checkout) still shows its
commit. The jj check only runs on a detached HEAD.
… branch

On a detached HEAD, Git Branch took any successful `jj root` to mean the
repo was a colocated jj repo, where jj keeps git's HEAD detached. But jj
finds its workspace by walking up to the nearest .jj, and doesn't stop at
a git repo's own root. So in a plain git repo anywhere under a jj
workspace (a home or dotfiles tree managed by jj, a git checkout inside a
jj monorepo), `jj root` succeeds too, and a rebase, bisect or tag
checkout there showed "no git", or hid the widget, instead of the
detached commit.

Now the repo counts as colocated only when git's top-level directory
holds the .jj. A git repo nested under a jj workspace shows its detached
commit again, while a colocated repo, and a jj workspace with its own git
worktree, still read as no branch. The extra git call only runs on a
detached HEAD where `jj root` has already succeeded.
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