Jujutsu (JJ): An Alternative to Git?

Jujutsu proposes to be simpler yet more powerful than Git. But is it worth switching? Let's find out.

We’ve already spent a lot of time using Git… But another tool is trying to offer enough advantages to justify making the switch. Let’s dig deeper.

Table of Contents

  1. But how did Jujutsu begin?
  2. But why give it a try?
  3. JJ vs Git
  4. Next steps
  5. References

This blog post is only part 1 of a two-part series. This first part focuses on the differences between Jujutsu and Git with concepts and practical examples, as well as some interesting tricks we can do with JJ to start using it in our repositories!

In the second blog post of the series, we will deal with conflicts, merges, pull requests, and more complex workflows. We’ll see how JJ makes these actions simpler, even though they are considered more time-consuming in Git.

With all that being said, Jujutsu proposes to be simpler, yet more powerful, compared to Git. That’s a big promise; let’s understand better how Jujutsu tries to fulfill it.

But how did Jujutsu begin?

Jujutsu is a version control tool that aims to make workflows more flexible. It rethinks some of the frustrations encountered when using Git.

To refresh your memory:

Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. – # Getting Started – About Version Control

That is what Jujutsu aims to do, though it redefines how you handle changes and the working directory differently from Git.

The focus is on being easy to use. JJ also uses Git repositories as its storage layer, which lets developers use Jujutsu with Git. That means you can use it with existing repositories!

Additionally, Jujutsu is a tool used internally at Google. Martin von Zweigbergk is the creator behind the project. He and a group of collaborators wanted to experiment with a new idea to improve their internal tools. That experiment aimed to handle changes without stash and explore other different concepts, like moving freely between commits, even when there are conflicts!

Also, Martin previously worked with Mercurial before shifting focus to JJ but continues to work on source control. The connection is clear when we se the project’s README mentioning how several Mercurial features inspired JJ’s design, along with ideas from Sapling and Darcs (such as treating conflicts as first-class objects). Helping the tool become what it is today.

Finally, JJ was officially released publicly in 2023. So yes, it’s a relatively recent tool. However, it has already been praised quite a bit!

But why give it a try?

Most developers are already used to Git, aren’t they? Why try out a new tool? As the old saying goes: "if it ain’t broke, don’t fix it," right?

But JJ tries to solve the major problems we run into with Git. Some of the features JJ offers that stand out to me most are simpler merges and fewer conflict headaches. Even though it works differently and takes some getting used to, the benefits are interesting.

All of that is possible because of some core principles:

  • The entire repository is under version control: every time you run a jj command, whether it’s jj status or jj evolog, it takes a snapshot of your working copy. Giving you a complete history of your repository. You can check this out with the jj op log command.

image

  • Conflicts can be recorded in commits: operations succeed even when there are conflicts! Those conflicts are stored, so you can resolve them later.

  • It has automatic rebase: whenever a commit is modified, its descendants are automatically rebased.

  • Comprehensive support for rewriting history: different commands let you move parts of a change from one commit to another. Plus, jj diffedit lets you edit a commit’s changes without checking it out.

Those benefits can help your workflow quite a bit, depending on how large your team is and how you handle PRs and branches.

But what many would like to know are the more practical differences between Git and JJ. After all, people need to figure out if the adaptation process is worth the effort.

Well, let’s check out some of them!

JJ vs Git

As mentioned, we can use JJ with Git. Martin von Zweigbergk made sure JJ is compatible with it. In fact, we can use jj git clone to grab a repository from GitHub, for example.

But there are still some differences that could define your decision when you try to test the tool in your next study project.

Let’s look at a quick comparison:

  • Git: you need to stage changes to create a commit, which can have at most two parents. Also, any commit made outside of a branch is not considered by Git. It’s seen as garbage. Your workflow is heavily based on different branches for various features.

  • JJ: there’s no staging area or index. Also, a commit in JJ can have more than just two parents, which increases flexibility. Additionally, JJ has no staging step. Every action triggers a commit. That means when you run jj git init, changes are automatically part of the working copy. No need for a command like git add some-file.js, as shown below:

❯ jj git init Initialized repo in "."
Hint: Running `git clean -xdf` will remove `.jj/`! 
❯ jj status
Working copy changes: 
A .gitignore 
A index.html 
A script.js 
Working copy (@) : nntrkurr e56acbdd (no description set) 
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)

