Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

In praise of human beings

This book has been written by a human (hello, I'm Arialdo, a pleasure), not AI, because I respect your intelligence.

It aims to be:

  • Essential, because your time is precious.
  • Practical, because "less talk, more action" is always 👌
  • Incremental, because you want to use jj from day 0.
  • No hand-holding, because I'd rather you discover than be told.
  • Unorthodox, because Jujutsu is extravagant, and who likes boring tutorials?

It is free (as in beer), because I am merely giving back what I was given. Also, because I love beer.

If you still want to pay some bucks for it, please, go make a donation to the charity of your choice. This will make me happy.

I am no authority, just a fellow student. I wrote this book the way I wish someone had explained Jujutsu to me.


English is not my native language. If you want to help, PRs are welcome!

What's This Book About

In your affair with Git, do you recognize yourself in any of these?

  • "Detached HEAD" makes you anxious.
  • You're not sure of the difference between git reset --soft, --mixed, and --hard.
  • You rebase with your fingers crossed. Or you don't dare, too hard.
  • You know the reflog exists, but it's an unexplored land.
  • When you make a mess, you don't know how to get back.

Or maybe you are on the other side, you've mastered Git and you are intrigued by the extravagant ideas:

  • Of working on multiple branches simultaneously.
  • Of rebasing interactively in real-time, as you type.
  • Of sending changes to past and future commits.

If either Git, deep down, scares you a little, or if you've already hit its ceiling and want to know what comes next, Jujutsu might change your relationship with version control.

What's Jujutsu?

Jujutsu is an open source Versioning Control System, like Git.

Git is amazing. But somehow it makes its model your problem. Jujutsu, on the other hand, has a ridiculously simple model that you'll pick up in a breeze. Despite being simpler, it lets you do things you didn't know were possible with Git.

Best of all: Jujutsu is 100% compatible with Git. If your colleagues keep using Git, they won't notice you are using Jujutsu. You too can keep invoking either git or jj commands in the same project.

So, you don't need to switch. Your current faith is perfectly fine, no conversion is necessary.

Is This Book For You?

This book is for two kinds of readers who couldn't be more different.

The first is the Git Wizard: the one who does interactive rebases blindfolded and bisects in their sleep. Who is still hungry for more power and will be delighted to find it in Jujutsu.

The second is the Scared Beginner: the person who freezes at a Git conflict and secretly hopes nobody asks them to rebase anything. And who will be happy to finally find autonomy.

This contradiction stems from one of the most surprising traits of Jujutsu: it is almost impossibly simple and yet spectacularly powerful. It's Dr. Jekyll and Mr. Hyde collaborating amiably in the same lab: a miracle of engineering design.

I hope this book appeals to the experts, inspiring them to learn, experiment and invent new reckless workflows; and that it gives the frightened ones the joy of finally versioning their projects on their own, with confidence.

Finally: this book, just like Jujutsu, surely isn't for everyone. It's for the Git Wizards and the Scared Beginners who share one thing: enough curiosity and enough bravery to step outside the box and to try something niche.

Quick Start

1. Setup

Install Jujutsu. Then, clone the sample repository:

jj git clone https://codeberg.org/arialdo/jj-quick-start.git
cd jj-quick-start

Track all the remote branches:

jj bookmark track "*" --remote origin

2. Git Compatible

This is an ordinary Git repo. You can mix Jujutsu's and Git's commands:

$ git log --oneline --graph --all
* f3d91ac
* 8dda056 (HEAD)
| * f0218b5
| * fa832fc (origin/store, store) Add `done` command
| * c495748 Store tasks in ~/.taskr.json
|/
| * beea00e (origin/dev, dev) WIP: parse --due dates
| * 6b0b88b Add LICENSE
| * 403d415 Sort tasks by priority
| * b6107c0 debug: dump args to stderr
| * b58bf2f Add serach by tag
| * 068609d Add --priority flag
|/
* 06acab9 (origin/main, main) taskr app

The project is a short todo list in Python. We don't care what it does. We will mainly focus on the shape of its history.

3. jj log

Log the history tree with Jujutsu:

jj log
The history
tree of the Git repository displaying 2 branches

I prefer a more compact template:

jj log -T builtin_log_oneline
The history
tree of the Git repository displaying 2 branches

Bullet shapes have the following meaning:

SymbolMeaning
Ordinary commits. Consider them editable.
Immutable commits that belong to the remote trunk. Git lets you play with them, then it will complain, too late, rejecting your next push. Jujutsu stops you beforehand.
@Your working copy.

Commits have 2 identifiers:

  • One on the right, such as fa832fce.
    This is the ordinary SHA1. Rebase or amend a commit, the SHA-1 will change.
  • One on the left, such as spkmxmnw.
    This is the Change ID. This identity stays the same no matter the operations you perform.

Notice how Jujutsu highlights the few characters that suffice to unambiguously reference a commit.

4. Commit

@ is both your working copy and a real commit. When you edit files, you also modify the commit itself. Editing the repository directly might sound scary, but don't worry: it's a feature, not a bug. You will learn in The Squash Workflow and Dispatching Edits from Megamerges how to leverage this super-power.

One surprising consequence: in Jujutsu, you typically commit before you code. Here's how it works.
Describe beforehand your work:

jj describe -m "docs: prove I have feelings"

Create a new empty commit:

jj new

Log all current commit's ancestors:

jj log -r ::@
History
tree showing 2 stacked empty commits

Do you see the 2 empty commits on the top? Think of them like the working copy and the index, in Git.
Now, code:

$ echo "Written with ❤️" >> README.md
$ jj st
Working copy changes:
M README.md
Working copy  (@) : qywvxquo f338a3de (no description set)
Parent commit (@-): plxwmqmu c9ec1ea2 (empty) docs: prove I have feelings

As you see, your changes are already part of the commit. Jujutsu has no equivalent to git add.
Finish the session by sending your changes 1 commit back, where they belong:

jj squash

Good, your change landed in nm and you are now in a fresh, empty commit.

5. The Typo In The Old Commit Message

Look at this commit:

History
tree showing a commit with a typo in its description

There's a typo in the description. Before, you have used jj description (desc for short) to describe the current commit. You can use the same command with past commits too:

$ jj desc -r w -m "Add search by tag"
Rebased 4 descendant commits
Working copy (@) now at: ovxurmlk 88f2c0d6 (no description set)

Fixed. Notice the message

Rebased 4 descendant commits

As you manipulate commits, Jujutsu performs the needed rebases on the fly. Basically, you can treat all the commits as mutable, no matter where they are.
Notice how the SHA-1 values changed, while the Change IDs are still the same.

6. Rebase

Say you want to rebase dev on top of store:

jj rebase -b dev -o store

Jujutsu replies with

Rebased 6 commits to destination

But then it adds:

New conflicts appeared in 5 commits:
  nttpknso 219791e0 dev* | (conflict) WIP: parse --due dates
  lwzqwkyz 83c89c2d (conflict) Add LICENSE
  unztuwko ee801a30 (conflict) Sort tasks by priority
  kvnpnqnw ed1c4a78 (conflict) debug: dump args to stderr
  wywpzopw 2ffce574 (conflict) Add search by tag
Hint: To resolve the conflicts, start by creating a commit on top of
the first conflicted commit:
  jj new wywpzopw
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.

Gosh, conflicts!

Two things to notice:

  • First, you rebased a branch while you were somewhere else. Git doesn't let you do that.
  • Second, interestingly Jujutsu did not stop because of conflicts: it completed the rebase nevertheless, and now it shows where the conflicts are. You can keep working and resolve the conflicts when you prefer. You'll find plenty of information and tricks in the chapter Conflicts.

Should you want to undo the rebase, you could run jj undo: it's a passe-partout command that undoes whatever operation you run, no exceptions. Give it a try:

jj undo

The rebase is gone, and so are the conflicts. You could keep running jj undo until you are back to the very moment you cloned the repository.
Now try to undo the undo:

jj redo

You are back to the future, to the conflicted state.

You can go back and forth in the operation history with jj undo, jj redo and other powerful sub-commands under the jj op command. Think of them as Git's reflog on steroids. You will read about them in It's History All The Way Down.

7. Rebase a Conflicted Branch

On second thought, you wanted to rebase store on top of dev, not the other way around. So, you need to move the commits from kk to s on top of n / dev:

Run:

jj rebase -r 'kk::s' -o dev

Much better, only 1 conflict.
Time to resolve the conflict. You move on top of the conflicted commit s:

jj new s

You'd better fix it later, though: your boss is yelling at you about some SUPER URGENT tasks.

8. Amending a Commit From a Distance

"RED ALERT!", "your boss panics "there's a bug! DROP EVERYTHING, you MUST fix it NOW!"
It's about this code:

if len(args) >= 2 and args[0] == "--priority":
    priority = int(args[1])
    args = args[2:]

Nothing alarming, you think: you get an exception if args[1] isn't a number. You just need to add error handling around it. Which commit introduced the expression int(args[1])?

$ jj log -r 'diff_lines(substring:"int(args[1])")'
○  rpzkmwvr arialdo 2026-07-16 09:54:27 068609d1 Add --priority flag
│
~

Here's the culprit.
diff_lines(substring:"int(args[1])") is an example of a Revset: an expression in a powerful (yet intuitive) purely functional language for selecting commits. In Git, revision selection is a pile of non-composable fragments (such as HEAD~3, main..feature, --author=alice, -S"int(args[1])") each available only where it was bolted on (e.g., git log takes -S, git rebase doesn't). In Jujutsu it's one single, consistent language that every command shares.

Anyway, look! The bug hasn't been introduced by your current commit, but 5 commits down the stack. No problem, you'll fix it from a distance.

Wait, what? You are in a conflicted commit! Can you really code while there are pending conflicts? Yes, you can: after all, the bug fix is about r, a conflict-free commit.

So, amend cmd_add in commands.py from your current commit:

def cmd_add(args):
    priority = 3
    if len(args) >= 2 and args[0] == "--priority":
        try:
            priority = int(args[1])
        except ValueError:
            print("priority must be a number", file=sys.stderr)
            return 2
        args = args[2:]
    due = None
    ...

Review it, then send it five commits down:

$ jj squash --into r
Rebased 7 descendant commits
Working copy  (@) now at: wmlyyqmm ef6c08d3 (conflict) (empty) (no description set)
Parent commit (@-)      : spkmxmnw e4538e55 store* | (conflict) Add `done` command

Cool, your fix landed in r. Seven commits, including your working copy, rebased on top of it. Boss (slightly) happier.
Jujutsu reminds you that you still have conflicts in s:

Warning: There are unresolved conflicts at these paths:
taskr.py    2-sided conflict
Existing conflicts were resolved or abandoned from 1 commits.
There are still unresolved conflicts in rebased descendants.
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
  jj new spkmxmnw
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.

9. Amending The Last Commit

Your boss (who else?) pushes for dev to be completed. The code in n is unfinished:

It parses --due but never displays it, which is probably why it still says WIP. "FINISH IT!", boss commands. Yes, Sir!
The commit dev lays right in the middle of a chain of commits, but that's not a problem, it's always the same pattern:

$ jj new TARGET   # create a working staging area.
$ emacs FILE      # write code (choose your editor wisely, no pressure)
$ jj diff         # review what you did.
$ jj squash       # incorporate in the commit.

So:

jj new dev

Change cmd_list in commands.py to:

def cmd_list(args):
    tasks = load()
    if not tasks:
        print("no tasks")
        return 0
    ordered = sorted(tasks, key=lambda t: t.get("priority", 3))
    for n, task in enumerate(ordered, 1):
        suffix = " (due %s)" % task["due"] if task.get("due") else ""
        print("%2d. [%d] %s%s" % (n, task.get("priority", 3), task["text"], suffix))
    return 0

Review your change before amending the commit:

$ jj diff
Modified regular file commands.py:
    ...
  47   47:         return 0
  48   48:     ordered = sorted(tasks, key=lambda t: t.get("priority", 3))
  49   49:     for n, task in enumerate(ordered, 1):
  50   50:         suffix = " (due %s)" % task["due"] if task.get("due") else ""
  50   51:         print("%2d. [%d] %s%s" % (n, task.get("priority", 3), task["text"], suffix))
  51   52:     return 0
  52   53:
  53   54:
   ...

Fine. Squash your work to n, and amend the description:

$ jj squash -m "Parse --due dates"
Working copy (@) now at: ovxurmlk 3e81b7a4 (empty) (no description set)
Parent commit (@-)      : qpvuwxyt 2c5f9e13 Parse --due dates

You'll read about this workflow in Don't Edit.

10. Throwing a Commit Away

You are ready to resolve the conflict when you notice the commit kv:

What is it about?

jj show kv

Ha! A stray troubleshooting print. The whole commit can be completely abandoned:

$ jj abandon kv
Abandoned commit vkrysnwp 2a9f0c15 debug: dump args to stderr
Rebased 4 descendant commits onto parent of abandoned commit
Working copy (@) now at: pzrtklvq 4a7c1d92 (empty) (no description set)

Looks like your boss isn't the only reason you've been putting off that conflict resolution, eh?

11. Resolve The Conflict

Where was the conflict, by the way?

$ jj log -r 'conflicts()'
×  spkmxmnw arialdo 2026-07-16 11:12:20 store* 07aa62de (conflict) Add `done` command
│
~

Let's go there:

$ jj new sp
$ jj resolve --list
taskr.py    2-sided conflict
$ jj resolve --tool meld
A conflict on 1
line of Python code displayed in meld, as a 3-way merge

Both sides want their command in the list. Both should succeed, so you can replace the conflict with:

COMMANDS = ["add", "done", "list", "search"]
$ jj st
Working copy changes:
M taskr.py
Working copy  (@) : pputmlzu 3474f499 (no description set)
Parent commit (@-): spkmxmnw 07aa62de store* | (conflict) Add `done` command
Hint: Conflict in parent commit has been resolved in working copy

Good, no more conflicts.

jj squash

All clear!

12. Moving License

Look at this commit:

It seems that the license file was added too late, only when somebody noticed it was missing. Move it where it belongs, before commit v. It should have been there from the start:

jj rebase -r l --before v

Uh oh! It's time for Jujutsu to whine:

Error: Commit 06acab9454ba is immutable
Hint: Could not modify commit: vmztuwwu 06acab94 main | taskr app
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
      - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
      - `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.

Good boy! Jujutsu saves you before the damage is done! Git would let you rebase and surprise you with a: "Hello, rejected push!" later.

If you really insist, you can always use --ignore-immutable:

jj rebase -r l --before v --ignore-immutable

Jujutsu makes it clear that the local main and the remote main diverge.

13. And Git?

$ git log --oneline --graph
git log --oneline --graph
* 5fbe696 (HEAD, store) Add `done` command
* 7123075 Store tasks in ~/.taskr.json
* 83e3f8f (dev) Parse --due dates
* 5691e5a Sort tasks by priority
* 038d354 Add search by tag
* cbc233b Add --priority flag
* 8679eed (main) taskr app
* 18ca92f Add LICENSE

It's still a 100% Git compatible repository. Your teammates won't even notice that you are playing with your brand new toy.

What's not to love?

Why?

Many people I know don't have much motivation to give Jujutsu a try because, they say, "Git works just fine". Fair enough! I assume that you are reading this book because you are curious to see what Jujutsu brings to the table.

A natural question to address is why Jujutsu exists and what problems it solves.
Spoiler alert: the funniest part is that it solves problems that you didn't know you had1.

So, the first question is: does Git really work as well as we think?

Isn't Git just enough?

A scenario I like to present in job interviews is: pick your favourite technology and imagine you had a magic wand. What are the 3 things that bother you most and you would change?

Without questioning that Git is a masterpiece of engineering (I'm myself a huge fan!), can we agree on 3 things that could be improved? Try answering this on your own.

Here's my take.

1. Git has too many building blocks

Consequently, some find it difficult.

  • Some people find the index a bit mysterious.
  • detached HEAD stresses them out.
  • They are not sure when to git reset with either --soft, --mixed or --hard.
  • Or when to use --staged with git restore.
  • When they mess up, they panic and struggle to undo their changes.

Could Git be simpler, without sacrificing its power?
With my magic wand I would summon a smarter model, based on far fewer, more intuitive elements designed to be composable. The result: something smaller yet more powerful than Git itself.

Jujutsu does this magic. With a tiny fraction of building blocks, it makes those operations that were intimidating or even impossible with Git straightforward.

2. Git's interface is not always consistent

"How can I view a list of all tags?"

"git tag", replied Master Git.

"How can I view a list of all remotes?"

"git remote -v", replied Master Git.

"How can I view a list of all branches?"

"git branch -a", replied Master Git.

"And how can I view the current branch?"

"git rev-parse --abbrev-ref HEAD", replied Master Git.

Steve Losh - Git Koans

My preferred example of inconsistency is the index, which for many Git novices is a source of confusion. It's the staging area where you forge the next commit. Pro Git calls it the "proposed next commit snapshot". Is it the next commit? Not quite. In fact, it is a quasi-commit.

  • It has a SHA1 checksum (tail -c 20 .git/index | xxd -p), but it's not its address. A branch cannot target it.
  • It's not a pointer to a tree-object like a real commit. So, you can't use git show to inspect its content. You have to use git diff --cached.
  • If you want to move a commit around, you use git rebase. If you want to move the index, you need git stash.

Index is a special case, so it needs special commands, completely inconsistent with the commands for real commits.

Magic wand, let me turn the index into a real commit, with all its power. Presto! All the commands involving the index and commits become consistent with each other. Well, actually, they collapse to the same commands.

API consistency results from the absence of special cases, and Jujutsu embraces this idea: it removes all the special cases. You still want to stash changes, don't you? But you don't want to have special stash pop, stash drop, stash apply commands: you want to reuse the building blocks you have already learned. Welcome, composition.

3. It's a leaky abstraction

Git feels hard at first, but it stops fighting you once you learn a bit of its internals.

Why doesn't git diff show staged files? How do you use reset and checkout to restore changes and start over? How do you undo a rebase? The answers click once you know a few implementation details such as:

  • Branches are mere pointers.
  • Git keeps the working directory, the index and the repository as three separate areas.
  • Commits are immutable, and any operation manipulating the history will just create new ones.

When people struggle with Git, it's often because they have built a wrong intuition about how it internally works. But, is this their fault? I don't think so.

If I had a magic wand, I'd ask for a tool with a truly opaque abstraction, completely hiding its internals, one that captures what I mean, the what, quietly handling the low-level mechanics, the how.

To see what I mean, take the example of "undoing stuff". You can always consult the Git reflog, check what you did and figure out which low-level operation you need to revert a change.

  • You did git rebase -i HEAD~2. To undo, git reset --hard ORIG_HEAD
  • Did a git stash apply? You need git stash drop.
  • Did a git tag? You need git tag -d
  • Did a git add? You need git restore --staged <file>
  • Did a git commit --amend? You need git reset --soft HEAD@{1}

That is: you need to derive the inverse operation for the command you want to undo, and this often requires knowing Git's internals.

What's the equivalent in jj? jj undo. Want to undo a fetch? jj undo. Want to undo a rebase, a merge, a commit, a conflict resolution? jj undo, always jj undo. It's consistently jj undo, no matter what change you made. jj undo is one of the Jujutsu killer features. I promise, you will wonder how you could have worked for so many years without undo. We'll see this in detail later.

First, let me show you an example with deeper consequences. We will talk about a well-designed piece of Git and you will learn your second Jujutsu command.


  1. This reminds me of The Blub Paradox (Paul Graham - Beating the Averages). "[People] are satisfied with whatever language they happen to use, because it dictates the way they think about programs.".
    That is: until you've used more powerful tools, you don't experience their advantages as missing features of your current tool. In a sense, you can't miss what you've never had.

Amending

You just found a typo in your last commit. How do you fix it?

A---B---C          <- main (HEAD)

Of course:

git commit --amend

Voilà, and the last commit is changed!

A---B---C'         <- main (HEAD)

This is what I call a beautiful abstraction! We all know that commits are immutable and that git commit --amend is no exception. Indeed, amend did not really change any commit: under the hood, it created a new commit with the same parent as the commit to fix.


A---B---C       <- main (HEAD)
     \
      C'        <- new, without any ref

Then, it reset the current branch ref to it:

        C
       /
A---B
       \
        C'      <- main (HEAD)

The old commit C is still there but it's hidden, so you only see:

A---B---C'      <- main (HEAD)

Git amend switched commits, pulling a fast one on you, giving you the impression that the last commit was editable. And this is the key: abstractions are all about providing a simplified view of the world, where details are kept out of sight.

Under the hood, git commit --amend is equivalent to something like:

git reset --hard HEAD^
git cherry-pick -n ORIG_HEAD
git add .
git commit -C ORIG_HEAD

But you don't need to know. The beauty of Git amend's abstraction is that it captures your intention, and it transparently deals with the internal mechanics.

Neat!

Leaky abstraction

How do you amend the second to last commit? Oops, there's no git commit --amend HEAD~1. Instead, you have to do:

git commit --fixup=HEAD~1
git rebase -i --autosquash HEAD~3

or:

git checkout HEAD~1
git add .
git commit --amend
git cherry-pick main
git branch -f main HEAD
git checkout main

Intimidating... The wonderful surface of abstraction has torn, and underneath you can see all the gears! It's no surprise that few people bother amending the penultimate commit.

What about amending Y?

A---B---C---D---E---F
     \         /
      X---Y---Z

Oh beautiful abstraction, where art thou?

More consistent abstraction

Can we do better? Although internally commits are immutable, when you amend your intention is to edit a commit. In other words, the metaphor underlying Git amend's abstraction is:

The last commit is editable.

Can we embrace this metaphor and make it universal? Doing so, one might conceive a command jj edit to modify any point of the history tree:

A---B---C          <- main (HEAD)

Want to amend C?

jj edit C

Want to amend B?

jj edit B

What if you had:

A---B---C---D---E---F
     \         /
      X---Y---Z

and you wanted to edit Y? You have already literally expressed your intention:

I want to edit Y.

Well, just type it!

jj edit Y

We went back to a beautiful, intuitive UX! jj edit captures your intention, the what, and deals with the necessary internal rebasing, resetting, cherry-picking, whatever, the how.

Of course editing Y implies cascading changes to Z, E and F, but what's the point of giving step-by-step instructions to your versioning system?

In practice

I promised this tutorial would be down-to-earth. So:

  • Install Jujutsu.
  • Crack open a shell.
  • cd to your Git repository.
  • jj git init. No worries, it's safe. Jujutsu happily lives together with Git. You don't need to switch to Jujutsu.
  • Keep working. Keep using Git.
  • Need to amend the penultimate commit or a commit deep down the history tree, and you struggle to do this with Git?
jj edit <SHA1>
# Do your changes
# Then switch back to where you were
git checkout <your-branch>

I'm lying a bit. You might encounter conflicts or other situations you'd better get prepared for. So, better keep reading just a bit more.

I hope you just had your first a-ha moment.

Rebasing with Git

If I could use the magic wand for the 4th time, I would improve how git rebase works.

I do love workflows based on rebase and, coming from CVS and Subversion, I still think rebase is one of the killer features of Git. But I cannot help thinking git rebase is broken. That's a bold claim. Let me unpack it.

Conflicts

Rebase is fantastic until it's not. Think about it. Git is designed to be very safe and conservative:

  • It treats your filesystem as sacred. Unless you explicitly tell it to git add files, it diligently keeps its hands off them. It won't touch your files unless you say so.
  • Internally, it is strictly append-only. It never deletes info. No matter the operation you perform, it's never destructive; there's always a way to revert what you did.

This behavior instills serenity. You can work relaxed; your code is in trusted hands. You can happily jump back and forth in your history, move files left and right: Git is always there, solid and stable, to support your needs.

Danger Mode

Until you rebase and there's a conflict. Then Git enters danger mode. It basically stops working, as if it's in panic, and offers you only 2 ways to exit:

  • Either you git rebase --abort.
  • Or you solve the conflicts, now!, and in the exact order it tells you.

You cannot do anything else. It does not even know, let alone tell you, how many conflicts you'll face. It just stopped at the first conflicted commit and handed it over to you.

While in this panic mode, Git does not work. You cannot jump back and forth in your history anymore. If you think that visiting another commit would help you fix the code, forget it: Git won't let you. You cannot cherry-pick, you cannot bisect. What if you have an urgency (like a hotfix to release) while you are struggling with a difficult conflict resolution? Can you pause the rebase, work, and recover your work later? Nope.

Your precious project does not work anymore, and Git quit supporting you exactly when you needed its help the most.

I know it's less dramatic than this; there are always ways to recover. No wonder many people avoid rebasing and stick with the more comforting merge.

Can we de-escalate this?

If I could ask for the moon, I would like a tool that:

  • Tells me at once all the conflicts a rebase will encounter.
  • Does not stop working just because of conflicts.
  • Lets me keep working on important stuff, postponing the conflict resolution.
  • Lets me solve conflicts both by editing files and also via whatever feature may help.

Jujutsu is that tool. You will see in the next pages: conflicts are first-class citizens and can be manipulated in far more convenient ways than just editing a file in an emergency. No stress, no drama. Jujutsu keeps working during conflicts, with all its features available.

And you will use them! Because it opens some interesting scenarios that are hardly conceivable with Git. Have you ever considered solving conflicts by editing a commit other than the ones being rebased? Or by performing another rebase (a rebase within a rebase!).

It's much simpler than it sounds. You'll see in a few pages.

In practice

Here's an experiment you can try. Next time you need to rebase a branch, make a copy of your repo (better safe than sorry, as long as you are a jj-newbie), jj git init it then try:

jj rebase -s <FROM> -o <TO>

where:

  • <FROM> where to cut.
  • <TO> where to paste.

For example, say you want to move the branch 3-4-5-6 on top of 11:

jj log

○  11          <---- paste
○  10
○  9
│ ○  8
│ ○  7
├─╯
│ ○  6
│ ○  5
│ ○  4
│ ○  3         <---- cut
├─╯
○  2
○  1

Then:

jj rebase -s 3 -o 11

will get you to:

○  6
○  5
○  4
○  3
○  11          <---- paste
○  10
○  9
│ ○  8
│ ○  7
├─╯
○  2
○  1

With the same ease you can select other cut/paste points, even in the middle of the history tree:

○  11
○  10          <---- paste
○  9
│ ○  8
│ ○  7
├─╯
│ ○  6
│ ○  5
│ ○  4         <---- cut
│ ○  3
├─╯
○  2
○  1

jj rebase -s 4 -o 10

○  11
│ ○  6
│ ○  5
│ ○  4
├─╯
○  10           <---- paste
○  9
│ ○  8
│ ○  7
├─╯
│ ○  3
├─╯
○  2
○  1

And if there are conflicts? jj log will tell you where.

○  11
│ ×  6           <---- conflict
│ ×  5           <---- conflict
│ ○  4
├─╯
○  10
○  9
│ ○  8
│ ○  7
├─╯
│ ○  3
├─╯
○  2
○  1

Then you can fix the conflicts where they are:

jj edit 5

It's likely that fixing the conflict in 5 will propagate the resolution to 6. Otherwise, you can continue with:

jj edit 6

In any case, you can count on jj undo.

In practice though, you will probably encounter confusing scenarios and need a few more tools.

So, my recommendation: play with jj log, jj rebase and jj undo in a safe copy of a real repository but, please, don't stop reading here; there are a few other things you will love to learn!

Log and Identity

Getting back to Git amend: although I believe it is a very nice abstraction, I still see a little leaky abstraction. See this:

A---B---C          <- main (HEAD)
git log --oneline
  ddb8cf5 (main) Publish shell script

echo "fix typo" >> myfile

git commit -a --amend --reuse-message=HEAD

 git log --oneline
  bea2735 (main) Publish shell script
A---B---C'          <- main (HEAD)

Within my metaphor, I modified the last commit. Yet git log insists on screaming at me:

"Hey! That's not the same commit! It's a new one! It was ddb8cf5 before, it's bea2735 now, completely different identity!"

Do I really need this constant reminder? Must Git always be so pedantic?

When I'm writing code and collaborating with colleagues, I think at a level above SHA1s. The meaning of "the last commit" is defined by how my team and I refer to it. And honestly, we always call it "the last commit", never bea2735. Can't we just happily stay in our sweet metaphor?

A good abstraction hides what you don't need to see. Git is already willing to hide the old commit. Why not hide the change of identity too?

Immutable identity

See how Jujutsu behaves:

jj log

  @  m arialdo@ik.me 2026-06-06 15:55:54 2caa192
  │  (empty) (no description set)
  ○  v arialdo@ik.me 2026-06-06 15:55:04 ddb8cf5
  │  Log and Identity
  ○  ym arialdo@ik.me 2026-06-06 15:29:39 429aa18
  │  Publish shell script
  ...

Focus on the line:

  ○  v arialdo@ik.me 2026-06-06 15:55:04 ddb8cf5

The rightmost ddb8cf5 is the Git Commit ID, a SHA1. Also notice the v on the left. This is the Change ID, an immutable reference to what you intuitively call "the last commit".

Let's Git amend, Jujutsu style:

jj edit v
echo "fix typo" >> myfile

jj log

  ○  v arialdo@ik.me 2026-06-06 15:55:04 bea2735
  │  Log and Identity
  ○  ym arialdo@ik.me 2026-06-06 15:29:39 429aa18
  │  Publish shell script
  ...

Interesting! While the Git SHA1 changed (you expected that), the Change ID did not. You can keep calling that Change v. It will always be v. Change IDs are 128-bit, random numbers, immutable and independent of the content, so they remain stable over time.

As small as this sounds, that's a huge turning point in the way you interact with your history. It's the entry point of a rabbit hole where you manipulate history in terms of what and not in terms of how. You will see tons of similar examples while using Jujutsu.

If you think about it: Git does something similar with branches. After an amend, main points to a different commit, yet everyone still calls it main. Jujutsu brings this idea to all commits.

Reverse Hex

Won't Commit IDs and Change IDs clash? Not at all. Git's Commit IDs are rendered in hexadecimal, with digits 0 to 9 and letters A to F. Change IDs such as v and ym are rendered in reverse hexadecimal, using letters from Z to K. They never overlap: a Change ID can never be confused with a Commit ID.

Smart, isn't it?

Show me less

Have you noticed that most characters of the IDs in jj log are grayed out?

output of jj log
where change ids and Change IDs have most of their last chars grayed
out

Internally, Commit IDs are SHA1s, 160-bit numbers represented with 40-char in hexadecimal. By default Git only displays 7 characters. Why exactly 7? Because it's usually enough; there is no ambiguity. In fact, often Git could use even less. But it doesn't try to optimize.

Jujutsu goes a step further. If it realizes that 2 chars are enough, it highlights 2 chars only. 1 is enough? It uses 1 only. Why display unnecessary information?

In fact, you can even hide it completely.

Take it to the limit

By default Jujutsu:

  • Still displays the Git Commit ID.
  • Highlights the strictly necessary characters, graying out the other ones.
  • Displays 2 lines per commit.

When something is Good™, it deserves to be taken to the next level. So:

  • I just hide the Git Commit ID. It's rarely used and it can be displayed on demand.
  • If a Change can be unambiguously identified with w, why highlight it in wnyxkzzm instead of just displaying w? If something is unnecessary, remove it.
  • 2 lines per commit waste valuable screen space.

Instead of:

  @  xlptstlm arialdo@ik.me 2026-06-06 17:18:46 039a04d
  │  (no description set)
  ◆  poqzxkvk arialdo@ik.me 2026-06-06 16:32:39 main 9376594
  │  Review of identity
  ~  (elided revisions)
  │ ○  zzmuxzrl arialdo@ik.me 2026-06-06 16:32:45 pages 29b4782
  │ │  typo: need -> needs
  │ ○  wuwotnsy arialdo@ik.me 2026-06-06 16:23:08 e655e34
  │ │  Review of Why
  │ ○  puvzpnwv arialdo@ik.me 2026-06-06 15:19:09 eae4a1f
  │ │  PRs are welcome
  │ ○  zzsynxzk arialdo@ik.me 2026-06-06 14:26:10 3e3cf7b
  │ │  Publish script
  │ ○  yroskows arialdo@ik.me 2026-06-06 14:08:55 af28aa37
  ├─╯  init pages branch
  ◆  zzz root() 0

I prefer:

  @  x
  ◆  po   🔒  Review of identity main
  ~  (elided revisions)
  │ ○  zzm      typo: need -> needs pages
  │ ○  w        Review of Why
  │ ○  pu       PRs are welcome
  │ ○  zzs      Publish script
  │ ○  y        init pages branch
  ├─╯
  ◆  zzz  🔒  empty

I will use this template from now on. You won't regret using it too. Read in Human Friendly Log how to configure your Jujutsu.

In practice

You've learned that Jujutsu and Git happily co-exist.

  • Run jj git init in any Git repo you like.
  • Git won't even notice: Jujutsu lives in an ignored .jj directory.
  • You can keep working with Git as usual.
  • In a sense, you can consider Jujutsu a Git client on steroids.
  • Try using jj log. It's harmless and often a better option than git log, because it tends to show only the information you need, hiding the inessential.
  • You will soon learn the Revset Language, then jj log will shine and you will probably never use git log anymore.

Let's Dive Into The Rabbit Hole

You may not have noticed, but during the "Jujutsu style amend" above, we glossed over some important details. Such as: we never committed. How could that work?

Time to find out. And time to stop talking about what can be improved, and to start learning Jujutsu for real.

Shopping list

Here's my formula to learn Jujutsu:

  1. Clear the slate, stop having Git as your mental model.
  2. Embrace a few new assumptions. They'll seem repugnant at first. Try to suspend judgment. They'll pay off.
  3. Learn the few basic building blocks. It's super easy, it's no more than 5 commands.
  4. Profit.

After this, you will:

  1. Be astounded by the new workflows you never thought were possible. Learn them, invent new ones.

I trust you with step 1. Let's go with 2. I'm going to tell you about a few traits of Jujutsu that will make you cringe.

Git Was An Adorable Maverick

Being an old programmer and coming from CVS and Subversion, I clearly remember when Git was presented. It really sparked a chorus of angry protests:

  • "Keeping a local copy of whole history! This is so stupid!"
  • "Branches are not real branches!"
  • "Rewriting the history! Heresy! To the stake!"
  • "No central server! The world will collapse into anarchy!"

Looking back, it was a fun show. I just never expected a sequel.

Jujutsu Is Being a Maverick Like Git

Better to hear all the bad news now so we can move on. Let's pull this tooth:

  • Jujutsu pushes are forced by default.
  • It commits by itself, at every possible occasion.
  • Even jj status commits.
  • Rebases happen automatically.
  • By default, Git ignores files. Jujutsu tracks them.
  • Commits are mutable.
  • Working in detached HEAD is the standard.
  • With Git, first you code, then you commit. With Jujutsu it's idiomatic to commit before coding.
  • jj won't update Git branches as you commit.
  • It deletes commits without any warning.

At times it feels like Jujutsu was designed to deliberately upset Git fans (it's not like this, of course, but it's still a funny show). In the same vein, Jujutsu is defined more by what it removes than by what it adds:

  • It's branchless.
  • There's no index.
  • Stash does not exist.
  • There's no merge command.
  • No cherry-pick, no reset, no add.

Given this iconoclastic baseline, you'll be dismayed knowing that people use Jujutsu:

  • working in the middle of a branch, not at its tip.
  • Sending commits to the past.
  • Developing on multiple features, then dispatching the changes around.
  • Working simultaneously on multiple branches.

Horrified? Wait until you realize you love every bit of it.

Still with me? Good: you are already at step 3. Let's learn the building blocks and finally profit.

Elements of a Grammar

Git has 160+ commands. Some are plumbing operations, such as hash-object and write-tree. Some are porcelain commands, such as commit and log. These are built on top of the plumbing commands.

Some porcelain operations could be thought of as combinations of other porcelain operations. For example: pull is clearly fetch followed by merge/rebase. cherry-pick is diff + apply + commit. I like to think of rebase as a series of cherry-picks followed by a reset.

Here's a challenge. Setting plumbing aside (we never want to break the abstraction), what basic commands would suffice to build an entire versioning system?

Think about it, write them down.

Basic Commands

If you thought of CRUD, you nailed it!

Indeed, here are Jujutsu's basic commands. To manipulate your history tree you need to:

OperationCommand
Createnew
Readshow
Updateedit
Deleteabandon

the repository Changes.

Notice that I wrote "Changes", not "commits". You remember that when you amend a commit Git creates a different one. Jujutsu lets you keep referencing it with the same Change ID, treating it as the same thing. That thing is the Change, defined as:

"a commit as it evolves over time" — Glossary.

On top of that CRUD, you'd probably also like to:

OperationCommand
Move a Change somewhere elserebase
Move the edits you made somewhere elsesquash
Copy files from a Change to anotherrestore
Display the history treelog
List / undo / redo operationsop log / undo / redo

That's... basically it? Learn these and you will master Jujutsu.

Macro Commands

Of course, on top of these, you can have commands that are in fact combinations of other commands. Some examples are:

OperationPossible command
Split a Change in 2split
Duplicate a Changeduplicate
Juggle Changes, interactivelyarrange
Bisect (you know, the best of Git commands)bisect
Magically, distribute edits where they belongabsorb

and some other ones. But, I swear, the very first 2 sets are enough to take you way further than you've ever been with Git.

Worth a try. Open a terminal, grab your keeb and prepare to get your hands dirty.

Two Recommendations Before Typing

  • We will use throwaway repos. Yet, you will want to try some commands on real ones. For safety, the first times make a copy. Jujutsu is very powerful and mistakes can leave you far from what you expected to be.

  • We will do many exercises. Some will look artificial, not something you'd typically do at work. We are here to learn how Jujutsu thinks, pushing it to the limit.

    The contrived cases (juggling commits, breaking the history tree apart and putting it back) will help you internalize the model underneath. Then, the practical cases will become muscular memory.

Finally, Change IDs are random: when the ones in your terminal don't match mine, please, adjust your commands accordingly.

Goal

Where you create a brand new repository.
And you figure out why it already contains 2 commits.

Init

You already saw that jj git init adds Jujutsu to an existing Git repository. If there is no Git repository yet, it will create one.

Let's start from an empty repo:

$ git init

Initialized empty Git repository in ~/jj-playground/init/.git/

Enable Jujutsu in it:

$ jj git init
Initialized repo in "."
Hint: Running `git clean -xdf` will remove `.jj/`!

Multiple backends

Why isn't it just jj init? Because Jujutsu is modular and can work with different storage backends. We will stick with Git here, so never mind.

Won't jj and Git clash?

Relax. Jujutsu lives in the hidden directory .jj, which Git ignores:

$ ls -a
. .. .git .jj

$ ls -a .jj
.  ..  .gitignore  repo  working_copy

$ cat .jj/.gitignore
/*

You probably noticed the line:

Hint: Running `git clean -xdf` will remove `.jj/`!

It makes sense: that git clean command is meant for deleting ignored directories.

jj git lets you also access other Git commands with jj git clone, jj git fetch etc.

Want To Remove Jujutsu?

Delete .jj and Jujutsu is gone.

Log

Try a git log:

git log
fatal: your current branch 'main' does not have any commits yet

As expected.
What about jj log?

$ jj log

@  x           empty
◆  z       🔒   empty

Jeez! Where Git fatally errors out, Jujutsu shows not 1, but 2 Changes! How can it be?

The Origin of Time

Let's inspect z:

jj show z
Commit ID: 0000000000000000000000000000000000000000
Change ID: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
Author   : (no name set) <(no email set)> (1970-01-01 02:00:00)
Committer: (no name set) <(no email set)> (1970-01-01 02:00:00)

    (no description set)

Looks like a Null Object Pattern. Indeed zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz is the "root commit", the original empty Change, the black hole every repository in the world originates from. In Git, it's implicit and has no concrete representation, so orphaned branches need special commands and options. That's not the case with Jujutsu: zzzz is a loving parent, and every orphan has a home.

Fine, zzzz makes sense. But what about the Change on top of it?

This requires a tiny bit of theory.

Collapsing areas

One approach to understanding Git is to visualize the different areas that exist in its model, and how commands operate on them.

different areas in Git @pabloulloacastro

In Jujutsu, the stash, the index, the working area and the local repository all collapse to a single area. So, Jujutsu's model is:

different areas in Jujutsu

This means that the @ you see in:

$ jj log

@  x           empty
◆  z       🔒   empty

represents the current file system, the working copy of your project. But this is also a commit in your repo. They just happen to be the same.

Why is it already there? Well, when you run git init the file system, the empty working copy of your project, was already there. Naturally, this should already be reflected in the log.

What's inside x?

Is x similar to zzzzz? What does it contain? How can you find out?

Solution

If you guessed jj show, good job!

$ jj show x
Commit ID: cb39db5ba245a72667c7bbd055b625e9522b74f8
Change ID: xmvkoxkuvlrynmwooqtrxtxptmswruqq
Author   : Arialdo <arialdo@ik.me> (2026-06-09 16:10:17)
Committer: Arialdo <arialdo@ik.me> (2026-06-09 17:44:18)

    (no description set)

It's a real commit. It has a real SHA1 (cb39db5), and Git itself acknowledges it has an author:

$ git show cb39db5
  commit cb39db5ba245a72667c7bbd055b625e9522b74f8
  Author: Arialdo <arialdo@ik.me>
  Date:   Tue Jun 9 16:10:17 2026 +0200

It's messageless and empty, but it still looks like a legit commit.

Goal

Where you find out what it means that Jujutsu automatically commits.

Committing

Wait a sec. If @ represents the current file system:

$ jj log

@  x           empty
◆  z       🔒   empty

which is empty because you have no file in your project yet, what happens if you make any change to the disk so the project is not empty anymore? That's an excellent question, and you could already guess the answer. Try creating an empty README.md:

$ touch README.md
$ jj log

@  x
◆  z       🔒   empty

Interestingly, x is not marked as empty anymore. If you ask Git, it will detect that there is a new, untracked file:

$ git status
No commits yet

Changes not staged for commit:
        new file:   README.md

no changes added to commit.

What's Jujutsu's opinion on the status?

$ jj status
Working copy changes:
A README.md
Working copy  (@) : xmvkoxku b727840f (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)

Two things to notice:

  • Jujutsu has already added the file (A README.md). It's not untracked. Unlike Git, new files are automatically tracked. There's no equivalent of git add.
  • Easy to miss: the SHA1 has changed.

Inspect x again. Will it be empty?

Solution

Of course it's not! x must contain README.md now. In fact, x and your filesystem are one and the same: change either, and you change both.

$ jj show x

Commit ID: b727840fcbf04053c43d30b8581876260a240840
Change ID: xmvkoxkuvlrynmwooqtrxtxptmswruqq
Author   : Arialdo <arialdo@ik.me> (2026-06-09 16:10:17)
Committer: Arialdo <arialdo@ik.me> (2026-06-09 16:10:17)

    (no description set)

Added regular file README.md:
    (empty)

Notice the:

Added regular file README.md

Oh dear! Just creating a file, you have in fact performed a git commit --amend.

Here's the final challenge. How would you add some content to README.md and amend x again?

Solution

Just edit README.md!

echo "Hello, world" > README.md

$ jj show x
Commit ID: 126c51d1a7bddbd205721e64a0824233c68a56a0
Change ID: xmvkoxkuvlrynmwooqtrxtxptmswruqq
Author   : Arialdo <arialdo@ik.me> (2026-06-09 16:10:17)
Committer: Arialdo <arialdo@ik.me> (2026-06-09 16:13:12)

    (no description set)

Added regular file README.md:
        1: Hello, world

If you guessed it right, bravo! In Jujutsu the working directory and the repository are just the same area, and they are always matching. The lines:

Added regular file README.md:
        1: Hello, world

confirm that x holds README.md with its content. It's yet another SHA1: under the hood, there was another git commit --amend, while from Jujutsu's perspective, it's still the Change x. You learned that already, didn't you?

The mental model I suggest is: by editing the file system, you are directly editing the commit. We will soon see how this is somewhat dangerous and how we can protect ourselves with better workflows like the Squash Workflow.

How Many Commits Have You Created and Abandoned, Already?

Does it matter? Those dangling commits are invisible to Git anyway. If you run git log, Git still throws its hands up, as though nothing has happened yet:

$ git log
fatal: your current branch 'main' does not have any commits yet

Those commits are real, but since they are not targeted by any branch, they won't get in your way. When using Jujutsu you can keep calling that Change x and forget about the low-level minutiae. That's why my log template doesn't even bother to print the Git SHA1: most of the time, it's just irrelevant.

For Git to see your commit, it needs a branch (bookmark, in Jujutsu's lingo) targeting it. Add one with:

jj bookmark set main

No need to worry; there's a whole chapter on bookmarks ahead. All in good time!

Goal

Where you create a new commit.
Then you start a new branch line. Next, you insert commits right in the middle of existing branches.
From there, you change the message of a commit down in the history tree.
And finally, you revert all you did.

It doesn't sound trivial, does it?

Describe + New + Abandon

$ jj log

@  x
◆  z       �   empty

First, let's give x a description (a message, in Git's lingo). You can do this with the command jj description or jj desc for short. Let's go:

$ jj desc -m "Initial commit"
Working copy  (@) now at: xmvkoxku 45467cdf Initial commit
Parent commit (@-)      : zzzzzzzz 00000000 (empty) (no description set)

$ jj log
@  x    Initial commit
◆  z       �   empty

Yes, it's another Git amend under the hood. Can we start ignoring this fact, already? Deal, I won't repeat it anymore.

New Change

How to create a new commit?
Remember the basic commands:

OperationCommand
Createnew
Readshow
Updateedit
Deleteabandon
Move a Change somewhere elserebase
Move the changes you made somewhere elsesquash
Move files somewhere elserestore
Display the history treelog
List / undo / redo operationsop log / undo / redo

I bet you guessed: you need jj new.

$ jj new -m "A license"
Working copy  (@) now at: xpsqkswo 6b9aeffd (empty) A license
Parent commit (@-)      : xmvkoxku 45467cdf Initial commit

$ jj log
@  xp   empty A license
○  xm   Initial commit
◆  z    empty

This creates a new, empty Change with the specified message. See it's marked with @? It means that xp is now the Current Change, and that you are editing it.
In Jujutsu it's idiomatic (but not mandatory) to commit before coding. Indeed, you committed A license before actually adding the license: the Change is still empty. More on this workflow later.

Providing a description is optional. You can always create a messageless Change and add a message later with jj desc.

Time to add the license file:

$ echo "This software is open source" > LICENSE

You can trust it's already stored in the commit. Ready for your next Change:

$ jj new -m "A dummy React.js application"
$ jj log
@  o            empty A dummy React.js application
○  t            A license
○  x            Initial commit
◆  z       �   empty

You get the point. Although this is a possible workflow, it's not the most efficient one. I recommend other ones. You'll learn more in a few pages.

Deleting Changes

How to delete a Change? Check the table above, you can't go wrong.

Solution

Of course! With abandon.
With jj abandon -r <CHANGE-ID> you can delete any Change you like, no matter where it's located in the history tree.

Let's get rid of the Change introducing the license:

Solution

$ jj abandon -r t
Abandoned 1 commits:
  tputwmoo c2541632 A license
Rebased 1 descendant commits onto parents of abandoned commits
Working copy  (@) now at: otsrwyym ecb4d95f (empty) A dummy React.js application
Parent commit (@-)      : xmvkoxku 45467cdf Initial commit
Added 0 files, modified 0 files, removed 1 files


$ jj log
@  o            empty A dummy React.js application
○  x            Initial commit
◆  z       �   empty

Check on the filesystem: the license is gone.

Hint: as we said, when there's no ambiguity, you can use positional arguments, so jj abandon t would work as well.

Notice that you manipulated a change while you weren't there. This happens every time with Jujutsu, and it's very powerful. We will talk about it in On Not Being There.

Resurrecting Changes

Did you change your mind? Want to resurrect the license commit?

Solution

Yes! Undoing things is always jj undo:

$ jj undo
Undid operation: 3a6c93cd543e (2026-06-10 15:48:34) abandon commit c25416325e097a36d0cc1216fb4938e0c44b28a8
Restored to operation: 579bbd1014a4 (2026-06-10 15:41:13) abandon commit 855f900edf5df7275f3b666a7c7c8b64f755c0ed
Working copy  (@) now at: otsrwyym 9c73504c (empty) A dummy React.js application
Parent commit (@-)      : tputwmoo c2541632 A license
Added 1 files, modified 0 files, removed 0 files

$ jj log
@  o    empty A dummy React.js application
○  t    A license
○  x    Initial commit
◆  z    empty

Here we go, back to life!

Adding Changes In Arbitrary Positions

jj new adds a new Change on top of the Current Change. But you can add Changes in arbitrary positions. Just select the Change you want to start from, with -r, the same option you used with abandon:

$ jj new -r x -m "One here"

@  k    empty One here
│ ○  o  empty A dummy React.js application
│ ○  t  A license
├─╯
○  x    Initial commit
◆  z    empty

Of course, this started a new branch, which you can grow with other jj new commands.

What if you want to insert a Change right before o (so between o and t)? Use --before:

jj new --before o -m "One there"

○  o    empty A dummy React.js application
@  zv   empty One there
○  t    A license
│ ○  k  empty One here
├─╯
○  x    Initial commit
◆  zz   empty

You can shorten --before with -B.

Everything Is Dual

Symmetrically, there's also --after / -A. Insert a commit right after zz:

Solution

jj new -A zz -m "Initial commit is no more"

○  o    empty A dummy React.js application
○  zv   empty One there
○  t    A license
│ ○  k  empty One here
├─╯
○  x    Initial commit
@  m    empty Initial commit is no more
◆  zz   empty

So, in summary:

OptionMeaningFromTo
-r RMake R its parent.o-o-R-o-o


o-o-R-o-o
     \
      X
--after RAdd after R,
between R and its children.
o-o-R-o-oo-o-R-X-o-o
--before TAdd before T,
between T and its parents.
o-o-R-o-oo-o-X-R-o-o

Notice: all those moves are perfectly possible with Git. After all, Jujutsu is operating on a Git repository. They just happen to be more cumbersome, since you would need to think about the underlying necessary moves. So, it's likely you have rarely performed them.

The Ubiquitous -r Option

x's message is Initial commit, but now we inserted a Change before it.

○  o    empty A dummy React.js application
○  zv   empty One there
○  t    A license
│ ○  k  empty One here
├─╯
○  x    Initial commit
@  m    empty Initial commit is no more
◆  zz   empty

It's mildly infuriating! How to amend its message?

Here's something I like in Jujutsu: once you have learnt an element of its grammar, you can use it everywhere it makes sense. -r can be used with jj new to reference a Change. It can be used with log, with show and with many other commands.
Including jj desc. So:

Solution

jj desc -r x -m "Hello, world"


○  o    empty A dummy React.js application
○  zv   empty One there
○  t    A license
│ ○  k  empty One here
├─╯
○  x    Hello, world
@  m    empty Initial commit is no more
◆  zz   empty

Fixed. Good job.

What's The Point of Creating Empty Commits?

You are right, these were silly exercises. Who would ever create empty commits like this? Yet, notice:

  • Nothing prevents you to jj edit those commits and add actual content as a second step.
  • As you'll see, committing before coding is really idiomatic in Jujutsu. There are workflows based on that.
  • Finally: editing Changes is not the only way to populate them. You will find yourself moving changes and files around, and you will find that empty commits are a useful tool.

Cleaning up

Here's the chapter closing exercise. Move to o:

jj edit o

Now, get rid of these dummy commits you just created:

  • k: One here
  • zv: One there
  • m: Initial commit is no more

How would you do that?

Solution

Of course, combining abandon and -r!

$ jj abandon -r k -r zv -r m

@  o    empty A dummy React.js application
○  t    A license
○  x    Hello, world
◆  z    empty

Congrats if you got it!
Indeed, many commands operate on one or multiple Changes, whether contiguous or sparse.

The following are equivalent:

  • jj abandon -r k -r zv -r m
  • jj abandon -r 'k | zv | m'
  • jj abandon k zv m

jj abandon k zv m works because Jujutsu allows passing revsets as positional arguments when nothing else could be meant. Commands that take filesets are a notable exception.

The last one uses the | operator of the Revset Language, a very powerful and yet simple language to select sets of Changes. You'll read a bit more about it in A Glimpse of the Revset Language.

Where To Go From Here?

Admittedly, those exercises were a bit contrived, weren't they? We needed them to learn the basic moves of Jujutsu. Make them your own, and in the next few pages we will see how to apply them to genuinely useful use cases.

In the meantime, be proud! You just made some manipulations that are far from trivial, if just using bare, raw Git.

Intermezzo: A Taste Of a Fanciful Workflow

Indulge me in a little digression.

Why would anyone ever need to create a commit in the middle of an existing branch? Whatever the use case, how frequent can this possibly be?

I'm happy you asked! This is the basis of some interesting workflows you might eventually love. Here's an example. You will experiment with similar ideas in detail in a separate chapter.

Say you have 3 local branches:

@  u       Z
○  tn      Y
○  tl      X
○  y       W
│
│ ○  n     C
│ ○  q     B
│ ○  k     A
├─╯
│ ○  m    Work C
│ ○  l    Work B
│ ○  p    Work A
├─╯
○  o
◆  z

You want to know how they work together. They'll eventually be merged by CI/CD, but why wait? Merge them locally now. After all, you can always delete the merge commit with jj abandon in a breeze. Without further ado, here's the Change x, merging the 3 branches together:

@  kx
○      x     Megamerge
├─┬─╮
│ │ ○  u    Z
│ │ ○  tn   Y
│ │ ○  tl   X
│ │ ○  y    W
│ ○ │  m    Work C
│ ○ │  l    Work B
│ ○ │  p    Work A
│ ├─╯
○ │  n       C
○ │  q       B
○ │  kt      A
├─╯
○  o
◆  z

You might be wondering: how did I create that merge commit x, if there's no jj merge command?
Believe it or not, you actually already have all the ingredients to figure it out. Think about it. You'll read the answer to this quiz in two pages.

Now, say you have to work on the feature A-B-C. Why don't you work from the megamerge, so you immediately make sure the feature A-B-C works well with the other 2?

So, do your work in kx. When you are done, you add a new empty Change on top of A-B-C:

@  kx
○      x     Megamerge
├─┬─╮
│ │ ○  u    Z
│ │ ○  tn   Y
│ │ ○  tl   X
│ │ ○  y    W
│ ○ │  m    Work 2
│ ○ │  l    Work 2
│ ○ │  p    Work 1
│ ├─╯
○ │  s       D <--- new Change
○ │  n       C
○ │  q       B
○ │  kt      A
├─╯
○  o
◆  z

and then you send your work from kz back to that newly created empty commit s, using jj squash -t s (you'll learn it).

Sounds insane? Yes, it is! I was also shocked the first time I found out it's a popular workflow. Honestly, it didn't take long before it felt more useful than weird. Now it's my daily driver.

You might even find it convenient yourself.

Let's go ahead, you are very close to learning it all.

Clone

Goal

In which you learn how to use Git features directly through jj.

Enough with fictional repositories. Clone this one:

https://codeberg.org/arialdo/jj-playground1.git

It contains code from the React.js Tic Tac Toe tutorial. Clone it only using jj. Check jj git --help for available options.

Solution

That was an easy one, right?

$ jj git clone https://codeberg.org/arialdo/jj-playground1.git
$ cd jj-playground1

Running jj log, you'll see Jujutsu shows only 2 Changes and marks one as a diamond . This means it is immutable.

@  s            empty
◆  z            Click alternates Xs and Os main
│
~

As a reference, here are the symbols used by jj log to represent nodes:

Symbol Meaning
Mutable node
Immutable node
@ The Current Change
@ The Current Change, immutable

Immutable nodes prevent you from modifying already-published commits. For simplicity, we'll disable this protection, adding a local bookmark (i.e., Jujutsu's branches) for each remote branch:

$ jj bookmark track '*' --remote origin

Good, you are ready to go:

$ jj log

@  p            empty
○  zx           Click alternates Xs and Os main
○  v            Click sets a X
○  s            Square has state
│ ○  o          Play instructions
│ ○  kx         Play manual
├─╯
○  n            Square is interactive
○    ru         Board() invokes Square()
├─╮
○ │  ym         fix name: Square -> Board
○ │  x          WIP delete me
○ │  yz         LICENSE
○ │  ws         A board full of X
├─╯
○  kk           Displays X X
○  wx           Fails
○  rx           Scaffold applicatio
◆  zz      🔒   empty

Merge

Goal

In which you learn there is no merge command, and yet you will merge two branches.

A couple pages ago I teased you with a puzzle: you've mastered creating branches of empty commits; but how do you merge them?

Say you want to merge zx with o, and go from:

○  zx           Click alternates Xs and Os main
○  v            Click sets a X
○  s            Square has state
│
│ ○  o          Play instructions
│ ○  kx         Play manual
├─╯
○  n            Square is interactive
│
~

to:

@    vz          Here's the merge
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  vv         Click sets a X
○ │  s          Square has state
├─╯
○  n            Square is interactive
│
~

Hint

Remember, if you want to add a Change on top of X you run:

jj new -r X

The -r X part actually means:

Hey, Jujutsu! Create a new Change, having X as its parent.

Another hint

Most of the commits have just 1 parent.
All merge commits have something special in common.

Solution

Sure, a merge commit is nothing but a new commit having 2 parents!
Therefore, to give the new Change 2 parents, simply pass both with -r. In this specific case:

jj new -r o -r zx -m "Here's the merge"

If you got it, kudos. That wasn't an easy one!

Unmerge

Goal

Where you find out that undoing a merge is trivial.

How to unmerge?

@    vz         empty Here's the merge
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  vv         Click sets a X
○ │  s          Square has state
├─╯
○  n            Square is interactive
~

That's an easy one! If merging was the last operation, just jj undo:

$ jj undo

@  p            empty
○  zx           Click alternates Xs and Os main
○  v            Click sets a X
○  s            Square has state
│ ○  o          Play instructions
│ ○  kx         Play manual
├─╯
○  n            Square is interactive
~

Unmerging with abandon

Otherwise, you could jj abandon the merge Change. Give it a try. Let's start over:

$ jj redo

Yes! You can redo an undo. Jujutsu's undo works like Emacs': rather than just going back in time, it appends the revert to an operation log, so no information is ever lost. If you are curious, read about it in Emacs' Undo.
You are back to:

@    vz         empty Here's the merge
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  vv         Click sets a X
○ │  s          Square has state
├─╯
○  n            Square is interactive
~

Now, do yourself a favour: move somewhere else before deleting the merge, or you'll be abandoning your Current Change1:

$ jj edit n

○    vz         empty Here's the merge
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  vv         Click sets a X
○ │  s          Square has state
├─╯
@  n            Square is interactive
~

Now your turn.

  • Unmerge, without using undo.
  • Then create a new merge, this time between 3 commits: zx, o and n.

Solution

Elementary, my dear Watson!

$ jj abandon vz

○  zx           Click alternates Xs and Os main
○  v            Click sets a X
○  s            Square has state
│ ○  o          Play instructions
│ ○  kx         Play manual
├─╯
@  n            Square is interactive
~

Then:

$ jj new -r zx o n

@      u       empty
├─┬─╮
│ ○ │  o                Play instructions
│ ○ │  kx               Play manual
│ ├─╯
○ │  zx         Click alternates Xs and Os main
○ │  v          Click sets a X
○ │  s          Square has state
├─╯
○  n            Square is interactive
○    ru         Board() invokes Square()
├─╮
○ │  ym         fix name: Square -> Board
○ │  x          WIP delete me
○ │  yz         LICENSE
○ │  ws         A board full of X
├─╯
○  kk           Displays X X
○  wx           Fails
○  rx           Scaffold applicatio
◆  zz      🔒   empty

Was it too easy? See? You already think like a jujutsuka!

In Git, undoing a merge ranges from easy to painful, depending on your experience. I hope you see how Jujutsu makes it trivial regardless.


  1. Abandoning the Current Change is perfectly legit, but the first time the behavior might surprise you. If you are curious, read more about this in Pulling the Rug Out Under Oneself

On Not Being There

A quick thought, before moving to more exciting features.

What's the Git command for merging the branch A with the branch B? Those who mostly use IDE plugins and rarely touch the terminal might answer, caught off guard:

git merge A B

The correct answer is:

Answer

It depends.

  • If the current branch is A, then it's git merge B.
  • If the current branch is B, then it's git merge A.
  • If it's neither, then you need git checkout A && git merge B.

With git merge the current branch is always assumed.

Why Is It Like That?

Why isn't it git merge A B? Why do commands like cherry-pick, rebase, pull and am assume the current branch as an implicit argument?

Because of conflicts. When something goes wrong, Git cannot store the result as a commit. It stops and asks you to resolve the problem first, and the only place where you can do that is the file system. So, you have to be there.

And Jujutsu?

You probably noticed: when merging with Jujutsu you don't have to be anywhere in particular. You select the parents A and B without being in either. You also abandon Changes from a distance. You can even commit from a distance.

This follows from the fact Jujutsu handles conflicts as first-class citizens. You'll learn what a lifesaver this is later on but, for now, just get the gist: you can operate on Changes without being there.

It's History all the way down

Goal

  • Where you discover there is a meta-repository in which every commit is itself a repository.
  • And it's nothing scary: simply, everything you do with jj is tracked down.

Back to The Start

For the next sections I will ask you to obtain a fresh copy of the same repo you cloned before. There are at least two ways to do this.

  1. You could delete the project directory and clone the repository again with:
$ jj git clone https://codeberg.org/arialdo/jj-playground1.git
$ cd jj-playground1
$ jj bookmark track '*' --remote origin
  1. Otherwise, you could keep running jj undo until you get the status right after you executed that:

jj git remote remove origin

But first, you might wonder where undo and redo fetch their information from. The next few pages will shed some light on the topic.

History of a Change

Goal

  • You learn how to inspect the past of a Change.

Every Change has its History.

Do this little exercise. Target any Change you like, such as n:

$ jj log

@  p            empty
○  zx           Click alternates Xs and Os main
○  v            Click sets a X
○  s            Square has state
│ ○  o          Play instructions
│ ○  kx         Play manual
├─╯
○  n            Square is interactive
○    ru         Board() invokes Square()
├─╮
○ │  ym         fix name: Square -> Board
○ │  x          WIP delete me
○ │  yz         LICENSE
○ │  ws         A board full of X
├─╯
○  kk           Displays X X
○  wx           Fails
○  rx           Scaffold applicatio
◆  zz      🔒   empty

Make some modifications to it, like:

  • Update its description
    • jj desc -r n -m foo
    • jj desc -r n -m bar
    • jj desc -r n -m baz
  • Check it out and edit some files:
    • jj edit -r n
  • Add a Change before it:
    • jj new -B n

Under the hood, each of those operations creates a new Git commit. Each time, Jujutsu makes sure the immutable Change ID n points to whichever commit is the most recent one. Throughout its existence, n goes through different states, each a different Git commit. n was born as commit 27ea11. Then it evolved to a different commit, then another, and so on. In a sense, n has its own history tree.

This isn't merely a metaphor. You can actually display that history with evolog:

jj evolog -r n

  ○  nvokxlrr arialdo@ik.me 2026-06-12 22:52:51 c655c8d7
  │  baz
  │  -- operation 2e1a28a42c55 abandon commit 668d65f4f0eea395a4c3d0a7ed890254a60865d4
  ○  nvokxlrr/1 arialdo@ik.me 2026-06-12 22:52:45 6e2c1c9a (hidden)
  │  baz
  │  -- operation 500080ebf19a new empty commit
  ○  nvokxlrr/2 arialdo@ik.me 2026-06-12 22:52:39 983a1599 (hidden)
  │  baz
  │  -- operation 3075743719f1 describe commit 22c5df185272af6f25b1d4227cd271971aa1baa5
  ○  nvokxlrr/3 arialdo@ik.me 2026-06-12 22:52:37 22c5df18 (hidden)
  │  bar
  │  -- operation 3daa0e64a476 describe commit 21a546e29d1cfcebdc0aafd8023c3d60416bff88
  ○  nvokxlrr/4 arialdo@ik.me 2026-06-12 22:52:34 21a546e2 (hidden)
  │  foo
  │  -- operation 4c7fc048be20 describe commit a857703785c7971ec20ccd826e36557f8b7d5a72
  ○  nvokxlrr/5 arialdo@ik.me 2026-06-12 22:52:28 a8577037 (hidden)
  │  Foo
  │  -- operation a6df8a6d9206 describe commit 03e16e725052f4ba32bb0a1a31a029860a86519a
  ○  nvokxlrr/6 arialdo@ik.me 2026-06-10 14:33:07 03e16e72 (hidden)
     Square is interactive

Consider this the meta-repository that tracks the history of n. Its commits are called n/1, n/2 etc. There's nothing special about these commits: you use them in commands just like any other. Meaning: you don't need to learn new special commands, like with Git reflog.

Want to inspect the changes stored in n/5?

Solution

Just run jj show n/5.

Nailed it? You rock!

In many cases, you'll use this meta-history to recover files you want to restore.

Goal

You find out how to travel your repository back in time, all the way when you cloned it.

The History Beneath the History

If every change has its own history, then your whole repository has its own history. It's only logical. Your repo:

  • In an initial stage (let's call it 000000000000) did not exist at all.
  • Then it was created.
  • Then you added a Change.
  • Then you edited a description.

and so on.

Jujutsu is stubborn, it remembers everything, since almost every command, including jj log, triggers a working-copy snapshot.

Inspect that meta-history with:

jj op log
  ○  3075743719f1 arialdo@mbuto default@ 19 minutes ago, lasted 22 milliseconds
  │  describe commit 22c5df185272af6f25b1d4227cd271971aa1baa5
  │  args: jj describe -r n -m baz
  ○  3daa0e64a476 arialdo@mbuto default@ 19 minutes ago, lasted 24 milliseconds
  │  describe commit 21a546e29d1cfcebdc0aafd8023c3d60416bff88
  │  args: jj describe -r n -m bar
  ○  4c7fc048be20 arialdo@mbuto default@ 19 minutes ago, lasted 23 milliseconds
  │  describe commit a857703785c7971ec20ccd826e36557f8b7d5a72
  │  args: jj describe -r n -m foo
  ○  a6df8a6d9206 arialdo@mbuto default@ 19 minutes ago, lasted 21 milliseconds
  │  describe commit 03e16e725052f4ba32bb0a1a31a029860a86519a
  │  args: jj describe -r n -m Foo
  ○  192c30f0cf47 arialdo@mbuto default@ 3 hours ago, lasted 5 milliseconds
  │  restore to operation 281b30d872a33f3dc6f93982343ab3fa4d2a4adb8b3bb4903421d84003d96e96e266d90bc4e6990526d8c5eea62423b19395d5f6d2639b6be26b77d301958274
  │  args: jj op restore '000000000000+++++'
  ○  2a2be2f2b8fc arialdo@mbuto default@ 3 hours ago, lasted 5 milliseconds
  │  restore to operation 8eea9aa811a1ed9ffd6e795d17573e77caad0ddf41c716489fbe8e7526aad797c10b2cbc4c45de0845b7e1f84c6a458f148e6eab9c1241fa72d7f62c1a2f89d3
  │  args: jj op restore '000000000000++++'
  ○  281b30d872a3 arialdo@mbuto default@ 3 hours ago, lasted 4 milliseconds
  │  remove git remote origin
  │  args: jj git remote remove origin
  ○  8eea9aa811a1 arialdo@mbuto default@ 3 hours ago, lasted 14 milliseconds
  │  check out git remote's branch: main
  │  args: jj git clone https://codeberg.org/arialdo/jj-playground1.git
  ○  eff465d67569 arialdo@mbuto default@ 3 hours ago, lasted 2 seconds
  │  fetch from git remote into empty repo
  │  args: jj git clone https://codeberg.org/arialdo/jj-playground1.git
  ○  ee32fa88d5f2 arialdo@mbuto default@ 3 hours ago, lasted 3 milliseconds
  │  add git remote origin
  │  args: jj git clone https://codeberg.org/arialdo/jj-playground1.git
  ○  23c04169eec1 arialdo@mbuto 3 hours ago, lasted 6 milliseconds
  │  add workspace 'default'
  ○  000000000000 root()

As expected, everything originates from 000000000000, the origin of times. From there you can travel through all the commands you ran. Each command created a snapshot of the whole repository.

It's a repository of your repository's history.

Rewind!

Restoring the repository exactly as it was before an operation is trivial. Each operation has its ID which you can reference with the command jj op restore.

You want to go back to when you performed:

jj git clone https://codeberg.org/arialdo/jj-playground1.git
cd jj-playground1
jj bookmark track '*' --remote origin

Solution

  • You run jj op log.
  • You identify the item for the command git remote remove origin. It should be the 5th operation, from 000000000000.
  • Find its ID. Then run: jj op restore <ID>

Spot on? Congrats, this was challenging!

You could also use:

$ jj op restore 000000000000+++++

to indicate the 5th op (+++++) after 000000000000.

Check the file system and the history log: everything is back to the moment you cloned the repo.

Like Emacs, Jujutsu Never Forgets

If you inspect the output of jj op log you will find that your last jj op restore has been appended to the top of all the other operation logs. So, you can still undo it by restoring its parent.

Basically:

  • Every operation, including op restore and undo, appends a full snapshot to the op log.
  • op restore reproduces an old snapshot and appends the result to the history.
  • undo restores the previous op.

We've all dreaded performing risky operations in Git. Armed with these tools, you should never feel that way again.

Ready?

After:

$ jj op restore 000000000000+++++

your repo should be:

  ○  zx       Click alternates Xs and Os main
  ○  v        Click sets a X
  ○  s        Square has state
  │ ○  o        Play instructions
  │ ○  kx       Play manual
  ├─╯
  ○  n        Square is interactive
  ○  ru       Board() invokes Square()
  ├─╮
  ○ │  ym       fix name: Square -> Board
  ○ │  x        WIP delete me
  ○ │  yz       LICENSE
  ○ │  ws       A board full of X
  ├─╯
  ○  kk       Displays X X
  ○  wx       Fails
  ○  rx       Scaffold applicatio
  ◆  zz   🔒  empty

Good. You are ready to play with rebase and squash.

Moving Things Around

Goal

  • Flip 2 commits in a branch.
  • Send 2 commits in 2 separate branches.
  • Move a file from a commit to a future one.
  • Fix a typo, send it 10 commits ago.
  • Rebase 2 branches simultaneously.
  • Reflect on the new unprecedented history-bending abilities you just gained. Invent new audacious (but useful) moves.
  • Profit.

You can do all sort of black magic sorcery with just 2 commands:

OperationCommand
Move a Change somewhere elserebase
Move the changes you made somewhere elsesquash

Best of all, they only take a few minutes to learn.

Goal

Where you learn that rebasing is not necessarily moving a branch to a different base.
Indeed, you will move 1 single Change inserting it in the middle of the history tree.

Moving Changes

rebase works like this:

  • you specify which Changes you want to move
  • and where you want to move them.

Simply:

rebase -r X -o Y

moves the Change X on top of Y. You can also specify the destination using the same --before / -B and --after / -A options you used with jj new.

You'll find the sample repository has many mistakes and unfinished work. I will invite you to fix all the problems in the next exercises.

First, the License

Oh, no! I've added the license to the repository a bit too late, in the Change yz:

  @  x        empty
  ○  zx       Click alternates Xs and Os main
  ○  v        Click sets a X
  ○  s        Square has state
  │ ○  o        Play instructions
  │ ○  kx       Play manual
  ├─╯
  ○  n        Square is interactive
  ○  ru       Board() invokes Square()
  ├─╮
  ○ │  ym       fix name: Square -> Board
  ○ │  x        WIP delete me
  ○ │  yz       LICENSE
  ○ │  ws       A board full of X
  ├─╯
  ○  kk       Displays X X
  ○  wx       Fails
  ○  rx       Scaffold applicatio
  ◆  zz   🔒  empty

$ jj show --summary yz
  Commit ID: 997391344e0b704d8ad78a51e8b9d8c367dc6998
  Change ID: yzxvwmnznuxqpsqttxntvzuqzyrtwsww
  Author   : Arialdo <arialdo@ik.me> (2026-06-10 14:08:47)
  Committer: Arialdo <arialdo@ik.me> (2026-06-10 14:09:15)

      LICENSE

  A LICENSE

Ideally, I would add the license as the very first commit. Somehow yz ended up in the middle of a merged branch.
Can you move yz so it becomes the initial commit?

Solution

$ jj rebase -r yz -A zz

or

$ jj rebase -r yz -B rx

Things Committed by Mistake

The Change x has a suspicious message: WIP: delete me. What does it contain?

Solution

$ jj show x --summary
  Commit ID: 22014d7c4604eb00430fe4e7a583e9aaf5afb77e
  Change ID: xxuknwmlponouxznpzmnqxqvrooxttru
  Author   : Arialdo Martini <arialdo.martini@gmail.com> (2026-06-13 15:46:46)
  Committer: Arialdo Martini <arialdo.martini@gmail.com> (2026-06-13 16:09:31)

      WIP delete me

  A node_modules/yaml/LICENSE
  A node_modules/yaml/index.js

Ouch! I really made a mess there, committing node_modules.

How to get rid of it?

Solution

$ jj abandon x

And x is no more.

Why this exercise? It seems unrelated to moving stuff. But Jujutsu did rebase all descendants of x onto yz under the hood.
Moving things doesn't always look like moving things: Jujutsu commands capture your intent, not the underlying operations.

Typos

Now that I notice, there's a typo in rx's message: applicatio instead of application.
How would you fix it?

Solution

Sure, it's:

$ jj desc -r rx -m "Scaffold application"
Rebased 13 descendant commits
Working copy  (@) now at: xxuknwml 770ae789 WIP delete me
Parent commit (@-)      : yzxvwmnz 02b946ad LICENSE

Again: the intent is editing a message. The log message Rebased 13 descendant commits reveals that, under the hood, it's still about moving commits.

Let's see other examples where your intent is really moving Changes.

Goal

Where you rebase a branch.
And you find out you can also rebase 2 branches simultaneously.

A Glimpse of the Revset Language

With Git you are probably used to rebasing whole branches. Jujutsu's rebase makes no assumptions about what you want to move.

You already saw how to rebase a single Change; you can just as easily move multiple Changes, even sparse ones across different branches: just specify them. For example:

jj rebase -r X -r Y -o O

would move both X and Y on top of O.
Instead of listing Changes one by one, you can use expressions in the Revset Language. Here's a foretaste:

NotationMeaning
X | YAll the Changes in set X plus all the Changes in the set Y
X & YThe Changes that are both in the set X and the set Y
~XChanges that are not in X.
X::X and all its descendants.
::XX and all its predecessors.
X::YAll the Changes between X and Y.

Each expression returns a set, so you can keep combining them into more complex ones.

Moving Sets of Changes

Say that for whatever reason, you want to move the license (Change yz) and the manual (Changes kx and o) on top of main.

  @  t        empty
  ○  zx       Click alternates Xs and Os main  <-- move them here
  ○  v        Click sets a X
  ○  s        Square has state
  │ ○  o        Play instructions              <-- this
  │ ○  kx       Play manual                    <-- this
  ├─╯
  ○  n        Square is interactive
  ○  ru       Board() invokes Square()
  ├─╮
  ○ │  ym       fix name: Square -> Board
  ○ │  x        WIP delete me
  ○ │  ws       A board full of X
  ├─╯
  ○  kk       Displays X X
  ○  wx       Fails
  ○  rx       Scaffold application
  ○  yz       LICENSE                          <--- this
  ◆  zz   🔒  empty

Solution

It's either:

jj rebase -r yz -r kx -r o -o zx

or:

jj rebase -r "yz | kx | o" -o zx

If you did it right, cool! Pause a second and think how you would get to the same result in Git. It's technically possible, sure, but would you call it straightforward?

Before moving to the next exercise, just:

jj undo

this last move, please.

Rebasing a Branch

Goal

In which you rebase a branch, then you rebase it back to where it was.

If you run jj undo from the previous page, your history tree should be:

  ○  zx       Click alternates Xs and Os main        <--- this branch
  ○  v        Click sets an X
  ○  s        Square has state
  │ ○  o        Play instructions
  │ ○  kx       Play manual
  ├─╯
  ○  n        Square is interactive
  ○  ru       Board() invokes Square()
  ~

How would you rebase the branch from s to zx on top of o?

Solution

The solution is almost the literal translation of the request:

$ jj rebase -r "s::zx" -o o

You should get to:


  ○  zx       Click alternates Xs and Os main
  ○  v        Click sets an X
  ○  s        Square has state
  ○  o        Play instructions
  ○  kx       Play manual
  ○  n        Square is interactive
  ○  ru       Board() invokes Square()
  ~

How would you undo that, using rebase and not undo?

Solution

The original base of s::zx was n, so the move is:

$ jj rebase -r "s::zx" -o n

○  zx       Click alternates Xs and Os main
○  v        Click sets an X
○  s        Square has state
│ ○  o        Play instructions
│ ○  kx       Play manual
├─╯
○  n        Square is interactive
○  ru       Board() invokes Square()
~

Give yourself a pat on the back if you got it right.

Rebasing 2 Branches Simultaneously

Goal

Where you discover you are a Jujutsu ninja and you are not surprised at all you can rebase 2 branches in 1 shot.

On top of n we have 3 branches (I'll name them by their tip Change ID):

  • rt, which introduces CSS.
  • zx, where new functionalities are.
  • o, which contains the manual.
  ○  rt       Minimal CSS css                     <- this
  │ ○  zx       Click alternates Xs and Os main   <- this
  │ ○  v        Click sets an X
  │ ○  s        Square has state
  ├─╯
  │ ○  o        Play instructions manual          <- this
  │ ○  kx       Play manual
  ├─╯
  ○  n        Square is interactive
  ~

We decide to include CSS in the other 2 branches. So, your goal is to rebase both zx and o on top of rt.
You could do this with:

    jj rebase -r kx:: -o rt

followed by

    jj rebase -r s:: -o rt

Would you be able to do this in 1 command?

Solution

Union set to the rescue!

jj rebase -r 'kx:: | s::' -o rt
  
○  zx       Click alternates Xs and Os main*
○  v        Click sets an X
○  s        Square has state
│ ○  o        Play instructions manual*
│ ○  kx       Play manual
├─╯
○  rt       Minimal CSS css
│ ~
├─╯
○  n        Square is interactive

Rebasing multiple branches is especially handy for keeping them up to date with trunk. Check out rebase-all, an alias by Steve Klabnik, you'll love it: you just run jj rebase-all so your local work is always based on the latest main.
People invented all sorts of amazing aliases: find them at https://github.com/jj-vcs/jj/discussions/8484.

Preview of Revsets

You should never fear a rebase: after all, you can always resort to jj undo. Also, you can always obtain a preview of the Changes involved in a Revset expression using log:

   jj log -r 'kx:: | s::'
   ○  zx       Click alternates Xs and Os main*
   ○  v        Click sets an X
   ○  s        Square has state
   │
   ~

   ○  o        Play instructions manual*
   ○  kx       Play manual
   │
   ~

Universality of Revsets

Oh, and by the way: once you have defined a Revset expression, you can use it with many commands:

  • Want to delete all those Changes? jj abandon -r 'kx:: | s::'
  • Want to inspect all of them? jj show -r 'kx:: | s::'
  • Want to change their author? jj metaedit -r 'kx:: | s::' --update-author "Joe Doe"

That's a general trait: Jujutsu has few building blocks, but they combine in countless ways.

Moving Edits Around

Goal

Where you learn to send a file back to the past.

Changes and Edits

A Change contains your modifications to files such as adding a class, renaming a function, and the like. You can see them as the diff shown by jj show (use --git if you want to use the Git format):

$ jj show rt --git
Commit ID: ad4aa4890ec32cf83c51a407a099afaf9af106b9
Change ID: rtsytxmrmouvklxkuuvqxolokmmkzssz
Author   : Arialdo <arialdo@ik.me> (2026-06-13 19:05:31)
Committer: Arialdo <arialdo@ik.me> (2026-06-15 08:10:28)

    Link to CSS

diff --git a/public/index.html b/public/index.html
index efd73153a2..fd6a1cbbfe 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,6 +4,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Document</title>
+    <link rel="stylesheet" href="index.css">
   </head>
   <body>
     <div id="root"></div>

The Jujutsu manual calls them "changes", but that term is already overloaded. Let me call them "edits" from now on, OK?

Finding the Source of a Typo

To narrow down jj log to some files, you can use the Revset Language function files(). Here's the change history of README.md:

$ jj log -r "files('README.md')"

○  kk           Displays X X
~  (elided revisions)
○  rx           Scaffold application
│
~

As you see, README.md was created in rx and modified in kk. Inspect what kk is about:

$ jj show kk --git

diff --git a/README.md b/README.md
index d8ce899bb8..ec822c4346 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
-# Learning Raect.js
+# Learning React.js

diff --git a/src/App.js b/src/App.js
index 3f8dbe3702..495f426244 100644
--- a/src/App.js
...

Uh oh! Together with a change to the JavaScript code in App.js, there's also a little fix to the word React.js in README.md. If you check the content of rx, you will see it's the one that introduced the typo:

$ jj file show -r rx README.md
# Learning Raect.js

Following the tutorial at https://react.dev/learn/tutorial-tic-tac-toe

So, it must have gone like this:

  • I wrote the typo Raect instead of React in rx.
  • I missed that so I kept building on top of that.
  • Then, while I was coding App.js I stumbled upon the typo.
  • Instead of fixing it by amending rx, I created a Change, kk, mixing the typo fix with other unrelated changes.

Silly me...
How to move the fix Raect.js -> React.js from kk to rx?

This is really about moving edits.

Sending Fixes Back to the Past

Rebase won't help here: you don't want to move the Change kk, but the edits it contains.
Enter squash. If you are familiar with git merge --squash and the git rebase --interactive's squash command, you know that squashing in Git is about combining multiple commits into a single one.

jj squash generalizes this concept. Think of it as a way to transfer edits from one Change to another. Though these concepts may seem unrelated, Jujutsu's squash is general enough to cover both. The basic syntax is:

jj squash --from A --into B [FILES]

or more concisely:

jj squash -f A -t B [FILES]

The net effect: the edits to [FILES] move from A to B. Your edits stay the same; only which Change owns them differs.

So, fixing my mess with Raect.js is a matter of running:

Solution

$ jj squash -f kk -t rx README.md

Yes, of course! Bravo!

Check the content of kk:

$ jj show --summary kk

Commit ID: 3c72f2573cc5235622030aac110e884014e8ec6b
Change ID: kkqvwlzrztzvlmtyqksulvyuwsorpppx
Author   : Arialdo <arialdo@ik.me> (2026-06-10 14:02:12)
Committer: Arialdo <arialdo@ik.me> (2026-06-15 14:50:39)

    Displays X X

M src\App.js

README.md is not listed anymore. Indeed, in rx the typo is gone:

$ jj file show -r rx README.md
# Learning React.js

Following the tutorial at https://react.dev/learn/tutorial-tic-tac-toe

because the typo fix has been absorbed by the edits already there.

Cool! Next time you are focused on some development task and you find a typo in an unrelated file, you can fix it on the spot and then send it back to the past, where it belongs.

Goal

In which you start believing in magic.

Dispatching Edits from Megamerges

Why did I say "absorb" in the previous chapter? Because you could have done that squash using jj absorb. Run a jj undo and give it a try. Go to kk:

jj edit kk

and run:

jj absorb README.md

jj absorb will accomplish what you just did manually: it will identify the source of the typo and will move the edit there.

If you don't specify the file, jj absorb will analyze all the edits you did. For each individual edit, it will figure out the closest ancestor it can safely land in. Got multiple edits across multiple files? No problem: jj absorb will split them apart and dispatch each one to its rightful Change back in your history.

If you are anything like me, you're probably grumbling: "no way this works without breaking something". I swear I thought the same, and I swear that Jujutsu proved me wrong.

Is it safe?

First of all, you always have jj undo.
Besides this, yes, it is: jj absorb is very conservative: when the destination Change can't be unambiguously determined, jj absorb does nothing rather than risk a mess.

When Do You Use It?

Remember the Megamerge Workflow I told you about in Intermezzo: A Taste Of a Fanciful Workflow? That's the typical case where jj absorb shines.

The idea is: you have multiple Pull Requests waiting to be merged:

○
○
○
│
│ ○
│ ○
│ ○
├─╯
│ ○
│ ○
│ ○
├─╯
○
◆

and over time you receive feedbacks on them. Rather than jumping left and right to fine-tune your code, you create an octopus merge of all your PRs, the megamerge, and you sit on top of it, in a new empty Change:

@     <- Your staging area
○     <- The megamerge
├─┬─╮
│ │ ○
│ │ ○
│ │ ○
│ │ ○
│ ○ │
│ ○ │
│ ○ │
│ ├─╯
○ │
○ │
○ │
├─╯
○
◆

This lets you work on all your branches from a single staging area.

How does it work? As feedback comes in on one of your Pull Requests, you make the necessary change directly on top of the megamerge, which contains the sum of all your work; then you use jj absorb to dispatch the change where it belongs.

You won't have to worry about merge conflicts, because:

  • jj absorb would stop rather than creating one.
  • The megamerge already proves all your PRs work together, so conflicts are caught early by design.

You can read more about this workflow in the following two articles. There's a video in the first one if you want to see how this works in practice.

Hammers and Nails

Allow me a brief reflection.

The language we use defines the boundaries of what we can think.

The same applies to the tools and the programming languages we use. By making some operations easy and others hard, they make some concepts thinkable and others invisible.

A workflow requiring 40 fragile steps in Git isn't just avoided: it ceases to exist as a mental category. My thesis is that implementation difficulty leads to conceptual absence. Or, as my grandma would say:

What's hard to do becomes hard to think.

Jujutsu illustrates this point beautifully. Megamerge has always been technically achievable in Git, hasn't it? Yet nobody invented it. Why? Because it required a chain of operations so convoluted that it was just too hard to even ideate them.
By elevating its interface from implementation details to intent, Jujutsu turned some convoluted operations into trivial ones, and this allowed people to envision new workflows.

I'm standing on the shoulders of giants with this thought. Ken Iverson, the APL's creator, titled his 1979 Turing Lecture Notation as a Tool of Thought arguing exactly that: notation doesn't carry preexisting thoughts but shapes them.

The first corollary is that the limits of our tools (and our thought!) are invisible from the inside. Remember that I cited Graham's Blub Paradox? It states that we can see what's missing from poorer languages, but not what's missing from ours, compared to more powerful ones.

My personal corollary is maybe counterintuitive (and not necessarily compatible with your career ambitions): always deliberately expose yourself to tools and languages you'll never seriously use. Maybe you will never switch to them, but the space of what will be conceivable by your fantasy will never be the same.

We've always thought in Git. Jujutsu expands the boundaries of our thought. I hope to live long enough to see what will come next.

Moving Chunks Around

Goal

In which you send a single a line of a file back to the past.

jj absorb can dispatch multiple edits within a single file to different Changes. So far, you've only learned to use jj squash -f FROM -t TO FILES, which moves all the edits in the given files at once. But moving every edit in a file is not necessarily what you want. Usually you need to be more selective.

See this case, for example:

$ jj show ym --git

    fix name: Square -> Board

diff --git a/src/App.js b/src/App.js

--- a/src/App.js
+++ b/src/App.js

-// Main apication
+// Main application
...
-export default function Square() {
+export default function Board() {

The main goal of the Change was to rename a function from Square() to Board(). Mixed with this, though, I also fixed another typo (again!): apication -> application.

The line // Main apication was introduced by Change rx. Doing:

$ jj squash -f ym -t rx src/App.js

is too coarse, it would drag the Square() -> Board() rename into rx as well, which is not what you want. jj absorb would work, but there are alternatives which give you more control.

Interactively Select Chunks

Use the --interactive / -i option of jj squash:

jj squash -f ym -t rx -i

An interactive editor will open, with which you can select which chunk to include in the squash:

[File] [Edit] [Select] [View]
( ● ) src\App.js

[●] Section 1/2
  [●] - // Main apication ⏎
  [●] + // Main application ⏎
       2 // Display a 3x3 board and let the user ⏎
       3 // play Tic Tac Toe ⏎

[ ] Section 2/2
  [ ] - ⏎
  [ ] - export default function Square() {⏎
  [ ] + ⏎
  [ ] + export default function Board() {⏎
       6   return ( ⏎
       7     <> ⏎
       8       <div className="board-row"> ⏎
             ⋮

You can use the following keys:

KeyPurpose
Up / DownMove up and down
SpaceSelect a line
fFold / unfold a file
cConfirm
qQuit without confirming

You can also use -i to select whole files: that's often a convenient alternative to listing file names in jj squash -f FROM -t TO FILES.

Building On Top Of Squash

By now, you should have got a solid grip on squash. In combination with rebase, it's one of the sharpest tools in the box.

Take a breath and look at the range of operations now at your fingertips. Odd are, you are already doing things you've never attempted with Git. Be proud of yourself.

It's time for you to see the Squash Workflow, a popular, idiomatic Jujutsu workflow. After this, you won't miss Git's index anymore.

Goal

You see how moving changes from a source Change to a target Change might imply abandoning the source.

Squashing Changes

The way jj squash parameter defaults interact is quite clever.The basic syntax is:

jj squash -f A -t B FILES
  • If you omit the list of files, then all the changes in the Change are moved.
  • If you omit either -f or -t, then the Current Change @ will be implied.

So:

jj squash -f z

moves the edits from z to your current working copy.
If you do:

jj squash -t x

your edits move to x, no matter where it sits in your history, either in the past or in the future, in the same branch or elsewhere.

This technique is truly powerful: it lets you edit something here and send it somewhere else when you're done.
For example, if you notice an unrelated typo while working on a feature, fix it on the spot and squash it where it belongs.
Or, say you spot something that needs to be done in a different branch. You can write the change immediately and then move it there with a jj squash. In a sense, this is the basic move that lets you work on multiple features simultaneously.
These moves are so idiomatic that Jujutsu offers a "macro command": absorb. I'll cover it in the following pages with practical exercises.

Abandoning Changes

When the source Change is left empty after a move, Jujutsu automatically abandons it. For example, say you have:

$ jj log -r "n::zx"

○  zx
○  v
○  s
○  n

If you squash v into n:

Solution

jj squash -f v -t s

then you end up with:

○  zx
○  s
○  n

What Happens To Messages?

As with Git, Jujutsu always preserves information. If squashing results in deleting a Change, and that Change has a description, Jujutsu will stop and interactively let you define one. You will typically merge all the original messages into one.
Otherwise, you can always set the message with -m, or use --use-destination-message / -u.

Quiz Time!

Check out this sloppy mistake:

$ jj show yo- --git

--- a/src/App.js
+++ b/src/App.js
...
+    <button
+      className="square"
+      onClick={handleClic}
...

Oops, Clic instead of Click. Have I fixed it in yo-? Not at all! I committed a fix on top of it, with the very expressive message "Fix":

$ jj show yo --git

--- a/src/App.js
+++ b/src/App.js
@@ -11,7 +11,7 @@
   return (
     <button
       className="square"
-      onClick={handleClic}
+      onClick={handleClick}
     >

Terrible. Help me, please: get rid of that noisy yo Change and make yo correct:

Solution

One way to do that is:

jj edit yo
jj squash -u

Remember? -u is the short for --use-destination-message.

Or, without moving away from your Current Change:

jj squash -f yo -t n -u ```

Just jj squash

If you run the bare jj squash, without any argument, then:

  • it will move all the changes (indeed, no files are specified)
  • from the Current Change to its parent.
  • Should your Current Change have no message, it will be abandoned and a fresh new one will be immediately created.

Say you are on the tip of a branch:

@  sw           empty
○  zx           Click alternates Xs and Os main
○  v            Click sets a X
○  sn           Square has state

You code, editing @. Then you run jj squash.
Can you see where this goes? It's the equivalent of having the Git index!

Let me show you how this works.

Goal

You learn a new workflow which might look both:

  • Exotic: you would commit before coding!
  • And familiar: you won't miss the Git index anymore.

The Squash Workflow

Apparently, this is the preferred workflow by Martin von Zweigbergk, the guy who invented Jujutsu. So it is probably deeply rooted in Jujutsu's philosophy.

It revolves around these 2 ideas:

  • You start by committing.
  • Then, you code in a disposable Change on top of your commit.

When you are done with your work, you squash down the changes. Let me just show you; it's faster than explaining.

$ jj log

○  q    Add the CSS file css
○  rt   Link to CSS
│ ○  o  Play instructions manual
│ ○  kx Play manual
├─╯
│ ○  zx Click alternates Xs and Os main
│ ○  v  Click sets a X
│ ○  s  Square has state
├─╯
○  n    Square is interactive
~

The Changes rt and q are about the CSS. Let's say that you want to improve the web page layout, adding a mouse hover effect.

First: Commit!

You start by declaring your intent, committing to a goal:

$ jj new -r q -m "Hover effect to board squares"

○  rt   empty Hover effect to board squares
○  q    Add the CSS file css
○  rt   Link to CSS
~

You haven't started coding yet, so of course the Change is empty. Eventually, it will be the next commit in your history. Think of the Git index, it's pretty much the same idea: a Candidate Commit.

Second: Set Up Your Workspace

Then build a disposable Change on top of your Candidate Commit:

$ jj new

○  xm   empty
○  rt   empty Hover effect to board squares
○  q    Add the CSS file css
○  rt   Link to CSS
~

That empty, messageless Change may seem odd, but squint a little, it's just like Git. With Git you start with:

In GitIn Jujutsu
An empty Index, the Candidate Commit.This is your rt Change.
A working directory with no pending changes.This is equivalent to your empty, Current Change xm.

Third: You Code

Nothing new to see here: Jujutsu reflects every file system change in your tip Change xm.

Finally: You Squash

When you're done, review and move your changes back to the Candidate Commit.

jj squash

or:

jj squash -i

if you want to do this chunk by chunk.

When jj squash completes, the now-empty tip Change is automatically abandoned and replaced with a fresh one.

Try It Out

Give it a try. Say you want to add:

.square:hover {
  background: #e0f7fa;
  cursor: pointer;
  transition: background 0.2s;
}

to style.css. Your turn!

Solution

Most of the work is already done! The Candidate Commit is already there. You just need to do the funniest part (coding! 😎).
Then:

jj squash -i

or just jj squash.

Goal

You learn to send edits to the future, and to split a Change in 2.

Moving Edits To The Future

You saw how to send an edit back to a past Change. In fact, there is nothing in the --from and --into options of jj squash that constrains their position in the history tree. You can send edits to the past, to the future, even to unrelated branches.
But why would you send edits to the future?

A common use case is the following. Inspect the Change rv: it contains 2 unrelated edits, one to CSS and one to the logic:

○  kt   empty Hover effect to board squares
○  rv   Changes to both CSS and app.js
○  q    Add the CSS file css
~

How would you split that Change in two? A possible approach is:

  • To create a Change after rv.
  • To move part of the edits of rv in that future Change.

Come on, try it yourself. Split the CSS edits from the app.js edits into 2 separate Changes:

Solution

$ jj desc -r rv -m "Changes to CSS only"
$ jj new -A rv -m "Changes to app.js only"
$ jj squash -f @- -i

Split

This move is so common that Jujutsu offers a macro command: jj split. It is equivalent to creating a new Change, either after (-A) or before (-B), and then running a jj squash -i.

jj undo the previous move and do it again with jj split.

Goal

You learn to copy/paste from one Change to another.

Copying Files

Good things always come in threes. You have met jj squash and jj rebase; meet jj restore. All for one and one for all: together those three musketeers are all you need to bend the history tree to your will:

OperationCommand
Move a Change somewhere elserebase
Move the changes you made somewhere elsesquash
Copy files from one Change to anotherrestore

jj restore is a command for copying file contents, and it shares the same syntax as jj squash (have I already told you that Jujutsu is consistent?)

jj restore -f FROM -t TO FILES

This would copy the content of FILES from FROM to TO.
Think of it as copy-paste: you copy the content of FILES from FROM and paste them into TO. The original in FROM stays unmodified.
This includes deletions: if a file was deleted in FROM, it will be deleted in TO too.

As for squash, if you omit either -f or -t, the Current Change @ will be implied.
If you omit FILES, the whole working copy will be restored.

When Is It Useful?

Days ago your README.md had rich examples and beautiful ASCII diagrams. Then your PM asked for something "more executive". Fine, you trimmed it down. PMs are always right. Especially on Friday afternoon.

Today the same PM says (because of course they do): "You know what? Actually, I liked the long version better".

You sigh and call jj restore to the rescue:

  • You jj log and find the Change X with message docs: README with beautiful diagrams
  • jj restore --from X README.md

Done.
You see? You are not "going back in time": you are pulling a file out of an old snapshot and pasting it into the Current Change.

Is This Use Case Really That Common?

Not particularly. But like with jj squash, there are interesting consequences to the jj restore's default arguments.

Goal

You learn to completely clean up the working area and to start over.

Restarting

The jj restore --help page states:

When neither --from nor --into is specified, the command restores into the working copy from its parent(s).

That's a neat default! It means that:

jj restore

is equivalent to:

jj restore -f @- -t @ .

which makes the Current Change identical to its parent. It's a convenient way to clean up everything and start over.

Give it a try:

○  rt           Link to CSS
○  n            Square is interactive
○    ru         Board() invokes Square()
├─╮
○ │  ym         Rename: Square -> Board
○ │  xx         WIP delete me
○ │  yz         LICENSE
○ │  ws         A board full of X
├─╯
○  kk           Displays X X
○  wx           Fails
○  rx           Scaffold application

Select any Change you like (say: ru) and create a new Change on top of it:

jj new ru

Make any modification you like:

$ touch unnecessary.txt
$ echo "a bug" >> src/App.js
$ rm src/index.js
$ jj st
Working copy changes:
M public/index.html
M src/App.js
D src/index.js
A unnecessary.txt

2 modified files, 1 deleted and 1 added.
How to start over and get back to an empty Change?

Solution

But it's trivial!

$ jj restore
Added 1 files, modified 2 files, removed 1 files

$ jj st
The working copy has no changes.

Back to the start.

Isn't This equivalent to git restore?

Not really. In Git, the same information is spread across three layers, HEAD, the Index, and the working tree, plus the fourth grey area of untracked files. As a result, you need different commands and options depending on which subset you want to affect.

GoalGit
Discard unstaged changesgit restore .
Discard both staged and unstaged changesgit restore --staged --worktree . or
git reset --hard HEAD
Clean restart, equivalent to jj restoregit reset --hard HEAD && git clean -fd
Nuclear cleanup (including .gitignored files)git reset --hard HEAD && git clean -fdx

Goal

You conclude that the Git Index was a Good Thing®.
So, you learn a micro-workflow that builds on top of the same idea and will save you a lot of headaches.

Don't Edit

In the chapter Committing you read that:

By editing the file system, you are directly editing the commit.
We will soon see how this is somewhat dangerous.

In hindsight, I was a bit dramatic. After all, Jujutsu, like Git, never deletes information, so there's nothing really dangerous.
But why let accuracy spoil good drama? So, let me reformulate the idea as follows:

Directly performing open-heart surgery on a Change with jj edit is dramatically more perilous than working from a distance, on a new Change on top of it.

Terrified? Good. (Really, you have no reason to be, but good).

What's Wrong With Editing?

jj edit is ruthless: you edit the filesystem and each and every change is unconditionally committed to the Current Change. Even worse: if you are editing a Change deep in the history tree, this will also recursively propagate the changes to all the descendants, with an automated rebase.

The problem is that not every change is intentional:

  • Building the project can create artifact files. Jujutsu might track them before you get the chance to add them to .gitignore.
  • We all have fat fingers, and we all make mistakes. Combine "mistakes" with "automatically and recursively propagated" and have a recipe for disaster.
  • Editors and tools might create stray files. jj edit won't care: it will commit and rebase anyway.

A good rule of thumb, with Jujutsu just as with Git, is:

Always review the changes before making them part of a commit.

That's why the Git Index exists. jj edit is often just too rushed.

The solution is pretty easy: whenever you are tempted to run jj edit X, run jj new X instead. Rather than starting open-heart surgery on X (sirens blaring!), jj new prepares a safe working area for you to work in peace (elevator music...).

Mini-workflow

Here's a little recipe. If you want to edit X:

  1. jj new X
  2. Make your edits.
  3. When you are done, review your work with jj diff and squash it back with jj squash.
  4. If you want to start over, jj restore.

jj diff will show you the changes you made.
jj restore will revert only your changes, leaving the original target Change intact.

See This In Practice

Again using our sample repository. Say you want to edit v:

○  zx           Click alternates Xs and Os main
○  v            Click sets a X                    <-- What you want to edit
○  s            Square has state
│ ○  kt         empty Hover effect to board squares
│ ○  rv         empty Changes to both CSS and app.js
│ ○  q          Add the CSS file css
│ ○  rt         Link to CSS
├─╯
│ ○  o          Play instructions manual
│ ○  kx         Play manual
├─╯
○  n            Square is interactive
~

Instead of jj edit v, go with $ jj new v:

@  l            empty                             <-- Your Current Change
│ ○  zx         Click alternates Xs and Os main
├─╯
○  v            Click sets a X                    <-- What you want to edit
○  s            Square has state
│ ○  kt         empty Hover effect to board squares
│ ○  rv         empty Changes to both CSS and app.js
│ ○  q          Add the CSS file css
│ ○  rt         Link to CSS
├─╯
│ ○  o          Play instructions manual
│ ○  kx         Play manual
├─╯
○  n            Square is interactive
~

Notice where you landed:

  • on an empty, messageless Change
  • on top of the Change you want to edit.

Let me call that empty Change the Buffer Change.

If you squint, you could think of that Buffer Change as a Git Index-like structure. Only, it's an ordinary Change, like all the other ones, so it's persistent and operable with the ordinary commands.

Isn't That The Same As jj edit?

Definitely no.
If you did jj edit v directly:

  • jj diff would display your changes plus all the changes originally in v, mixed together. Reviewing your work would be less convenient.
  • Similarly, jj restore would revert your changes plus all the changes originally in v, basically reverting everything to v's parent s: definitely, not what you want.

The Joy Of Not Having Special Cases

The fact this Buffer Change is an ordinary Change has a couple of interesting consequences.

There's No Need for jj stash

You need to drop everything and switch to something else:

  • With Git, before running git checkout, you need to git stash push or you will lose your work.

  • With Jujutsu? Fear no more: go safe with jj new somewhere else: your Buffer Change won't be lost.

Since you are working with ordinary Changes, there's no separate stash mechanism to learn: suspended work lives alongside all other Changes and never gets lost. The git stash machinery collapses into the Jujutsu primitives you already know.

Multiple Indexes

Have you ever thought you can have multiple indexes? Say that while you are editing v (from a distance) you come up with another possible approach to complete your work. No problem: run another jj new v and you'll end up with a second, independent Index-like Change. Eventually, you will jj abandon one and jj squash the other. Or maybe you want to mix them? Your choice.

Goal

You realize that jj new can be used to browse the repository.

Wandering Around

When you run jj new and you land in a new Change, if the Change you left behind is empty, Jujutsu abandons it.

That's a particularly happy default: it implies that you can jump back and forth in your history tree targeting different Changes with jj new.

Target X and run jj new X: by design, the new Change you jump to has the same content that X has. So, you can use it as a safe copy to see what's inside X, replicating its content on your disk.

By sitting on top of the Change you want to visit, you are sure that:

  • You won't pollute the history with empty leftover Changes.
  • There's no risk of unintentional modifications.

In practice, this is a very cheap way to get read-only access to your repository.

Conflict Resolution

Enough with the easy cases. Let's talk about the exact opposite situation.

The mini-workflow (jj new && (jj squash | jj revert)) you saw in the previous page is particularly handy when resolving conflicts.
So, it's definitely time to tackle this topic.

Goal

  • You start learning how to resolve conflicts by editing files.
  • You learn that conflict resolution propagates to descendants.
  • Then you see some cases where you can resolve a rebase conflict by other means, like by doing another rebase or by abandoning Changes.
  • Finally, if you are curious, you read how Jujutsu handles conflicts, internally, and you get what it means that they are first-class citizens.

Conflicts

We will spend quite a bit of time on this topic. Not because it's tricky, but because it's one of the scariest topics in Git, and I'm determined to put that fear to rest once and for all.

We will play with conflicts in practice, with a real repository. Clone it with:

jj git clone https://codeberg.org/arialdo/jj-playground-conflicts.git

Then, as before, create a tracked local branch for each remote branch:

jj bookmark track '*' --remote origin

You haven't seen the first command before: it creates a local bookmark for each remote branch. We'll see bookmarks in a few pages.

Here's the history tree:

@  mv   hello mundo
│ ○  nz hola world
├─╯
○  vr   hello world
○  ypl
○  vw
│ ○  nl
│ ○  ypy
│ ○  mw
│ ○  ww
│ ○  wq
├─╯
○  x
○  vn
○  r
│ ○  no  feat-1
│ ○  vt
│ ○  s
│ ○  u
│ ○  vz
├─╯
○  q
◆  z

Ready to go! We will do the following moves, each producing some conflicts, progressively more challenging (but I swear: they are all trivial):

  1. We merge mv and nz to get a merge conflict.
  2. Then we rebase the branch feat-1 to get 1 single rebase conflict.
  3. Then the branch feat-2, to get multiple conflicts.

Then, we will play with some novel conflict resolution techniques that are just not available with Git.

Ready? Hands on the keyboard!

Goal

  • You start with the simplest case: a merge conflict.
  • You see in practice what it means that conflicts are not blocking.
  • You learn how to read the conflict markers.
  • And, ultimately, how to solve the conflict.

Focus on the Changes:

○  nz        merge-b
│ ○  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b
│
~

They have vr as their shared base. vr contains a text file with some Lorem Ipsum lines. Among them, you'll find the line:

We will create a conflict here.

If you inspect m / merge-a:

jj show m

you see that it edits exactly that line, adding:

: merge-a added this text.

By the way: you can also reference m using the bookmark merge-a. Bookmarks are equivalent to Git's branches.
The Change nz / merge-b does the same, only with the text:

: merge-b added this text.

Since the two changes are incompatible, merging them will certainly result in a conflict.
Exactly what we want!

Goal

You see how Jujutsu warns you when there is a conflict.

○  nz        merge-b
│ ○  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b
│
~

So, let's create this conflict. Merge merge-a and merge-b, with the message Merge conflict. Do you remember how to merge branches?

Solution

jj new merge-a merge-b -m "Merge conflict"

or:

jj new a nz -m "Merge conflict"

Of course, nothing stops you from swapping the arguments:

jj new merge-b merge-a -m "Merge conflict"

or:

jj new nz a -m "Merge conflict"

Swapping arguments won't change the result, but the conflict details will be swapped too. Not a big issue at all, but for the sake of simplicity, I assume you specified a / merge-a as the first argument.

Jujutsu will let you know you have a conflict:

Working copy  (@) now at: nlpwoyvx 0670a921 (conflict) (empty) (no description set)
Parent commit (@-)      : mvxxwqoo e1acf668 merge-a | (no description set)
Parent commit (@-)      : nzsunxwp 0004a47a merge-b | (no description set)
Added 0 files, modified 1 files, removed 0 files
Warning: There are unresolved conflicts at these paths: file.txt    2-sided conflict

Notice: it's warning, not an error. When you are in a conflicted Change, jj log will display the @ marker in red. Any other conflicted Change will be displayed as a red ×.

As we've noted before, having a conflict won't prevent you from continuing your work.
Let's challenge this notion.

Goal

  • You learn that you can keep working as usual.
  • You will even rebase the conflicted Change and commit on top of it.

Living with Conflicts

Let's see if before resolving the conflict you can rebase the whole branch vr:: from its base q to r:

@  zo       empty Merge conflict
├─╮
│ ○  nz        merge-b
○ │  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b   <- current base
│ ○  y        empty  feat-2
│ ○  vw       empty
│ ○  x        empty
│ ○  vn       empty
│ ○  r        Rebase vr:: here            <- rebase it here
├─╯
│
~

Solution

There are a couple of ways to do this.
You can specify the Changes to rebase with vr:: (vr and all its descendants):

jj rebase -r vr:: -o r

Otherwise, you can specify the cut & paste points, with:

jj rebase -s vr -o r

as you saw in Git Rebase.

The rebase succeeds. Unlike Jujutsu won't whine with something like:

error: cannot rebase: You have unstaged changes.
error: additionally, your index contains uncommitted changes.
error: Please commit or stash them.

or:

fatal: It seems that there is already a rebase-merge directory, and
I wonder if you are in the middle of another rebase.  If that is the
case, please try
        git rebase (--continue | --abort | --skip)
If that is not the case, please
        rm -fr ".git/rebase-merge"
and run me again.  I am stopping in case you still have something
valuable there.

Of course, even if jj rebase succeeded, the conflict is still there, and you will be reminded of that:

Warning: There are unresolved conflicts at these paths:
file.txt    2-sided conflict

But again, it's just a warning, not a showstopper.
Can you even commit on top of a conflicted Change? Of course, why not! Give it a try:

jj new -m "My parent is conflicted"
jj new
echo "I can keep working" > some-work-file.txt
jj squash


@  l        empty
×  zm       My parent is conflicted
×  zo       Merge conflict
├─╮
│ ○  nz        merge-b
○ │  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b
│
~

As you see, Jujutsu won't forget that there is a conflict back in history, so it will keep displaying the warning:

Warning: There are unresolved conflicts at these paths:
file.txt    2-sided conflict

and jj log will highlight all the Changes inheriting that conflict using × and red @ symbols.

Time to resolve the conflict.

Goal

Where you see how to interpret the conflict markers and you see how a conflict resolution automatically propagates.

Resolving A Conflict by Editing It

The most straightforward and natural way to solve a conflict is by editing the affected files.
In this case, it's a matter of editing the merge Change zo:

@  l        empty
×  zm       My parent is conflicted
×  zo       Merge conflict           <- Resolve the conflict here
├─╮
│ ○  nz        merge-b
○ │  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b
│
~

Notice: you've surely obtained a different Change ID. For convenience, let's call it nz+, meaning the nz's direct child.

Did I Tell You Not To Edit Changes?

If you are tempted to run jj edit zo, please, don't! Remember my heartfelt plea in Don't Edit. Do jj new zo instead. Remember that any change you perform in zo is automatically propagated. This way, you can review your resolution before finalizing it and easily undo any mistake.
So, do yourself a favor and create a Buffer Change:

jj new zo

This is what you get:

@  k        empty                    <- Your Buffer Change
│ ×  zm     My parent is conflicted
├─╯
×  zo       empty Merge conflict     <- The Change to fix
├─╮
│ ○  nz        merge-b
○ │  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b
│
~

Of course, even this Buffer Change will be conflicted. That's exactly the point: you want to resolve the conflict from there, so it must inherit it.

So, here's the idiomatic workflow for attacking conflicts:

  1. Run jj new targeting the conflicted Change.
  2. When you've solved the conflict, jj squash, as per the mini-workflow.
  3. If you want to start over, jj restore.

This is exactly the mini-workflow you saw in Don't Edit.

Resolving The Conflict

Edit the conflicted file file.txt. You will see this content:

Donec neque quam, dignissim in, mollis nec, sagittis eu, wisi.
Phasellus neque orci, porta a, aliquet quis, semper a, massa.
Etiam vel neque nec dui dignissim bibendum.
Nunc porta vulputate tellus.
Nunc eleifend leo vitae magna.
Sed id ligula quis est convallis tempor.
<<<<<<< conflict 1 of 1
%%%%%%% diff from: vrzponzu b12f7845 "Base of merge-a and merge-b"
\\\\\\\        to: mvxxwqoo e1acf668
-We will create a conflict here.
+We will create a conflict here: merge-a added this text.
+++++++ nzsunxwp 0004a47a
We will create a conflict here: merge-b added this text.
>>>>>>> conflict 1 of 1 ends
Nullam rutrum.
Curabitur vulputate vestibulum lorem.
Nullam eu ante vel est convallis dignissim.
Sed bibendum.
Aliquam posuere.

Let's learn how to read the conflict:

<<<<<<< conflict 1 of 1
%%%%%%% diff from: vrzponzu b12f7845 "Base of merge-a and merge-b"
\\\\\\\        to: mvxxwqoo e1acf668
-We will create a conflict here.
+We will create a conflict here: merge-a added this text.
+++++++ nzsunxwp 0004a47a
We will create a conflict here: merge-b added this text.
>>>>>>> conflict 1 of 1 ends

Interpreting Conflicts

The first and the last lines are obviously the conflict, with the total number of conflicts:

<<<<<<< conflict 1 of 1

>>>>>>> conflict 1 of 1 ends

All the lines above and below those boundaries are fine: both parents agree on them.
Between those 2 markers, you will find 2 elements:

  1. How the 1st Change wanted to change the common Base.
  2. The content of the 2nd Change.

Look:

%%%%%%% diff from: vrzponzu b12f7845 "Base of merge-a and merge-b"
\\\\\\\        to: mvxxwqoo e1acf668
-We will create a conflict here.
+We will create a conflict here: merge-a added this text.

Read it as:

%%%%%%% diff from: v "Base of merge-a and merge-b"       <- from the common Base
\\\\\\\        to: mvxxwqoo e1acf668                     <- the first Change merge-a
-We will create a conflict here.                         <- wants to apply this diff.
+We will create a conflict here: merge-a added this text.

Of course, - means deleting a line, + means replacing the line with some other content.

Resolving The Conflict

Here's a possible resolution:

Sed id ligula quis est convallis tempor.
We will create a conflict here: merge-a added this text, merge-b added this text.
Nullam rutrum.

*Very* interestingly, if you run jj st, you will see this message:

Hint: Conflict in parent commit has been resolved in working copy

Cool! That's how Jujutsu tells you:

Hey, pal! If you squash back this change, you fix the conflict.

Let's do that

jj squash

Perfect, it's all clear!
What about the Buffer Change? No worries, it's an empty, messageless Change. It will be automatically abandoned once you move away; it was a temporary working area from the start.

Conflict Resolutions Propagate

Look how beautiful:

@  l        empty
○  zm       My parent is conflicted
○  zo       Merge conflict
├─╮
│ ○  nz        merge-b
○ │  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b
│
~

Not only is zo clear: the conflict resolution propagated to zm and to @ too.

How cool is that?

Resolutions All The Way Down

That's a general rule. If you build on top of a conflict:

×  j     <- also in conflict
×  i     <- also in conflict
×  h     <- also in conflict
×  g     <- also in conflict
×  f     <- also in conflict
×  e     <- also in conflict
×  d     <- conflict
○  c
○  b
○  a

The moment you fix the conflict somewhere, the resolution will be propagated. Say you fixed the conflict in f, you'd get:

○  j
○  i
○  h
○  g
○  f     <- resolved here
×  e     <- conflict
×  d     <- conflict
○  c
○  b
○  a

Of course, ideally you'd fix the conflict where it originated. In this case, since you already have a resolution in f you can jj squash it to d:

○  j
○  i
○  h
○  g
○  f
○  e
○  d
○  c
○  b
○  a

I hope you got the idea.
Ready to see some more challenging cases?

Goal

In which you use your good, old merge tool.

Using the GUI

Before proceeding: do you prefer using a GUI tool such as kdiff3?
No problem:

jj resolve --tool kdiff3

Git Style Conflict Markers

Jujutsu is not an opinionated tool. Do you prefer Git-style conflict markers?
Configure your repo with:

jj config set --repo ui.conflict-marker-style git

and, presto!, you are back to your familiar format.

Goal

You will solve 2 cases:

  1. A rebase producing a single conflict. You'll resolve it by editing the file and you'll watch the resolution propagate to all descendants.

  2. A rebase producing 2 conflicts: you'll resolve them by editing Changes other than the conflicted ones, something you probably haven't done before in Git.

Goal

You see that you already know how to resolve a rebase conflict.

Rebase Conflicts

I bet this chapter will be a mere formality for you. You already know how to resolve a rebase conflict. Don't you think? Challenge yourself. Move the feat-1 branch on top of vr:

○  so
○  o        Merge conflict
├─╮
│ ○  nz        merge-b
○ │  m         merge-a
├─╯
○  vr       Base of merge-a and merge-b base
│ ○  y        empty  feat-2
│ ○  vw       empty
│ ○  x        empty
│ ○  vn       empty
│ ○  r        Rebase vr:: here
├─╯
│ ○  no       empty  feat-1
│ ○  vt       empty
│ ○  sy       empty
│ ○  u        empty
│ ○  vz       empty
├─╯
○  q        empty  main
◆  z    🔒  empty

Solution

If you are on top of the feat-1 branch:

jj new feat-1

then specifying the new base is enough:

jj rebase -o vr

Otherwise, if you rebase from a distance, specify both source and target:

jj rebase -s vz -o vr

Here we go, a conflict in s:

@  no        feat-1
×  vt
×  s        Will conflict
○  u
○  vz
│ ○  y        empty  feat-2
│ ○  vw       empty
│ ○  x        empty
│ ○  vn       empty
├─╯
○  r        Rebase vr:: here
│
~

As you see, the conflict arises in s and is propagated to all descendants.
How to resolve it? Remember to use the jj new && (jj squash || jj restore) workflow you saw in Don't Edit.

Solution

jj new -r s

You will find this conflict in feat-1.js:

<<<<<<< conflict 1 of 1
+++++++ unmwmutn 91e9daba "" (rebase destination)
  // This comment will get a conflict
%%%%%%% diff from: unmwmutn e6f1eb22 "" (parents of rebased revision)
\\\\\\\        to: nolxxptq 09ad5fbc (rebased revision)
+  // Handle theme color and mode
>>>>>>> conflict 1 of 1 ends

A possible resolution is to replace the whole section with:

  // Handle theme color and mode

Then:

jj squash

and you are done!

If you got it right, good job! You will see how the resolution propagate from s all the way down its descendants:

○  no        feat-1
○  vt
○  s        Will conflict
○  u
○  vz
│ ○  y        empty  feat-2
│ ○  vw       empty
│ ○  x        empty
│ ○  vn       empty
│ ○  r        Rebase vr:: here
├─╯
~

See? A walk in the park.

Multiple Rebase Conflicts

@  zq       empty
○  y          feat-2
○  unv
○  vw       Will conflict
○  xw
○  xl       Will conflict
│ ○  nz        merge-b
│ │ ○  m         merge-a
│ ├─╯
│ ○  vr       Base of merge-a and merge-b base
│ ○  r        Rebase here
│ ○  l        No idea why
├─╯
│ ○  no        feat-1
│ ○  vt
│ ○  s        Will conflict
│ ○  unm
│ ○  vz
├─╯
○  q         main
◆  zz   🔒  empty

Without further ado, rebase feat-2 on top of r:

Solution

jj rebase -s xl -o r

or

jj rebase -r xl:: -o r
@  zq       empty
×  y          feat-2
×  unv
×  vw       Will conflict
×  xw
×  xl       Will conflict
○  r        Rebase here
○  l        No idea why
│
~

Boom.

First Conflict

What's the conflict in xl?

Solution

jj show xl
Commit ID: efab44df7b62465e66edaec732f3a394090da0d3 Change ID: xlkttytwklswvptoywpyouywqwopyzzr Author : Arialdo (2026-07-06 17:47:05) Committer: Arialdo (2026-07-06 18:38:28)
    Will conflict

Created conflict in README.md:
    ...
   4    4: problem has finally been solved. This app lets you write things down
   5    5: and then cross them out. So cool.
   6    6:
   7    7: ## Features You Won't BeleiveBelieve
   8    8:
   9    9: - Adding tasks. You type words. They appear. Groundbreaking.
  10   10:
  11   11: - Completing tasks. Click a tiny box and watch your task get a line
  12   12:   through it, wich is basically like deleting it.
       13: <<<<<<< conflict 1 of 1
       14: %%%%%%% diff from: qmyyoorn bdcd7f2b (parents of rebased revision)
       15: \\\\\\\        to: roulqqom 67bb45ac "Rebase here" (rebase destination)
  13   16: +:quit
  14   17: +:qw
  15   18: +:wq
  16   19: +:q!
  17   20: +:x
  18   21: +:exit
  19   22: +:how to quit this editor?
  20   23: +:emacs
  21   24: +:emacs help
       25: +++++++ xlkttytw e08dfcc6 "Will conflict" (rebased revision)
       26:
       27: - Filters. All, Active, Completed. Choose your poison.
       28:
       29: - Inline editing. Made a typo? Double-click and fix it. So cool!
       30: >>>>>>> conflict 1 of 1 ends

Ha, that's embarrassing! Turns out that in Change r I was wrestling with Vim and accidentally mangled README.md. Oh well, I'm an Emacs user, but I should at least learn how to quit Vim.

You could resolve the conflict in x, but why keep those messy Vim commands in README.md? Why not tidy r up directly? Let's do that applying the conventional mini-workflow:

jj new r
jj restore -f r- README.md
jj squash


@ r           empty
│ ×  y          feat-2
│ ×  unv
│ ×  vw       Will conflict: deleted file
│ ○  xw
│ ○  xl       Will conflict: Vim
├─╯
○  r        Rebase here
○  l        No idea why
│
~

Cool! That solved the problem in xl and the resolution propagated to xw too.

This is probably your first time resolving a conflict without touching the conflicted commit itself.

There's still a conflict in vw. Time to tackle it.

Second Conflict

What is it about? If you inspect vw with jj show vw you will see:

Added conflict feat-2.js: 1: <<<<<<< conflict 1 of 1 2: %%%%%%% diff from: xwzyvpnm c8dcd10f (parents of rebased revision) 3: \\\\ to: xwzyvpnm c8dcd10f (rebased revision) 4: -import { useState } from "react"; 5: -import { Plus, Check, X } from "lucide-react"; 6: - [...] 104: -

105: -
106: - ); 107: -} 108: +++++++ vwqzzmtx 46b8d529 "Will conflict: deleted file" (rebased revision) 109: import { useState } from "react"; 110: import { Plus, Check, X, Pencil } from "lucide-react"; 111: [...] 306:
307: ); 308: } 309: >>>>>>> conflict 1 of 1 ends

