
Working properly with Git is a chore.
I need to make sure the commits are descriptive
(often following Conventional Commits),
and the commits need to be atomic.
I used
to spend a non-trivial amount of time and effort
trying to keep my history sensible, and still failed.
While I knew about history editing,
I always thought it was for experts.
Also, when I used git
,
I preferred to use JetBrains’ IDE features rather than the CLI,
and even then, I stuck to the basic add
,
commit
, push
, checkout
commands,
and occasionally merge
and rebase
.
Jujutsu (jj
) changed that.
jj
made it easy to fix the usual mistakes I’d make with git
.
It’s different from git
,
but once they clicked, I couldn’t go back.
Also, from what I’ve been told,
you will also like jj
more
if you’re a fan of Mercurial.
You don’t need to choose between jj
and git
either, you can use both!
Learn more here:
Co-located Jujutsu/Git repos.
That’s what I did to learn jj
.
In this blog post I want to show you the building blocks to use jj
.
ASSUMED AUDIENCEExisting
git
users who share the same pain points.Somebody who loved
hg
, and is unhappy aboutgit
.
Minimal workflow
Let’s
talk about the standard minimal workflow I used for git
,
and compare it with jj
.
Action | Git | Jujutsu |
---|---|---|
Start tracking a file | git add . | All files are tracked by default |
Commit the file | git commit -m "(hopefully descriptive) commit message" | jj commit -m "commit message (easy to change later)" |
Push to repo | git push | jj git push -b "branch-name" |
Check status | git status | jj st |
That’s it!
When I first learned about git
,
those were the only commands I’d use.
Later, I learned about branches. I had no use for branches until I had to collaborate with people in college (college rant1).
Branches
In git
, you first create a branch, and then start working on it.
So, I had to be careful of what branch I’m working in.
Digital artists have the problem of “Working on the wrong layer”;
I had the problem of “Working on the wrong branch”.
jj
sidesteps the problem, you commit changes, and then assign the branch equivalent (bookmark) to those changes.
More on branches later in # Working with branches/bookmarks
Partially committing changes
Part of keeping a good git history is only using atomic commits.
I’d always get distracted and do more than one commit worth of work.
So, I’d want to commit only certain files, and certain sections of the file.
I never found out how to do that using the git
cli.
I always did it via my IDE.
NOTEAs far as partially committing changes goes, in git you can only stage some and not all files (and only parts of them by using
git add -p
IIRC) or you can usegit gui
.~ ayumi
In jj
, you can split a commit by using jj split
.
You then select the changes you want to commit in a TUI.
Working with the jj split
TUI
Down arrow
to move the “cursor” down, and Up arrow
to move the “cursor” up
Right arrow
to expand a section, and Left arrow
to collapse the section

In the image above,
I’ve expanded the changes for TermErrors.yml
file.
Currently, none of the changes are selected.
Selecting a change adds it to first commit, and the rest of the changes are in the second commit.
Let’s select the first change in TermsErrors.yml
.
spacebar
to select changes
- Press
Down arrow
- Press
spacebar
You’ll notice there are three selection states:
[ ]
: None of the children are selected.[◐]
: Children are partially selected.[●]
: All the children are selected.
c
to confirm changes
I use Helix (hx
) in terminal,
so, the commit messages are edited in hx
.

JJ: This commit contains the following changes:
JJ: M .vale-styles\RedHat\TermsErrors.yml
Only the changes I selected from TermsErrors.yml
are in the commit.
Let’s check the status
jj st

