Lessons learnt from jj

05 May 2025

I’ve written about jj, a Git compatible VCS with better UX, before.

A couple of months passed since my initial post and I now have more experience with jj in multiple projects and setups. The more I use this tool the more I respect the its design decisions. I keep saying this but I truly believe that jj is to git what Tailwind is to CSS.

A couple of days ago, I came across a post titled What I’ve learned from jj by Nathan Witmer. It is a great read and I highly recommend it.

Conceptual compression

One of the most important design decisions behind jj is to compress many different git concepts (stash, working copy, git index, commits, etc) into a single concept called “change”. This conceptual compression compounds and makes it much easier to work with the tool over time.

Any modification to your files, whether editing, moving, renaming, or deleting, is automatically captured by jj. You don’t need to explicitly tell jj about what you’ve done in your working copy, it’s already tracked. This removes the need for an “index” or staging area, since everything is captured already as part of the current change.

A change is associated with one or more parents, just like a git commit. The set of edits it captures may be empty.

Instead of switching branches as you work, you “edit a revision”. When you switch to a different or new change in your repository, that also updates the files on disk to match. […]. That means switching around to different changes in the repository is safe, and you won’t lose any modifications you’ve made, no matter where you are in the repository’s history.

Want to work on something else? Just run jj new and start making changes. Want to branch from a previous commit? Just run jj new and a new branch will pop up. Want to modify an old commit? Just run jj new and then jj squash (you can also jj edit as well).

Often I create a high-level overview of what I want to do for a feature by running jj new and creating multiple empty changes with descriptions of the tasks. Then I modify them one by one to make the required changes.

Better commits and focused pull requests

These small but important differences from the usual git workflow have meant that I’m more mindful about where any particular code changes “belongs”. It’s easy to put things in the right place, and to build a commit or change series that’s a logical and well-encapsulated series of discrete steps. I’m a lot less likely now to make a series of commits and then throw a few “whoops” and “fix that thing I missed” commits at the end, because I can effortlessly split, squash, update, and rearrange the history.

This is one of the first realizations I had when I started using jj. It is not that jj does things that git cannot, it is just that jj makes them so easy to use that they become a habit.

I can easily split changes, reorder commits and rearrange trains of pull requests that get automatically rebased when I make changes to the base. My pull requests are cleaner and more focused than ever.

Safety and undoing changes

Going back to the previous state is as easy as jj undo, or jj op restore for an earlier state. Whether what I’ve done is as simple as a jj new or as complex as a multi-headed jj rebase, going back to the prior state is that easy.

The jj undo command is one of the biggest UX improvements over git. It allows you to undo anything that you did before. It is hard to overstate how useful this is. It has made me orders of magnitude more open to experiment with the repository than I was with Git. If I made a mistake I’m just one jj undo command of going back to the previous state.

Conflict resolution

The conflict markers take some getting used to. But once you’ve resolved a conflict, any child changes will be automatically rebased again. That often clears the conflict for the rest of the branch.

jj conflict markers are different from git’s. Once you get used to them they make it much easier to resolve conflicts. Quoting from the jj docs:

Compared to just showing the content of each side of the conflict, the main benefit of Jujutsu’s style of conflict markers is that you don’t need to spend time manually comparing the sides to spot the differences between them. This is especially beneficial for many-sided conflicts, since resolving them just requires applying each diff to the snapshot one-by-one.

Since jj automatically rebases child changes after making changes, most of the time resolving a conflict in a change will automatically resolve the conflicts in child changes as well.

Try it if you haven’t yet

It is very interesting to see how the design decisions behind jj compound. Many of the posts I’ve read about the topic end up with similar conclusions: the simpler mental model and the ease of branching and rebasing make it much easier to have better history.

If you haven’t tried jj yet I highly encourage you to do so. You can use it on any existing git repo by running jj git init --colocate. This creates a .jj directory and allows you to use both git and jj at the same time in the repository. If you don’t like jj and want to drop it you just need to run rm -rf .jj/ and go back to git.