which is how Jujutsu tells you:

Interesting! Who deleted feat-2.js?

jj log -r 'files("feat-2.js")'

×  y          feat-2
~  (elided revisions)
@  vw       Will conflict: deleted file
~  (elided revisions)
○  l        No idea why
○  q         main
│
~


jj show --summary q
...
A README.md
A feat-1.js
A feat-2.js

jj show --summary l
..

    No idea why

D feat-2.js

Here's the culprit!

@ r           empty
│ ×  y          feat-2
│ ×  unv
│ ×  vw       Will conflict: deleted file
│ ○  xw
│ ○  xl       Will conflict: Vim
├─╯
○  r        Rebase here
○  l        No idea why
│
~

Why did I do that? Well, the message says it all: I had no idea what I was doing... What if we remove that obviously mistaken Change?

Solution

jj abandon l

All clear!

○  y          feat-2
○  unv
○  vw       Will conflict: deleted file
○  xw
○  xl       Will conflict: Vim
○  r        Rebase here

Amazing! Solving a conflict by deleting past commits!

A Buffet of Conflict Resolution Approaches

With great power comes great fun.

Git has drilled one idea into our heads: resolving conflicts means editing conflicted files. It feels so self-evident that no alternative ever crosses our mind.

Would you believe that a rebase conflict can be resolved by running another rebase? Or by editing a file that has no conflicts at all? Sounds ridiculous, right?