NOTEI use
jj st
is an alias forjj status
in the cli. I use it so often, I instinctively typest
instead ofstatus
.
Reading the status
Let’s address the sections one by one.
PS D:\Sync\Projects\personal-website-v5> jj st
+ Working copy changes:
M .vale-styles\RedHat\TermsErrors.yml
D src\content\posts\draft.md
A src\content\posts\img.png
A src\content\posts\img_1.png
A src\content\posts\img_2.png
A src\content\posts\img_3.png
M src\content\posts\jj-my-beloved.md
Working copy (@) : omrruoov cb05513b (no description set)
Parent commit (@-): lwymzvzm 1263aaec feat: reduce vale noise
What’s a working copy?
jj
doesn’t have a staging area.
It treats the current state of the repo as a commit,
and when you edit the repo,
you’re editing the commit.
PS D:\Sync\Projects\personal-website-v5> jj st
Working copy changes:
+ [M] .vale-styles\RedHat\TermsErrors.yml
+ [D] src\content\posts\draft.md
+ [A] src\content\posts\img.png
+ [A] src\content\posts\img_1.png
+ [A] src\content\posts\img_2.png
+ [A] src\content\posts\img_3.png
+ [M] src\content\posts\jj-my-beloved.md
Working copy (@) : omrruoov cb05513b (no description set)
Parent commit (@-): lwymzvzm 1263aaec feat: reduce vale noise
You can notice that there are three prefixes for the files.
A
: New file that will be addedD
: File deletedM
: File modified
PS D:\Sync\Projects\personal-website-v5> jj st
Working copy changes:
M .vale-styles\RedHat\TermsErrors.yml
D src\content\posts\draft.md
A src\content\posts\img.png
A src\content\posts\img_1.png
A src\content\posts\img_2.png
A src\content\posts\img_3.png
M src\content\posts\jj-my-beloved.md
+ Working copy ([@]) : omrruoov cb05513b (no description set)
+ Parent commit ([@-]): lwymzvzm 1263aaec feat: reduce vale noise
There’s also @
and @-
.
“What are those?” you might ask.
@
is the current commit. This is the commit you’re editing.
@-
is the previous commit.
Similarly, @+
is the next commit.
You can add multiple +
or -
.
Another way to think about the number of -
(or +
) is
if @-
is the “father” of the commit, then
@---
refers to “grand-grandfather” of the commit.
There’s also two of what seem to be hashes.
- Working copy (@) :
+ [omrruoov] [cb05513b] (no description set)
- Parent commit (@-):
+ [lwymzvzm] [1263aaec] feat: reduce vale noise
The first hash on each line
(omrruoov
and lwymzvzm
) are change ID.
These will remain persistent through changes.
NOTEIf you’re familiar with Mercurial’s “revset” terminology, the change ID is the ID for the revset.
For others, you can think of “revset” as a commit in
git
. It’s technically wrong, but I can’t think of a parallel ingit
.You already know one revset: ”@”.
An interesting revset is
root()
which is a virtual commit, and all the commits are “descendants” of “root()”.So, the first commit in the repo is
root()+
, and all the commits fromroot()
to the current commit@
can be expressed asroot()..@
.Revsets are really cool, read more about them on the docs here: https://jj-vcs.github.io/jj/latest/revsets/.
I will be using “revset” from here on where it’s appropriate as it will reduce confusion in the long term.
The second hash on each line
(cb05513b
and 1263aaec
) are commit ID.
These will change as you modify the commit.