Even though I just started my project, my three main files are already automatically part of the working copy. Cool, right? If I used Git, I would need to add these changes to my current commit.

In the repository below, for example, I’d have to add my modifications before doing a push:

image

It’s also important to note that git commit and jj commit are different commands. Git’s command creates a commit, while Jujutsu’s command lets you edit the description of your current commit that already has content and create a new empty commit to work on.

See the example below showing my working copy that added some changes to the project’s HTML. After running jj commit, my system’s default text editor opened (in my case, nano), and I wrote the description "Semantic HTML":

image

Notice that the JJ command behaved differently. It only let me move to the next commit, finalizing the current one. The parent commit is no longer "Footer" because the commit with the HTML changes I made was already saved.

Now, any new changes I make will be part of the commit with the change id wuoplktq, which is currently empty and has no description.

We’ll talk about the other concepts shown above later in this post, don’t worry.

Additionally, Jujutsu focuses more on changes than the idea of branches. That’s why the concept of change id is so important. We’ll soon see it in practice.

With these differences, our workflow changes. However, that comes with advantages. Let’s see them in practice.

JJ & VSCode

Before we get started, I want to recommend a tool in case you’re using VSCode like me. (It also works with Cursor).

VisualJJ is very interesting and can help us visualize the structure of our modifications, especially if you’re coming from Git.

image

It provides a tab in VSCode that tracks the changes you’ve made. With this visual support, you can better handle the idea of multiple changes pointing to the same parent.

See the example below in a small project:

image

Besides that, it’s also able to run some commands for you. However, our focus will be to use it mainly as visual support since it’s important to build the habit of writing these new commands so they make sense to you.

Consequently, during our tutorial below, I’ll mention VisualJJ a few times so we can visualize our progress. Next, let’s move on!

JJ in practice

Now that you understand some of the basic concepts, let’s move to using it in a mini project that will help us explore the tool.

We saw that we can start a project using JJ and Git as storage with the jj git init command or use jj git clone. In this post, to keep our learning straightforward, let’s start with a plain folder that isn’t a repository yet!

Name your folder mini-jj-project or hello-world, whatever you prefer. Then create a few simple files just to test versioning. You could create a script.js file and an index.html, for example.

Note: Jujutsu recognizes the .gitignore file. If your small project needs an ignore file, feel free to create it. It will work the same way it does with Git.

Finally, run jj git init inside the folder. You can also use your preferred IDE. In this blog post, I’ll use VSCode, as mentioned in the previous subtopic.

After running the command, you’ll see something like this:

jj_init

Cool! Shall we check the status of our repository?

jj_st

Notice that a shortcut for jj status is jj st. Both work fine! Also, check out the different colors in the terminal. Depending on your settings, these colors will be slightly different, but they’re important for us to see the change id and commit id of our working copy and the parent commit more easily.

In our working copy, nntrkurr is the change id. While e56acbdd is the commit id. The highlight color we see in the image above shows that we can refer to these ids by just the colored letter. This means there are no other changes with similar ids yet, so for now you can refer to these ids by just the first digit. Useful, right?

Now, let’s look at some initial concepts. Pay close attention to the parent commit:

parent_commit

That is the root commit. It will always have the ids: zzzzzzzz 00000000. Also, see that the working copy was created above it. It already contains changes, the files we created, so that’s why it doesn’t show: (empty).

To set the user and email that will be linked to your changes, you can run the following commands:

> jj config set --user user.name "Cool developer"
> jj config set --user user.email "cooldev@dev.com"

Now, want to add a description to our working copy? The jj describe command will open your system’s default text editor. In my case, nano opened:

nano

We see information related to our change here. And the text "Hello world description" was the description I added.

After writing our description, we can save our changes. Now, notice that something else also changed. Every time we update the description, our commit id will change.

Note: remember that the change id is the one that appears first, and the second id shown is the commit id.

See the case below:

change_id

Now our commit id is b67419cc. However, our change id wasn’t modified. That means that even though your change evolves over time, receiving various commit ids after changes, you’ll still have a stable change id to refer to that version of your code. Not just describe but many other commands trigger a change in the commit id, so keep that in mind if you notice changes in them while testing JJ commands!

Now, run jj new so we can start a new working copy. Consequently, there are no changes yet. To confirm that, we can run jj st again and check:

jj_st_2

Let’s also use the Visual JJ extension to help us visualize our changes. See that now we have a simple flow:

visualjj_first

Let’s make a few more simple changes, like comments, for example. My description will be "Documentation & comments addition."

