Like? This isn't explained, I'm curious on why I would want to use it, but this is just an empty platitude, doesn't really give me a reason to try.
Like? This isn't explained, I'm curious on why I would want to use it, but this is just an empty platitude, doesn't really give me a reason to try.
You can do all that in Git, but I sure as hell never did; and my co-workers really appreciate PRs that are broken into lots of little commits that can be easily looked over, one by one.
It’s for other branches that hang off the commit that introduced the conflicts.
B --> X --> Y (main) --> Z --> @
\
--> G --> H
B is a base; yesterday the name "main" pointed to it, and today "main" points to Y. Z is a commit you wrote that you haven't published yet. "@" means "Working copy", which is a way of saying "what your filesystem looks like." So, at this time, you see the changes from B, X, Y, Z, but not G or H.You want to rebase G --> H from B to Y. But unfortunately, G conflicts with X. H does not conflict with anything. When you run this rebase in Git, you will actually have to immediately fix the conflict between G and X in order for the rebase to continue. If you do not solve it right then, the entire rebase fails. Git's rebase is actually an algorithm represented by a state machine; you must solve the conflict to proceed from "conflicted state" and `git rebase --continue` the rebase algorithm. (If you imagine what you would need to do to actually implement 'git rebase' as it works today in your own code, this state machine model makes immediate sense.)
In Jujutsu, rebase is a non-stop operation and it always succeeds. There is no state machine. It will update the commit graph to look like this:
B --> X --> Y (main) --> Z --> @
\
--> G --> H
C C
Now G and H are marked as "conflicted". If any commit is marked as conflicted, then all (transitive) children are marked as conflicted, too. If you "switch over" to working on G, then you can solve the conflict and commit the solution. That will solve the conflict in G, and also H as well.But you don't have to do anything. In the above graph, G and H are conflicted, but because they are not a parent of `@`, then it does not matter. They exist in a parallel universe that does not influence your own. You can keep compiling code as usual. If you "switched over" to G, then the conflict is "materialized" in your working copy (filesystem) by putting conflict markers in the files, and so you have to solve it to keep compiling.
In short, Jujutsu separates conflict computation (do patches X,Y have a conflict?) from conflict materialization (make the conflict appear with markers in a file), and materialization of conflicts is "lazy" -- it only happens if a conflict exists transitively in the history of your working copy. Resolution is then done at your leisure.
A more brainiac way of thinking about it is that Jujutsu is a tool for manipulating _commit graphs_, and that is a purely computational notion; adding edges, removing edges, etc are all just basic algorithms. The graph's nodes contain "content" and states like "conflicts" are just defined as a relationship C(X,Y) on nodes in the graph. But all of this is "purely computational." Imagine implementing Jujutsu's rebase command; it is just a trivial reparenting of some graph nodes, something an amateur programmer could do. Calculating the relationship `C` is a bit more involved, but not complete black magic. But none of this involves "reading files from disk" or whatever. The side effect of "update the files on your filesystem to look like state XYZ in the graph" is just that: a side effect that the tool does when it is needed. Git, in contrast, only works through "side effects" in that it tends to only operate on the working copy, and never the "holistic commit graph". And so Jujutsu works at a higher, more "pure" level.
-----
Fun fact: in some cases, you do not actually have to "switch over" to G in order to solve this conflict, either. It is actually possible to craft a "solution" to the conflict in G while on top of Z. Then you can do `jj squash --from @ --into G` and you can "teleport" the resolution into the conflicted commit, solving both G and H, without ever making it appear in the working copy. This happens in cases like "G modified a file named readme.txt that was deleted by commit X"; all you have to do is "re-delete" the file inside commit G and it is trivially solved. This is something that is, quite literally, impossible to do in Git.
When you want to work on an older commit for a longer time and don't want to stay in a rebase, you just check it out and work normally, when you are done and want to propagate your changes, then you do a single rebase.
[G: original, G' with conflicts, G" resolved]
What value do you get from G' and H' existing with conflicts when you can't use the working tree until after you have resolved the conflicts?
So in Git it would be G -> G", but in JJ you can do G -> G' -> G". But G" in both cases only exist, until after you have put in the work of solving the conflict. And G' only ever exists without a usable working tree. So what do you get from having G' earlier, when you still have G" only after the same work?
You can do anything with a Turing machine. That you can isn’t the point. The point is the tool does all the things you can automatically and correctly so you don’t have to. There’s no ’just do this or that during rebase, or outside of it’. There’s only ‘it rebased everything correctly without a single thought, nice’.
Yes, and my point is that having a rebase and edit everything isn't too different from first modifying everything and then doing an automatic rebase.
A lot of the jj strategies in this thread are a bit more cowboy, and I’m surprised.
With `jj new` + `jj squash`[2], you're collecting work that you can review as a separate thing anytime as you go along. You don't have to remember anything. If you throw in an unrelated change, you'll notice it if you review the changes before squashing them, so you can split it out then. And I'm pretty much always working in this state even when I'm at the top of my branch, so `jj new some-deep-node` doesn't really change anything. If I get called away and have no memory of what I was doing when I return, it doesn't matter: my jj state tells me exactly where things are and what I was doing.
[1] Which is not a huge problem, you have deferred conflict resolution so if something goes wrong you can probably just repair it with normal editing or your editor's undo functionality.
[2] I don't usually bother with `jj new -A`, since I'm going to squash my "out of line" temporary commit into the linear chain anyway. `jj new -A` is more similar to `jj edit` than `jj new` -- it shares some but not all of the modal disadvantages. So perhaps my answer to your actual question is: "yeah, I dunno either."
There is value here, but I think it is more like “add a new command consisting of 50-100 lines of code” not “write an entirely new VCS.”
1. Your working copy contains whatever mish-mash of changes you want.
2. When you’re ready to stage and commit these changes, run `jj commit --tool gitpatch`
3. The iterative “stage this hunk?” UI from git lets you choose what to commit.
4. Your editor opens for a commit message.
5. The changes you selected are now in a new parent commit of your working copy, and the remaining changes are left in the working copy commit.
In addition to the _same_ workflow, jj makes it easier to have other workflows as well (you may be interested in the megamerge workflow if you’re always working on multiple tasks at once).
[1]: https://zerowidth.com/2025/jj-tips-and-tricks/#hunk-wise-sty...
Pretty easy. While inaccurate, it's useful to think of jj as two separate repositories. One is the "clean" one that has everything nice and neat. The other is a repository of all your (very) incremental changes.
You have to actively decide what goes in the "clean" one. jj automatically puts stuff in the messy one. Any time you actively commit something, you're committing to the clean one. So you decide what goes in there.
When you do a push, only the "clean" commits are pushed.
#1: Squashing
Create a revision for the feature, then create another revision atop that.
$ jj new main -m 'feature'
$ jj new
$ jj
@ trtpzvno samfredrickson@gmail.com 2025-09-01 12:32:33 9ac76a0f
│ (empty) (no description set)
○ wvzltyyr samfredrickson@gmail.com 2025-09-01 12:32:31 80b2d5d0
│ (empty) feature
◆ zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
│ all the stuff
$ vim
$ jj
@ trtpzvno samfredrickson@gmail.com 2025-09-01 12:34:50 5516c2b9
│ (no description set)
○ wvzltyyr samfredrickson@gmail.com 2025-09-01 12:32:31 80b2d5d0
│ (empty) feature
◆ zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
│ all the stuff
~
$ jj squash -i
# interactively choose hunks to squash into parent
$ jj
@ oxqnumku samfredrickson@gmail.com 2025-09-01 12:35:48 8694aa34
│ (empty) (no description set)
○ wvzltyyr samfredrickson@gmail.com 2025-09-01 12:35:48 47110bff
│ feature
◆ zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
│ all the stuff
~
#2: SplittingCreate a revision for the feature, then split it up retroactively.
$ jj new main -m 'feature'
$ jj
@ snomlyny samfredrickson@gmail.com 2025-09-01 12:38:39 84c6ecaa
│ (empty) feature
◆ zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
│ all the stuff
~
$ vim
$ jj
@ snomlyny samfredrickson@gmail.com 2025-09-01 12:39:51 8038bdd4
│ feature
◆ zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
│ all the stuff
~
$ jj split
# interactively choose hunks to keep, splitting the rest into a new revision
$ jj
@ zpnpvvzl samfredrickson@gmail.com 2025-09-01 12:41:47 5656f1c5
│ debugging junk
○ snomlyny samfredrickson@gmail.com 2025-09-01 12:41:44 1d17740b
│ feature
◆ zxrulorx samfredrickson@gmail.com 2024-12-11 03:44:38 main 351a2b30
│ all the stuff
~
jj's handling of merge conflicts is pretty much like in Git committing the conflict markers in git and editing the commit message to say "conflicting".