I had been using Git for years.
It worked.
I wasn’t stuck.
But jj made me realize how much of my thinking I had been translating into commands.
If that sounds familiar—if you keep seeing “jj” on X or in blog posts, but you’re thinking “I’m fine with Git”—this post is for you. I’m not here to dunk on Git. I’m here to explain the one thing jj changed for me:
It changed my mental model of what a “commit” is.
Once that clicked, a bunch of day‑to‑day version control friction disappeared, and I started shipping changes with less ceremony.
What jj is (in one paragraph)
Jujutsu (jj) is a version control system that’s compatible with Git repos and Git remotes (so your commits look like normal Git commits, and you can switch back if you want). It’s also explicitly experimental, which is why I recommend trying it on one repo first. The “headline” design choice is simple: the working copy is represented as a commit, and most commands automatically amend that working-copy commit.
If you want the official starting points: install & setup, the tutorial, and the jj vs git command table.
The audience I’m writing for (because this used to be me)
I first heard about jj the same way many people are hearing about it now: repeated drive‑bys.
I think I first saw it on X and in blog posts by individual developers. At the beginning, I honestly didn’t even know what “jj” was…
But after seeing it for the third time or so, I started to feel like, “Is this becoming a thing?”
So I looked it up.
My default assumption was: “It’s probably a new CLI wrapping Git.” Curiosity made me try it.
The key point: I didn’t try to learn jj “properly.” I cloned the repository, opened my editor, and asked an AI assistant things like “How do I do X in jj?” and copy‑pasted commands until I developed a feel for it.
That is a great onboarding path if you’re busy and mildly skeptical. You don’t need a pilgrimage. You just need one workflow that feels better than what you’re doing today.
The tiny frictions in Git that add up
Git can do almost anything, but there are two frictions I kept paying for:
-
Starting work means naming it early
- I always felt like I should create a branch.
- The annoying part is that at the start, I often don’t know what the branch should be called.
- I used to hack around this by generating random branch names and opening PRs that way. It worked… but it was a smell.
-
The “three areas” mental tax
- Working copy
- Staging area
- Committed history
- Even if you understand these intellectually, you keep context‑switching between “what’s staged,” “what’s committed,” and “what’s still dirty.”
None of this is catastrophic. It’s just… constant.
The jj idea that rewired my brain: the working copy is a commit
In jj, your working copy is not a special pile of uncommitted changes. It's represented as a real commit. And when you edit files, your working‑copy commit is amended automatically by your next jj command.
If "amended automatically" sounds alarming, I get it. It felt impolite at first—like the tool was doing something behind my back. And yes, you can push that auto‑amending commit to GitHub and open a PR while still editing (under the hood it's a force‑push). Whether that's a good idea for reviewers is another story; I don't do it myself. But the point is: auto‑amend is the default, and you learn to trust it quickly.
That one decision collapses a lot of complexity:
- You don't need an explicit staging/index concept for normal work.
- "Dirty working directory" errors basically stop being a thing.
- "Stash because I need to switch context" becomes less central (because your working copy is already recorded as a commit).
Once the current commit has taken shape, you name it:
jj describe -m "your commit message"
Then you move on to the next commit:
jj new
(There's also jj commit -m "message", which does both in one step.)
This is how you grow a commit: edit, auto‑amend, edit some more, then name it and start the next one. A commit stops being a single decisive action and becomes a stretch of focused work with a beginning and an end.
This matches what I felt before I could articulate it:
With jj, it feels like that management collapses into a single, coherent model.
There isn’t this constant switching of mindset between “what’s staged,” “what’s committed,” and “what’s still dirty.”
And it leads to the biggest shift:
In Git, a commit ends work. In jj, a commit begins work.
My Git habit looked like this:
- Start coding (often on a branch).
- When it feels “done,” write a commit message and commit.
- A commit marks the end of something.
In jj, the flow feels more like:
- Start a new piece of work (a new commit you’ll shape).
- Give it a description early (so your future self knows what you’re doing).
- Keep refining it as you edit code.
- When you’re ready to move on, start the next commit.
Or in commands:
# Start new work from main/trunk
jj new main
# Describe the work early
jj describe
# ...edit files...
# Move on to the next piece of work
jj new
This is the line that stuck with me:
A commit is no longer the end of work.
A commit is the beginning of work.
That sounds small, but it changes your default behavior. Instead of “wait until it’s perfect, then commit,” you naturally do “name the intent, then iterate.”
The “no branch name yet” problem basically goes away
jj has bookmarks (similar to branches), but the day‑to‑day flow doesn’t pressure you to name everything up front. You can just keep creating commits and shaping them, and only later decide what deserves a bookmark to publish.
That matches how work actually unfolds:
- at the start you have a hunch,
- then you explore,
- then the shape emerges,
- then you can name it.
If you’ve ever paused work just to invent a branch name you don’t believe in yet, you’ll immediately recognize the relief.
A concrete mini‑workflow (Git vs jj)
Let’s say you want to start a small change, and you’d like to write the message early so you don’t forget the intent.
Git (typical)
git checkout -b ??? # name it early (or rename later)
# edit files
git add -p # decide what “counts”
git commit # write message at the end
jj (how it feels)
jj new main # start new work from main
jj describe # write message early
# edit files
jj diff # review whenever
jj new # start the next piece of work
It's not that Git can't approximate this. It's that jj makes it the default path of least resistance.
You'll find plenty of Git‑vs‑jj comparison tables online (or from your favorite AI). Honestly, they're hard to internalize until you've used jj for about three days. If any of this sounds interesting, just try it—the feel comes faster than the understanding.
Revsets: "a query language over the commit graph"
The other “oh wow” moment for me was revsets.
Once you understand revision sets as a kind of query language over the commit graph, things click.
You can picture the graph in your head and then write exactly what you want on the command line…
In practice, that means many commands accept expressions like:
jj log -r ::@ # ancestors of the working copy
jj log -r '@ | bookmarks() | tags()'
jj log -r 'remote_bookmarks()..' # commits not on any remote bookmark
You stop thinking in "what sequence of commands do I need?" and start thinking in "what set of commits do I mean?"
I'm only scratching the surface here—revsets deserve their own post. For now, just know they exist, and they're one of the things that makes jj feel expressive rather than procedural.
The safety net: operation log + undo
If you’ve ever typed a history‑rewriting command and immediately thought “wait—what just happened?”, jj’s operation log is a breath of fresh air.
Every operation is recorded. You can inspect it:
jj op log
And you can undo the last operation:
jj undo
This changes the emotional cost of experimenting. You can try powerful operations earlier because the “oops” recovery story is built‑in.
A gentle, practical promise
This is the tone I want for the Git vs jj comparison:
It’s not about saying Git is bad or jj is objectively better.
For me, jj’s mental model and command-line philosophy simply fit better.
Also: jj is Git‑compatible. You can use it with Git remotes, and you can switch back.
So the pitch is not “throw away Git.” The pitch is:
Try jj on one repo. For one small change.
If it doesn’t click, you’ve lost very little.
Try jj in 10 minutes (no ideology required)
Pick a small repository (or a throwaway one) and do this:
- Install jj (macOS Homebrew):
brew install jj
- Configure your identity:
jj config set --user user.name "Your Name"
jj config set --user user.email "you@example.com"
- Clone a Git repo using jj:
jj git clone <your-repo-url>
cd <your-repo>
- Look at status and log:
jj st
jj log
- Start new work from your main branch, describe it early, then edit files:
jj new main
jj describe
# edit a file
jj diff
- Move on (start the next commit):
jj new
- If you get lost, use the safety net:
jj op log
jj undo
If, after this, you feel the "commit begins work" mental model even a little, keep going. If you don't, that's fine. jj isn't trying to win an argument; it's trying to make certain workflows feel obvious.
Real‑world example: my daily commands
I won't explain every flag here—ask your coding agent or check the docs. But as an index of what daily jj usage looks like for me, here are the commands I reach for most often.
# Fetch the remote main branch (bookmarks ≈ branches)
jj git fetch --remote=origin --bookmark=main
# Start a new commit on top of main
jj new main
# jj diff doesn't show add/delete status as text, so for clipboard I use --git
jj diff --git | pbcopy
# Update the commit message from clipboard
pbpaste | jj describe --stdin @
# Or update the *previous* commit's message
pbpaste | jj describe --stdin @-
# Push the current commit to a remote branch
jj git push --named <branchname>=@-
# After adding more commits, update the bookmark and push
jj bookmark set <branchname> -r @-
jj git push --bookmark <branchname>
# Split a commit interactively (by file or by line)
jj split
# A TUI opens; select what goes into the first commit. Very handy.
Closing thought
If Git feels like “translating your intent into commands,” jj sometimes feels like you can just… state your intent.
That’s the whole reason I’m writing this post.