Here's a small collection of real-world cases and their possible resolutions.

Wrong Operation

Use Case

Solution

 jj undo

There is no jj rebase --abort because, as you know by now, the rebase completed and there nothing pending to abort.

Wrong Source

Use Case

○  h  <- Paste
○  g
│ ○  f
│ ○  e
│ ○  d  <- Cut
│ ○  c  <- You should have cut here
├─╯
○  b
○  a
◆
$ jj rebase -s d -o h

×  f
×  e
×  d
○  h  <- Paste
○  g
│
│ ○  c  <- Left over
├─╯
○  b
○  a
◆

Solution

Add the missing Changes:

jj rebase -r c --after h

○  f
○  e
○  d
○  c
○  h
○  g
○  b
○  a
◆

Wrong Destination

Use Case

○  h  <- You should have rebased here
○  g  <- Paste
│ ○  f
│ ○  e
│ ○  d
│ ○  c  <- Cut
├─╯
○  b
○  a
◆
$ jj rebase -s c -o g

×  f
×  e
×  d
×  c
│ ○  h
├─╯
○  g
○  b
○  a
◆

Solution

Rebase onto the intended Change.

jj rebase -s c -o h

○  f
○  e
○  d
○  c
○  h
○  g
○  b
○  a
◆

Mistakes in Past Changes