After that, we can do the same flow as before and create one more change with jj new.

In this case, add changes to more than one file, like fixes to the JavaScript function text and changes to the HTML file. After that, write a description of your changes. However, this time I don’t want to use the default text editor. Let’s write it right in the command. Something like:

❯ jj describe -m "Fix incorrect naming"

Now, after these two changes, check VisualJJ again.

visual_jj_incorret_naming

So far, we have several similarities with Git. But it’s interesting to note that we don’t need any equivalent to git add. Changes are simply already being tracked. With this change finalized, let’s start another one with one more jj new.

Another interesting detail is that when we run jj log, we see that our root commit has "root()":

❯ jj log
@  vpnnnrsl user@local.com 2026-02-24 23:12:11 d1f8dbb2
│  (empty) (no description set)
○  xlvvuzyl user@local.com 2026-02-24 23:11:17 d8cb31cf
│  Fix incorrect naming.
○  pspqyylr user@local.com 2026-02-24 23:09:00 c1cb04b0
│  Documentation & comments addition.
○  nntrkurr user@local.com 2026-02-24 22:54:46 b67419cc
│  Hello world description
◆  zzzzzzzz root() 00000000

That is a revset. Jujutsu’s documentation comprehensively explains:

Jujutsu supports a functional language for selecting a set of revisions. Expressions in this language are called "revsets" (the idea comes from Mercurial). The language consists of symbols, operators, and functions. – Jujutsu docs

root() is a function in that language and it returns the root commit we mentioned earlier. Using revsets is how Jujutsu does listing and various other useful commands. We’ll explore some of these functions in the next sections and in part 2.

Additionally, notice that @ represents the current working copy. We don’t have any finalized changes from the working copy yet, and its parent commit is xlvvuzyl (which is a change id). You can switch to any of these previous changes with the command jj edit change-id-here:

❯ jj edit nntrkurr
Working copy  (@) now at: nntrkurr e17a9a1c Hello world description
Parent commit (@-)      : zzzzzzzz 00000000 (empty) (no description set)
Added 0 files, modified 2 files, removed 0 files

JJ provided the difference between versions through the message: "Added 0 files, modified 2 files, removed 0 files". In my case I had only changed the index.html and the script.js.

Cool, with basics understood, we can already use JJ in small repositories. We also practiced using change ids following the idea of always being in a working copy. But, there are some other useful commands that might come in handy during our studies, let’s see?

JJ useful commands and workflow

Okay, it is clear that JJ is similar to Git but also has its specific commands and concepts. Let’s see some of them while we learn more about its flow, just like in any other version control system. To see that idea in practice, let’s first test some commands below:

  • jj duplicate change-id: self-explanatory. We can test it with our change nntrkurr:

jj_duplicate

The log now shows the duplication:

jj_duplicate_response

It shows which one we’re currently on with the "@". Cool, right?

  • jj diff -r (a range here): This command lets you see the difference between a group of changes using their change ids. Test it in your terminal and see the diff:

jj_diff_range

  • jj config set --user operation.hostname "anything" and jj config list operation, which let you change the hostname and list its configuration, respectively. Remember that the hostname change will take effect from the moment it’s made onward. Past changes will still have the old hostname, for example. We can check the hostname shown after running jj op log, which in turn shows us the log of operations done with JJ:

jj_op_log

Now if you want to update the author in a range of changes, you can run:

❯ jj metaedit --update-author -r 'id..@'

And in the case above, we update all the changes starting from the working copy. If you want to update all: jj metaedit --update-author -r 'all()' (Note the use of a revset function here, all()). Remember that the old hostname will still be accessible in the op log, for example, since JJ stores this information even after config changes.

Let’s explore another command now: jj squash.

First, let’s describe an empty change, I’ll call it "Log goodbye and hello". Then, let’s create another change with jj new. In summary, we described an empty change and moved on to start working on another.

Then, make the modification described in the previous change that creates a function that logs goodbye and hello in one of your files, like the script.js file, for example.

Now let’s do squash. We’ll do this from the terminal with the command:

jj_squash_terminal

Notice that we described what we were going to do and then, after we created another change and finalized the feature, we used squash to pass our modifications to the previous change. This flow is called The Squash Workflow. It’s a workflow more similar to Git, since we emulate the idea of having a kind of "staging" before we finalize a change.

So we use the parent commit as the commit that will actually receive the changes we made in the working copy.

