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 was written by a human (hello, I’m Arialdo), not AI, because I respect your intelligence.
It is available in HTML and PDF.

It aims to be:

  • Lean: because your time is precious.
  • Practical: because less talk, more action is 👌
  • Incremental: because you want to start using jj from day 0.
  • No hand-holding: because discovering is more fun than being told.
  • Unorthodox: because Jujutsu is extravagant, and ordinary tutorials are boring.

It’s free1 (as in beer) because I’m merely giving back what I was given. And because I love beer.

If you’d like to pay for it, please donate to a charity of your choice.

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

It’s long, but it’s mostly commands and terminal output. You’ll be fine.


English isn’t my native language. If you want to help, PRs are welcome!


  1. CC BY-SA 4.0 - https://creativecommons.org/licenses/by-sa/4.0/deed.en

What’s This Book About?

In your affair with Git, do you recognize yourself here?

  • “Detached HEAD” gives you anxiety.
  • git reset --soft, --mixed, and --hard blur together.
  • You rebase with crossed fingers, or you don’t dare.
  • The reflog remains an unexplored land.
  • When you make a mess, you don’t know how to get back.

Or maybe you’ve mastered Git and are intrigued by extravagant ideas like:

  • Working on multiple branches simultaneously.
  • Rebasing interactively as you type.
  • Sending changes to past and future commits.

Whether Git scares you, or you’ve hit its ceiling and crave more power, Jujutsu might forever change your relationship with version control.

What’s Jujutsu?

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

Git is amazing, but makes its model your problem. Jujutsu offers a ridiculously simple model you’ll pick up in a breeze, yet unlocks workflows you never knew were possible.

Best of all, it’s 100% Git-compatible. Mix git and jj commands freely in the same project; your colleagues won’t even notice.

There’s no need to switch. Keep your current faith: no conversion is required.

Is This Book For You?

This book targets two radically different readers.

The Git Wizards, who play with reflog blindfolded and who crave more power. They will find it in Jujutsu.

The Scared Beginners, who freeze at conflicts and dread rebasing. They’ll finally gain autonomy with Jujutsu.

This contradiction stems from Jujutsu’s surprising nature: impossibly simple yet spectacularly powerful. It’s Dr. Jekyll and Mr. Hyde collaborating amiably in the same lab. A design miracle.

I hope to inspire experts to invent reckless new workflows, and give the frightened ones the joy of versioning on their own, with confidence.

Ultimately, this book, like Jujutsu, isn’t for everyone. It’s for Wizards and Beginners sharing the curiosity and bravery to step outside the box.

Conventions

Important

Panels like this one give you the gist of a chapter before you dive in.

echo Command snippets look like this:
jj new -m "Feel free to type them into your terminal"

Warning

I’ll occasionally throw in a quiz.
Can you guess what the panel below is for?

Tip

Right! It’s the answer!

In HTML, it’s collapsible. In the PDF, not peeking is on you ;)

Happy versioning!

Acknowledgments

I’m grateful to Ferdinando Santacroce for tricking me into writing this book, and to Paolo “Nusco” Perrotta for his outstanding suggestions.

Thanks to the reviewers Alberto Acerbis, Mickey Petersen, sm4llth1ng, Francesco Serra, Emanuele Firmani, Giuseppe Caferra and Luca Giovenzana.


The cover is an original hand-drawn illustration by Nanou1.


  1. Nanou is a young local artist and does not have an online presence.

At a Glance

This book assumes you have a working knowledge of Git. It will help you explore Jujutsu step by step, from the basics to more advanced topics.

Here’s a bird’s-eye view of the path ahead:

  • Why Jujutsu exists and how it differs from Git.
  • The happy path: committing, moving things around, and other everyday moves.
  • Living with conflicts and resolving them.
  • Remote branches and collaborating with others.

The very first, few pages are mostly conceptual and motivational: they help you develop the mental model before you touch the keyboard.
The rest of the book is heavily hands-on, with a very few theoretical interludes to explore the ideas behind the tool.

For the Impatietn

If you can’t wait to get your hands dirty, jump straight to the Quick Start in the Appendix for a little taste.

Why?

Many have little motivation to give Jujutsu a try because “Git works just fine”. I guess you’re here because you’re curious to see what Jujutsu brings to the table. So the next natural question is why Jujutsu exists and what problems it solves. Spoiler alert: it solves problems you didn’t know you had1.

3 Things to Improve

In interviews, I like to ask: if you had a magic wand, what three things would you change about your favorite tech? Here’s how I would answer about Git.

1. Git Has Too Many Building Blocks

The more building blocks, the harder it gets:

  • Changes live in 3 distinct places: working copy, index, and repository.
  • Consequently, git restore can be used with either --staged, --worktree or both, getting to different results.
  • Same for git reset with --soft, --mixed and --hard.
  • Stashes live in a side-store with separate rules.
  • Pseudo-refs like MERGE_HEAD pop up during conflicts.

I’d ask for a model with fewer building blocks, but more composable: powerful as ever, yet with intimidating operations made straightforward.

2. Inconsistent interface

“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 favorite inconsistency is the index. Pro Git calls it the “proposed next commit snapshot”. Is it the next commit? Not quite: it’s a quasi-commit. It has a SHA1, but branches can’t target it. You can’t git show it. You rebase commits, but stash the index. It’s a special case requiring special commands.

If I had a magic wand, I would ask for no exceptions, so all commands would be consistent. Indeed, Jujutsu removes all the special cases. Want to stash? It lets you, reusing the building blocks you have already learned. Welcome, composition.

3. Leaky abstraction

Git stops fighting you once you learn its internal model. When people struggle, it’s often because they built a poor intuition of it.

If I had a magic wand, I’d want an opaque abstraction capturing what I mean, quietly handling the how.

Take “undoing stuff.” In Git, to undo a command you first need to know how it works under the hood, so you can derive its inverse:

  • You did git rebase -i. 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}

What’s the equivalent in jj? jj undo.

  • Undo a fetch? jj undo.
  • Undo a rebase, a merge, a commit, a conflict resolution? jj undo.

Consistently jj undo, no matter the change. You will wonder how you could have worked for so many years without jj undo.

We’ll touch on this later. First, let’s explore a well-designed piece of Git to 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

Important

You see how Jujutsu takes git commit --amend and makes it universal.

Found a typo in your last commit?

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

Easy: git commit --amend. Voilà!

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

This is an elegant abstraction. Commits are immutable, so amend actually creates a new commit with the same parent, then resets the branch:

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

Git pulls a fast one, giving you the impression the last commit was editable. 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! This is the beauty of amend: it captures your intention and hides the mechanics. Neat!

Leaky abstraction

How do you amend the second-to-last commit? There’s no git commit --amend HEAD~1. You need a terrifying sequence of checkouts, cherry-picks, and branch resets. The abstraction tears, revealing the gears beneath.

What about amending Y here?

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

Oh beautiful abstraction, where art thou?

More consistent abstraction

When you amend, your intention is to edit a commit. The underlying metaphor is: The last commit is editable.

Can we make this universal? Imagine jj edit modifying any point in history.

  • Want to amend C? jj edit C.
  • Want to amend Y? jj edit Y.

We return to intuitive UX. jj edit captures the what and manages the internal how. Editing Y cascades changes to Z, E, and F automatically.

In practice

  • Install Jujutsu.
  • Run jj git init in your Git repo. It safely coexists with Git.
  • Need to amend a commit deep in history?
jj edit <SHA1>
# Do your changes
git checkout <your-branch>

I’m lying slightly. You might hit conflicts, so keep reading. But I hope you just had your first a-ha moment.

Rebasing with Git

Honestly, if I had a magic wand, I would also improve git rebase. It’s a killer feature, but feels broken. It’s bold claim, so let me unpack that.

Conflicts

Git instills serenety: it is safely append-only and it treats your filesystem as sacred. You can manipulate history relaxed, knowing Git is never destructive.

Until a rebase conflict. Git then enters danger mode. It basically stops working, offering two exits only: abort, or solve conflicts now, in the exact order demanded.

You cannot do anything else. You can’t cherry-pick or bisect to help fix the code. Got an urgent hotfix? You can’t pause the rebase and checkout another commit. Your project is broken and Git abandoned you exactly when you need it most. No wonder many stick to merges.

Can we de-escalate this?

I’d want a tool that:

  • Reveals all rebase conflicts at once.
  • Stays functional during conflicts.
  • Lets me postpone resolution.
  • Allows solving conflicts using all available features.

Jujutsu is that tool. Conflicts are first-class citizens. No stress, no drama: Jujutsu stays fully functional. You can solve conflicts by editing files, editing different commits, or even performing another rebase.

In practice

Next time you rebase, copy your repo, run jj git init, and try:

jj rebase --source <FROM> --onto <TO>

where:

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

To move 3-4-5-6 on top of 11:

jj log
○  11          <---- paste
○  10
○  9
│ ○  8
│ ○  7
├─╯
│ ○  6
│ ○  5
│ ○  4
│ ○  3         <---- cut
├─╯
~
jj rebase -s 3 -o 11
○  6
○  5
○  4
○  3
○  11          <---- paste
○  10
○  9
│ ○  8
│ ○  7
├─╯
~

If there are conflicts, jj log shows them:

○  11
│ ×  6           <---- conflict
│ ×  5           <---- conflict
│ ○  4
├─╯
~

You can fix conflicts exactly where they are:

jj edit 5

Fixing 5 often propagates the resolution to 6.

Whenever you need, jj undo will be your best friend.

Log and Identity

Important

You discover Change IDs: immutable identifiers referencing commits as they evolve.

Git amend is a nice abstraction, but still a little abstraction leak. See this:

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

Within Git amend’s metaphor, you modified the last commit. Yet git log insists on screaming at you:

That’s not the same commit! It was ddb8cf5 before, it’s bea2735 now!

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

We often communicate above the SHA1 level, just saying “the last commit”. Git hided the old commit, for a good reason; why not hide the identity change too?

Immutable identity

Enter Jujutsu:

jj log
@  mttlksm arialdo@ik.me 2026-06-06 15:55:54 2caa192jj
│  (empty) (no description set)
○  **v**syzrms arialdo@ik.me 2026-06-06 15:55:04 ddb8cf5
│  Log and Identity
...

Focus on the last item. The rightmost ddb8cf5 is the Git SHA1. The vsyzrms (v for short) on the left is the Change ID, an immutable reference to “the last commit”:

Let’s amend, Jujutsu style:

jj edit v
echo "fix typo" >> myfile
jj log
@  **v**syzrms arialdo@ik.me 2026-06-06 15:55:04 bea2735
│  Log and Identity
...

Interesting! While the SHA1 changed, the Change ID remained stable. You can keep calling that Change v forever.

Git does this with branches (main points to new commits but remains main). Jujutsu extends this to all commits.

Reverse Hex

Will Change IDs and Git Commit IDs clash? They can’t! Git SHA1s use hex symbols (0-9, A-F), Change IDs use reverse hex (Z-K). They never overlap. Smart!

Show me less

Notice how jj log grays out most ID characters?

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

Git only displays the first 7 characters of SHA1s. Why 7? Because it’s usually enough to prevent ambiguity. In fact, Git could use less, but it doesn’t try to optimize.

Jujutsu goes a step further: if 2 chars prevent ambiguity, it grays out the rest.

Take it to the limit

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

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

This style:

  • Hides the Git SHA1 entirely.
  • Displays only bare minumum of Change IDs.
  • Use 1 line per commit.

I’ll use this template from now on. Check Human Friendly Log to have it too.

In practice

  • Run jj git init in any Git repo.
  • It’s safe: it lives in a .jj directory Git will ignore.
  • Keep using Git as usual.
  • When you need, use Jujutsu as a Git client on steroids.
  • Try jj log. It’s harmless and often cleaner than git log.

Let’s Dive In

During our amend, you never committed, yet you amended commits. How can it be?
Time to find out!

Pulling Teeth

Here’s my formula to learn Jujutsu:

  1. Clear the slate. Drop your Git mental model.
  2. Embrace a few new assumptions. They’ll seem repugnant at first. Suspend judgment: they pay off.
  3. Learn the very few building blocks.
  4. Profit.

After this, you will:

  1. Be astounded by amazing, extravagant workflows. Learn them, invent new ones.

Let’s tackle step 2. Prepare to cringe.

Git Was An Adorable Maverick

As an old programmer, I clearly remember Git’s debut sparking angry protests:

  • “A local copy of the whole history? It’s so stupid!”
  • “Branches aren’t real branches!”
  • “Rewriting history? Heresy!”
  • “No central server! The world will collapse into anarchy!”

It was a fun show and I never expected a sequel.

Jujutsu, More Maverick Than Git

Let’s pull this tooth (feel free to yell at clouds):

  • It force-pushes by default.
  • It commits by itself.
  • Even status and log commit.
  • Rebases happen automatically.
  • By default, Git ignores files. Jujutsu tracks them.
  • Commits are mutable.
  • Detached HEAD is the standard.
  • You idiomatically commit before coding.
  • It deletes commits without warning.