This is a surprisingly common scenario: the fix lies in editing a Change that is conflict-free and wasn't even rebased.

Use Case

○  h
○  g     <- Function rename here
│ ○  f     <- Original function name used in this branch
│ ○  e
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆
$ jj rebase -s c -o h
×  f
×  e
×  d
×  c     <- Conflict on the function name
○  h
○  g     <- Function rename here
○  b
○  a
◆

Solution

The idea is that you can change your mind on that function rename: Jujutsu doesn't restrict your edits to the conflicting Changes during conflict resolution. Sometimes conflicts can be resolved from a distance.

$ jj new -g
$ sed -i 's/new_name/old_name/g' file.py
$ jj squash

Alternatively, you can abandon the Change entirely.

○  f
○  e
○  d
○  c     <- No more conflicts
○  h
○  g     <- Back to the old name
○  b
○  a
◆

Wrong Order

Sometimes it's not the changes that conflict: it's the order they end up in.

Use Case

○  h
○  g
│ ○  f  <- removes file README.md
│ ○  e  <- modifies file README.md
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆
$ jj rebase -r f -o h

○  f  <- removes file README.md
○  h
○  g
│ ○  e  <- modifies file README.md
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆
$ jj rebase -r f -o h

×  e  <- modifies file README.md
○  f  <- removes file README.md
○  h
○  g
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆

Solution

The two rebases result in e and f being swapped from their original order. An easy solution is to swap them again:

$ jj rebase -r e --before f

○  f  <- removes file README.md
○  e  <- modifies file README.md
○  h
○  g
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆

And the conflict is gone.

Move The Conflict Away

Here's an eccentric case: Jujutsu lets you move conflicts around.

Got a conflict on a branch after a rebase? Relocate it elsewhere.

Use Case

○  h
○  g
│ ○  f   <- Branch about PDF generation
│ ○  e
│ ○  d   <- Unrelated changes to authentication.ts
│ ○  c
├─╯
○  b
○  a
◆
$ jj rebase -s c -o h
×  f
×  e
×  d   <- Conflicts in file authentication.ts
○  c
○  h
○  g
○  b
○  a
◆

Solution

The idea is to move the conflicts somewhere else, separating PDF generation (which is conflict-free) and authentication.

There are several ways to do that.

With jj squash

$ jj new -r a -m "authentication"
×  f
×  e
×  d   <- Conflicts in file authentication.ts
○  c
○  h
○  g
○  b
│ ○  x authentication
├─╯
○  a
◆
$ jj squash -f d -t x authentication.ts

○  f
○  e
○  d
○  c
○  h
○  g
○  b
│ ×  x authentication
├─╯
○  a
◆

