okay. you know already know what jj (jujutsu for long) is, right? like, vaguely? it’s like a
git but it’s compatible with git and your most annoying oomfs all love it and you always say you’ve been meaning to
give it a try some day but like you aren’t really pressed because git is like, good enough…? that one. let me teach you how to become one of those
annoying oomfs. you’re gonna love it.
getting started
step one is you install jj. i literally get it from nixpkgs and winget which covers linux mac and windows; jj is probably in
your system’s1 package manager as well.
step two is you use jj. i will teach you how to use jj in just a second.
the jj mental model
at a super high level, as a git-compatible version control system, jj tracks a bunch of commits. in jj, each commit is part of a ‘revision’ which has a stable change ID, separate to its commit ID: jj gives each commit a “visible” flag, and gives you the illusion of few mutable revisions by creating many immutable commits under the same change IDs, marking the present version hidden and the new version visible, while automatically rebasing child and descendant commits to the new version.
that’s pretty abstract, so let’s take a look at how jj presents revisions to us:
watch-party/ (jj: rsku) $ jj log
@ r skulwum charlotte@som.codes 2026-05-13 00:32:36 0 01afa51
│ (empty) (no description set)
◆ m xklzpnz charlotte@som.codes 2026-05-12 14:02:50 main fb4 5c965
│ tweak the sounds a little
◆ ll oylxvy charlotte@som.codes 2026-05-12 10:33:29 81 ed6047
│ add deno task for formatting and run it
◆ zn lzuqxr charlotte@som.codes 2026-05-12 10:33:29 e0 d55474
│ add readycheck command with notification sounds
~
you can see both IDs in action here - each revision has a change ID in jj’s “reverse-hex notation” (it uses the last 16 letters of the
alphabet!) as well as a disambiguating commit ID in regular hex notation (1:1 with git) to distinguish two commits with the same change
ID (e.g. different iterations of the same revision).
the other thing you can note is the green @ indicating that we’re currently checked out on an empty descriptionless commit - this is the
working-copy commit: instead of having a staging area that you git add files to, jj automatically tracks all workdir changes
whenever it’s invoked and makes you “edit” a commit. this is both simpler and more powerful2: jj unifies the experience of editing a brand new commit and editing any historical or parallel revision, which makes working with branching history
or rewriting past history easy. at any time, you can run jj edit <rev> to move your workdir to a specific revision, and then
directly make your changes on the working copy.
baby’s first commit
now that we’ve built a mental model of jj (malleable revisions made of immutable commits, plus a working-copy commit), let’s get more concrete.
you can type jj git init in an empty directory to create a new git-backed jj repo (à la git init), or in an
existing git repository to convert it to a git-backed jj repo. by default, these repos have git colocation, which means you can
use both git commands and jj commands to operate on them.
to begin, we’ll set our user.name and user.email, just like git:
$ jj config set --user user.name "My Name"
$ jj config set --user user.email "me@my-site.com"
we first create an empty repository, and see we’re on an empty working-copy commit:
$ mkdir hello-jj && cd hello-jj/
hello-jj/ $ jj git init
hello-jj/ (jj: rkxs) $ jj log
@ r kxsqzky charlotte@som.codes 2026-07-13 10:57:55 2 f832520
│ (empty) (no description set)
◆ z zzzzzzz root() 0 0000000
then we can write a conversation to a file, and take a look again. that second line will also give jj a stable diff anchor when we edit history in a couple steps.
hello-jj/ (jj: rkxs) $ echo 'A: meow!' > conversation.txt
hello-jj/ (jj: rkxs) $ echo 'B: what?' >> conversation.txt
hello-jj/ (jj: rkxs) $ jj log
@ r kxsqzky charlotte@som.codes 2026-07-13 10:58:01 1 e8c2f8b
│ (no description set)
◆ z zzzzzzz root() 0 0000000
you can see our commit ID has changed from 2f83 to 1e8c, but our revision is still rkxs. let’s give this commit
a description:
hello-jj/ (jj: rkxs) $ jj describe -m "say meow"
Working copy (@) now at: r kxsqzky 8 ee550a9 say meow
Parent commit (@-) : z zzzzzzz 0 0000000 (empty) (no description set)
hello-jj/ (jj: rkxs) $ jj log
@ r kxsqzky charlotte@som.codes 2026-07-13 10:58:06 8 ee550a9
│ say meow
◆ z zzzzzzz root() 0 0000000
baby’s second commit
now we can work on top of this revision. let’s add some more content to conversation.txt:
hello-jj/ (jj: rkxs) $ jj new
Working copy (@) now at: z tklkwuv 6 7bf7c3a (empty) (no description set)
Parent commit (@-) : r kxsqzky 8 ee550a9 say meow
hello-jj/ (jj: ztkl) $ echo 'A: i mean, hello.' >> conversation.txt
hello-jj/ (jj: ztkl) $ cat conversation.txt
A: meow!
B: what?
A: i mean, hello.
hello-jj/ (jj: ztkl) $ jj describe -m "say hello as well"
[…]
hello-jj/ (jj: ztkl) $ jj log
@ z tklkwuv charlotte@som.codes 2026-07-13 11:21:01 6 5f77d30
│ say hello as well
○ r kxsqzky charlotte@som.codes 2026-07-13 10:58:06 8 ee550a9
│ say meow
◆ z zzzzzzz root() 0 0000000
the real power of jj isn’t the usual ‘append commit’ workflow though, it’s in the ease of history editing.
baby’s second first commit
let’s go back and change the meow commit to say ‘woof’ instead!
hello-jj/ (jj: ztkl) $ jj edit rk
[…]
hello-jj/ (jj: rkxs) $ echo 'A: woof!' > conversation.txt
hello-jj/ (jj: rkxs) $ echo 'B: what?' >> conversation.txt
the change with id ztklkwuv will be automatically rebased onto our new version of rkxsqzky, and the files will reflect the
synthesis of both commits:
hello-jj/ (jj: rkxs) $ jj edit zt
[…]
hello-jj/ (jj: ztkl) $ cat conversation.txt
A: woof!
B: what?
A: i mean, hello.
hello-jj/ (jj: ztkl) $ jj log
@ z tklkwuv charlotte@som.codes 2026-07-13 11:45:26 3 4f3de32
│ say hello as well
○ r kxsqzky charlotte@som.codes 2026-07-13 11:44:21 3 4af2929
│ say meow
◆ z zzzzzzz root() 0 0000000
and we can easily reword the commit as well:
hello-jj/ (jj: ztkl) $ jj describe -r rk -m "say woof"
Rebased 1 descendant commits
Working copy (@) now at: z tklkwuv 2 a2e7291 say hello as well
Parent commit (@-) : r kxsqzky 2 65ff44e say woof
as with git, you can also run jj describe without a -m flag, which will open an editor for you to write a full commit
description (i use helix, personally) - you can configure which editor is used via jj config set --user ui.editor <editor>, which
will edit ~/.config/jj/config.toml.
working with remotes
so that’s all good, but how do we push and pull to a jj remote? we haven’t talked about branches at all!
in jj, the analog to git’s branch model is bookmarks. bookmarks work anywhere jj expects a revset (e.g. a change ID or
commit ID) to identify a commit, so jj describe -r main is a valid command, for example.
anyway, let’s say you want to push up to a git remote at my-server.com. we add our remote via git as usual (remember we have
colocation!), set a bookmark to create a new one, and then track it to the pertinent remote. after all that, we can push:
hello-jj/ (jj: ztkl) $ git remote add origin 'git@my-server.com:hello-jj.git'
hello-jj/ (jj: ztkl) $ jj bookmark set main
Created 1 bookmarks pointing to z tklkwuv 2 a2e7291 main | say hello as well
hello-jj/ (jj: ztkl) $ jj bookmark track main --remote=origin
Started tracking 1 remote bookmarks.
hello-jj/ (jj: ztkl) $ jj git push
Changes to push to origin:
bookmark: main [add to 2a2e7291]
[…]
there’s no pull, but when there are remote changes you want to incorporate you can jj git fetch and
jj new [bookmark] to create a new revision ‘on top’ of the bookmark and switch to it as the working copy.
hello-jj/ (jj: ztkl) $ jj git fetch
[…]
bookmark: main@origin [updated] tracked
hello-jj/ (jj: ztkl) $ jj new main
Working copy (@) now at: x wzkpmyw c 807550a (empty) (no description set)
Parent commit (@-) : o vxwxtzx d 3be7058 main | add another greeting
Added 0 files, modified 1 files, removed 0 files
hello-jj/ (jj: xwzk) $ cat conversation.txt
A: woof!
B: what?
A: i mean, hello.
C: hi guys
next steps
the main goal of this primer is to be a shortcut that makes other material more approachable for you, but there’s a lot of ground i’ve left uncovered!
for instance, you will probably want to get comfortable with jj squash / jj rebase / jj arrange for actual
history-gardening, but these are well-covered by others’ writings on jj.
in terms of further reading, Steve’s Jujutsu Tutorial covers a lot of good workflows (and is how i learned jj!) and the jj documentation itself is very useful once you’re familiar.