Additionally, there’s another way to do squash if you’re following your workflow in VisualJJ as well. Open the extension tab and see that there’s a button that lets us squash from its interface:

visual_jj_squash

You can also choose specific files to squash. So jj squash script.js works. The command jj squash -i will let you use your default text editor to choose which files to include in the squash! Try modifying more than one file and test it in your terminal!

But what if I want to get rid of what I’ve done so far in my working copy? The command for that is jj abandon. It will remove all the changes made, so be careful.

Now, let’s explore how to create changes before the current one since non-linear workflow constantly happens when we deal with versioning. Let’s see how JJ handles these cases:

First, check if you’re in an empty change, add a description to it related to logging, like "Hello, world!". When you’re satisfied with the code you made, create a new change with jj new.

Below, I create a new change after having created a small function that logs the desired phrase:

edit_workflow

Now, "Printing hello world" has become the parent commit.

If we check our VisualJJ, we see:

jj_before

But what if we want to add some comments to the code before the current change? It’s simple! We can run: jj new -B @ -m "comments description here!".

See below:

❯ jj new -B @ -m "Even more comments"
Rebased 1 descendant commits
Working copy (@) now at: pyrpxqlz 238e34f4 (empty) Even more comments 
Parent commit (@-) : lowkruxs aa3e6872 Printing hello world
❯ jj new -m "Documentation - comments"
Working copy (@) now at: wqylpmwv bef37a76 (empty) Documentation - comments 
Parent commit (@-) : pyrpxqlz f1d8cba6 Even more comments

Above, we see that a rebase happens automatically when we create a change before the working copy: "Rebased 1 descendant commits".

That action will always succeed because of how Jujutsu handles conflicts and rebases. In JJ, conflicts are part of the history, they’re stored and can be resolved later, as mentioned in previous paragraphs. However, in our simple case, there were no conflicts. We’ll see more about conflicts and rebases in part 2!

Also, in the image above, I started a new change for documentation now. But what if we need to do some refactoring before writing our docs? For example, maybe we want to remove some unused functions. Let’s use the before flag "-B" again and create a change before the working copy:

❯ jj new -B @ -m "Removal of old functions"
Rebased 1 descendant commits 
Working copy (@) now at: ylmyvtts eb701ce9 (empty) Removal of old functions 
Parent commit (@-) : pyrpxqlz f1d8cba6 Even more comments

Let’s see how our workflow looks in VisualJJ:

jj_before_2

Nice! What we did is usually called "The Edit Workflow", which focus on creating new changes before the main one when a big task needs to be broken in smaller features, for example.

You can choose the flow that better fits your project, of course. Both Edit Workflow and Squash Workflow are useful.

Now, let’s analyze how we create bookmarks in Jujutsu. And remember that bookmarks are basically different commits with the same parent. Observe the small workflow below:

small_bookmark_flow

You may have noticed that more digits started to have the highlight color in the commit and change ids. This just helps us understand that there’s more than one with the same starting digit, for example. In the case above, we created a new change from wqy if we had only typed wq, there would be two similar id options since we have wqlpytzw and wqylpmwv, see below:

❯ jj new wq
Error: Change ID prefix `wq` is ambiguous

Okay, let’s check our workflow now:

visual_jj_bookmark

Cool! We created our first bookmark. That lets us work on a feature while the project can continue growing. You can jump to different commits with jj edit id and explore creating more and more bookmarks! Remember that you won’t be able to run jj edit zzz since it’s immutable and is the result of a revset:

❯ jj edit zzz
Error: The root commit 000000000000 is immutable

However, with these initial commands, you’re already able to work with JJ in small study projects and experiment with its commands!

Next steps

We saw that JJ is at least a tool worth studying and experimenting with. Even if your clients’ real projects don’t use it officially, you can test it! That way, the ease of using Jujutsu can become clearer as you use it with more repositories.

Since this tool is in its early stages, new interesting features will still be added, making the learning curve more organic when you start early.

But we’ve only seen the initial concepts of JJ here! They’re great commands to start your studies and create small repositories. In the next blog, post we’ll analyze some more complex commands. That way, we’ll be able to improve our use of JJ and expand our knowledge about the overall behavior of version control tools.

References

We want to work with you. Check out our Services page!

Iasmim Cristina

Frontend developer and UX enthusiast. Passionate about expanding my knowledge through projects, tools, and broad perspectives.

View all posts by Iasmim Cristina →