The file authentication.ts may or may not have conflicts, depending on where you moved it.

jj new followed by jj squash can be done in one step, using jj squash -o a, which will create a new Change on top of a:

jj squash -f d -o a authentication.ts

With jj split

$ jj split -r d -m "authentication"
×  f
×  e
×  d
×  x  authentication
○  c
○  h
○  g
○  b
○  a
◆
jj rebase -r x -o a
○  f
○  e
○  d
○  c
○  h
○  g
○  b
│ ×  x authentication
├─╯
○  a
◆

Artifact Files You Should Have Ignored

Use Case

    ○  h
    ○  g
    │ ○  f
    │ ○  e
    │ ○  d
    │ ○  c
    ├─╯
    ○  b
    ○  a
    ◆
    $ jj rebase -s c -o h

    ×  f
    ×  e
    ×  d
    ×  c
    ○  h
    ○  g
    ○  b  <- Binary files added the first time here
    ○  a
    ◆

Solution

The idea is:

It works like this:

    $ jj log -r 'files(".bin")'
    $ jj new -r b
    $ echo bin/ > .gitignore
    $ jj squash
    $ jj squash -f 'files("bin")' bin
    $ jj abandon

How Are Conflicts Handled?

Goal

You satisfy your nerdy curiosity and you dare to look under Jujutsu's hood.
You finally grasp what "conflicts are first-class citizens" means and see the light.

