The vocabulary jj borrows from Git, changes, and drops

The trap with jj is that it reuses several Git words for different things, rather than inventing new ones across the board. That’s worse than a clean break would have been, at least early on: a genuinely new word forces you to look it up, a familiar one lets you assume you already know it.

Branch became bookmark, and the difference is not cosmetic. A Git branch always tracks its tip - commit on top of it and it moves with you, automatically. A jj bookmark does none of that; it sits where you put it until you move it with jj bookmark set. The first time I committed three times expecting my feature bookmark to follow along and found it still pointing at the first commit, this stopped being a naming quirk and became a fact I had to actually internalize.

HEAD became @, and @ is a real commit, not a pointer to one. Git’s HEAD is a ref that points at a commit; jj’s @ names the working-copy commit directly, snapshotted from disk before every command runs. There’s no equivalent of “detached HEAD” because there’s nothing to detach from - @ is just wherever you currently are.

A commit SHA became two ids instead of one. The commit id is the same kind of content hash Git has always had, and it changes every time the commit is rewritten, same as a SHA would. The change id is new: a stable label for “this piece of work” that survives edits, rebases, and amends untouched. Referring to work by change id instead of commit id is the single habit that paid off the most - the reference just keeps working after a rebase that would have orphaned a Git SHA.

Underneath the renamed words, a few concepts don’t map onto anything Git has at all. The operation log is broader than a reflog - it’s a record of every change to repository state, not just ref moves, which is why jj undo can revert a rebase, a describe, or a config change with the same command. Revsets replace the mess of ref names and range syntax Git accumulated over fifteen years with an actual small query language. And colocated mode itself - .jj sitting next to .git, reading the same object database - has no Git-side name because Git never needed one; it’s a jj-side answer to “how do I sit on top of something that already exists.”

And then there’s what simply isn’t there anymore. No staging area, because the working copy already is a commit. No detached HEAD, for the reason above. No stash, because setting work aside is just jj new - the old work is still a real commit, nothing needs shelving. No mid-rebase state to get stuck in: a conflicted rebase produces a commit with the conflict recorded inside it and lets you keep going, instead of freezing the repository until you resolve or abort.

Worth saying plainly, because it’s easy to lose track of once the differences pile up: remotes, fetching, pushing, conflict markers, .gitignore - none of that was reinvented. jj’s own concepts stop exactly at the boundary of the Git object database, and everything on the far side of that boundary means precisely what it always did.