Entering the full hash is inconvenient,
and unless you have really good memory,
you can’t memorize the hash.
So, jj
allows you to use the least number of letters from the hash
that won’t cause any collisions,
and highlights them.
In this case om
, cb
, lw
, and 1
.
So, I can use om
refer to commit with change ID omrruoov
,
and lw
for the previous one.
Working with branches/bookmarks
Bookmarks are what git
calls branches.
While working in git
,
the best practice is to work in a separate branch,
and then merge all the changes into the main branch.
But you might’ve noticed I forgot to create a branch!
If I was using git
, I would be panicking now.
However, as discussed in # Branches, I don’t need to panic.
Let’s create the bookmark
jj bookmark create jj-my-beloved -r "@"
NOTEI need to wrap the
@
in double quotes as I’m using PowerShell.From https://jj-vcs.github.io/jj/latest/windows/#typing-in-powershell:
PowerShell uses @ as part the array sub-expression operator, so it often needs to be escaped or quoted in commands:
The command seems fine till jj-my-beloved
,
and then there’s -r "@"
.
What does that mean?
-r
is shorthand for --revision
.
You pass the change ID or revset to it
(tbh I assumed -r
stood for revset
).
jj
will then create a new branch,
and the latest commit in the branch will be that revset.
@
, as discussed before, is just shorthand for the current commit.
So,
the command will select the current working commit as the latest commit.
Pushing changes
Let’s try pushing changes to the repo
jj git push -b "jj-my-beloved"
NOTENotice that the command is
jj git...
and not justjj...
That’s because
jj
was built with different backends in mind, where backend in this case refers to different VCS. So, you can add support for multiple VCS tojj
. You aren’t just limited togit
.I would guess that if there are some backend-specific features, they will have the backend’s name in the command (like
git
here).

Here, we run into one of the many safeguards jj
has.
In this case, the branch jj-my-beloved
doesn’t exist in origin
.
To allow jj
to create a new branch you need
to pass the -N
/--allow-new
flag.
So,
jj git push -b "jj-my-beloved" -N
WARNINGHARSH TRANSITION
I like to commit all my changes at the end of day.
NOTEOr you know,
If I’m using git
,
I can’t just use the usual command of git commit
.
I would need
to take aside time to ensure my git history is “good enough”.
But with jj
,
I can do jj commit
and end my work any time,
and then deal with the messy history afterward.
I can just commit everything using the below command, and then worry about splitting and combining the changes tomorrow.
jj commit -m "checkpoint: EOD"
NOTEIf you didn’t notice
-m
is the same flag you use to add messages usinggit

NOTEYou can do the same with different branch:
git checkout -b tmp && git all -A && git commit -m "checkpoint: EOD"
or directly on your branch, if it’s just yours. Then just do
git reset HEAD~
.~ Andrew
Squashing changes
You might’ve noticed,
I’m writing this blog post
and showing you examples of how I use jj
by showing screenshots of the commands I’m using.
So, all the changes since the “EOD” commit haven’t been commited.
How can I be sure my changes aren’t commited? Let’s check the status.
jj st

It says (no description set)
,
which means I haven’t described the change in the commit,
but it doesn’t say (empty)
, which means the commit isn’t empty.
You can interpret that
as I have changes that I haven’t commited yet.
In git
I think I would need to amend the changes.
I never did it using the cli tho, always through my IDE.
In jj
however, the command is
jj squash

Voilà!
Cleaning yesterday’s history mess
Yesterday I used jj commit
, so all the changes were commited,
so now I’m editing a different commit.
When I commited, I accidentally commited the temporary images,
and deleted draft.md
.
Let’s revert those mistakes!
As mentioned in # Reading the status, you can just use the highlighted part of the change ID instead of typing the whole thing. So, from here-on, I’m going to use the shortened form.
jj split yx
I want to keep the changes to this file, so I need to add the file to first commit, and the rest will in the working commit.
I also need to undo deleting draft.md
,
let’s split the current change again,
this time, draft.md
will be in first commit.

Reverting commits
There are two ways to undo the mistake in git
AFAIK,
git reset
and git revert
.
jj
has similar commands.
jj backout -r <revision>
- Reverts the commit by creating a commit that cancels out the commit.
- Reversible.
- similar to
git revert
jj abandon -r <revision>
- Just, straight up, delete the commit.
- Irreversible.
- similar to
git reset --hard
In my case, I’d prefer to not touch the file. Since the “last updated date” on my posts are taken from the git history.
So, I’m going to use abandon.
jj abandon -r pp
Editing history
This is where the power of jj
comes through the most.
In # Partially committing changes,
I only commited part of the TermsErrors.yml
file.
Let’s fix that.
First I need to find the change ID for the commit.
Let’s use
jj log

