Things on this page are fragmentary and immature notes/thoughts of the author. Please read with your own judgement!
In a colocated repo (created by jj git init --colocate),
Git and jj share the same commits
but each tracks its own position:
jj’s @ is the commit holding your current edits,
and jj keeps Git’s HEAD pointing to the parent of @.
Unlike Git,
jj has no separate staging area or uncommitted state —
every jj command (jj status, jj log, etc.)
automatically snapshots your files into @.
Committing with Git (e.g., git add a && git commit)
while @ holds jj-snapshotted edits
leaves the old @ behind as an orphan commit.
Start:
@(call itW) is on top of commitP, and Git’sHEADisP.You edit files
aandband run any jj command. jj snapshots both edits intoW.You run
git add a && git commit. Git knows nothing aboutW, so it creates a new commitX(containinga) on top ofPand movesHEADtoX.On the next jj command, jj sees that
HEADmoved toXand creates a new@on top ofX(holdingb, which is still on disk).The old
W(containingaandb) is neither@norHEAD. jj never automatically discards a non-empty commit, soWstays around as an orphan, duplicating changes that now also live inXand the new@.
In the diagram below, arrows point from a commit to its parent.
P ← W (a + b) ← orphan
↖
X (a) ← @ (b) Git HEAD = XIn this exact scenario,
W holds nothing beyond what X and the new @ already have,
so you can simply run jj abandon W.
To spot such leftovers, list the mutable heads (tips of branches not yet merged into main) other than the working copy. Not every result is an orphan — feature branches you are still working on show up too.
jj log -r 'heads(mutable()) ~ @'To check whether an orphan’s changes have already been merged into main (trunk()),
test-rebase it onto main and inspect the result:
empty means fully merged,
conflicted means main changed the same lines differently (needs review),
and neither means it has changes (at least partially) not yet in main.
Then undo the rebase.
--ignore-working-copy keeps jj log from snapshotting the working copy,
so that jj undo undoes the rebase rather than a new snapshot.
jj rebase -r <commit> -o 'trunk()'
jj log --ignore-working-copy -r <commit>
jj undoThe fish function
jj_orphans
automates this check for all orphan heads
without modifying them.
If the test rebase makes an orphan empty
(or jj_orphans labels it empty),
it can be safely removed with jj abandon <commit>.
To avoid orphan commits in the first place, use the jj way of staging changes (see Jujutsu Equivalent of Git Stage) instead of committing with Git.