Jujutsu seems to be designed to upset Git fans, and by what it removes:

  • It’s branchless.
  • There’s no index.
  • No stash.
  • No merge command.
  • No cherry-pick, reset, or add.

Given this baseline, you might be surprised that Jujutsu users:

  • Often work in the middle of a branch, not at its tip.
  • Send changes forward and backward through time.
  • Work on multiple branches at once.
  • Develop features simultaneously, then dispatch changes to their branches.

If you are horrified, wait until you realize you might love every bit of it.

Still here? Good. Ready for step 3. Let’s learn the building blocks and finally profit.

Elements of a Grammar

Git has 160+ commands. Some are plumbing (hash-object, write-tree); some are porcelain (commit, log), built on top of the underlying plumbing commands.

Some porcelain operations are combinations of others. For example, pull is fetch + merge/rebase. cherry-pick is diff + apply + commit. A rebase is basically a series of cherry-picks followed by a reset.

Here’s a challenge. What basic porcelain commands would suffice to build an entire versioning system?

Think about it.

Basic Commands

If you thought of CRUD1, you nailed it! To manipulate the Changes of your history tree, you need:

OperationCommand
Createnew
Readshow
Updateedit
Deleteabandon

them.
Notice I wrote “Changes”, not “commits”. Jujutsu creates a different commit when you amend, referencing it with the same Change ID so treating it as the same thing. That thing is a Change:

“a commit as it evolves over time” — Glossary.

Beyond CRUD, you’ll also want to:

OperationCommand
Move a Changerebase
Move edits to another Changesquash
Copy files between Changesrestore
Display the history treelog
List / undo / redo operationsop log / undo / redo

That’s… basically it? Learn these and you master Jujutsu.

Macro Commands

Like in Git, some commands combine others:

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

I swear, the first two sets alone will take you further than Git ever did.

Open a terminal, grab your keyboard, and prepare to get your hands dirty.


  1. https://en.wikipedia.org/wiki/Create,_read,_update_and_delete

Recommendations Before Typing

  • We’ll use throwaway repos. When you test commands on real ones, make a copy first. Jujutsu is powerful; mistakes can leave you far from where you started.

  • We’ll do many exercises. Some will feel artificial, not something you’d typically do at work.
    We’re here to learn how Jujutsu thinks by pushing it to the limit. Contrived cases help you internalize the underlying model. Eventually, practical cases becomes muscle memory.

  • Finally, Change IDs are random. When yours don’t match mine, please adjust your commands accordingly.

Init

Important

Where you discover why a brand new repository already contains 2 commits.

jj git init adds Jujutsu to an existing Git repository. If there isn’t one, it creates it.

Let’s start an empty repo:

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

Multiple backends

Why isn’t it just jj init? Jujutsu is modular and supports different storage backends. We’ll stick with Git, so never mind.

Won’t jj and Git clash?

Relax. Jujutsu lives in the hidden .jj directory, which Git ignores. You probably noticed the hint:

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

It makes sense: git clean deletes ignored directories.

jj git also offers sub-commands like jj git clone and jj git fetch1.

Want To Remove Jujutsu?

Delete .jj and Jujutsu is gone2.

Log

Try a git log:

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

As expected. Git shows no history without commits.
What about jj log?

jj log
@  x           empty
◆  z       🔒   empty

Jeez! Where Git fatally errors out, Jujutsu shows 2 Changes! How?

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)

zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz looks like a Null Object Pattern: it is the “root Change”, the black hole every repository originates from. In Git, it’s implicit, meaning orphaned branches need special commands. Not in Jujutsu: zzzz is a loving parent, giving every orphan a home.

Fine, zzzz makes sense. What about the Change on top of it? This requires a bit of theory.

Collapsing areas

One way to understand Git is visualizing its distinct areas and how commands operate on them3:

In Jujutsu, stash, index, working area, and local repository all collapse into a single area:

different areas in Jujutsu

The @ in:

@  x           empty
◆  z       🔒   empty

represents the current file system. But it’s also a commit in your repo. The two just happen to be the same.

Why is it already there? Well, when you run git init, the empty working copy already existed: it’s only fair this is reflected in the log.

What’s inside x?

Is x similar to zzzzz? What does it contain?

Warning

How can you find out?

Tip

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 descriptionless and empty, but it’s a legit commit with its own SHA1 (cb39db5). Git itself acknowledges it as real:

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

Basically, you’ve already committed! Curious how to do it deliberately next time?


  1. In practical terms, using git or jj git is essentially the same; jj git just adds a touch of extra safety, e.g. it won’t create new branches unless you explicitly ask.

  2. Strictly speaking, Jujutsu leaves behind internal refs, that you can clean with git for-each-ref --format='delete %(refname)' refs/jj/ | git update-ref --stdin.

  3. Image by @pabloulloacastro - https://medium.com/@pabloulloacastro/software-en-equipo-git-stash-3e6adbea821c

Committing

Important

Where you get what it means that Jujutsu automatically commits.

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

@  x           empty
◆  z       🔒   empty

which is empty because your project is also empty, what happens if you add a file? Let’s see:

touch README.md
jj log
@  x
◆  z       🔒   empty

Interestingly, x is no longer empty. Git detects a new, untracked file:

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

What’s Jujutsu’s opinion?

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 automatically tracked the file (A README.md). In fact, there’s no equivalent of git add.
  • Easy to miss: the SHA1 changed.

Warning

Inspect x again. What’s in it?

Tip

It must contain README.md! 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)

Oh dear! By creating a file, you performed a git commit --amend under the hood.

Warning

How do you add content to README.md and amend x again?

Tip

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 right, bravo! In Jujutsu, the working directory and repository are the same area and always match. The lines:

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

confirm x holds README.md with its content.
It’s yet another SHA1, but to Jujutsu, it’s still Change x.

The mental model is: editing the file system directly edits the commit. This may feel dangerous, but you will see some safer practices like the Squash Workflow later.

How Many Commits Have You Created and Abandoned Already?

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

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

For Git to see your commit, it needs a branch (a Bookmark, in Jujutsu’s lingo) pointing to it:

jj bookmark set main

Don’t worry, there’s a whole chapter on Bookmarks coming up. All in good time!

Creating, Describing And Abandoning Changes

Creating, Describing And Abandoning Changes

Important

Where you learn to create, describe, and abandon commits.

@  x
◆  z       �   empty

Let’s give x a description (a message, in Git terminology) using jj description or jj desc:

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

@  x    Initial commit
◆  z       �   empty

Yes, it’s another hidden Git amend: we won’t repeat that anymore.

New Change

How do you create a new commit?
Remember the CRUD table:

OperationCommand
Createnew
Readshow
Updateedit
Deleteabandon

Sure, it’s jj new.

jj new -m "A license"
@  xp   empty A license
○  xm   Initial commit
◆  z    empty

This creates a new, empty Change with the specified message. The @ means xp is now the Current Change you are editing.
In Jujutsu, it’s idiomatic (though optional) to commit before coding. You haven’t added a license file yet, so the Change is empty. We’ll expand on this workflow later.

Descriptions are optional. You can create a descriptionless Change and add a message later with jj desc.

Let’s add the license file:

echo "This software is open source" > LICENSE

Rest assured, on the next jj command, the file is secured in the repository. Ready for the next Change:

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

You get the point. This is a possible workflow, but we’ll soon see more convenient ones, like The Squash Workflow.

Deleting Changes

Warning

How do you delete a Change? Check the table above.

Tip

Of course, jj abandon.
With jj abandon -r <CHANGE-ID> you can delete any Change, no matter its position in the history tree.

Warning

Get rid of the Change adding the license.

Tip

jj abandon -r t
@  o            empty A dummy React.js application
○  x            Initial commit
◆  z       �   empty

Check the filesystem: the license is gone.

Hint: when there’s no ambiguity, you can use positional arguments, so jj abandon t works too.

Notice you manipulated a Change you weren’t currently on. This feature is everywhere in Jujutsu: we cover it in On Not Being There.

Resurrecting And Branching

Important

You play with undoing operations, start new development lines by inserting Changes mid-branch, and edit messages deep in the history tree. It’s easier than it sounds!

Warning

Changed your mind? Want to resurrect the license commit?

Tip

Yes! Undoing is always jj undo:

jj undo
Undid operation: 3a6c93cd543e (2026-06-10 15:48:34) abandon commit c25416325e097a36d0cc1216fb4938e0c44b28a8
@  o    empty A dummy React.js application
○  t    A license
○  x    Initial commit
◆  z    empty

Back to life!

Adding Changes In Arbitrary Positions

jj new adds a Change on top of the Current Change. But you can insert Changes anywhere. Select where using -r (the same option used with abandon):

jj new -r x -m "One here"
@  k    empty One here                        <-- this one
│ ○  o  empty A dummy React.js application
│ ○  t  A license
├─╯
○  x    Initial commit
◆  z    empty

This started a new branch line, which you can grow with more jj new commands.

Want to insert a Change right before o (between o and t)? Use --before (or -B):

new --before o -m "One there"
○  o    empty A dummy React.js application
@  zv   empty One there                       <-- this one
○  t    A license
│ ○  k  empty One here
├─╯
○  x    Initial commit
◆  zz   empty

Everything Is Dual

Symmetrically, there’s also --after (or -A).

Warning

Insert a commit right after zz.

Tip

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

In summary, starting from:

o-o-R-o-o

This is what you get with the 3 different options:

jj new -r R

o-o-R-o-o
     \
      X
jj new --after R

o-o-R-X-o-o
jj new --before R

o-o-X-R-o-o

These moves are perfectly possible in Git, but cumbersome because you must handle the underlying mechanics. So, it’s unlikely you have performed them often.

The Ubiquitous -r Option

x’s message is Initial commit:

○  x    Initial commit                     <-- this one
@  m    empty Initial commit is no more
◆  zz   empty

Having inserted a Change before it, the description is now misleading. Mildly infuriating! How to amend it?

You’ll love this about Jujutsu: once you learn a grammar element, you can use it everywhere it makes sense. -r can be used with jj new to position a Change, with log to define what to display, with jj desc to target a message…

Warning

How to fix x’s description?

Tip

jj desc -r x -m "Hello, world"
○  x    Hello, world
@  m    empty Initial commit is no more
◆  zz   empty

Fixed. Good job.

What’s The Point of Creating Empty Commits?

Who would ever create empty commits, though? Notice:

  • You can jj edit them later and add actual content.
  • Committing before coding is idiomatic in Jujutsu workflows.
  • Editing isn’t the only way to populate Changes. You’ll soon move edits and files from a distance, and empty commits act as useful placeholders.

Cleaning up

Here’s a Chapter closing exercise. Move to o:

jj edit o

Get rid of the dummy commits you just created:

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

Warning

How would you do that?

Tip

Combine 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!

Many commands operate on single or multiple Changes, contiguous or sparse. These are equivalent:

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

jj abandon k zv m works because when there is no ambiguity, revsets can be passed as positional arguments.

The last example uses the | operator from the Revset Language, a powerful yet simple syntax for selecting Change sets. Read more in A Glimpse of the Revset Language.

Where To Go From Here?

Admittedly, these exercises were a bit contrived. Make them your own, and we will soon apply them to genuinely useful cases.

In the meantime, be proud! You just executed manipulations that are far from trivial in raw Git.

Intermezzo: A Taste Of a Fanciful Workflow

Indulge me in a little digression.

Why create a commit in the middle of a branch? How frequent can this use case possibly be?

I’m glad you asked! It’s the basis of powerful workflows you might eventually love. We explore this in Dispatching Edits from Megamerges. Here’s a preview.

Say you have 3 local branches:

@  u       **feat-3**
○  tn
○  y
│
│ ○  n       **feat-2**
│ ○  k
├─╯
│ ○  m       **feat-1**
│ ○  l
│ ○  p
├─╯
~

CI/CD will eventually merge them, but why wait? You can merge locally , and get rid of the merge with jj abandon in a breeze. Without further ado:

@  kx
○      x     Megamerge
├─┬─╮
│ │ ○  u       **feat-3**
│ │ ○  tn
│ │ ○  y
│ ○ │  m       **feat-1**
│ ○ │  l
│ ○ │  p
│ ├─╯
○ │  n       **feat-2**
○ │  kt
├─╯
~

You might wonder: how did I create merge commit x without a jj merge command?
Believe it or not, you already have the ingredients to figure it out. Think about it. The answer is coming up in two pages.

Now, say you need to develop feat-2. Why not work directly from the megamerge, ensuring feat-2 integrates smoothly with the other 2 branches immediately?

Do your work in kx. When finished, add a new empty Change on top of feat-2:

@  kx
○      x     Megamerge
├─┬─╮
│ │ ○  u       **feat-3**
│ │ ○  tn
│ │ ○  y
│ ○ │  m       **feat-1**
│ ○ │  l
│ ○ │  p
│ ├─╯
○ │  s                      <--- new Change
○ │  n       **feat-2**
○ │  kt
├─╯
~

