Posted by Bogdanp 8/31/2025
https://zed.dev/blog/sequoia-backs-zed
DeltaDB (CRDT based)
Jujutsu does not treat merge commits any more or less special than non-merge "1-parent" commits. (We used to do this, actually, and occasionally special case merges, but most users found it very confusing.) This regularity means most commands work fine on merges. 'jj new X' is a new commit on top of X. 'jj new X Y' is a merge commit with X and Y as parents. 'jj new X Y Z...' is a 3-way merge, and so on and so forth for any number of commits. Similarly, 'jj rebase' can handle moving commits and preserving the graph structure no matter how many edges are involved in particular, and can add or remove parents from a given commit. This means that conceptually the jj commit graph is merely a simple, ordinary DAG, and operations are transformations on the DAG like you expect.
Actually, this exact workflow is beloved by many community members, and I guess I can take responsibility for popularizing it originally, the "Mega Merge" technique. Instant, easy rebase is an essential part of making this technique viable.
- https://ofcr.se/jujutsu-merge-workflow - https://v5.chriskrycho.com/journal/jujutsu-megamerges-and-jj...
Let's say you've got a few feature branches, all based of the trunk branch.
$ jj
@ ozywpwxm samfredrickson@gmail.com 2025-08-31 13:21:59 b2f1364d
│ (empty) (no description set)
│ ○ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:21:27 9cda0936
├─╯ (empty) Feature C
│ ○ ukqynvts samfredrickson@gmail.com 2025-08-31 13:21:26 ceee7029
├─╯ (empty) Feature B
│ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:21:24 9ccbedf6
├─╯ (empty) Feature A
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 master git_head() 8e80b150
│ Update Claude Code to 1.0.93.
One neat workflow supported by Jujutsu is "working on all branches at the same time." $ jj new q u n
Working copy (@) now at: zzxxqlzr fb73d4dc (empty) (no description set)
Parent commit (@-) : qxoklwxv 9cda0936 (empty) Feature C
Parent commit (@-) : ukqynvts ceee7029 (empty) Feature B
Parent commit (@-) : nwtxnvxp 9ccbedf6 (empty) Feature A
$ jj
@ rzouzmyw samfredrickson@gmail.com 2025-08-31 13:25:56 fb73d4dc
├─┬─╮ (empty) (no description set)
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:21:24 9ccbedf6
│ │ │ (empty) Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 13:21:26 ceee7029
│ ├─╯ (empty) Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:21:27 git_head() 9cda0936
├─╯ (empty) Feature C
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 master 8e80b150
│ Update Claude Code to 1.0.93.
~
Now you can use the merge revision as a scratch space, and then squash changes from it into one of the feature revisions. $ vim README.md
$ jj squash --into n
Working copy (@) now at: rzouzmyw 30ff9b0f (empty) (no description set)
Parent commit (@-) : qxoklwxv 9cda0936 (empty) Feature C
Parent commit (@-) : ukqynvts ceee7029 (empty) Feature B
Parent commit (@-) : nwtxnvxp fb3cca28 Feature A
$ jj
@ rzouzmyw samfredrickson@gmail.com 2025-08-31 13:27:41 30ff9b0f
├─┬─╮ (empty) (no description set)
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:27:41 fb3cca28
│ │ │ Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 13:21:26 ceee7029
│ ├─╯ (empty) Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:21:27 git_head() 9cda0936
├─╯ (empty) Feature C
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 master 8e80b150
│ Update Claude Code to 1.0.93.
Later, you decide to fetch changes from your remote, and notice that your revisions are based on an out-of-date version of the trunk. $ jj git fetch
remote: Enumerating objects: 17, done.
remote: Total 12 (delta 6), reused 0 (delta 0), pack-reused 0
bookmark: master@origin [updated] tracked
$ jj
@ rzouzmyw samfredrickson@gmail.com 2025-08-31 13:27:41 30ff9b0f
├─┬─╮ (empty) (no description set)
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:27:41 fb3cca28
│ │ │ Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 13:21:26 ceee7029
│ ├─╯ (empty) Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:21:27 git_head() 9cda0936
├─╯ (empty) Feature C
│ ◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ │ Update Claude Code to 1.0.98.
│ ~ (elided revisions)
├─╯
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 8e80b150
│ Update Claude Code to 1.0.93.
~
With Jujutsu, you can run one command to rebase _everything_ against the latest trunk revision. $ jj rebase -s 'roots(trunk()..mutable())' -d 'trunk()'
Rebased 4 commits to destination
Working copy (@) now at: rzouzmyw 88ed8085 (empty) (no description set)
Parent commit (@-) : qxoklwxv 005442c3 (empty) Feature C
Parent commit (@-) : ukqynvts 23923cf2 (empty) Feature B
Parent commit (@-) : nwtxnvxp 769d0539 Feature A
Added 0 files, modified 2 files, removed 0 files
$ jj
@ rzouzmyw samfredrickson@gmail.com 2025-08-31 13:32:08 88ed8085
├─┬─╮ (empty) (no description set)
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:32:08 769d0539
│ │ │ Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 13:32:08 23923cf2
│ ├─╯ (empty) Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:32:08 git_head() 005442c3
├─╯ (empty) Feature C
◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ Update Claude Code to 1.0.98.
~
I use this command so much that it's aliased as "jjsr", "Jujutsu Super Rebase".The "super rebase" seams to be nice though. However I just tested it and achieved the same with git rebase --rebase-merges --update-refs. Have I missed something?
Anyway, I just tried that command you suggested, but it didn't seem to work?
$ jj new
$ jj bookmark create woot -r @-
$ jj
@ wxkrvmxs samfredrickson@gmail.com 2025-08-31 14:53:19 4bbb7f5a
│ (empty) (no description set)
○ rzouzmyw samfredrickson@gmail.com 2025-08-31 13:27:41 woot git_head() 30ff9b0f
├─┬─╮ (empty) (no description set)
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:27:41 fb3cca28
│ │ │ Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 13:21:26 ceee7029
│ ├─╯ (empty) Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:21:27 9cda0936
├─╯ (empty) Feature C
│ ◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ │ Update Claude Code to 1.0.98.
│ ~ (elided revisions)
├─╯
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 8e80b150
│ Update Claude Code to 1.0.93.
~
$ git checkout woot
$ git log --graph
*-. commit 30ff9b0f274c9adaca4eeadcf21d5e918e4e3578 (HEAD -> woot)
|\ \ Merge: 9cda093 ceee702 fb3cca2
| | | Author: Sam Fredrickson <samfredrickson@gmail.com>
| | | Date: Sun Aug 31 13:27:41 2025 -0700
| | |
| | * commit fb3cca2823b4dffe374b67d28e3c91c206828d47
| | | Author: Sam Fredrickson <samfredrickson@gmail.com>
| | | Date: Sun Aug 31 13:21:24 2025 -0700
| | |
| | | Feature A
| | |
| * | commit ceee7029730c49ae30890c1641c7d6645e60fca4
| |/ Author: Sam Fredrickson <samfredrickson@gmail.com>
| | Date: Sun Aug 31 13:21:26 2025 -0700
| |
| | Feature B
| |
* | commit 9cda09363efe257a954b5563ce6af287a506d808
|/ Author: Sam Fredrickson <samfredrickson@gmail.com>
| Date: Sun Aug 31 13:21:27 2025 -0700
|
| Feature C
|
* commit 8e80b15010c4d9373c5828fdf8a83c53df75ec00
| Author: Sam Fredrickson <samfredrickson@gmail.com>
| Date: Wed Aug 27 09:48:45 2025 -0700
|
| Update Claude Code to 1.0.93.
$ git rebase --rebase-merges --update-refs master
Trying simple merge with cfa85486fa3d53401be518cb42936fb6fd3c128c
Trying simple merge with ffebef1cb34ca1670b48c594b2014e71be7d1b2b
error: Empty commit message.
Not committing merge; use 'git commit' to complete the merge.
Could not apply 30ff9b0... rev-ceee702 rev-fb3cca2 #
$ git status
interactive rebase in progress; onto 658a3d1
Last commands done (10 commands done):
pick 9cda093 # Feature C # empty
merge -C 30ff9b0f274c9adaca4eeadcf21d5e918e4e3578 rev-ceee702 rev-fb3cca2 #
(see more in file .git/rebase-merge/done)
No commands remaining.
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: README.md
$ jj
Reset the working copy parent to the new Git HEAD.
@ tymwqlyq samfredrickson@gmail.com 2025-08-31 14:57:22 d083f61e
│ (no description set)
○ vmnnlqro samfredrickson@gmail.com 2025-08-31 14:56:55 git_head() f92f7505
│ (empty) Feature C
◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ Update Claude Code to 1.0.98.
~ (elided revisions)
│ ○ rzouzmyw samfredrickson@gmail.com 2025-08-31 13:27:41 woot 30ff9b0f
│ ├─┬─╮ (empty) (no description set)
│ │ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:27:41 fb3cca28
├─────╯ Feature A
│ │ ○ ukqynvts samfredrickson@gmail.com 2025-08-31 13:21:26 ceee7029
├───╯ (empty) Feature B
│ ○ qxoklwxv samfredrickson@gmail.com 2025-08-31 13:21:27 9cda0936
├─╯ (empty) Feature C
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 8e80b150
│ Update Claude Code to 1.0.93.
~
Maybe I'm using the git command incorrectly?Also, though, I'm assuming that git command will only rebase the branch you have currently checked out, whereas the jj command I gave will rebase _everything_, not just revisions that are parents of HEAD.
Edit: I figured out my issue. Git doesn't like empty commits & merge commits with no description. After addressing that, then the `git rebase --rebase-merges --update-refs master` command worked.
There's still the caveat though that the Git command will only rebase the "woot" branch. If I had some other "feature D" commit that wasn't included in "woot", that commit wouldn't be rebased. But the `jjsr` command would see and rebase that commit as well.
$ jj
@ qsvwusnk samfredrickson@gmail.com 2025-08-31 16:17:35 6fd02707
│ (empty) (no description set)
○ rzouzmyw samfredrickson@gmail.com 2025-08-31 16:17:35 woot git_head() e6d64886
├─┬─╮ (empty) Merge
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 13:27:41 fb3cca28
│ │ │ Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 16:16:36 9957dca2
│ ├─╯ Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 16:16:44 4361de8b
├─╯ Feature C
│ ○ nwoxyzlx samfredrickson@gmail.com 2025-08-31 16:14:42 ce8de62d
├─╯ Feature D
│ ◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ │ Update Claude Code to 1.0.98.
│ ~ (elided revisions)
├─╯
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 8e80b150
│ Update Claude Code to 1.0.93.
~
$ git checkout woot
Switched to branch 'woot'
$ git rebase --rebase-merges --update-refs master
Trying simple merge with f339926f729437f78ac407c28fc84ce3b0441eb7
Trying simple merge with 4372b2c4f538164a10bb9d8e81899bf61eeecaf2
Merge made by the 'octopus' strategy.
Successfully rebased and updated refs/heads/woot.
$ jj
Reset the working copy parent to the new Git HEAD.
Abandoned 4 commits that are no longer reachable.
Done importing changes from the underlying Git repo.
@ zlxqyrmq samfredrickson@gmail.com 2025-08-31 16:17:59 970c80f8
│ (empty) (no description set)
○ qumulkxr samfredrickson@gmail.com 2025-08-31 16:17:42 woot git_head() cc189c82
├─┬─╮ (empty) Merge
│ │ ○ vkuwsssr samfredrickson@gmail.com 2025-08-31 16:17:42 4372b2c4
│ │ │ Feature A
│ ○ │ lmsrxxzm samfredrickson@gmail.com 2025-08-31 16:17:42 f339926f
│ ├─╯ Feature B
○ │ wrsnrokn samfredrickson@gmail.com 2025-08-31 16:17:42 53ec4dc0
├─╯ Feature C
◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ Update Claude Code to 1.0.98.
~ (elided revisions)
│ ○ nwoxyzlx samfredrickson@gmail.com 2025-08-31 16:14:42 ce8de62d
├─╯ Feature D
◆ yxuvtolz samfredrickson@gmail.com 2025-08-27 09:49:16 8e80b150
│ Update Claude Code to 1.0.93.
~
Versus: $ jjsr
Rebased 6 commits to destination
Working copy (@) now at: qsvwusnk e793b1e4 (empty) (no description set)
Parent commit (@-) : rzouzmyw 3927be34 woot | (empty) Merge
Added 0 files, modified 2 files, removed 0 files
$ jj
@ qsvwusnk samfredrickson@gmail.com 2025-08-31 16:18:58 e793b1e4
│ (empty) (no description set)
○ rzouzmyw samfredrickson@gmail.com 2025-08-31 16:18:58 woot git_head() 3927be34
├─┬─╮ (empty) Merge
│ │ ○ nwtxnvxp samfredrickson@gmail.com 2025-08-31 16:18:58 12f659cf
│ │ │ Feature A
│ ○ │ ukqynvts samfredrickson@gmail.com 2025-08-31 16:18:58 5c0ce98f
│ ├─╯ Feature B
○ │ qxoklwxv samfredrickson@gmail.com 2025-08-31 16:18:58 f9f217e8
├─╯ Feature C
│ ○ nwoxyzlx samfredrickson@gmail.com 2025-08-31 16:18:58 373d6ab1
├─╯ Feature D
◆ zvpmmzru samfredrickson@gmail.com 2025-08-29 15:59:55 master 658a3d12
│ Update Claude Code to 1.0.98.
~
jj doesn't have an explicit concept of the staging area or the stash, but it supports both and in a more powerful way.
In jj, the staging area is simply a terminal node in the graph. You make the changes you want. And when you are ready to commit, you simply push all the changes to the parent node. The terminal node is your index, the parent is the committed node. This is a construct that is entirely in your mind. You don't have to work this way, but if you really like the concept of a staging area, that's how you do it.
Have you ever done a git add, then made some changes, done a subsequent git add, only to realize you clobbered some important code that was in your first git add? How are you going to recover from this? I don't know if the git reflog has this information.
In jj, everything is saved. Think of each commit in jj as a supernode. Inside the supernode are a bunch of atomic nodes. You can think of each atomic node as the equivalent of doing a git add, with each git add being another atomic node in that supernode. So if you've ever clobbered your changes like this, you simply go and remove the last atomic node or modify it however you want to resolve it.
Effectively, jj gives your index its own version control.
Similarly, in jj, a stash is simply a branch. It's a node in a branch that stores the state of the working directory. If you're in the middle of a feature and suddenly need to stop working on it to work on something else, you simply make a new node off the relevant node you're going to work from. In other words, you will just create a new branch while retaining your work in its own branch.
Have you ever popped from the stash, made some changes, and then realized you clobbered something really important and wished you'd applied the stash instead of popping it? That's not at all a concern in jj. Effectively, you have a version controlled stash, and you naturally stash in jj without ever having to know the concept of a stash.
After I'd been using jj, having a separate concept called "index" and a separate concept called "stash" suddenly seemed ridiculous. I don't know why Git decided to have these distinct concepts. At the end of the day, it's all a graph and you are manipulating nodes and the contents within each node. What you need are operations to help you manipulate those.
I have to really emphasize that a typical jj user gets all this power just by learning a few operations - they don't have to learn so many different concepts.
How many different ways are there to do a git reset? In jj, I've only ever had to do jj undo and it covers pretty much all those use cases.
For the defense of git add and the index, I actually personally enjoy it. I discovered git add -p recently, and it's quite nice.
It's like a way to say: let's prepare a commit with only this part of the file, but not yet this one as it's not yet finished.
For example, you can git add -p only the dependencies of a package.json, push that so others have it, and continue working on the scripts part.
I use often as well the index in order to have more granular commits that follows well Conventional Commits, when I want to have a `docs:...` for the readme update and a `feat: ...` for the src change
jj looks interesting, I will take a look!
I'm not criticizing it. In fact, I'm pointing out how, if you like the index/staging workflow, you can have it in jj with much more power (your index is version controlled).
Oh, if you thought this line was the criticism:
> After I'd been using jj, having a separate concept called "index" and a separate concept called "stash" suddenly seemed ridiculous.
I'm not saying the index/stash workflow is ridiculous. I'm saying that having them as separate constructs, and giving them names, is silly. The index need not be distinct from your overall graph.
I think it's great when people become famous for building useful, high-quality works, and other pro-social activities!
> money
Jujutsu is free software in both senses of the word.
It is not free for me. I would need to spend my time on learning it if I would ever encounter it in the wild. The time I could otherwise spend on my family, projects or hobbies. While having no benefits over git, it's trying to steal my time and make my life more complicated, by trying to introduce just another code versioning system no one asked for.