jj notes

jj notes

innocentzero

2026-05-26

A brief on working with jj based on steve klabnik's guide

jj workflows

Squash workflow

Create an empty change on top with the changes that you want to create:

jj new

N (empty) (no description set)
|
...

Describe the change you want in this one

jj describe

Editor opens, add your change description

N (empty) My fancy description
|
...

Now, this change is empty. Let it be empty. Create a new blank change.

S (empty) (no description set)
|
N (empty) My fancy description
|
...

Now you have an empty, undescribed change on top of an empty, described change.

Do the deed and make changes here.

S (no description set)
|
N (empty) My fancy description
|
...

Move the contents down to the parent change using jj squash

Takes the file path, can stage hunks using --interactive, to move changes from a particular commit that's not @, use --from option and to move to something that's not the parent commit, use the --into/--to option. So squashing is pretty powerful in general. If you totally destroy a change in between, then you can still keep the emptied change with --keep-emptied

Edit workflow

Create an empty change on top of a fully populated change, and proceed to add stuff and such like regular

A - My awesome description
|
B - My awesome parent change
|
C - Moar boosters
|
...

now, we want to make a change to somewhere in between. So, we do it

jj new --before B or jj new --after C

Can also chain them to create funky graphs, but please don't

Make changes, and then go to top again with either repeated jj next --edit or jj edit <change ID>

rebasing

jj rebase does what you think it does. It takes a commit, and puts them on top of other commits. We can specify a source using the -s/--source. This would move all the revision and the children. This is practically the only thing that matters.

We can specify where to rebase in three ways: --onto/-o rebases on top of the specified change and create a new chain/anonymous branch.

jj rebase -s M -o O

P
|
O           N'
|           |  P
| N         M'/
| |         |/
| M         O
| |    =>   |
| | L       | L
| |/        | |
| K         | K
|/          |/
J           J

--insert-after/-A inserts it after the given change and then the children of that change are moved to the top of the chain. --insert-before/-B inserts it before the given change and then that change are moved to the top of the chain.

jj rebase -s M -A O (alternately)
jj rebase -s M -B P

P           P
|           |
O           N'
|           |
| N         M'
| |         |
| M         O
| |    =>   |
| | L       | L
| |/        | |
| K         | K
|/          |/
J           J

conflict resolution

This is fairly straightforward. If the rebase/merge/whatever fails, that change will be marked as conflicted. Just open the editor and remove the conflicts. Often, jj will remove the conflicts above as well. If there are no further conflicts on the higher level.

jj concepts

revision sets

Describe a set of changes in the history of the repo. @ is the current revision. You can go up and down with @+ and @- respectively

anonymous branches

They are nothing but a particular change that have more than two children. From that point on the changes are said to diverge or branch off.

named branches

Create using jj bookmark create <name>. A bookmark is nothing but a specially marked change. To update it, you need to run jj bookmark set <name> whenever you want to set it to a new change revision.