The letters I need to enter are “lw”
Let’s commit the TermsErrors.yml
file, so jj split
.

I need to move the commit into “lw”
jj squash --from sx --into lw


NOTEIn # Reading the status I said that commit ID changes, and change ID remains the same.
You can see it here!
Notice how earlier, the commit ID for “lw” was “1”, and now it’s “24”.
Show the changes in commit
Let’s see the changes in the revision/change “lw”
jj show lw

Oops, I forgot to remove the duplicated description from the commit message.
Let’s edit the commit message.
jj describe lw
NOTEYou can change it with
git commit --amend hash
. But only first, the rest throughgit rebase -i
. I assumejj
does this behind the scene anyway, because changing any commit will change the hash of the rest.~ Andrew

Done!
NOTEunlike
git
’s way of doing things,jj describe
can be used any time.In fact,
jj commit -m "msg"
is an alias forjj new
followed byjj describe -m "msg"
.
Merging branches
Another common operation is merging branches.
Let’s say I want to update the template for my website. For my setup, I have “origin” as my fork, and “upstream” as the original template.
Let’s fetch the changes from all remotes.
jj git fetch --all-remotes
Merging changes in jj
is the same
as creating a new commit with two parents.
To use a different remote for a branch/bookmark,
you need to write it in the form bookmark@remote
.
jj new "@" "main@upstream"

Oh, no! So many conflicts!
Fortunately,
conflicts feel easier to solve in jj
for whatever reason.
Solving merge conflicts
In jj
, conflicts are first-class citizens.
You can just, leave the conflicts as they as are if you need to.
The recommended way to resolve merge conflicts is to create a new commit, resolve the conflicts, and then squash the commit. This is called the Squash Workflow. More on that later in # Squash Workflow.
So, jj new
.
jj
also has an inbuilt conflict resolver, but I prefer to resolve them in my IDE.
You can use the inbuilt resolver by using
jj resolve
OK, now, the template is updated, I want to remove the images from git history, again ^^; I need to first change to the “rs” commit, and then split.
jj edit rs
jj split
Now, I have a problem.
I need to bring the images’ commit to latest commit.
Let’s think about what I need to do to achieve that.
- I need to rebase the commits.
- The change ID for the images’ commit is “sw”.
- The change ID for the latest commit is “wm”.
- I need to move the commit so “sw” is before “wm”.
jj rebase -r sw --insert-before wm
So what does the command do?
It moves the revision “sw”,
and insert it before “wm”.
Perfect!

Done!
That being said, I need to squash “wm” into “sw”, and then split again, as I’m editing the blog post live ^^; The changes from me writing into this document are in “wm”.
No problem, easy enough, just use split!
Case Study
As you see, even with just a few commands, I can do so much. Let’s talk about a real world example where editing history came useful.
As I mentioned in # Reverting commits, I use git history to find the last updated date.
When I released Hyper-V shenanigans with nixos-generators
,
I ran the linter in my IDE and didn’t check what files it “touched”.
Turns out, it touched all the Markdown files,
and so, the “last updated date” for all my posts became 2025-04-25
.
It took me ~10 minutes to fix the mistake, where I spent a lot of time just fumbling commands.
While doing it, one of jj
’s safeguards activated,
preventing me from editing a commit that I have already pushed.
NOTE
git
will stop you from pushing edited commits, but not editing them locally.~ Andrew