Then, you send your work from kz back to the empty commit s using jj squash -t s (you’ll learn how to dispatch edits soon).

Sounds insane? It is! I was also shocked the first time. It didn’t take long before it felt more useful than weird. It’s now one of my daily drivers. You might find it convenient too.

Let’s continue; you’re close to mastering the basics.

Clone

Important

You learn how to invoke Git from Jujutsu.

Enough with fictional repositories. Let’s use this one:

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

It contains code from the React.js Tic Tac Toe tutorial.

Warning

Clone it using only jj. Check jj git --help for options.

Tip

Easy, right?

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

jj git clone is equivalent to git clone + jj init. Check out jj git --help for the other commands.

jj log shows only 2 Changes and marks one with a diamond , meaning it’s immutable:

@  o        ▢
◆  k    🔒  Displays X X **main**
│
~

main is a Git branch (a Bookmark, in Jujutsu’s lingo).

For reference, here are the symbols jj log uses:

SymbolMeaning
Mutable node
Immutable node
@The Current Change
@The Current Change, immutable

Immutable nodes prevent modifying published commits. For simplicity, we’ll disable this protection by adding a local Bookmark for each remote branch:

jj bookmark track '*'

You’ll learn about branches and remotes in Bookmarks.

Ready to go! The log should look like this:

@  ok       ▢
│ ○  qn       Add the CSS file **css**
│ ○  rt       Link to CSS
│ │ ○  oz       Play instructions **manual**
│ │ ○  kx       Play manual
│ ├─╯
│ │ ○  z        Click alternates Xs and Os **dev**
│ │ ○  v        Click sets a X
│ │ ○  s        Square has state
│ ├─╯
│ ○  qq       Fix
│ ○  n        Square is interactive
│ ○  ru       Board() invokes Square()
╭─┤
│ ○  ym       fix name: Square -> Board
│ ○  x        WIP delete me
│ ○  yz       LICENSE
│ ○  w        A board full of X
├─╯
◆  kk   🔒  Displays X X **main**
│
~

Merge

Important

Although there’s no merge command, you will still merge two branches.

A couple pages ago I teased a puzzle: you can create lines of commits, but how do you merge them?

Say you want to merge dev with manual, that is z with o, going from:

○  z           Click alternates Xs and Os **dev**
○  v            Click sets a X
○  s            Square has state
│
│ ○  o          Play instructions **manual**
│ ○  kx         Play manual
├─╯
~

to:

@    vz          Here's the merge
├─╮
│ ○  o          Play instructions instructions **dev**
│ ○  kx         Play manual
○ │  z          Click alternates Xs and Os **manual**
○ │  vv         Click sets a X
○ │  s          Square has state
├─╯
~

Your turn. I promise you already know the command.

Warning

What would you type?

Note

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

jj new -r X

The -r X part means:

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

Note

Most commits have 1 parent.
All merge commits share something special.

Tip

A merge commit is simply a new commit with 2 parents!
To give a new Change 2 parents, pass both using -r. In this case:

jj new -r dev -r manual -m "Here's the merge"

Notice that neither dev nor manual moved.

If you got it, kudos! That wasn’t easy!

Unmerge

Important

You discover that undoing a merge is trivial.

How do you unmerge?

@    nvn      ▢ Here's the merge
├─╮
│ ○  o        Play instructions **manual**
│ ○  kx       Play manual
○ │  z        Click alternates Xs and Os **dev**
○ │  v        Click sets a X
○ │  s        Square has state
├─╯
│
~

Easy! If merging was your last operation, just use jj undo:

jj undo
○  o        Play instructions **manual**
○  kx       Play manual
│ ○  z        Click alternates Xs and Os **dev**
│ ○  v        Click sets a X
│ ○  s        Square has state
├─╯
│
~

Unmerging With abandon

Alternatively, you could jj abandon the merge Change. Let’s try it. First, start over:

jj redo

Yes! As seen in the Quick Start, you can undo an undo . Jujutsu’s undo works like Emacs’: instead of merely going back in time, it appends the revert operation to a log. No information is ever lost. Curious? Read Emacs’ Undo.

You are back to:

@    vz          Here's the merge
├─╮
│ ○  o          Play instructions instructions **dev**
│ ○  kx         Play manual
○ │  z          Click alternates Xs and Os **manual**
○ │  vv         Click sets a X
○ │  s          Square has state
├─╯
~

Do yourself a favor: move somewhere else before deleting the merge, or you’ll abandon your Current Change1:

jj edit n

Your turn.

Warning

Unmerge without using undo.

Tip

Elementary, my dear Watson!

jj abandon vz
○  o        Play instructions **manual**
○  kx       Play manual
│ ○  z        Click alternates Xs and Os **dev**
│ ○  v        Click sets a X
│ ○  s        Square has state
├─╯
~

Can you merge more than 2 Changes? Sure! In Git this is called an “octopus merge”: it’s rare enough to be considered an anti-pattern. Jujutsu embraces it, instead. You’ll see fancy workflows based on it.

Warning

Now, create an octopus merge between css, manual and dev:

Tip

jj new -r css -r manual -r dev
@      kn       ▢
├─┬─╮
│ │ ○  z        Click alternates Xs and Os **dev**
│ │ ○  v        Click sets a X
│ │ ○  s        Square has state
│ ○ │  o        Play instructions **manual**
│ ○ │  kx       Play manual
│ ├─╯
○ │  qn       Add the CSS file **css**
○ │  rt       Link to CSS
├─╯
~

Too easy? See? You already think like a jujutsuka!
Notice: you merged the tips of 3 branches, but nothing stopped you from referencing a Change that wasn’t at a branch tip.

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


  1. Abandoning the Current Change is perfectly legit, but it might surprise you the first time. Read Pulling the Rug Out Under Oneself for details.

On Not Being There

A quick thought before moving to more exciting features.

Warning

What’s the Git command to merge branch A with branch B?

Caught off guard, some might answer:

git merge A B

The correct answer is:

Tip

It depends.

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

git merge always assumes the current branch.

Why Is It Like That?

Why isn’t it git merge A B? Why do commands like cherry-pick, rebase and pull assume the current branch implicitly?

Because of conflicts. When things go wrong, Git can’t store the result as a commit. It stops, demanding you resolve the problem on the file system. So, you have to be there.

And Jujutsu?

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

This is possible because of the unique way Jujutsu handles conflicts, as first-class citizens. You’ll soon see what a lifesaver this is. For now, just grasp the gist: you can operate on Changes without being there.

It’s History All The Way Down

Important

  • You discover a meta-repository where every commit has its own history

  • You find out that everything you do with jj is safely tracked.

Back to The Start

For the next sections, you need a fresh copy of the repo you cloned before. There are at least two ways to get it.

  1. Re-clone.

  2. Or, keep running jj undo until you reach the state right after executing jj git remote remove origin.

Which begs the question: where do undo and redo fetch their information from? The next few pages shed light on this, which will give you a 3rd option.

So, hold on a second before restarting from the scratch.

History of a Change

Important

You learn how to inspect a Change’s past.

Every Change has its History.

Try this exercise. Target any Change you like, such as n. Make some modifications to it:

  • 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 operation creates a new Git commit, but the immutable Change ID n always points to the most recent one. So, throughout its existence, n goes through different states: in a sense, n has its own history tree.

This isn’t just a metaphor. You can actually isplay 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 tracking n’s history. Its states are called n/1, n/2, etc. The /1 is the Change Offset. These are ordinary, but hidden, commits: unlike Git’s reflog, you can use them in commands without learning special syntax.

Warning

How do you inspect the changes stored in n/5?

Tip

Just run jj show n/5.

Simple, isn’t it?

You’ll often use this meta-history to recover lost files.

The History Beneath the History

Important

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

If every Change has its own history, your whole repository must have one too. It’s only logical:

  • Initially (000000000000), it didn’t exist.
  • Then it was created.
  • Then you added a Change.
  • Then you edited a description.

Jujutsu remembers everything: almost every command triggers a working-copy snapshot. You can inspect this meta-history with:

jj op log
  ○  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-playground.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-playground.git
  ○  ee32fa88d5f2 arialdo@mbuto default@ 3 hours ago, lasted 3 milliseconds
  │  add git remote origin
  │  args: jj git clone https://codeberg.org/arialdo/jj-playground.git
  ○  23c04169eec1 arialdo@mbuto 3 hours ago, lasted 6 milliseconds
  │  add workspace 'default'
  ○  000000000000 root()

Can you envision what this is? It’s the repository of your repository’s history!

Rewind!

Restoring your repo exactly as it was before an operation is trivial. Everything originates from 000000000000, each command creates a full snapshot with its ID. Feed that ID to jj op restore and you jump back to that exact moment.

Warning

You want to go back to when you performed:

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

Tip

  • Run jj op log.
  • Find the item for git remote remove origin. It should be the 5th operation from 000000000000.
  • Copy its ID and run: jj op restore <ID>

Spot on? Congrats!

You could also use:

jj op restore 000000000000+++++

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

Check the file system and log: you’re back to when you cloned the repo. Here’s the 3rd option we mentioned before.

Like Emacs, Jujutsu Never Forgets

Check jj op log again. You will find a record for the jj op restore you have just run. Yep! Even going back in time is tracked down. This lets you undo an undo by restoring its parent. Or, simply: this is what jj redo does.

Basically:

  • Every operation (including restore and undo) appends a snapshot to the op log.
  • op restore applies an old snapshot and appends the result to history.
  • undo restores the previous op.

We’ve all dreaded performing risky Git operations. I hope that armed with these tools, you’ll never feel that way again.

Ready For The Next Chapter?

Run:

jj op restore 000000000000+++++

Makes all Changes of main mutable running this extra command:1

jj config set --repo 'revset-aliases."trunk()"' 'none()'

Your repo should be:

○  qn       Add the CSS file **css**
○  rt       Link to CSS
│ ○  o        Play instructions **manual**
│ ○  kx       Play manual
├─╯
│ ○  zx       Click alternates Xs and Os **dev**
│ ○  v        Click sets a X
│ ○  s        Square has state
├─╯
○  qq       Fix
○  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 **main**
○  wx       Fails
○  rx       Scaffold applicatio
◆  zz   🔒  ▢

Good. You’re ready to play with rebase and squash.


  1. Jujutsu keeps the Changes of trunk (main) immutable by default. It’s a safety net against rewriting published commits. We’re about to rewrite history on purpose so we can turn that guardrail off instructing Jujutsu that we don’t have a trunk.
    You’ll see that safety net at work when we talk about remotes. I promise: it’s amazing.

Moving Things Around

Important

  • Flip 2 Changes in a branch.
  • Send 2 Changes into 2 separate branches.
  • Move a file to a future Change.
  • Fix a typo from 10 Changes ago.
  • Rebase 2 branches simultaneously.
  • Reflect on your new history-bending abilities. Invent new audacious (and useful) moves.
  • Profit.

You can perform all sorts of black magic with just 2 commands:

OperationCommand
Move a Changerebase
Move your editssquash

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

Moving Changes Around

Moving Changes

Important

You discover that rebasing isn’t just moving a branch to a different base.
You can insert a single Change right in the middle of the history tree.

jj rebase works by specifying:

  • Which Changes you want to move (-r).
  • Where you want to move them (--onto, --before or --after, same options of jj new).

So:

jj rebase -r X -o Y

moves X on top of Y.

The sample repository is full of mistakes and unfinished work. Let’s fix them.

First, the License

Oh, no! I added the license too late, in Change yz:

~
├─╮
○ │  ym       fix name: Square -> Board
○ │  x        WIP delete me
○ │  yz       LICENSE                      <-- here
○ │  ws       A board full of X
├─╯
○  kk       Displays X X **main**
○  wx       Fails
○  rx       Scaffold applicatio
◆  zz   🔒  empty                          <-- should have been right after root()
jj show --summary yz
  A LICENSE

Ideally, the license should be the very first commit. Somehow, yz ended up stranded in a merged branch.

Warning

Can you move yz so it becomes the initial commit?

Tip

jj rebase -r yz -A zz

or:

jj rebase -r yz -B rx

Things Committed by Mistake

Talking about that branch, its Change x has the suspicious message WIP: delete me.

Warning

What does it contain?

Tip

jj show x --summary
A node_modules/yaml/LICENSE
A node_modules/yaml/index.js

Ouch! I committed node_modules

Warning

How do you get rid of it?

Tip

jj abandon x

And x is gone.

Why this exercise? Because under the hood, while abandoning, Jujutsu did move Changes: it rebased x’s descendants onto yz. Moving things doesn’t always look like moving things: Jujutsu commands capture your intent, not the underlying mechanics.

Typos

Notice the typo in rx’s message: applicatio instead of application.

Warning

How would you fix it?

Tip

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: your intent was editing a message, but the log (Rebased 13 descendant commits) reveals it’s still about moving Changes.

