charlotte's jujutsu tutorial

a little primer on becoming a version control hipster

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
@  rskulwum charlotte@som.codes 2026-05-13 00:32:36 001afa51(empty) (no description set)
  mxklzpnz charlotte@som.codes 2026-05-12 14:02:50 main fb45c965
│  tweak the sounds a little
  lloylxvy charlotte@som.codes 2026-05-12 10:33:29 81ed6047
│  add deno task for formatting and run it
  znlzuqxr charlotte@som.codes 2026-05-12 10:33:29 e0d55474
│  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
@  rkxsqzky charlotte@som.codes 2026-07-13 10:57:55 2f832520(empty) (no description set)
  zzzzzzzz root() 00000000

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
@  rkxsqzky charlotte@som.codes 2026-07-13 10:58:01 1e8c2f8b(no description set)
  zzzzzzzz root() 00000000

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: rkxsqzky 8ee550a9 say meow
Parent commit (@-)      : zzzzzzzz 00000000 (empty) (no description set)
hello-jj/ (jj: rkxs) $ jj log
@  rkxsqzky charlotte@som.codes 2026-07-13 10:58:06 8ee550a9say meow
  zzzzzzzz root() 00000000

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: ztklkwuv 67bf7c3a (empty) (no description set)
Parent commit (@-)      : rkxsqzky 8ee550a9 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
@  ztklkwuv charlotte@som.codes 2026-07-13 11:21:01 65f77d30say hello as wellrkxsqzky charlotte@som.codes 2026-07-13 10:58:06 8ee550a9
│  say meow
  zzzzzzzz root() 00000000

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
@  ztklkwuv charlotte@som.codes 2026-07-13 11:45:26 34f3de32say hello as wellrkxsqzky charlotte@som.codes 2026-07-13 11:44:21 34af2929
│  say meow
  zzzzzzzz root() 00000000

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: ztklkwuv 2a2e7291 say hello as well
Parent commit (@-)      : rkxsqzky 265ff44e 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 ztklkwuv 2a2e7291 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: xwzkpmyw c807550a (empty) (no description set)
Parent commit (@-)      : ovxwxtzx d3be7058 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.