It was an error like the image above.
I bypassed the safeguard by using --ignore-immutable
flag,
and then force pushed.
NOTE
jj
has more safeguards likejj op log
,jj undo
.But I don’t know how to use them yet ^^;
Squash Workflow
The squash workflow is a general good practice.
It goes as follows:
- Create a new commit (do not edit the commit)
- Make the required changes
- Squash the changes into the old commit
jj
can directly edit the commits,
so why would you go through all this effort?
This workflow ensures that the previous commit remains as is, and if required, you can reset the changes. The changes also remain “atomic”, in the sense that either you fix the problem (by squashing), or you don’t. You won’t be stuck in a middle phase.
Links to learn more from experts!
- Official docs
- The tutorial I followed by Steve Klabnik (@steveklabnik.com) that initially sold me on at least trying out
jj
- An excellent essay by Chris Krycho (@chriskrycho.com) on
jj
- https://v5.chriskrycho.com/essays/jj-init/
- It is also available in video form on YouTube: What if version control was AWESOME?
- An episode of Bits and Booze by GitButler where they discuss
jj
- An article recommended by amos in goblin mode (@[email protected])
- https://zerowidth.com/2025/what-ive-learned-from-jj/
- Source:
hachyderm.io
post - Alternative source:
bluesky
post
- Orhun Parmaksız (@orhun.dev)‘s stream VOD dedicated to learning Jujutsu on YouTube
Inspiration for writing the blog post
Amos (also known as fasterthanlime) posting “jj is going to make me worse at git isn’t it”

If you want to read more about other people’s experiences, you can read them on fediverse, and bluesky.
A conversation on Typst’s discord server
I recognized
the person was using jj
because of the distinct output for jj log
,
and started talking about Amos’ post, and how jj
made life easier.

A random person replied to my message asking how Amos’ post can be taken in a negative light.

Misc
jeez i had a variety of hacks in place to manage simultaeneous streams of work with git (branches, stashes, worktrees).
… and jj just has one tool: jj new. fewer concepts but so much more powerful.
— akshay (@oppi.li) May 10, 2025 at 10:34 PM
Post-writing discussion
I asked ayumi if, after reading the blog post,
she’s at least somewhat interested in jj
.
Her reply:
Maybe once it’s finished—for example nowadays I mainly contribute to Tangara firmware and I need Git submodules support for that, which appears to be absent in jj. Some other things like SHA–256 Git repositories also appear not to work. I’m sure that those two things will be implemented eventually. I played with it a little and getting used to the different workflow would probably take some time in my case, though if I were to guess jj would probably be simpler to use for people who either didn’t use a VCS before or haven’t got used (or can’t get used) to Git yet.
Garnet chimed in
and said that she uses git commit --amend
very often,
and ayumi concurred.
Garnet also commented:
I like how jj handles commit hashes (as in, allowing you to use the shortened hash) but on Git I don’t have a much of a problem either because the CLI supports tab completion also git has an interactive rebase feature
git rebase --interactive
it is a little confusing to me sometimes but I take it as a skill issue I’m not convinced that it’s worth switching to jj but that’s just my opinion
ayumi also had a “fun fact” about jj
if you set up commit signing and like me have the signature stored on a U2F key it will ask you to tap the key every time it sees changes.
By “it sees changes”, ayumi meant everytime jj
is executed.
While trying out jj
, ayumi noticed a problem,
even three line jj st
output was shown in the pager.
The solution she found was
jj config set --repo ui.pager 'less -F'
Another interesting thing,
Proofreaders
Divyesh Patil
ayumi (also the comment in # Partially committing changes giving an idea of how to do it in git ayumi’s comment)
- Opted out of sharing socials
Garnet
- Opted out of sharing socials
Footnotes
Footnotes
-
See, in Indian colleges, the “team” in “team project” is in name only. Most of the projects are either copied directly from GitHub, or from a YouTube video. One person does the “coding”; another does the documentation required for the project (which is just asking ChatGPT to write the docs), and the rest just exist. The number of people in a class who actually program are usually in single digits. If the stolen projects don’t run, they just go to the classmates who know, and then make them fix it, sometimes in exchange for money. So, I never really got the chance to do teamwork until last semester of third year, and last year. I even conducted git and GitHub workshops. But, I’m bad at explaining, so while I did make the slides, I left the explanation to my peers who were way better at teaching. ↩