Two Philosophies

Git and Jujutsu differ fundamentally in where a conflict lives. Or even: in what they think a conflict is.

To begin with, in Git there is no such thing as a "conflicted commit". A conflict itself is not a Git object; it's a state that occurs when two changes cannot be merged. It blocks whatever operation you are performing until you completely resolve the problems. Git materializes conflicts by writing conflict markers like <<<<<<<, =======, and >>>>>>> in your files.

In Jujutsu, conflicts are first-class citizens, just like Changes are. As such, they are part of history and Jujutsu knows how to represent and manipulate them algebraically.

The practical consequence, as you saw before, is that no operation really fails because of conflicts: conflicts get recorded and you resolve them whenever you like. Best of all, "editing the broken files" is not the only way to resolve conflicts. In this page you will see an example about resolving a rebase conflict by running another rebase.

But "being first-class citizen" is a very vague concept. Let's try to get a better grasp of it.

What's Inside A Commit

We need a bit of background, but I promise we won't go too much into details.
In both Git and Jujutsu, a (non-conflicted) commit holds two distinct things:

Let me stress that a non-conflicted commit points to exactly 1 Tree. So, it targets a coherent photograph of the project. Conversely, if there's 1 Tree, it must be coherent and complete.

From this shared basis, Git and Jujutsu dramatically diverge.

Conflicts in Git

All operations in Git are atomic and transactional: they either succeed or roll back leaving the repository exactly as it was. The one notable exception is whenever there is a conflict.

When this happens, Git is stuck halfway through of an incomplete transaction. It would know where to put the next commit (after all, its parents are known) but it really cannot forge it: there's no way to create a coherent tree.

So, Git opts for the following:

Which tools are we talking about? Git spreads its helpers in 3 places:

Long story short, as long as this situation is in place:

Conflicts in Jujutsu

In case of a conflict, Jujutsu doesn't panic and doesn't bring out the big guns. It does not even generate any conflict marker. Instead, it creates the merge Change regardless. But how?

After all: nothing prevents us from storing more than one Tree.

Conflicted Changes

Yep. Changes in Jujutsu can carry more than 1 Tree. What are you staring at? Haven't you ever seen a little Change with leg braces before?1

A Jujutsu Change in general holds a collection of Trees.

This normalization lets Jujutsu consistently treat unconflicted and conflicted Changes. Again: the design principle is reducing exceptional cases to the bare minimum.

The Algebra Of Conflicts

An amazingly convenient way to represent and interpret those multiple Trees referenced by a Change is reading them as an algebraic expression with + (apply) and - (diff) signs. Imagine:

Don't worry, it's just basic arithmetic.

A failed merge between X and Y is stored like Y + (X − B). It means:

In the happiest scenario, Jujutsu will run that operation converging to a nice, conflictless result.
If it fails, well, it just records the trees of Y, X and B, meaning:

I need to do Y + (X − B), help me.

A Concrete Example

Say you have the 1-line file greetings.txt in 3 Changes: B, the common base, and 2 changes, X and Y. For the sake of simplicity, Change messages match the file content:

○    Y    hello mundo
│ ○  X    hola world
├─╯
○    B    hello world

Merge X and Y with jj new X Y:

×    M    hola mundo   <- conflict
├─╮
│ ○  Y    hello mundo
○ │  X    hola world
├─╯
○    B    hello world

Both changes touch the same line, so you get a conflict.
The merge Change M records three tree pointers: [X, Y, B] with this interpretation:

X + (Y − B)

Read it as:

Starting from B, we obtained Y thanks to the diff (Y - B).
We want to apply the same change to X.
So, the result is X + (Y - B)

Jujutsu will fail to reduce this to 1 single Tree, so the Change is conflicted. Yet it's there, and no fire alarm is wailing.

You can safely jj edit M and resolve the conflict. Take your time; there's no hurry.

Simplifications

You can interpret those expressions the same way you read sums and subtractions.
Say a Change has content: B + (X - B). It means:

Think about it: this is equivalent to X, isn't it?
Even further, the grade school student in you would recall that:

B + (X - B)

simplifies to

X

simply because B and -B cancel. You see how the 2 models match.

Wrongly Applied Diffs

In a sense, you could say that:

A conflict is a diff applied to the wrong base.

The diff (X - B) applies cleanly to base B:

B + (X - B) = X

Apply it to the wrong base:

Y + (X - B)

and you might get a conflict: Jujutsu will do its best, based on the actual file contents, to apply that diff. If it fails, it will store the Trees of Y, X and B hoping that:

Back to Grade School!

This idea of handling conflict via algebraic expressions is cool. Let's play with the greetings.txt file again. Consider this:

                                        the base    the diff
                                              |      |
                                              |      |
○  S    hola -> salut => salut world     S  = H + (S − H)
○  H    hello -> hola => hola world      H  = B + (H − B)
○  B    hello world

Now,

×  P'   conflict                         P' = S + (B − H)
○  S    hola -> salut => salut world     S  = H + (S − H)
○  H    hello -> hola => hola world      H  = B + (H − B)
○  B    hello world

A revert is just the inverse edit: so, it carries the inverse of H's diff, -(H - B) or (B − H).
The diff's natural base is H. But it landed on S.

P' wants to apply (B − H), which expects hola under it. Instead it sits on S, which says salut. A diff on the wrong base: conflict.

You know where the conflict comes from: a wrong base. Change it:

jj rebase -r P' -d H

○  P'   hello world                   P' = H + (B − H) = B
│ ○  S    salut world
├─╯
○  H    hola world
○  B    hello world

We went from S + (B − H) to H + (B − H). Now, +H and −H cancel, leaving B.The expression collapsed to a single Tree: the conflict is gone.

You resolved the conflict with a rebase, no file edits needed. Without the algebraic model, this would seem like magic.

Resolving Conflict by Abandoning Changes.

Abandoning the middle Change would have worked too. Going back 1 step:

×  P'   conflict                         P' = S + (B − H)
○  S    hola -> salut => salut world     S  = H + (S − H)
○  H    hello -> hola => hola world      H  = B + (H − B)
○  B    hello world

Drop S and P' lands on H:

jj abandon S

○  P'   conflict                         P' = H + (B − H) = B
○  H    hello -> hola => hola world      H  = B + (H − B)
○  B    hello world

Same cancellation, different move.

That's the idea: a conflict is just a diff parked on the wrong base. rebase, abandon, whatever move puts the diff back where the algebra simplifies it makes the conflict vanish.

Markers Are An Illusion

Have you noticed? When you visit a Conflicted Change, you'll find conflict markers. Yet conflict markers don't exist in the repository! They're generated on the fly, the moment you visit the Change. At that point Jujutsu makes its analysis, isolates the unresolved parts, and translates them into text, for your convenience.
When you save the edited file it goes the other direction: it parses the conflict markers and recreates the logical conflict state from them.

Because markers are generated on the fly, you can pick which style Jujutsu uses. There are 3 built-in styles.
In Git this would not be possible: there the markers are the conflict.


  1. https://www.youtube.com/watch?v=ocEuDQk7bEI

Goal

In the next few pages you'll discover:

  • How to create and manipulate local Bookmarks.
  • How Bookmarks won't follow your work, like they do in Git. Aargh!
  • How to bind Bookmarks to remote branches.
  • What Bookmark conflicts are and how to resolve them.

Bookmarks and Remotes

Managing branches with Jujutsu is easy. If you like, you could mentally translate what Git calls "branches" to "Bookmarks" and you are done: just a matter of naming.

But this would be a horrible oversimplification: it won't help reasoning about certain cases that are too common to be ignored, such as when Jujutsu tells you that a Bookmark has a conflict.
Huh? Conflicts in Bookmarks? Git has nothing like that...

So, the first thing I'll help you do is build a more solid and complete mental model.
Let's get started!

Goal

In which you learn that Bookmarks are like branches, but they may point to multiple commits.

Bookmarks are Not Branches

Let's dive right into the oddities. Jujutsu has gotten you used to them by now, right?

That should not sound that weird. After all, it's consistent with what you've already seen.
Remember how Jujutsu handles conflicts? All Changes, we saw, have a collection of Trees:

The same logic applies to Bookmarks. Think of Bookmarks as labels that point to one or more revisions. Normally, a Bookmark points to a single revision:

@  x  <--------- foo
│ ○  y
├─╯
○  q
◆  z

In this happy case the Git and Jujutsu models are extraordinarily similar. Jujutsu will display a history tree like:

@  x  foo
│ ○  y
├─╯
○  q
◆  z

When a Bookmark points to multiple revisions:

@  x  <--------- foo
│                /
│ ○  y <--------'
├─╯
○  q
◆  z

then there's a conflict and you'll need to resolve it, finding a way to make the Bookmark point to 1 revision only. Jujutsu will show you a log like this:

@  x  foo??
│ ○  y  foo??
├─╯
○  q
◆  z

In the next few pages you'll see how to resolve these cases. Spoiler: conflicts happen when both you and some colleague of yours on a remote have moved the same branch to two irreconcilable positions.

The good news is: the logic for resolving code conflicts is similar to those for resolving bookmark concflits. Even easier, in fact.


  1. If it weren't for the name "branch" itself, which always confuses newcomers. In Git a branch is both a pointer to a commit and a sequence of commits that diverges from the trunk.

Commands

As long as there are no conflicts, you can treat Jujutsu's local Bookmarks just like Git's local branches. Here are the commands to use:

GoalJujutsu commandGit command
Listjj bookmark listgit branch -v
Create newjj bookmark create <NAME>git branch <NAME>
New or Updatejj bookmark set <NAME> -r <REV>git branch -f <NAME> <REV>
Renamejj bookmark rename <OLD> <NEW>git branch -m <OLD> <NEW>
Movejj bookmark move <NAME> --to <REV>git branch -f <NAME> <REV>
Follow commitsjj bookmark advance <NAME> --to <REV>Automatically performed by git commit
Delete locallyjj bookmark forget <NAME>git branch -d <NAME>
Delete locally and remotelyjj bookmark delete <NAME>git branch -d <NAME> +
git push <REMOTE> --delete <NAME>

Hint: you can use a single letter for Bookmark-related commands. For example, instead of jj bookmark list, just jj b l will do.

Give these commands a try:

jj bookmark create my-branch -r x
jj bookmark set my-branch -r y
jj bookmark delete my-branch

Goal

In which you find out that there's no current branch following your work as you commit.

Moving Bookmarks

Did you notice the jj bookmark advance command? Mysterious, isn't it? It deserves a few words.

In fact, the only observation worth noting about local Bookmarks is about how and when Git and Jujutsu decide to move them on their own.

As a general rule, in Jujutsu Bookmarks stick to the Change they were created on. You can move entire trees of your history around and they'll move with it, rather then being left behind on old commits as in Git. In other words: Bookmarks stick to Changes, not to the underlying Git commits.

In Git you can get the same behavior with the --update-refs option of rebase 1. In Jujutsu, it's the default.

Bookmarks Don't Follow Commits

In Git there's a notion of the current branch which follows your work as you make new commits.

There's nothing of the sort in Jujutsu: a Bookmark never changes the Change it points to, unless you explicitly ask it to. Not even when you make commits. Look. Here I'm using jj commit, which is equivalent to jj desc followed by jj new:

jj git init
jj bookmark create main
jj commit -m "Hello"
@  w    (empty)
○  k    (empty) main Hello
◆  z    (empty)
echo 'print("Hello, World!")' > main.py
jj squash
@  zl   (empty)
○  k    main Hello
◆  zz   (empty)
jj commit -m "Hola"
echo 'print("Hola, Mundo!")' >> main.py
jj squash
@  n    (empty)
○  o    Hola
○  k    main Hello
◆  z    (empty)

main stayed behind, stuck to k!

Git does the opposite, and it is easy to see why: after git commit, if the branch was not moved onto your new commit, you would be left in detached head, a condition that Git considers dangerous. Jujutsu has a different opinion on danger.

Manually Updating Bookmark Positions

But isn't updating Bookmarks by hand just an annoyance? Indeed, this often makes newcomers frown. I promise that over time it's easy to come to appreciate the elegance.

I like to see it this way: I love how Jujutsu keeps nudging me toward code reviews. If you think about it, Jujutsu's Stash Workflow revolves around the idea of getting a chance to run a jj diff before squashing the edits into a Change.
With Bookmarks it's not that different. The tree manipulation is so frequent and pervasive that it makes to review the history before publishing the result.

On top of that, what normally happens is even simpler: Bookmarks are created only at the very last moment, right before creating the Pull Request. The idiomatic way to use Jujutsu is as a branchless versioning system.

Bottom line: jj advance is the operation equivalent to what Git automatically does every time you git commit. Being manual, it gives you the time to review the history before a push.

This is all you need to know about Jujutsu's local Bookmarks. Things get more interesting when remotes come into play.


  1. https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---update-refs

Goal

In which you find out that your local Git repository is itself a remote.

Every Repo is a Remote

You've already seen how Jujutsu favors consistency of the model: a minimal bunch of building blocks that compose well together. The same principle applies to remotes.

Git treats the local repository and remotes very differently. Jujutsu offers a simplified model: everything is a remote. You can imagine that your Jujutsu workspace is connected to a collection of remote repositories: your local Git repository is just one of them. Jujutsu doesn't make a big distinction.
Do you have origin hosted on GitHub or Codeberg? In the same way, you have the git remote: it just happens to be hosted locally, on your disk.

Keep this model in mind: it will help you understand why sometimes you'll see branches displayed as my-branch@origin or my-branch@git.

Pseudo-remote

Actually, I lied a bit.

The git repository is more of a pseudo-remote than a true remote. For example, you cannot fetch from and push to it. In fact, for practical purposes, Jujutsu treats the git pseudo-remote in a privileged way: it keeps it updated in real time. You can just imagine that it runs git fetch and git push automatically on every command you run.1

Don't take it too literally: use it as a mental model; it will help reasoning.


  1. For the sake of correctness: instead of fetch and push, Jujutsu dedicates 2 commands for syncing with the git pseudo-remote, jj git import and jj git export. You're unlikely to need them.

Goal

You will play with Git and Jujutsu, to convince yourself that Local Bookmarks and Git branches at aligned in realtime.

Local Bookmarks and Branches

@  n    (empty)
○  o    Hola
○  k    main Hello
◆  z    (empty)

Try creating a branch in Git:

git branch dev

Notice how a Bookmark was instantly created in Jujutsu.

@  n    (empty)
○  o    dev Hola
○  k    main Hello
◆  z    (empty)

This is also refleted in the output of jj bookmark list:

$ jj b l
foo: nwupqqun 9cad0562 (empty) (no description set)
main: kxrxykwo 709deefc Hello```

Try moving the Bookmark `main`:

```console
$ jj bookmark move main -t o
Moved 1 bookmarks to ovrsvuxw 7c6a6f6c dev main* | Hola
PS C:\prg\personal\jj\remotes\commit> jj
@  n    (empty)
○  o    dev main Hola
○  k    Hello
◆  z    (empty)

Look at Git:

$ git log --oneline --graph
* 7c6a6f6 (HEAD, main, dev) Hola
* 709deef Hello

The Git's local branches and the Jujutsu's local Bookmarks are always aligned, to the point that you can think of them as the same thing.

Can They Be Not Aligned?

Technically, it's possible to misalign local Git branches and Jujutsu Bookmarks, but these are degenerate situations: they won't happen in your day-to-day work.

But since you asked, if they happen to be diverge, you'd see a history tree like this:

@  x   foo@git 
│ ○  y   foo*
├─╯
○  q
◆  z    (empty)

You can interpret this situation as follows:

The two Bookmarks don't agree, and there is really nothing Jujutsu can do on its own to resolve the situation.