Let’s look at cases where your explicit intent is actually moving Changes.

A Glimpse of the Revset Language

Important

You learn to describe sets of Changes, and how rebase couldn’t care less about their position.

With Git, you typically rebase entire branches. Jujutsu’s rebase makes no assumptions about what you want to move.

You saw how to move a single Change. You can just as easily move multiple Changes, by specifying them:

jj rebase -r X -r Y -o O

This moves both X and Y onto O. It doesn’t matter where they are: they can even be sparse!

Instead of listing Changes one by one, use the Revset Language. Here’s a taste:

NotationMeaning
X | YChanges in set X plus Changes in set Y.
X & YChanges in both X and Y.
~XChanges not in X.
X::X and all its descendants.
::XX and all its ancestors.
X::YAll Changes between X and Y, included.

X and Y don’t have to be single Changes; they can be sets themselves. Each expression returns a set, and you can recursively combine them. This expressiveness, along with custom functions, makes Revsets immensely more powerful than Git’s revspecs.

Moving Sets of Changes

Here’s a silly exercise: can you move sparse Changes too? Sure, why not! In fact, rebase works with whatever set of Changes you pass it. Say that for some (odd) reason you want to move v, and yz on top of rt:

  m        ▢
○  qn       Add the CSS file **css***
○  rt       Link to CSS                        <-- here
│ ○  o        Play instructions **manual***
│ ○  kx       Play manual
├─╯
│ ○  zx       Click alternates Xs and Os **dev***
│ ○  v        Click sets a X                   <-- v
│ ○  s        Square has state
├─╯
○  qq       Fix
○  n        Square is interactive
○    ru       Board() invokes Square()
├─╮
○ │  ym       fix name: Square -> Board
○ │  ws       A board full of X
├─╯
○  kk       Displays X X **main***
○  wx       Fails
○  rx       Scaffold application
○  yz       LICENSE                            <-- yz
◆  zz   🔒  ▢

Warning

How would you do that in one shot?

Tip

Using a revset expression:

jj rebase -r 'yz | v' -o rt
@  m        ▢
○  qn       Add the CSS file css*
│ ○  v        Click sets a X                     <-- v
│ ○  yz       LICENSE                            <-- yz
├─╯
○  rt       Link to CSS                          <-- new base
│ ○  o        Play instructions manual*
│ ○  kx       Play manual
├─╯
~

Before the next page, undo that move:

jj undo

Feel free to spend some time moving sets of Changes around. Then pause and think how you’d do the same move in Git. it will always be technically possible just rarely this straightforward.

Rebasing a Branch

Important

You rebase a branch, then you move it back.

Rebasing sparse Changes is rarely that useful. More often you’ll rebase whole branches. Revset helps here too.
If you ran jj undo previously, your tree should look like this:

@  m        ▢
○  qn       Add the CSS file **css***
○  rt       Link to CSS
│ ○  o        Play instructions **manual***         <-- move it here
│ ○  kx       Play manual
├─╯
│ ○  zx       Click alternates Xs and Os **dev***   ⎫
│ ○  v        Click sets a X                    ⎬ this  branch
│ ○  s        Square has state                  ⎭
├─╯
○  qq       Fix
~

Warning

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

Tip

The solution is the literal translation of the request:

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

You should arrive at:

○  zx       Click alternates Xs and Os main
○  v        Click sets an X
○  s        Square has state
○  o        Play instructions
○  kx       Play manual
│ ○  qn       Add the CSS file css
│ ○  rt       Link to CSS
├─╯
○  qq       Fix
~

Alternatively, you can use --source / -s to mark the cut point for the branch you want to rebase:

jj rebase -s s -o o

Warning

How would you undo that using rebase instead of undo?

Tip

The original base of s::zx was qq, so:

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

Give yourself a pat on the back if you nailed it.

Rebasing 2 Branches Simultaneously

Important

In you which you discover you’re a Jujutsu ninja: rebasing 2 branches at once won’t even surprise you.

By now you may have guessed that a single Revset expression can describe more than one branch.

On top of qq we have 3 branches, css, manual and dev:

○  qn       Add the CSS file **css***
○  rt       Link to CSS
│ ○  o        Play instructions **manual***
│ ○  kx       Play manual
├─╯
│ ○  zx       Click alternates Xs and Os **dev***
│ ○  v        Click sets a X
│ ○  s        Square has state
├─╯
○  qq       Fix
~

Your goal is to rebase both dev and manual on top of css. You could do this sequentially:

jj rebase -r kx:: -o qn
jj rebase -r s:: -o qn

Warning

Could you do this in 1 command?

Tip

Union Revset to the rescue!

jj rebase -r 'kx:: | s::' -o qn
○  o        Play instructions **manual***
○  kx       Play manual
│
│ ○  zx       Click alternates Xs and Os **dev***
│ ○  v        Click sets a X
│ ○  s        Square has state
├─╯
○  qn       Add the CSS file **css***
○  rt       Link to CSS
○  qq       Fix

Rebasing multiple branches is handy for keeping them updated with trunk. Check out rebase-all, an alias by Steve Klabnik: just run jj rebase-all to rebase your local work onto the latest main.

People invented all sorts of amazing aliases: find them at https://github.com/jj-vcs/jj/discussions/8484.

Preview of Revsets

Never fear a rebase: jj undo has your back. Plus, you can preview any Revset with jj log before applying it:

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

○  o        Play instructions
○  kx       Play manual
│
~

Universality of Revsets

Further more! Once you define a Revset like kx:: | s::, you can use it anywhere:

  • Delete those Changes? jj abandon -r 'kx:: | s::'
  • Inspect them? jj show -r 'kx:: | s::'
  • Change their author? jj metaedit -r 'kx:: | s::' --update-author "Joe Doe"

This never stops amazing me: Jujutsu’s has few building blocks, but they combine in countless ways.

Moving Edits Around

Important

You learn to send file edits back to the past.

Changes and Edits

A Change contains modifications (adding a class, renaming a function, etc.) which you can inspect as a diff with jj show. The manual calls them “changes”, but it’s an overloaded term: I’ll call them “edits” from now on, OK?

Finding the Source of a Typo

To narrow down the log to single files you can use the Revset function files(). For example, the history of README.md is:

jj log -r "files('README.md')"
○  kk           Displays X X **main***
~  (elided revisions)
○  rx           Scaffold application
│
~

README.md was created in rx and modified in kk. Inspect kk:

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 @@                            ⎬   changes to README.md
-# Learning Raect.js                       │
+# Learning React.js                       ⎭

diff --git a/src/App.js b/src/App.js       ⎫
index 3f8dbe3702..495f426244 100644        ⎬   changes to src/App.js
--- a/src/App.js                           ⎭

Uh oh! In kk I mixed a Javascript update in App.js with a fix to a typo in README.md. The typo was introduced in rx:

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

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

So, I wrote the typo in rx, missed it, found it later while coding App.js, and lazily mixed the fix into kk. How do we move the fix from kk back to rx?

Sending Fixes Back to the Past

Rebasing won’t help here: rebase moves Changes, while we want to move edits.

Enter squash. In Git squash combines multiple commits. jj squash generalizes this concept; more generally, it transfers edits from one Change to another. The basic syntax is:

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

or, shortly:

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.

Warning

Help me fix the mess I made in Raect.js.

Tip

jj squash -f kk -t rx README.md

Bravo! You are basically saying: “I typed this in kk. Rewrite history so I typed it in rx.”

Check kk:

jj show --summary kk
M src\App.js

README.md is gone. In rx, the typo is fixed:

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

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

The typo fix was absorbed into the past. Next time you find a typo while working on something unrelated, fix it on the spot, then send it back where it belongs.

Dispatching Edits from Megamerges

Important

Where you start believing in magic.

Why did I use the word “absorb” in the last page? Because you could have used jj absorb. Try it:

jj undo
jj edit kk

Then run:

jj absorb README.md
Absorbed changes into 1 revisions:
  rxwzvtlz 458df236 Scaffold application

jj absorb accomplishes the same you did manually: it identifies the source of the typo (rx) and moves the edit there, automatically.

If you don’t specify a file, jj absorb analyzes all your edits. For each one, it finds the closest ancestor it can safely land in. Got multiple edits across multiple files? No problem. jj absorb dispatches each to its rightful historical Change.

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, you always have jj undo.
Second, yes: jj absorb is highly conservative. If the destination Change is ambiguous, it does nothing rather than risk a mess.

When Do You Use It?

Whenever you find a typo or an urelated change.
But also, remember the Megamerge Workflow from Intermezzo? That’s where jj absorb shines.

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

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

As you receive feedback, rather than jumping between branches, you create an octopus merge of all your PRs, the megamerge, and sit on a new empty Change on top of it:

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

This lets you work on all your branches from a single staging area. You code directly on top of the megamerge, then you use jj absorb to dispatch each change to its proper branch, automatically.

You won’t worry about merge conflicts because:

  • jj absorb aborts rather than creating one.
  • The megamerge already proves your PRs integrate smoothly; conflicts are caught early by design.

You can read more about this workflow here:

Hammers and Nails

Allow me a brief reflection.

The language we use defines the boundaries of what we can think. The same applies to programming languages and tools: by making some operations easy and others hard, they make certain concepts thinkable and others invisible.

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

What’s hard to do becomes hard to think.

Jujutsu illustrates this beautifully. Megamerging was technically achievable in Git, yet nobody invented it. Why? Because the required chain of operations was too convoluted to even ideate.
By elevating its interface from implementation details to pure intent, Jujutsu turned convoluted operations into trivial ones, allowing people to envision entirely new workflows.

I stand on the shoulders of giants here. Ken Iverson titled his 1979 Turing Lecture Notation as a Tool of Thought, arguing that notation doesn’t just carry preexisting thoughts: it shapes them.

The corollary is that the limits of our tools are invisible from the inside. I cited Graham’s Blub Paradox already: we can spot missing features in poorer languages, but not what our own language lacks compared to more powerful ones.

My personal advice: deliberately expose yourself to tools and languages you’ll never seriously use. You may not switch to them, but the space of what your imagination can conceive will surely expand.

We’ve always thought in Git. Jujutsu expands the boundaries of our thought. Honestly, I’m very curious to know what will come next.

Moving Chunks Around

Important

You send a single line of a file back to the past.

You learned to move all edits in specific files using jj squash -f FROM -t TO FILES. Often, you need to be more selective. See this case:

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                      ⎬ Typo fix
+// Main application                    ⎭
...                                     ⎫
-export default function Square() {     ⎬ Goal change
+export default function Board() {      ⎭

Given the description, my goal was clearly renaming Square() to Board(). But I also mixed in a typo fix (apication -> application). The typo originated in rx.

Running:

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

is too coarse; it drags the Square() -> Board() rename into rx too. jj absorb works here, but squash provides manual control.

Interactively Select Chunks

Give --interactive / -i a try:

jj squash -f ym -t rx -i

An editor opens, letting you select which chunks to move:

[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 these keys:

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

Isn’t it cool? -i is also convenient to select entire files instead of typing their paths.

Building On Top Of Squash

Look at the operations at your fingertips with squash and rebase: odds are you’re already doing things you never attempted with Git.

Squashing Changes

Important

You learn to replicate Git’s squash and you see when Jujutsu automatically abandons empty Changes.

The jj squash default parameter are clever:

jj squash -f A -t B FILES
  • Omitting FILES moves all edits in the Change.
  • Omitting -f or -t implies the Current Change @.

So:

jj squash -f A

moves all edits from A to your Current Change.
And:

jj squash -t B

moves your current edits to B, wherever it sits in history.

This technique is truly powerful. It lets you edit something here and send it somewhere else. Notice an unrelated typo while working on a feature? Fix it on the spot and squash it where it belongs. Spot some work needed in another branch? Write the change here and move it there. Do you see how this move allows you to work on multiple features simultaneously?

You should also see jj absorb in its proper light: it’s this exact move, applied automatically to every chunk. I promise that once you get the hang of it, there is no going back.

Abandoning Changes

After a squash, Jujutsu abandons empty Changes automatically.

For example:

jj log -r "n::zx"
○  zx
○  v
○  s
○  n

Warning

You want to move the edits in v into n. How?

Tip

jj squash -f v -t n

You get:

○  zx
○  s
○  n

Notice that v disappeared: it was left empty. Use --keep-emptied if you want to retain it.

What Happens To Messages?

Jujutsu does it best to preserve information. If squashing abandons a Change with a description, Jujutsu interactively prompts for you to merge the message in the target Change. Alternatively, you can either directly set it with -m, or you can use --use-destination-message / -u to keep the target’s message.

Quiz Time!

Check out this sloppy mistake:

jj show n --git
--- a/src/App.js
+++ b/src/App.js
...
+    <button
+      className="square"
+      onClick={handleClic}    <-- typo
...

Oops, Clic instead of Click. But look! I fixed the typo in the next Change qq, with the very expressive message “Fix”:

jj show qq --git
-      onClick={handleClic}
+      onClick={handleClick}

I could have amended n, directly.

Warning

Get rid of the noisy qq, integrating the typo fix in qq-.

Tip

Option 1:

jj edit qq
jj squash -u

Option 2, without moving:

jj squash -f qq -t n -u

And qq is no more!

Just jj squash

Finally, the most common use. Running bare jj squash:

  • Moves all edits (no files specified)
  • from the Current Change to its parent
  • and, if the Current Change has no message, it abandons it and promptly provides you with a fresh new one.

Say you are on a branch tip:

@  sw           empty                               <-- your working area
○  zx           Click alternates Xs and Os main     <-- your target
○  v            Click sets a X
○  sn           Square has state

You code in @, then run jj squash. Your edits are absorbed by zx and you receive a fresh new @.

See where this leads? It’s the equivalent of the Git index! Let’s explore the Squash Workflow, which builds on this idea. Then you’ll never miss Git’s index anymore.

The Squash Workflow

Important

You learn a workflow that feels both exotic (committing before coding!) and familiar (it acts like the Git index).

This is apparently the preferred workflow of Martin von Zweigbergk, the creator of Jujutsu, so it must be deeply rooted in its philosophy.

In a nutshell:

  • You start by committing .
  • You code in a disposable Change on top of it.
  • When done, you squash your work down.

Let me show you:

@  p        ▢
○  o        Play instructions **manual***
○  kx       Play manual
~

Let’s say you want to add the obligatory cat picture to your project manual.

First: Commit!

You start by declaring your intent:

jj new -r manual -m "Cat picture"
@  vt       ▢ Cat picture
○  o        Play instructions **manual***
○  kx       Play manual
~

You haven’t coded yet, so the Change is empty. It’s the candidate for the next commit, much like the Git index.

Second: Set Up Your Workspace

You build a disposable Change on top of your Candidate Change:

jj new
@  xq       ▢
○  vt       ▢ Cat picture
○  o        Play instructions **manual***
○  kx       Play manual
~

This empty, descriptionless Change mimics Git:

In GitIn Jujutsu
An empty Index, the Candidate Commit.Your vt Change.
A working directory with no pending changes.Your empty Current Change @ / xq.

Third: You Code

Nothing new to see here: Jujutsu reflects every file system change in your Current Change @. Here comes the cat:

echo '![cat](https://upload.wikimedia.org/wikipedia/commons/5/58/Cat_Sima.jpeg)' >> manual.md

Finally: You Squash

The tip Change is populated, the Candidate Change is still empty. This give you the time to review your code. When you are happy, you squash your work back into the Candidate Change:

jj squash

If you want to squash interactively chunk by chunk, you can do:

jj squash -i

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

@  zu       ▢
○  vt       Cat picture
○  o        Play instructions **manual***
○  kx       Play manual
~

Lather, rinse, repeat.

Your next step would be describing the Current Change with your next goal (jj desc -m DESCRIPTION) and building a working area on top of it (jj new). This workflow is so idiomatic that Jujutsu provides a shortcut for it: jj commit -m DESCRIPTION1.


  1. Does committing before coding feel weird? Think of it as the TDD equivalent for commits: you declare intent before doing, turning commit into an actual commitment. You can read more on this idea in Pre-emptive Commit Messages.

Moving Edits To The Future

Important

You send edits into the future and you split a Change in two.

You saw how to send your work back to a past Change. But nothing was forcing you to look backwards. The --into / -t Change can be anywhere: sideways to unrelated branches, or forwards into the future.
This begs the question: why send things into the future?

A common use case is splitting. You commit, then realize you bundled unrelated edits. Tidying up means pulling them apart into independent, possibly descendants, Changes.

See rx:

○  rx       Scaffold application
○  yz       LICENSE
◆  zz   🔒  ▢

It contains the scaffold application plus the README file:

jj show -r rx --summary
A .gitignore
A README.md            <-- isolate this
A package-lock.json
A package.json
A public/index.html
A src/App.js
A src/index.js
A src/styles.css

Say that you want to have the README file first, the application right next. You could split rx in two, like this:

  • You create a Change after rx.
  • You move part of rx’s edits into that future Change.

Try it yourself.

Warning

Split the application and README.md into 2 separate Changes.

Tip

jj new -A rx -m "Scaffold application"
jj desc -r rx -m "README file"
jj squash -f rx -t rx+ -i

Split

This move is so common that Jujutsu offers a macro-command: jj split. It lets you interactively select part of the edits and move the rest to a new child Change, in one shot. Do a jj undo and run:

jj split -r rx -m "README file"

and select the README.md:

○  yo       Scaffold application   <-- the rest, moved to the future
○  rx       README file            <-- what you selected
○  yz       LICENSE
◆  zz   🔒  ▢

If you already know what to select, just specify it directly:

jj split -r rx -m "README file" README.md

Making It Your Workflow

Splitting isn’t just for fixing mistakes. It can be a deliberate move: code first without worrying about history, then easily reshape the result into a tidy, logical series of commits.

In Git, rewriting history is expensive enough that you usually plan upfront. Jujutsu makes cleanup so cheap that you can defer the decision until you’re finished coding.

When done deliberately, this move may become the building block of your next workflow.

Copying Files

Important

Your learn to copy/paste files from one Change to another.

Good things come in threes. You’ve met jj squash and jj rebase; meet jj restore. Together, these three musketeers bend the history tree to your will:

OperationCommand
Move a Changerebase
Move your editssquash
Copy files from one Change to anotherrestore

jj restore is similar to Bash’s cp: it copies file contents. It uses the same syntax as jj squash (how many times did I tell you Jujutsu is beautifully consistent?):

jj restore -f FROM -t TO FILES

This copies FILES from FROM to TO. The original in FROM remains unmodified.
If a file was deleted in FROM, it gets deleted in TO as well.

Like squash, omitting -f or -t implies the Current Change @.
Omitting FILES restores the entire working copy.

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:

  • Find the old Change X (docs: README with beautiful diagrams).
  • Run jj restore --from X README.md

Done.
You aren’t going back in time: instead, you pulled a file out of an old snapshot and pasted it into your Current Change.

Is This Common?

You won’t do it every day, but just like jj squash, jj restore’s default arguments lead to very interesting consequences. Let’s see in the next page.

Restarting

Important

Yoou learn to clean up your working area completely and start over.

The jj restore --help page states:

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

This means:

jj restore

is equivalent to:

jj restore -f @- -t @ .

It makes your Current Change identical to its parent, providing a convenient way to wipe your slate clean and start over.

Give it a try. Select any Change and create a new Change on top:

jj new ru

Make random modifications:

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

You have 2 modified, 1 deleted, and 1 added file.

Warning

How do you start over and return to an empty Change?

Tip

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. Git spreads state across HEAD, the Index, the working tree, and untracked files. You need different commands based on the layer you target:

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

followed by:

git clean -fd

Don’t Edit

Important

You conclude that the Git Index was a Good Thing®, and you learn a micro-workflow that builds on the same idea.

In Committing, I warned:

By editing the file system, you directly edit the commit. 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: it unconditionally commits every filesystem change. Even worse, editing a historical Change recursively propagates modifications to its descendants. The problem is, not all changes are intentional:

  • Build tools create artifacts before you can add them to .gitignore.
  • We all make fat-finger mistakes. “Mistakes” plus “automatic recursive propagation” is the recipe for disaster.
  • Editors leave stray files behind.

Indeed, a universal rule for both Git and Jujutsu is:

Always review changes before committing them.

This is why the Git Index exists. jj edit is often too rushed.

The solution: whenever you’re tempted to jj edit X, use jj new X instead. Instead of performing surgery directly on X, jj new provides a safe, isolated working area.

Mini-workflow

To safely edit X:

  1. Run jj new X.
  2. Make your edits.
  3. Review your work with jj diff, then squash it back with jj squash.
  4. To start over, use jj restore.

See This In Practice

Using our sample repo, say you want to edit v:

○  zx       Click alternates Xs and Os **dev***
○  v        Click sets a X                    <-- What you want to edit
○  s        Square has state
~

Instead of jj edit v, use jj new v:

@  p        ▢                                 <-- Your Current 
│ ○  zx       Click alternates Xs and Os **dev***
├─╯
○  v        Click sets a X                    <-- What you want to 
○  s        Square has state

Notice you landed:

  • On an empty, descriptionless Change.
  • Sitting directly 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 it as a Git Index-like structure. Only, it’s an ordinary Change, so it’s persistent and operable with the ordinary commands.

Isn’t This Just jj edit?

Definitely no. If you ran jj edit v:

  • jj diff would mash your edits together with v’s original changes, making review difficult.
  • jj restore would wipe both your new changes and v’s original changes, resetting everything to s.

The Joy Of No Special Cases

Because the Buffer Change is an ordinary Change, you gain huge benefits.

There’s No Need for jj stash

Need to switch context immediately?

  • In Git, you must git stash push or lose your work.
  • In Jujutsu, just run jj new somewhere else. Your Buffer Change waits safely.

There’s no separate stash machinery. Suspended work lives as normal Changes. Remember when in Why I said that the Git index is like a commit, but not quite? If you think about it, the Git stash is another case of quasi-commit, requiring a special treatment.

Jujutsu turns the entire stash concept into basic primitives you already know.

Multiple Indexes

Have you ever thought you can have multiple indexes?
Say you want to try a different approach while editing v. You run another jj new v and, voilà!, you get two independent Buffer Changes. Work on both, abandon one, or squash both. Your choice.

Wandering Around

Important

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

Running jj new X creates a new Change containing the exact same content as X. But it’s not X, it’s a separate jchange, so even if you edit files, you won’t modify X. So, it’s a safe sandbox to explore what’s inside X on your disk.

Interestingly, if you jump away with a second jj new and leave your Buffer Change empty, Jujutsu automatically abandons it.

This is a brilliant default! You can jump back and forth targeting different Changes using jj new:

  • Without polluting your history with empty leftovers.
  • Without risking unintentional modifications to historical commits.

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

Conflict Resolution

Interestingly enough, the same mini-workflow:

jj new && (jj squash | jj revert)

is exceptionally handy for resolving conflicts. It’s time to tackle this topic!

Conflicts

Important

  • Resolve conflicts by editing files.

  • Learn that conflict resolution propagates to descendants.

  • Resolve rebase conflicts through alternative moves like rebasing or abandoning Changes.

  • See under the hood how Jujutsu treats conflicts as first-class citizens.

We will spend time on this topic not because it’s tricky, but because conflicts are one of the scariest parts of Git, and I want to put that fear to rest.

We will play with a real repository. Clone it:

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

then track remote branches:

jj bookmark track '*'

Here’s the history tree:

@  vs   (empty)
│ ○  y    feat-2
│ ○  unv
│ ○  vw   Will conflict: deleted file
│ ○  xw
│ ○  xl   Will conflict: Vim
├─╯
│ ○  nz   merge-b
│ │ ○  m    merge-a
│ ├─╯
│ ○  vr   base Base of merge-a and merge-b
│ ○  r    Rebase here
│ ○  l    No idea why
├─╯
│ ○  no   feat-1
│ ○  vt
│ ○  s    Will conflict
│ ○  unm
│ ○  vz
├─╯
◆  q    main
│
~

We will perform moves producing progressively more challenging conflicts:

  1. A simple merge conflict.
  2. A rebase generating 1 single conflict.
  3. A rebase with multiple conflicts.

Then we’ll explore conflict resolution techniques unavailable in Git.

Ready? Hands on the keyboard!

Merge Conflicts

Important

  • Start with the simplest case: a merge conflict.
  • See how conflicts are non-blocking.
  • Learn to read conflict markers and resolve them.

Focus on Changes nz and m:

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

Their shared base vr contains a text file with:

We will create a conflict here.

m (merge-a) changes this to:

We will create a conflict here: merge-a added this text.

nz (merge-b) changes it to:

We will create a conflict here: merge-b added this text.

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

Visualizing Conflicts

Important

You see how Jujutsu warns you about a conflict.

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

Let’s create the conflict.

Warning

Merge merge-a and merge-b with the message Merge conflict.

Tip

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

Swapping arguments (jj new merge-b merge-a) swaps the conflict details in the file, but doesn’t change the logic. I assume you used merge-a first.

The merge succeeds, but Jujutsu warns:

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

Notice: it’s a warning, not an error.

jj log displays conflicted Changes with red @ and × symbols:

§    t    (empty) Merge conflict
├─╮
│ ○  nz   **merge-b**
○ │  m    **merge-a**
├─╯
~

As anticipated, a conflict won’t stop your work. Let’s challenge this notion.

Living with Conflicts

Important

  • You experience how conflicts won’t stop your work.
  • You rebase a conflicted branch and commit on top of it.

Let’s start with a quiz!

Warning

Before resolving the conflict, can you rebase branch vr:: from its base q onto 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
 ├─╯
 │
 ~

Tip

Either specify the set:

jj rebase -r vr:: -o r

Or use cut & paste points:

jj rebase -s vr -o r

Surprise surprise! The rebase succeeds.

Jujutsu won’t emit errors like Git’s:

    fatal: It seems that there is already a rebase-merge directory, and
I wonder if you are in the middle of another rebase.

jj rebase succeeded, yet the conflict persists, and Jujutsu reminds you:

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

Again, it’s just a warning, not a showstopper. Which begs the question: can you commit on top of a conflicted Change? Why not!

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
│
~

Jujutsu won’t forget about conflicts and will highlight them with red × and @ symbols.

We’ve dragged our feet for too long. Time to resolve the conflict.

Resolving Conflicts

Resolving A Conflict by Editing It

Important

You interpret conflict markers and see how resolution automatically propagates.

The natural way to solve a conflict is fixing the affected files.

§  l        empty
×  zm       My parent is conflicted
×  zo       Merge conflict           <- Resolve the conflict here
├─╮
│ ~
~

You have 3 conflicted Changes. @ and zm are conflicted only because they inherit zo’s conflict. zo is the origin to fix.

Don’t Edit Changes Directly

If you are tempted to run jj edit zo, don’t! As I recommended in Don’t Edit, create a Buffer Change instead:

jj new zo
§  k        empty                    <- Your Buffer Change
│ ×  zm     My parent is conflicted
├─╯
×  zo       empty Merge conflict     <- The Change to fix
├─╮
│ ~
~

The Buffer Change is conflicted too: that’s the point. You’ll resolve from there.

Here’s the idiomatic resolution workflow:

  1. jj new targeting the conflicted Change.
  2. Resolve the conflict, then jj squash.
  3. If you want to start over, jj restore.

Resolving The Conflict

If you edit the conflicted file.txt you’ll find:

Donec neque quam...
<<<<<<< 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...

Interpreting Conflicts

See the conflict markers?

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

Lines outside are unchanged.

Inside, two elements exist:

  1. How the first change modified the base. 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.

  2. The second change’s contribution:

    +++++++ nzsunxwp 0004a47a We will create a conflict here: merge-b added this text.

Basically, you have to combine:

We will create a conflict here: merge-a added this text.
We will create a conflict here: merge-b added this text.

into

We will create a conflict here: merge-a and merge-b added this text.

That fixs the conflict. jj st will show:

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

Good. Squash it:

jj squash

All clear! The Buffer Change is automatically abandoned.

Conflict Resolution Propagates

Look the beauty!

@  l        empty
│ ○  zm     My parent is conflicted
├─╯
○  zo       Merge conflict
├─╮
│ ~
~

The resolution automatically propagated upward to zm. How cool is that?

Resolutions All The Way Down

Conflicts propagate upward. Fix them in any point, and descendants heal.

×  g     ⎫
×  f     │
×  e     ⎬  inherit the conflict
×  d     │
×  c     ⎭ 
×  b     <- conflict origin
○  a

Ideally, you’ll fix the conflict at its origin, then jj squash to propagate it:

○  g
○  f
○  e
○  d
○  c
○  b     <- resolution here
○  a

Got the idea? Ready for tougher cases?

Using the GUI

Important

Use your good, old merge tool.

Jujutsu isn’t opinionated. Do you prefer a GUI like kdiff3 or meld?

jj resolve --tool meld
meld's UI showing the
conflict to resolve

Git Style Conflict Markers

May be like Git-style markers? Sure, configure your repo:

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

Presto! You’re back to your familiar format.

Rebase Conflicts

Important

You will solve 2 cases:

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

  2. A rebase producing multiple conflicts. You’ll resolve them by editing non-conflicted Changes, something you’ve probably never done in Git.

A Single Rebase Conflict

Important

You already know how to resolve a rebase conflict!

This chapter should be a mere formality. Challenge yourself.

  ○  so
  ○  o        Merge conflict
  ├─╮
  │ ○  nz        **merge-b**
  ○ │  m         **merge-a**
  ├─╯
  ○  vr       Base of merge-a and merge-b base
  │ ○  no       empty  **feat-1**
  │ ○  vt       empty
  │ ○  sy       empty
  │ ○  u        empty
  │ ○  vz       empty
  ├─╯
  ○  q        empty  **main**
  ◆  z    🔒  empty

Warning

Move the feat-1 branch on top of vr.

Tip

jj rebase -s vz -o vr

Here we go: a conflict in s:

§  no        **feat-1**
×  vt
×  s        Will conflict
○  u
○  vz
○  r        Rebase vr:: here
│
~

The conflict arises in s and propagates to all descendants.

Warning

How to resolve it? Remember the jj new && (jj squash || jj restore) workflow from Don’t Edit.

Tip

jj new -r s

You will find this 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

Done!

If you got it right, good job! Watch the resolution propagate from s down its descendants:

@  no        **feat-1**
○  vt
○  s        Will conflict
○  u
○  vz
○  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
│ ~
│ ○  vr       Base of merge-a and merge-b **base**
│ ○  r        Rebase here                          <-- here
│ ○  l        No idea why
├─╯
○  q         **main**
~

Warning

Rebase feat-2 on top of r.

Tip

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                          <-- here
○  l        No idea why
│
~

Boom.

First Conflict

Warning

What’s the conflict in xl?

Tip

jj show xl
Created conflict in README.md:
    ...
   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

The conflict is in xl, but originated in r, its new base. Apparently I wrestled with Vim and mangled README.md. As an Emacs user, that’s embarrassing.

You could resolve the conflict in xl, but why keep messy Vim commands in README.md? Why not tidy up r directly?

Resolving The Conflict at Its Root

Let’s use the mini-workflow:

jj new r
jj restore -f r- README.md
jj squash
@ q           empty
│ ×  y          **feat-2**
│ ×  unv
│ ×  vw       Will conflict: deleted file
│ ○  xw
│ ○  xl       Will conflict: Vim
├─╯
○  r        Rebase here
○  l        No idea why
│
~

Cool! That fixed xl and the resolution propagated to xw.

Look what happened: you had a conflict in xl and solved it by editing its parent r, which had no conflicts at all! Git has nothing like this. It’s a small revelation: fixing a conflict without opening the conflicted file.

Second Conflict

There’s still a conflict in vw. What is it?

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: -
        [...]
      108: +++++++ vwqzzmtx 46b8d529 "Will conflict: deleted file" (rebased revision)
      109: import { useState } from "react";
      110: import { Plus, Check, X, Pencil } from "lucide-react";
      111:
      [...]
      309: >>>>>>> conflict 1 of 1 ends

Jujutsu tells you:

  • x deleted feat-2.js.
  • v tries to edit it (Pencil at line 110).

The root of conflict is the deletion of feat-2.js. Where did it happen?

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
│
~

feat-2.js was created in q. What about l?

jj show --summary l
    No idea why

D feat-2.js

Here’s the culprit! The message says it all: I had no idea what I was doing.

Warning

What if you remove that mistaken Change?

Tip

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 a rebase conflict can be resolved by running another rebase? Or by editing an unconflicted file? Sounds ridiculous, right?

Here is a collection of real-world cases and their alternative resolutions.

Wrong Operation

Use Case

  • You rebase.
  • Conflicts.
  • You realize rebasing wasn’t your goal at all.

Solution

jj undo

There is no jj rebase --abort because rebases always succeed. There’s nothing pending to abort.

Wrong Source

Use Case

○  h  <- Paste
○  g
│ ○  f
│ ○  e
│ ○  d  <- Cut
│ ○  c  <- You should have cut here
├─╯
○  b
○  a
◆
  • You rebase:
jj rebase -s d -o h
×  f
×  e
×  d
○  h  <- Paste
○  g
│
│ ○  c  <- Left over
├─╯
○  b
○  a
◆
  • Conflicts!
  • You realize you forgot to include Change c.

Solution

Add the missing Change:

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
◆
  • You rebase.
jj rebase -s c -o g
×  f
×  e
×  d
×  c
│ ○  h
├─╯
○  g
○  b
○  a
◆
  • Conflicts!
  • You notice the rebase target was wrong.

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 conflict-free Change that wasn’t even rebased.

Use Case

○  h
○  g     <- Function rename here
│ ○  f     <- Original function name used in this branch
│ ○  e
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆

Throughout c::f, the code uses foo().
g renames foo() to bar(), making itself incompatible with c::f.

  • You rebase.
jj rebase -s c -o h
  • Conflicts!
×  f
×  e
×  d
×  c     <- Conflict on the function name
○  h
○  g     <- Function rename here
○  b
○  a
◆
  • You realize the conflict originates from the function rename in g.
  • You decide you don’t want that new function name after all.

Solution

Jujutsu doesn’t restrict your edits to the conflicting Changes. Sometimes conflicts can be resolved from a distance.

  • Identify the Change with the unwanted edit (g).
  • Edit it:
jj new -g
sed -i 's/bar/foo/g' file.py
jj squash
  • The conflicts disappear:
○  f
○  e
○  d
○  c     <- No more conflicts
○  h
○  g     <- Back to the old name
○  b
○  a
◆

Wrong Order

Sometimes conflicts are not about Changes content, but in the order they end up in.

Use Case

○  h
○  g
│ ○  f  <- removes file README.md
│ ○  e  <- modifies file README.md
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆
  • Rebase f on top of h:
jj rebase -r f -o h
○  f  <- removes file README.md
○  h
○  g
│ ○  e  <- modifies file README.md
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆
  • Rebase e on top of f:
jj rebase -r e -o f
×  e  <- modifies file README.md
○  f  <- removes file README.md
○  h
○  g
│ ○  d
│ ○  c
├─╯
○  b
○  a
◆
  • Conflict.

Solution

The 2 rebases swapped e and f from their original order. An easy solution is to swap them back:

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

  • You just completed a branch about PDF generation:
○  h
○  g
│ ○  f   <- Branch about PDF generation
│ ○  e
│ ○  d   <- Unrelated changes to authentication.ts
│ ○  c
├─╯
○  b
○  a
◆
  • You rebase it on top of h:
jj rebase -s c -o h
  • Conflicts.
×  f
×  e
×  d   <- Conflicts in file authentication.ts
○  c
○  h
○  g
○  b
○  a
◆
  • You notice the conflicting files are about a different feature (authentication) and have nothing to do with this branch.

Solution

Move the conflicts elsewhere, separating the conflict-free PDF generation from authentication.

There are several ways.

With jj squash

  • Create a Change elsewhere (e.g., on top of a) to hold the conflicts:
jj new -r a -m "authentication"
×  f
×  e
×  d   <- Conflicts in file authentication.ts
○  c
○  h
○  g
○  b
│ ○  x authentication
├─╯
○  a
◆
  • Move the conflicts there:
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. You can do jj new and jj squash in one step using jj squash -o a (which creates a new Change on top of a):

jj squash -f d -o a authentication.ts

With jj split

  • Split the conflicted Change in two:
jj split -r d -m "authentication"
  • Select all unrelated changes for authentication.ts. Keep the PDF generation changes aside.

  • jj split creates a new Change containing only the unrelated changes (and thus the conflicts):

×  f
×  e
×  d                    <- Split part with related changes
×  x  authentication    <- Split part with unrelated changes
○  c
○  h
○  g
○  b
○  a
◆
  • Move the conflicts away:
jj rebase -r x -o a
○  f
○  e
○  d                    <- Split part with related changes
○  c
○  h
○  g
○  b
│ ×  x authentication    <- Split part with unrelated changes
├─╯
○  a
◆

Artifact Files You Should Have Ignored

Use Case

○  h
○  g
│ ○  f
│ ○  e
│ ○  d
│ ○  c
├─╯
○  b  <- Binary files added the first time here
○  a
◆
  • You rebase.
jj rebase -s c -o h
×  f
×  e
×  d
×  c
○  h
○  g
○  b  <- Binary files added the first time here
○  a
◆
  • Conflicts.
  • The conflict involves .bin files that should have been Git ignored from the start.

Solution

The idea is:

  • Git ignore the binary files in the Change where they were initially added.
  • Move all the edits to .bin in a temporary Change, then abandon it.

Here’s how:

  • Find the Change that originally added .bin:
jj log -r 'files(".bin")'
  • Create a Buffer Change on top of it:
jj new -r b
  • Add the missing item to .gitignore:
echo .bin/ >> .gitignore
jj squash
  • Collect all edits to .bin from the whole history into a temporary Change:
jj squash -f 'files(".bin")' bin
  • Abandon the temporary Change:
jj abandon

How Are Conflicts Handled?

Important

To satisfy your curiosity, you look under Jujutsu’s hood, and grasp what “conflicts are first-class citizens” means.

Two Philosophies

Git and Jujutsu fundamentally differ in what they think a conflict is.

To begin with, in Git there is no such thing as a “conflicted commit”. A conflict is not a Git object, but a state blocking operations until completely resolved.

In Jujutsu, conflicts are part of history, and can be manipulated like Changes. Operations creating conflicts don’t fail; conflicts are recorded for you to resolve later. Best of all, “editing broken files” isn’t the only way out.

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

In both Git and Jujutsu a non-conflicted commit holds:

  • Pointers to its parents (that is, its position in history).
  • A pointer to exactly 1 Tree: a coherent photograph of the project’s filesystem.
A Git Tree pointing to 2 Blobs and 1 Tree

From here, Git and Jujutsu dramatically diverge.

Conflicts in Git

All Git operations are atomic: they either succeed or roll back leaving the repository exactly as it was. Except during conflicts. When merging fails, Git cannot forge a coherent Tree, so it is stuck halfway through of an incomplete transaction. So it:

  • Gives up creating the next dommit.
  • Blocks the repository.
  • Hands the problem over to you.

Git does it best to help you solve the conflict, spreading helpers in 3 places:

  • In the index: for each conflicted file, 3 versions (:1 ancestor, :2 ours, :3 theirs).

  • In the working copy: this is the only time when Git violates its own rules and tampers with your files, filling them with conflict markers <<<<<<<, =======, and >>>>>>>.

  • In .git: a bunch of state files like MERGE_HEAD to keep track of the transaction.

In the meanwhile, your repo remains an exceptional no-fly zone.

Conflicts in Jujutsu

Jujutsu doesn’t panic. It doesn’t even inject conflict markers on disk. It creates the Change anyway. But how?

  • Position? Easy, the parents are known.

  • Content? All Jujutsu knows is that a consistent Tree must be somehow derived combining the Parent 1’s Tree, the Parent 2’s Tree and the Base’s Tree.

Why not point to all of them? After all: nothing prevents a Change from targeting more than one Tree, like in Git.

Conflicted Changes

Yep. A Change internall hold always a collection of Trees. That’s the trick.

  • If that collection contains 1 Tree only, then it’s an unconflicted Change.
  • If there’s more than 1, then there is merge to complete.

This normalization lets Jujutsu treat all Changes consistently, no matter the conflicts.

The Algebra Of Conflicts

A convenient way to represent the Trees referenced by a Change is as an algebraic expression with + (apply) and - (diff) signs:

  • T − F is the diff between the content of Change T and the content of Change F, so the changes to apply from F to get to T.
○ T  ─╮
│     │   T - F
○ F  ←╯
  • F + diff means applying the edits diff on top of a the content of Change F, getting back a new Change T.
                              ○ T        ─╮
           + diff      ═══⇒   │           │   T - F = diff
○ F                           ○ F        ←╯

A failed merge between X and Y is stored as Y + (X − B). It means:

Apply the diff (X - B) onto Y.

In the happy scenario, Jujutsu will execute that operation, converging to a conflictless single Tree.

It it can’t reduce this to 1 Tree, it records [X, Y, B] meaning: “I need to do Y + (X − B), help me.”

A Concrete Example

Say greetings.txt exists in B, X and Y with these content:

○    Y    hello mundo
│ ○  X    hola world
├─╯
○    B    hello world

Merging X and Y you get:

×    M                <- conflict (we would like "hola mundo")
├─╮
│ ○  Y    hello mundo
○ │  X    hola world
├─╯
○    B    hello world

Both changes touch the same line, so you get a conflict. M records X + (Y − B). Jujutsu failed to reduce it, but M exists anyway as an ordinary Change. You can jj edit M to resolve the conflict whenever you like.

Simplifications

If you apply the diff (X - B) to its natural base B you get:

B + (X - B)

The grade school student in you recalls that B and -B cancel, so this simplifies algebraically to:

X

Wrongly Applied Diffs

This lets you interpret a conflict as those cases where you cannot simplify algegraic expressions, or:

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 stores [Y, X, B] hoping you either edit files, or move Changes so algebra simplifies the conflict away.1

Markers Are An Illusion

When you visit a Conflicted Change, you’ll find conflict markers. Yet conflict markers don’t exist in Jujutsu’s repositories: they are generated on-the-fly when you visit a Conflicted Change.
When you save, Jujutsu parses them back into logical states.

Because they are dynamic, you can choose between 3 built-in styles, including the original Git’s one.


  1. How can possibly algebra simplify the conflict away? If you at curious, check out Back to Grade School!

Bookmarks and Remotes

Important

You discover:

  • How to play with local Bookmarks.
  • Why Bookmarks don’t follow your work like Git branches do. Aargh!
  • How to work with remote branches.
  • How to resolve Bookmark conflicts.

Managing branches in Jujutsu is easy. You could mentally translate Git’s “branches” to Jujutsu’s “Bookmarks.”

However, that’s a naïve oversimplification. It won’t help you reasoning about certain cases that are too common to be ignored, like when you get a Bookmark conflict.
Huh? Conflicts in Bookmarks? Git has nothing like that…

So, first, I’ll help you build a more solid mental model.

Bookmarks are Not Branches

Important

You learn that Bookmarks can point to multiple commits simultaneously.

Let’s dive into the oddities. You’re used to them by now.

  • In Git, a branch is a label pointing to exactly 1 commit. A simple model1.
  • In Jujutsu, Bookmarks are labels that can point to multiple revisions.

This shouldn’t sound too weird; it’s consistent with what you know about conflicts:

  • Changes pointing to 1 Tree are conflict-free.
  • Changes pointing to multiple Trees have conflicts.

The same logic applies to Bookmarks. Normally, a Bookmark points to a single revision:

@  x   ◀─────── **foo**
│ ○  y
├─╯
○  q
◆  z

In this happy case, Git and Jujutsu are identical. You’ll see a log like this:

@  x  **foo**
│ ○  y
├─╯
○  q
◆  z

It may happen that you and a remote colleague move the same branch to irreconcilable positions. In that case, the Bookmark points to multiple revisions:

@  x   ◀────────┐
│               ├─ **foo**
│ ○  y   ◀──────┘
├─╯
○  q
◆  z

You have a Bookmark conflict. You must resolve it so the Bookmark points to just 1 revision. Jujutsu displays this as:

@  x  **foo??**
│ ○  y  **foo??**
├─╯
○  q
◆  z

The good news: the logic for resolving code conflicts applies here, and it’s even easier.


  1. If it weren’t for the confusing name “branch”. In Git, a branch is both a reference to a commit and a sequence of commits diverging from the trunk.

Commands

Commands

As long as there are no conflicts, you can treat Jujutsu’s local Bookmarks exactly like Git’s local branches.

GoalJujutsu command and its Git equivalent
Listjj bookmark list
git 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: Use a single letter for Bookmark commands. Instead of jj bookmark list, jj b l works perfectly.

Creating, moving and deleting local branches is trivial:

jj bookmark create my-branch -r x
jj bookmark set my-branch -r y
jj bookmark delete my-branch

Moving Bookmarks

Important

You find out that there is no current branch following your work as you commit.

Did you notice the mysterious jj bookmark advance command? It deserves a few words.

The main observation worth noting is how and when Git and Jujutsu decide to move local branches.

In Jujutsu, Bookmarks stick to the Change they were created on. If you move history trees around, the Bookmarks move with them. They stick to Changes, not to the underlying Git commits. Git can do this via rebase --update-refs1; in Jujutsu, it’s the default.

Bookmarks Don’t Follow Commits

Git has a notion of current branch which automatically advances as you create commits.

Jujutsu has not: a Bookmark never replaces the Change it points to, unless explicitly told to. Look what happens with jj commit (which is just jj desc followed by jj new):

jj git init
jj bookmark create main
jj commit -m "Hello"
@  w    (empty)
○  k    (empty) Hello **main**
◆  z    (empty)
echo 'print("Hello, World!")' > main.py
jj squash
jj commit -m "Hola"
echo 'print("Hola, Mundo!")' >> main.py
jj squash
@  n    (empty)
○  o    Hola
○  k    Hello **main**
◆  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

Isn’t updating Bookmarks by hand annoying? Indeed, this makes newcomers frown. I promise you’ll appreciate its elegance over time.

Jujutsu constantly nudges you toward code reviews. If you think about it, the Squash Workflow revolves around the idea of giving you a chance to run jj diff before finalizing your edits. Similarly, tree manipulation is so pervasive that it makes sense to explicitly review history before moving Bookmarks and pushing them.

Even further: typically, Bookmarks are created at the very last moment, right before opening a Pull Request. Idiomatically, Jujutsu is used as a branchless system.

Bottom line: jj bookmark advance let you manually do what git commit does automatically, giving you a review phase before pushing.

That’s all for 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

Every Repo is a Remote

Important

You see how your local Git repository is a pseudo-remote.

Jujutsu favors consistency: a minimal bunch of building blocks that compose well together. This applies to remotes, too.

Git treats the local repository and remotes very differently. Jujutsu simplifies this: everything is a remote. Imagine your Jujutsu workspace connects to a collection of remote repositories. Your local Git repo is just one of them, named git. You have origin on Codeberg? Similarly, you have git hosted locally on your disk.

Keep this mental model: it helps understand why branches display as my-branch@origin or my-branch@git.

Pseudo-remote

Actually, I lied a bit.

The git repository is more a pseudo-remote than a true one. For example, you cannot fetch from or push to it. Jujutsu treats it with privilege, keeping it updated in real-time. You can imagine that it runs git fetch and git push automatically on every command you execute.1

So, don’t take this literally; just use it as a mental model. It helps reasoning.


  1. For correctness: to sync with the git pseudo-remote, instead of fetch and push Jujutsu uses jj git import and jj git export. You will rarely need them, though.

Local Bookmarks and Branches

Important

You see how Local Bookmarks and Git branches stay aligned in real-time.

@  n    (empty)
○  o    Hola
○  k    main Hello
◆  z    (empty)

Create a Git branch:

git branch dev

Notice how Jujutsu instantly reflects this as a new Bookmark dev:

@  n    (empty)
○  o    Hola **dev**
○  k    Hello **main**
◆  z    (empty)

This is also reflected in jj bookmark list:

jj b l
**foo**: nwupqqun 9cad0562 (empty) (no description set)
**main**: kxrxykwo 709deefc Hello

Now, move the main Bookmark:

jj bookmark move main -t o
Moved 1 bookmarks to ovrsvuxw 7c6a6f6c dev main* | Hola

@  n    (empty)
○  o    Hola **dev** **main**
○  k    Hello
◆  z    (empty)

Check Git:

git log --oneline --graph
* 7c6a6f6 (**HEAD**, **main**, **dev**) Hola
* 709deef Hello

See? Local Git branches and local Jujutsu Bookmarks are always aligned; you can safely consider them identical.

Can They Diverge?

Technically, yes, but only in degenerate situations, outside day-to-day work. Here’s what you would see:

@  x   **foo@git**
│ ○  y   **foo***
├─╯
○  q
◆  z    (empty)

You can interpret this as:

  • Local Bookmark foo is on y. Jujutsu would like to set the Git branch here too. The asterisk * invites you to align them.
  • The remote Bookmark foo@git indicates that the last known position of foo in the git pseudo-remote is x.

Usually, Jujutsu aligns this situation autonomously.

The intuition I suggest you to develop is:

Jujutsu’s Bookmarks indicate the position Jujutsu wants the Git branches and the remote branches to take.

By the way: that’s a good chance to talk about remotes.

Tracked and Tracking Bookmarks

Important

You learn to control remote branches.

A local Bookmark can be bound to a remote Bookmark. They follow each other based on 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.

Remotes in Practice

Let’s create 3 local repositories:

RepositoryDescription
project-remoteA bare Git repo to act as the remote.
oursA clone of project-remote. Your working repo.
theirsAnother clone. The working repo of a hypothetical coworker.

There are only 3 commits, but don’t worry: it’s plenty to chew on.

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

By default, Git and Jujutsu only show the main branch.

git log --oneline --graph
* 7dac65a (**HEAD**, **origin/main**, **main**)
jj log
@  p    (empty)
◆  u    **main**
│
~

Remote branches are displayed only when asked explicitely:

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)

This means:

  • bar exists only on origin.
  • foo exists only on origin.
  • main exists as a local Bookmark, as a branch on git, and as a remote Bookmark on origin.

The indentation under main helps you see that it tracks both git and origin. Jujutsu will try to keep these 3 bookmarks aligned. Right now, all point to 7dac65ac.

Hidden Changes

Important

You see how Jujutsu hides information to keep things clean, preventing you from tampering with published commits.

By default, neither Git nor Jujutsu display commits from remote branches:

jj log
@  p    (empty)
◆  u    **main**
│
~

Why hide them?

Jujutsu is parsimonious: it shows only the strict minimum information. Git behaves similarly, but Jujutsu is even stricter (you’ve surely noticed: jj log often omits Changes replacing whole branches with a ~ symbol).

The idea is that remote branches contain other people’s work. Unless you ask, Git and Jujutsu assume you aren’t contributing to them, so they hide the commits and won’t create local tracking branches.

Immutable Changes

Jujutsu goes even further: assuming you shouldn’t interfere with colleagues’ work, unless you directly say you want, it protects remote branches by making their Changes immutable.

See. Log all the descendants of the root() Change (zzzzzzzzzzzzz):

jj log -r 'trunk()::'
@  p    (empty)
│ ◆  s    **foo@origin**
├─╯
│ ◆  o    **bar** **bar@origin**
├─╯
◆  u    **main**
│
~

The diamond means u, o and s are immutable. You would get the same information using the immutable() Revset function:

jj log -r 'immutable()'

Modifying Immutable Changes

What if you try to modify an immutable Change? Jujutsu will stop you:

jj edit -r o
Error: Commit fa22ec047bc7 is immutable

This is safer that Git, which lets you tamper with published commits, only to complain later when you push.

You can always force edits with --ignore-immutable, but it’s better to declare your intent by creating a tracking Bookmark. We’ll cover that in the next page.

Tracking Remote Bookmarks

Important

You see how to reveal hidden remote branch Changes and make them mutable.

How do you tell Jujutsu you want to work on foo, so it:

  • Stops hiding its Changes?
  • Stops treating them as immutable?

You just declare that you want a local Bookmark foo tracking the remote Bookmark foo@origin:

jj bookmark track foo
Started tracking 1 remote bookmarks.
@  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:

  • A local Bookmark foo was created.
  • Consequently, a Git branch foo (foo@git) appeared.
  • The remote Bookmark foo@origin is displayed, properly indented under foo, indicating that foo is tracking it.

Unlike Git, all 3 Bookmarks must share the exact same name.

Run jj log again:

@  p    (empty)
│ ○  s    **foo**
├─╯
◆  u    **main**
│
~

As expected:

  • foo Changes are not hidden anymore.
  • And are no longer immutable (see instead of ?)

I guess now you understand why, after jj git clone, I always suggested to run:

jj bookmark track '*'

Tracking bar

Quiz time:

Warning

Track bar too.

Tip

jj bookmark track bar --remote=origin

Or abbreviated:

jj b t bar

The log now shows:

@  p    (empty)
│ ○  s    **foo**
├─╯
│ ○  o    **bar**
├─╯
◆  u    **main**
│
~

Bravo!
It’s time to play with fetch and push.

Moving and Pushing Bookmarks

Important

You finally publish your branches to remotes.

Build a commit on top of foo:

jj new foo
echo "hello world" > hello.txt
@  k
○  s    **foo**
│ ○  o    **bar**
├─╯
◆  u    **main**
│
~

As expected, local Bookmark foo didn’t follow your Current Change.

Moving Bookmarks

To update foo to point to k, you have 2 options:

  • Explicitly specify the target with jj bookmark move (jj b m):
jj bookmark move foo -t k
  • Let Jujutsu automatically move it forward with jj bookmark advance (jj b a):

Misalignments

Look at what you get moving foo:

@  k    **foo***
○  s    **foo@origin**
│ ○  o    **bar**
├─╯
◆  u    **main**
│
~

Warning

Can you explain to yourself what this means?
Let jj bookmark list help you.

Tip

jj bookmark list --all-remotes shows:

**foo**: ksvskswq dd2904b3 (no description set)
 **@git**: ksvskswq dd2904b3 (no description set)
 **@origin** (behind by 1 commits): stnkrwop 00687915 (no description set)
  • Local foo is on k, aligned with Git branch foo@git.
  • The origin remote stayed behind (behind by 1 commits) on s.
  • foo should be pushed so foo@origin can align.

If you specify a Bookmark, Jujutsu shows only the essential:

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. jj log agrees:

@  k    **foo***
○  s    **foo@origin**
│ ○  o
├─╯
◆  u
│
~

Pushing to Remotes

To align foo@origin with your local foo, you need jj git push. Indeed, the asterisk in foo* is Jujutsu suggesting you to push:

jj git push --bookmark=foo --remote=origin

or, just:

jj git push -b foo

If you try, you’ll get:

Error: Won't push commit dd2904b35cee since it has no description
Hint: Rejected commit: ksvskswq dd2904b3 foo* | (no description set)

Good boy, Jujutsu! It gives you local freedom but strictly adheres to Git’s conventions when dealing with remotes: Git rejects descriptionless commits. Fine:

jj desc -m "Hello, world"
jj git push -b foo

Done. Admire how your Bookmarks are nicely aligned:

@  k    Hello, world **foo**
○  s
│ ○  o    **bar**
├─╯
◆  u    **main**
│
~

Fetching

Important

You learn to update from a remote.

Switch to your coworker’s perspective. Go to the theirs repository:

cd ../theirs
@  s    (empty)
◆  u    **main**
│
~

Only main is visible. Your coworker hasn’t declared intent to work on foo or bar, so they remain 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)

Track all the Bookmarks:

jj bookmark track "*" --remote=origin
@  sz   (empty)
│ ○  st   **foo**
├─╯
│ ○  o    **bar**
├─╯
◆  u    **main**
│
~

There they are: all visible and mutable. Your coworker doesn’t see your Change k yet. This is like with Git: a repository aligns with its remotes only during fetch and push.

Here, foo@origin represents the last known position of foo on the remote origin. Time to refresh it.

Fetching From a Remote

jj git fetch
@  sz   (empty)
│ ○  k    Hello, world **foo**
│ ○  st
├─╯
│ ○  o    **bar**
├─╯
◆  u    **main**
│
~

There’s k! Notice foo (and 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 tells Jujutsu to:

  • Retrieve all Changes from origin and integrate them locally.
  • Update the position of every remote Bookmark, if possible.
  • Align each local Bookmark to its tracked remote Bookmark, if possible.

Why did I say “if possible”? Because things don’t always go smoothly.

Indeed: enough with the happy path. Let’s see how to tackle the case where you and your coworker update the same remote Bookmark in irreconcilable ways. Finnaly, conflicts in Bookmarks.

Bookmark Conflicts

Important

You sort out situations where two developers move the same Bookmark into incompatible positions.

In general, moving a Bookmark forward usually suceeds. Moving it backwards or sideways to a parallel branch is less natural. Git allows it locally, but rejects the subsequent push complaining the move was non-fast-forward.

Jujutsu anticipates the problem and raise an error before the push, right when you attempt the move.
In theirs, try moving foo from k to o:

jj bookmark move foo --to o
Error: Refusing to move bookmark backwards or sideways: foo
Hint: Use --allow-backwards to allow it.

Better safe than sorry.

Your Coworker Forces Their Way

If you really want to win this fight, you can always use the --allow-backwards parameter.

jj bookmark move foo --to o --allow-backwards
@  sz   (empty)
│ ○  k    Hello, world **foo@origin**
│ ○  st
├─╯
│ ○  o    **bar** **foo***
├─╯
◆  u    **main**
│
~

2 things to notice:

  • Because foo and foo@origin are now on different Changes, Jujutsu displays both.
  • The asterisk in foo* invites your coworker to align.

Indeed, your colleague pushes:

jj git push -b foo
@  sz   (empty)
│ ○  k    Hello, world
│ ○  st
├─╯
│ ○  o    **bar** **foo**
├─╯
◆  u    **main**
│
~

then calls it a day. Bye bye!

What Happens in Your Repository

Back in ours repository. Unaware of your colleague’s move, you keep working on foo, adding another Change:

jj new -m "Hola, mundo"
echo "Hola, mundo" > hello.txt
@  ol   Hola, mundo
○  k    Hello, world **foo**
○  s
│ ○  os   **bar**
├─╯
◆  u    **main**
│
~

and you advance foo to ol:

jj bookmark advance
@  ol   Hola, mundo **foo***
○  k    Hello, world **foo@origin**
○  s
│ ○  os   **bar**
├─╯
◆  u    **main**
│
~

Jujutsu marks foo*, inviting you to push:

jj git push -b foo
Error: Failed to push some bookmarks

Hint: Try fetching from the remote, then make the bookmark point to where 
      you want it to be, and push again.

Rejected.

Conflicts

Here’s the log after you fetch:

@  ol   Hola, mundo **foo??** **foo@git**
○  k    Hello, world
○  s
│ ○  os   **bar** **foo??** **foo@origin**
├─╯
◆  u    main
│
~

Listing bookmarks also helps:

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)

OK, calm down: there are 4 instances of foo now. How should you interpret the situation?

  • Two foo?? markers (on ol and os) means that the local foo points to both because you wanted it on ol, but origin says it should be on os. Undecided, Jujutsu targets both.
  • foo@git means that, given the unclarity, the Git branch remains on ol.
  • foo@origin shows the last known position in origin.

This 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 having foo holding both the Changes in ol and those in os is to create a merge Change:

Change:

jj new -r ol -r os -m "Merge"
@    p    (empty) Merge
├─╮
│ ○  os   **bar** **foo??** **foo@origin**
○ │  ol   Hola, mundo **foo??** **foo@git**
○ │  k    Hello, world
○ │  s
├─╯
◆  u    main
│
~

p is a good place to move foo:

jj bookmark move foo -t p
@    p    (empty) Merge **foo***
├─╮
│ ○  os   **bar** **foo@origin**
○ │  ol   Hola, mundo
○ │  k    Hello, world
○ │  s
├─╯
◆  u    **main**
│
~

Lovely! No conflicts anymore, only the invitation to push:

jj git push -b foo
@    p    (empty) Merge **foo**
├─╮
│ ○  os   **bar**
○ │  ol   Hola, mundo
○ │  k    Hello, world
○ │  s
├─╯
◆  u    **main**
│
~

All clear. Your coworker is forgiven.

Human Friendly Log

Run:

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()
  )