The intuition I suggest you develop is:

Jujutsu's Bookmarks indicate the position that Jujutsu would like the Git branches (and, in general, the remote Bookmarks) to take.

This interpretation will help you tackle the real case of remotes.

Goal

You create 2 repository with a shared remote, and you will learn to control remote branches.

Tracked and Tracking Bookmarks

A local Bookmark can be bound to a Bookmark on a remote repository. One will follow the other.
The two Bookmarks will have these roles:

BookmarkRoleMeaning
LocalTrackingMonitors the position of a remote Bookmark and tries to stay aligned with its position, following a git fetch
RemoteTrackedTries to align with the position of a local Bookmark during a git push.

With the next commands we'll create 3 repositories on the local filesystem:

RepositoryDescription
project-remoteA bare repository to use as a remote.
oursA clone of project-remote. It will be your working repository.
theirsA second clone of project-remote. It will be the working repository of a hypothetical coworker of yours.

It's an extraordinarily simple repository: only 3 commits. Yet, plenty to chew on anyway to learn a lot. Let's begin:

git clone ssh://git@codeberg.org/arialdo/jj-remote.git project-remote --bare
jj  git clone project-remote ours
jj  git clone project-remote theirs
cd ours

Displaying Remote Bookmarks

Both Git and Jujutsu show only the main branch.

$ git log --oneline --graph
* 7dac65a (HEAD, origin/main, main)

$ jj l
@  p    (empty)
◆  u    main
│
~

and both reveal the existence of the remote branches if asked to:

$ git branch -a
* (no branch)
  main
  remotes/origin/bar
  remotes/origin/foo
  remotes/origin/main

$ jj bookmark list --all-remotes
bar@origin: ossuomlo fa22ec04 (no description set)
foo@origin: stnkrwop 00687915 (no description set)
main: urvnvxtx 7dac65ac (no description set)
  @git: urvnvxtx 7dac65ac (no description set)
  @origin: urvnvxtx 7dac65ac (no description set)

Learn to read the jj bookmark list output. It tells you:

The indentation under main helps visualizing that it is tracking 2 remote Bookmarks: the one on git and the one on origin. These 3 bookmarks will do their best to stay aligned with each other. In fact, right now all 3 point to the same commit 7dac65ac.

Goal

You realize that Jujutsu moves things from sight for your benefit.
It also saves you from tampering with published commits.

Hidden Changes

Neither Git nor Jujutsu display the commits of the remote branches by default:

$ jj log
@  p    (empty)
◆  u    main
│
~

Why aren't all the Changes visible?

This is Jujutsu's philosophy: to be very sparing in the information it provides, to show only the strict minimum and omit what's superfluous. In this case, Git behaves the same way.

The idea here is that remote branches contain the work of other developers. Git and Jujutsu assume you're not interested in contributing, so they don't bother creating a local branch and, unless you ask explicitly, they don't even show their commits.

Immutable Changes

Jujutsu goes further: it also assumes you're interested in not interfering with your colleagues' work. So, it protects all the remote branches Changes by making them immutable. Ask Jujutsu to show all the descendants of the Change zzzzzzzzzzzzz, called root():

$ jj log -r 'trunk()::'
@  p    (empty)
│ ◆  s    foo@origin
├─╯
│ ◆  o    bar bar@origin
├─╯
◆  u    main
│
~

See how s and o are displayed with a diamond ? It means they're immutable. In fact, you could have verified this by using the Revset function immutable():

$ jj log -r 'immutable()'
◆  s    foo@origin
│ ◆  o    bar bar@origin
├─╯
◆  u    main
◆  z    (empty)

Modifying Immutable Changes

If you run a command that modifies any of those Changes, Jujutsu will stop you in time:

$ jj rebase -r o -o s
jj rebase -r o -o s
Error: Commit fa22ec047bc7 is immutable
Hint: Could not modify commit: ossuomlo fa22ec04 bar bar@origin | (no description set)
Hint: Immutable commits are used to protect shared history.
Hint: For more information, see:
      - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
      - `jj help -k config`, "Set of immutable commits"
Hint: This operation would rewrite 1 immutable commits.

You can always force your way through with --ignore-immutable.

But, in general, it's always better to declare your intention to work on those branches by creating a tracking Bookmark. That's what you will learn in the next page.

Goal

You learn how to reveal the hidden Changes of remote branches and how to make them mutable.

Tracking Remote Bookmarks

This bears the question: how to tell Jujutsu that you want to work on foo, so:

$ jj bookmark list --all-remotes
bar@origin: ossuomlo fa22ec04 (no description set)
foo@origin: stnkrwop 00687915 (no description set)
main: urvnvxtx 7dac65ac (no description set)
  @git: urvnvxtx 7dac65ac (no description set)
  @origin: urvnvxtx 7dac65ac (no description set)

You declare that you want a local Bookmark tracking foo@origin. Use this command:

$ jj bookmark track foo --remote=origin
Started tracking 1 remote bookmarks.
$jj log
@  p    (empty)
│ ○  s    foo
├─╯
◆  u    main
│
~

$ jj bookmark list foo
foo: stnkrwop 00687915 (no description set)
  @git: stnkrwop 00687915 (no description set)
  @origin: stnkrwop 00687915 (no description set)

Everything checks out:

Notice how, again, the 3 Bookmarks share the same name. This is a difference from Git, where a local branch can track a remote branch with a completely independent name.

Also note two things:

@  p    (empty)
│ ○  s    foo
├─╯
◆  u    main
│
~

You should have understood by now why, in the past exercises, right after a jj git clone, I always invited you to do:

jj bookmark track "*" --remote origin

It served to make all the Changes mutable.

Tracking bar

Alright, track bar too:

Solution

Sure! jj bookmark track bar --remote=origin

You could also have abbreviated it as:

jj b t bar

Now the log shows:

@  p    (empty)
│ ○  s    foo
├─╯
│ ○  o    bar
├─╯
◆  u    main
│
~

All the Changes (except for those of the main trunk) are now mutable. As expected, each local Bookmark is tracking a respective Bookmark on git and on origin:

$ jj bookmark list -a
bar: ossuomlo fa22ec04 (no description set)
  @git: ossuomlo fa22ec04 (no description set)
  @origin: ossuomlo fa22ec04 (no description set)
foo: stnkrwop 00687915 (no description set)
  @git: stnkrwop 00687915 (no description set)
  @origin: stnkrwop 00687915 (no description set)
main: urvnvxtx 7dac65ac (no description set)
  @git: urvnvxtx 7dac65ac (no description set)
  @origin: urvnvxtx 7dac65ac (no description set)

Time to discover what happens when you move Bookmarks around and you play with fetch and push.

Goal

You finally play with fetch and push.

Moving and Pushing Bookmarks

Build a commit on top of foo:

jj new foo
echo "hello world" > hello.txt
@  k
○  s    foo
│ ○  o    bar
├─╯
◆  u    main
│
~

As expected, the local Bookmark foo didn't follow you.

Moving Bookmarks

If you want to update foo so it points to the latest Change k, you have 2 options:

jj bookmark move foo -t k
jj bookmark advance

Misalignments

Look at what you get by moving foo:

$ jj log
@  k    foo*
○  s    foo@origin
│ ○  o    bar
├─╯
◆  u    main
│
~

Can you explain to yourself what this means? Let the output of jj bookmark list help you too:

Solution

jj bookmark list --all-remotes shows:

$ jj b l -a
foo: ksvskswq dd2904b3 (no description set)
  @git: ksvskswq dd2904b3 (no description set)
  @origin (behind by 1 commits): stnkrwop 00687915 (no description set)
  • The local Bookmark foo is on k, aligned with its corresponding Git branch (foo@git).
  • The Bookmark on the origin remote stayed behind (behind by 1 commits), on s.

If you omit the --all-remotes / -a argument, Jujutsu would be less verbose, showing you only the essentials:

$ jj b l foo
foo: ksvskswq dd2904b3 (no description set)
  @origin (behind by 1 commits): stnkrwop 00687915 (no description set)

stating: foo and foo@origin don't match anymore.
It's the very same information you see the log, with foo* and foo@origin:

@  k    foo*
○  s    foo@origin
│ ○  o
├─╯
◆  u
│
~

Pushing to Remotes

If you want foo@origin to align with your local foo, you need to use push. The asterisk in foo* is Jujutsu's way of telling you it's a good moment to do so:

jj git push --bookmark=foo --remote=origin

which you can abbreviate as:

jj git push -b foo

If you run it, you get:

Error: Won't push commit dd2904b35cee since it has no description
Hint: Rejected commit: ksvskswq dd2904b3 foo* | (no description set)

Ha, good boy! Jujutsu gives you a lot of freedom, locally, but when it comes to sharing your work with remote Git repositories it sticks with Git's conventions. In this case, Git isn't happy to receive commits without a description, and Jujutsu avoids getting it worked up.
Fine, add a description:

jj desc -m "Hello, world"
jj git push -b foo

Log again and admire how Bookmarks are nicely aligned:

$ jj
@  k    foo Hello, world
○  s
│ ○  o    bar
├─╯
◆  u    main
│
~

$ jj b l
bar: ossuomlo fa22ec04 (no description set)
foo: ksvskswq 938fe989 Hello, world
main: urvnvxtx 7dac65ac (no description set)

Goal

You learn to update from a remote.

Fetching

Let's find out how the repository evolved, from your coworker perspective. Go into the theirs repository:

cd ../theirs
jj
@  s    (empty)
◆  u    main
│
~

Only main is visible. Makes sense, your coworker hasn't yet declared any intention to work on foo and bar, so those remote branches are hidden:

$ jj bookmark list -a
bar@origin: ossuomlo fa22ec04 (no description set)
foo@origin: stnkrwop 00687915 (no description set)
main: urvnvxtx 7dac65ac (no description set)
  @git: urvnvxtx 7dac65ac (no description set)
  @origin: urvnvxtx 7dac65ac (no description set)

Declare that you want to track them:

jj bookmark track "*" --remote=origin
@  sz   (empty)
│ ○  st   foo
├─╯
│ ○  o    bar
├─╯
◆  u    main
│
~

There they are, as expected.
Notice that your coworker doesn't see your Change k yet:

○  k    foo Hello, world

This is consistent with Git: a local and a remote repository align only during fetch and push operations. This means that:

foo@origin: stnkrwop 00687915 (no description set)

shall be interpreted as the last known position of the remote Bookmark foo on origin.

Fetching From a Remote

I imagine you know what happens if you run a fetch:

jj git fetch
@  sz   (empty)
│ ○  k    foo Hello, world
│ ○  st
├─╯
│ ○  o    bar
├─╯
◆  u    main
│
~

There's k! Notice how foo (and consequently foo@git) aligned with the new position of foo@origin:

$ jj bookmark list foo
foo: ksvskswq 938fe989 Hello, world
  @git: ksvskswq 938fe989 Hello, world
  @origin: ksvskswq 938fe989 Hello, world

In essence:

jj git fetch [--remote=origin]

tells Jujutsu:

Why did I say "If possible"? Because things don't always go this smoothly. Indeed: enough with the happy paths! Time to shake things up a little.
In the next page you'll try to sort out a situation where you and your fictional coworker have updated the same remote Bookmark in 2 irreconcilable ways.

Goal

You learn to resolve those cases when 2 developers try to move the same remote Bookmark in 2 different, incompatible positions.

Bookmark Conflicts

In general, as long as updating a Bookmark's position means moving it forward along the history tree, everything goes smoothly.

It's less natural for a Bookmark to go backwards in history, or even to jump to a parallel branch. If you try to do that with Git, it won't complain, but your next git push will likely be rejected, with an error such as:

! [rejected]        feature/my_feature_branch -> feature/my_feature_branch (non-fast-forward)
error: failed to push some refs to 'ssh://git@yourrepo.com/repo/my -project.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Jujutsu tries to anticipate the problem and shows an error even before the push, already when you attempt to move the Bookmark.
In the theirs repository, try moving the foo Bookmark from k to o:

@  sz   (empty)
│ ○  k    foo Hello, world
│ ○  st
├─╯
│ ○  o    bar
├─╯
◆  u    main
│
~
$ jj bookmark move foo --to o
Error: Refusing to move bookmark backwards or sideways: foo
Hint: Use --allow-backwards to allow it.

Your Coworker Forces Their Way

If you really want to win this fight, you can always use the --allow-backwards parameter. Let's suppose that's the path your coworker decides to take:

jj bookmark move foo --to o --allow-backwards
@  sz   (empty)
│ ○  k    foo@origin Hello, world
│ ○  st
├─╯
│ ○  o    bar foo*
├─╯
◆  u    main
│
~

2 things to notice:

jj git push -b foo
@  sz   (empty)
│ ○  k    Hello, world
│ ○  st
├─╯
│ ○  o    bar foo
├─╯
◆  u    main
│
~

What Happens in Your Repository

Go back to ours repository. Unaware of the Bookmark's move, you keep working on foo, adding another change:

jj new -m "Hola, mundo"
echo "Hola, mundo" > hello.txt
@  ol   Hola, mundo
○  k    foo Hello, world
○  s
│ ○  os   bar
├─╯
◆  u    main
│
~

You even reckon it's a good moment to advance foo from k to ol:

jj bookmark advance
@  ol   foo* Hola, mundo
○  k    foo@origin Hello, world
○  s
│ ○  os   bar
├─╯
◆  u    main
│
~

Right, Jujutsu marks foo with a * inviting you to align foo@origin with a push:

$ jj git push -b foo
Changes to push to origin:
  bookmark: foo [move forward from 938fe989965e to 0952644707a7]
Warning: The following references unexpectedly moved on the remote:
  refs/heads/foo (reason: stale info)
Hint: Try fetching from the remote, then make the bookmark point to where you want it to be, and push again.
Error: Failed to push some bookmarks

Rejected.

Conflicts

Here's the log:

@  ol   foo?? foo@git Hola, mundo
○  k    Hello, world
○  s
│ ○  os   bar foo?? foo@origin
├─╯
◆  u    main
│
~

It also helps to list the bookmarks:

$ jj bookmark list foo
foo (conflicted):
  - ksvskswq 938fe989 Hello, world
  + olnvmuqu 09526447 Hola, mundo
  + ossuomlo fa22ec04 (no description set)
  @git (behind by 1 commits): olnvmuqu 09526447 Hola, mundo
  @origin (behind by 3 commits): ossuomlo fa22ec04 (no description set)
Hint: Some bookmarks have conflicts. Use `jj bookmark set <name> -r  <rev>` to resolve.

OK, calm down: there are 4 instances of foo now. How should you interpret the situation?

What to do? Undecided, Jujutsu lets foo target both. That's why it displays a ??.

Apparerently the situation can't be reconciled automatically. You'd better talk to your coworker and clarify what they had in mind.

Resolve a Bookmark Conflict

One possible solution for having in foo both the changes in ol and those in os is to create a merge Change:

jj new -r ol -r os -m "Merge"
@    p    (empty) Merge
├─╮
│ ○  os   bar foo?? foo@origin
○ │  ol   foo?? foo@git Hola, mundo
○ │  k    Hello, world
○ │  s
├─╯
◆  u    main
│
~

The new Change p looks like a good place to put the foo Bookmark:

jj bookmark move foo -t p
@    p    (empty) foo* Merge
├─╮
│ ○  os   bar foo@origin
○ │  ol   Hola, mundo
○ │  k    Hello, world
○ │  s
├─╯
◆  u    main
│
~

Lovely! There are no more conflicts, only the invitation to push:

jj git push -b foo
@    p    (empty) foo Merge
├─╮
│ ○  os   bar
○ │  ol   Hola, mundo
○ │  k    Hello, world
○ │  s
├─╯
◆  u    main
│
~

All clear. Your coworker is forgiven.

Human Friendly Log

jj config edit --user

and add:

[templates]
log = "simple_log"

[template-aliases]
empty_commit_marker = "label('empty', '▢')"
simple_log = """
  if(self.parents().len() > 1, raw_escape_sequence("\b\b")) ++
  separate(" ",
    pad_end(
      4,
      separate("/",
        self.change_id().shortest(),
        if(self.divergent() || self.hidden(), self.change_offset())
      )
    ),
    if(self.empty(), empty_commit_marker),
    self.bookmarks(),
    self.description().first_line()
  )
"""

Don't bother trying to understand the code above just yet. We will get to it.

And yes! Jujutsu provides you with a real purely functional programming language for defining templates. That's crazy powerful!

Pulling the Rug Out Under Oneself

Here's the situation:

$ jj edit x

@    vz          empty
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  v          Click sets a X
○ │  s          Square has state
├─╯
○  n            Square is interactive
○    ru         Board() invokes Square()

You want to abandon the merge Change vz, which coincidentally, is also the Current Change:

$ jj abandon -r vz

@    x          empty
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  v          Click sets a X
○ │  s          Square has state
├─╯
○  n            Square is interactive
○    ru         Board() invokes Square()

Uh! That was unexpected! The merge is still there! And with a different Change ID. Why is that?

This is what Jujutsu thinks:

So, another merge is generated.

This also happens in general: you can abandon your Current Change, but this will be immediately replaced by a new one.

When Jujutsu Deletes Changes Without Any Advice

There's more. Look what happens if you move somewhere else:

$ jj edit n

○    o          Play instructions
○    kx         Play manual
│ ○  zx         Click alternates Xs and Os main
│ ○  v          Click sets a X
│ ○  sn         Square has state
├─╯
@  n            Square is interactive
~

Woop! Jujutsu evaluated that the newly created merge Change was completely unused: not only was it empty, it was also messageless. Therefore, as you moved await from it, it abandoned it.

It's The Right Thing To Do

This might seem odd at first, but it's actually an astute design choice: Jujutsu tracks only what you explicitly scoped, preventing your repository from being cluttered with empty, unnecessary Changes.

Try this: target any Change in your tree history and visit it using jj new, instead of jj edit:

$ jj new n

@  m            empty
│ ○  o          Play instructions
│ ○  kx         Play manual
├─╯
│ ○  zx         Click alternates Xs and Os main
│ ○  v          Click sets a X
│ ○  s          Square has state
├─╯
○  n            Square is interactive
○    ru         Board() invokes Square()
├─╮
○ │  ym         fix name: Square -> Board
○ │  x          WIP delete me
○ │  yz         LICENSE
○ │  ws         A board full of X
├─╯
○  kk           Displays X X
○  wx           Fails
○  rx           Scaffold applicatio
◆  zz      🔒   empty
~

Jujutsu created a Change m, as a child of n. Move somewhere else:

$ jj new rx

@  sy           empty
│ ○  zx         Click alternates Xs and Os main
│ ○  v          Click sets a X
│ ○  sn         Square has state
│ │ ○  o                Play instructions
│ │ ○  kx               Play manual
│ ├─╯
│ ○  n          Square is interactive
│ ○    ru               Board() invokes Square()
│ ├─╮
│ ○ │  ym               fix name: Square -> Board
│ ○ │  x                WIP delete me
│ ○ │  yz               LICENSE
│ ○ │  ws               A board full of X
│ ├─╯
│ ○  kk         Displays X X
│ ○  wx         Fails
├─╯
○  rx           Scaffold applicatio
◆  zz      🔒   empty

Voilà, you are in a new Change on top of rx. And the previous Change has been cleaned up.

Hint

Visit Change using new, not edit: it's safer, because you never risk to unintentionally edit them.

Emacs' Undo

Here is how undo works in most of the editors. For each change, a state is recorded in some internal structure:

s1 --> s2 --> s3 --> s4 --> s5
                            |
                            last state

You can undo by going back in time, say to s2:

s1 --> s2 --> s3 --> s4 --> s5
        |
        undo reinstating this

You are still able to move forward in time (that is, redoing):

s1 --> s2 --> s3 --> s4 --> s5
               |
               redo reinstating this

But as soon as you make a modification, you break the undo history:

s1 --> s2 --> s3 ~~> s4 ~~> s5  // oh no! This branch is gone
                \
                 sx --> sy

Emacs won't delete this branch.
It undoes by appending new operations. In this sense, it is closer to git revert than git reset. So, if this is your undo history:

s1 --> s2 --> s3 --> s4 --> s5

undoing up to s2 means restoring s4, s3 and s2 re-appending them to the history:

s1 --> s2 --> s3 --> s4 --> s5
                              \
                               `--> s4 --> s3 --> s2

So, if you make changes from there, no information will be lost:

s1 --> s2 --> s3 --> s4 --> s5
                              \
                               `--> s4 --> s3 --> s2 --> sx --> sy 

Emacs ships with a little built-in package that displays this as a Git-like history tree:

s1 --> s2 --> s3 --> s4 --> s5
               \
                sx --> sy

and lets you navitate the history:

screen cast of a Emacs
buffer moving back and forth in the history tree with vundo

Jujutsu provides similar features with the subcommands of jj op.