"""

Pulling the Rug Out Under Oneself

Here’s the situation:

jj edit vz
@    vz          empty
├─╮
│ ○  o          Play instructions
│ ○  kx         Play manual
○ │  zx         Click alternates Xs and Os main
○ │  v          Click sets a X
○ │  s          Square has state
├─╯
~

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
├─╯
○  qq       Fix
○  n        Square is interactive
│
~

Uh! That was unexpected! The merge is still there, with a different Change ID. Why?

This is what Jujutsu thinks:

  • User asked to abandon vz. At once! Deleted!
  • Uhm. That Change was the Current Change and I cannot leave the user without one. I need to create a new Change!
  • Where? Best idea is to create one in the same position, so same parents.

And so another merge Change is generated.

This is a general rule: abandon your Current Change, a new one will be immediately created.

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.

Back to Grade School!

Imagine this (admittedly contrived) situation:

○  S    s/hola/salut/g  -> salut world     S  = H + (S − H)
○  H    s/hello/hola/g  -> hola world      H  = B + (H − B)
○  B                       hello world

Revert H with jj revert H. This applies - (H - B) = (B - H) to S:

×  P'   s/hola/hello/g                   P' = S + (B - H)
○  S    s/hola/salut/g  -> salut world   S  = H + (S − H)
○  H    s/hello/hola/g  -> hola world    H  = B + (H − B)
○  B                       hello world

The natural basis for (B - H) would be H, but it landed on S. Wrong base: conflict.

Resolve it by rebasing P' back to H:

jj rebase -r P' -d H
○  P'   s/hola/hello/g    -> hello world   P' = H + (B − H) = B
│ ○  S  s/hola/salut/g    -> salut world   S  = H + (S − H)
├─╯
○  H    s/hello/hola/g    -> hola world    H  = B + (H − B)
○  B                         hello world

H + (B − H) algebraically simplifies to B.

Conflict gone. No file edits needed. Magic!

Resolving by Abandoning

Alternatively, just jj abandon S:

jj abandon S
○  P'   s/hola/hello/g    -> hello world   P' = H + (B − H) = B
○  H    s/hello/hola/g    -> hola world    H  = B + (H − B)
○  B                         hello world

Same cancellation.

A conflict is really just a diff parked on the wrong base. Moving it where algebra simplifies it makes the conflict vanish.

Quick Start

Important

This chapter is a quick demo of Jujutsu. Skip it if you like: we’ll cover the same ground in the rest of chapters.

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 '*'

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.

jj log

Log the history tree with Jujutsu:

jj log

I prefer a more compact template:

jj log -T builtin_log_oneline

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.

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.

A Typo In an Old Commit Description

Look at this commit:

History
tree showing a commit with a typo in its description

See the typo?. 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.

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

Gosh, conflicts!

Two things to notice:

  • First, you rebased a branch while you were somewhere else. Git wouldn’t let you do that.
  • Second, conflict didn’t stop Jujutsu: 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

Voilà, 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 jj op. Think of them as Git’s reflog on steroids. You will read about them in It’s History All The Way Down.

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.

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: the commit rpzkmwvr.

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, 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
    ...

then send it five commits down:

$ jj squash --into r
Rebased 7 descendant commits

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

Amending The Last Commit

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

jj show dev

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.

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

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

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 status will tell you:

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

Good, no more conflicts.

jj squash

All clear!

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:

jj rebase -r l --before v

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

Error: Commit 06acab9454ba is immutable

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

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 diverged.

And Git?

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?