You get a nicer, significantly simpler interface. You don’t need any tricks. You don’t have to google how to work yourself out of a bad state, ever. And you get near-perfect git compatibility (ie you can use jj on a shared git repo, doing all the same things, and your teammates won’t know the difference).
I’ve wondered if there is a psychological thing here: someone who spent time memorizing all the git nonsense may have some pride in that (which is earned, certainly), that introduces some mental friction in walking away???
jj is harder to adopt for people with a thorough mental model of git, because it's harder to accept jj commands at face value. I know it's modifying my git tree, so I feel compelled to grok exactly what it's doing, but that's distracting and time consuming.
People like me should probably trial jj exclusively for two weeks, to shed that antijjotic resistance and form a more clear-headed opinion.
No, it really isn’t. I have used git since shortly after it was first released and I’ve written a git implementation.
I switched to jj in one day. And the amount of git arcana I have to keep in working memory is now basically nil. My VCS now works in almost a 1:1 mapping with how my brain wants to interact with my repo rather than having to go through a translation layer.
If you understand what git commands are doing, what jj does is essentially trivial to add to your mental model.
I also get the benefit of being able to use workflows that I always want to use in git but which are an enormous pain in practice. And I get access to wildly powerful new workflows I didn’t even consider because they would be outlandish in git.
What I will say is this: there is certainly an adjustment period, and I also totally hear you about how learning internals can be time consuming.
I think you can get a lot of the way there with at least the core concept with something like this, if you'll indulge me:
With git, you build up stuff on your filesystem. You then select which bits go into a diff in your index, and then when you're done, you stamp out a commit.
With jj, you instead start by creating a commit. In this case, it's empty. Then, every time you run a jj command, it does a snapshot, and this produces a new commit. However, because we want to have a stable identifier for a commit, we introduce a new one: the change id. This is basically an alias for the latest commit snapshot that was made. Your git history is just made up of each of these latest commits for each change.
... does that make any sense? Obviously there's more to all of the features than this, but that's sort of the core way that the histories map to each other.
I don't mind other people using jj, but I simply don't feel a need to try it. There's nothing prideful about that, it's just pragmatism.
I don't think it's just mental friction. Suppose you've learned git well enough that everything you do in it is automatic and fast, and the things which aren't fast by default you've built aliases and tooling for over the years. Yes, starting from ground zero you might want something like jj, but at the current point in your life you're not starting from ground zero. Switching to jj means learning another tool to achieve similar outcomes on your workflows.
With `jjui` this strategy takes only a few keystrokes to do operations like adding/removing parents from merge commits.
It's so nice to have like 4 parallel PRs in flight and then rebase all of them and all the other experimental branches you have on top onto main in 1 command.
Also, I cannot even stress to you how much first-class-conflicts is a game changer. Like seriously you do NOT understand how much better it is to not have to resolve conflicts immediately when rebasing and being able to come back and resolve them whenever you want. It cannot be overstated how much better this is than git.
Also, anonymous branches are SOOOO much better than git stashes.
You can do anonymous branches in Git as well. I use both for different use cases.
Also git has no equivalent to the operation log. `jj undo` and `jj op restore` are so sweet.
Also, I dislike all of the alternate git frontends I tried, because they are opinionated in a way they clash with my workflow.
Moreover, I don’t think the git CLI is that bad. Once you learn some basic concepts, it makes a lot of sense and is pretty consistent.
Most problems people report stem from a refusal to learn the underlying structure and models. That is on them. And when using a different frontend, they don’t disappear either. They are just abstracted, to allow you to avoid learning them. But they are still there, and you will probably still need to know them at some point.
It's very easy to fall into the trap of believing this: git's implementation fits together neatly enough that it feels like the best you could do. Like, yes it's complex, but surely that's just intrinsic complexity of the problem? (Also, I think we all sometimes feel like someone with a different view must just not know as much as us.)
But if you have used other version control systems (I'm thinking particularly Mercurial here) you realise that actually some of that complexity is just totally made up by git.
I explicitly said that git IS NOT the best we can do. But it is universal and good enough. Not nearly as bad as some people make it out to be.
i went from being a "jj cli power user" to relying on jjui for all of my complex rebase needs so quickly that i now have to read the man page to recall basic commands
The jj CLI is very easy to grok, even for a seasoned git user. Maybe even especially so for a seasoned git user.
1. I had quite a bit of trouble figuring out a workflow for branches. Since my companies unit of work is the branch, with specifically named branches, my `jj ls` was confusing as hell.
`jj st` might have helped a bit, but there were scenarios where creating an commit would abandon the branch... if i'm reading my post history correctly. My coworker who was more familiar explained my jj problems away with "definitely pre-release software", so at the time neither of us were aware of a workflow which considered branches more core.
Fwiw, I don't even remember when the jj workflow had branches come into play.. but i was not happy with the UX around branches.
2. iirc i didn't like how it auto stashed/committed things. I found random `dbg!` statements could slip in more easily and i had to be on guard about what is committed, since everything just auto pushed. My normal workflow has me purposefully stashing chunks when i'm satisfied with them, and i use that as the visual metric. That felt less solid with jj.
Please take this with a huge grain of salt, this is 10 month old memory i scavenged from slack history. Plus as my coworker was saying, jj was changing a lot.. so maybe my issues are less relevant now? Or just flat out wrong, but nonetheless i bounced off of jj despite wanting to stick with it.
Auto-commit is still a thing, but you can regain the stuff you like with a workflow change, this is called the "squash workflow" and is very popular: https://steveklabnik.github.io/jujutsu-tutorial/real-world-w...
It's not something I can just shift away from.
1. It's very new; I haven't had time to learn it properly yet.
2. It's very new and tooling doesn't support it well, e.g. VSCode. There aren't many GUIs yet.
3. I tried it once co-locating with Git and you definitely can't use both at the same time, even if it can use a `.git` directory. It ended up in a huge mess.
I'm definitely in favour of better-than-Git alternatives but I don't think it's reasonable to expect everyone to switch to JJ right now. It isn't so much better that abandoning the de facto standard is obviously worth it yet. (In contrast to things like the iPhone, Rust, SSDs, etc.).
Also I really wish they would focus on some of the bigger pain points of Git. I can deal with rebasing and whatnot. Sometimes it's painful but it's usually not that bad.
What I can't deal with are submodules and LFS. Both are awful and fixing them properly requires fundamental changes to the VCS which aren't going to happen in Git. JJ has an opportunity to do that. Imagine if JJ could say "we have submodules but they aren't awful!" or "you can check in large files!". Those are the sort of huge advantages that would mean you can say "why not just use JJ".
Both submodules and LFS are things that jj wants to address, but they take time.
Git is simple enough and has features and capabilities that jj does not have. Contrary to popular belief, git is not hard to use. I refuse to use any "simpler" system that is slower or less feature-rich than git. I don't even want to learn another commit graph model, because git's model is very good. About 95% of what people like yourself call "git nonsense" consists of useful features that many people would be annoyed to not have.
I believe that a large number of git or general VCS users have no idea about commit hygiene. They have not had to cherry-pick or edit commits, and have no idea what to do about conflicts. To people like that, git's features and methods will appear especially foreign.
I looked over jj specifically many moons ago and concluded it would annoy me and not function at my job. I forgot what the reasons were. One reason was most likely because I need submodules and worktrees to work. I just looked at its FAQ, and saw a bunch of nonsensical new terms as well. Nothing is more compatible with git than git itself, and I am very satisfied with how well git works for me.
worktrees are called "workspaces" in jj, but are the same.
A lot of people find jj easier to have good commit hygiene with, and find it simpler and more powerful, not less. But that said, if you're happy with git, you should continue to use it.
https://www.stavros.io/posts/switch-to-jujutsu-already-a-tut...
Also found https://github.com/gitbutlerapp/gitbutler
I wrote up a little way to use JJ's revsets to make it easy to push an entire stack of branches in one command:
https://crabmusket.net/2025/jj-bough-a-useful-alias-for-stac...
Does jj help with that at all?
I've experimented a bit with git-town.com (OSS) and now everyone at $DAYJOB uses graphite.com (SaaS) which does that part very well.
this is less of a problem if you're more into the 1 change == 1 commit workflow.
tug = ["bookmark", "move", "--from", "heads(::@- & bookmarks())", "--to", "@-"]
It moves the nearest bookmark to the commit before the current one (which should be your working commit).That doesn't mean it couldn't be a core command someday, but given that the alias works well for people, there's not a ton of reason to make a whole new command. You configure the alias and you're off to the races.
I used to dutifully read release notes for every git release, but stopped at some point. Apparently that point was more than three years ago.
Running a git command on one branch and multiple branches being affected is really unusual for me! This really does look like it is designed for just this problem, though. Simple overview: https://blog.hot-coffee.dev/en/blog/git_update_refs/
https://andrewlock.net/working-with-stacked-branches-in-git-...
Another commenter posted this link which was a bit more succinct
https://blog.hot-coffee.dev/en/blog/git_update_refs/
There isn’t much to it though, you just go to the branch and run git rebase with the update refs flag.
$ git checkout feature-1
$ git rebase --update-refs main
Successfully rebased and updated refs/heads/feature-1.
Updated the following refs with --update-refs:
refs/heads/feature-2-base
But all it did was update feature-2-base. It still left feature-2 pointing to the old commits. So I guess it automates "git branch -f feature-2-base feature-1" (step 3), but it doesn't seem to automate "git rebase --onto feature-1 feature-2-base feature-2" (step 2).Presumably I'm doing something wrong?
$ git rebase --update-refs main feature-2Second, you run it on the outermost branch: feature 2. It updates all refs in the chain.
This way I only need to worry about maintaining a single consistent lineage of commits. I've been using this workflow for about a year now and find it to be much easier than juggling and rebasing feature branches.
In case anyone's interested, I made a tool that automates this workflow. The worfklow and tool are described here: https://github.com/bjvanderweij/dflock/
(Plus, of course, if you have multiple branches stacked, `--update-refs` makes it easier to update all of them if you start from the outermost branch.)
(disclaimer: partly supported by AI, but most of the writing structure were made by hand)
Mercurial's "histedit" command offers two operations to combine the commits: "fold" and "roll", where "fold" brings the earlier commit into the later commit and "roll" does the reverse. The "fold" operation does exactly what I want in this case: It brings all the changes into the final commit from when I actually finished it, using the commit date of that commit.
Git's "rebase -i" command offers "squash" and "fixup" and "fixup -c" and "fixup -C", but they all do the same thing as "roll", i.e. they keep the earliest commit's date, the date when I started working on the thing and not the date when I finished working on it. (So I then cut and paste the correct date and update the commit afterwards. This may not be the best way to do it.)
One way to accomplish that is to reorder the commmits in `rebase -i`: "pick" the last commit first and squash/fixup the rest after. That can produce some very weird merge conflicts, but it works well more often than you might think, too.
At the very least, you can do your hand editing of the commits during the rebase instead of after by switching the earliest commit from "pick" to "edit" to have the full power to amend the commit before it moves on (with `git rebase --continue` when you are satisfied). (Versus "reword" if you just want to change the commit message only.)
Also, instead of naming commits things like "broke it" and "broke it more" an option is to use `git commit --fixup={previous commit hash}`. That auto-generates a "fixup!" name based on the previous commit and auto-marks it as a fixup if you use the `--autosquash` flag to rebase.
But in the case I'm describing, the earlier commits are essentially just temporary snapshots on the way to making a proper commit. Just a more explicit undo history, basically. I usually don't even bother naming them anything as meaningful as "broke it", actually. Maybe most people wouldn't even bother making these commits – or maybe they would if Git supported "fold".
Though there are also times I don't mind working in a very dirty worktree and `git add -p` (interactive add that also makes it easy to stage only parts of files) pieces only once they feel complete and start to tell a story. (And in those cases I may use a lot of `git stash -u` and `git stash pop` snapshots, too, especially if I need to switch branches in that worktree.)
$ git checkout feature-1
$ git reset main
$ git commit -a -C feature-1@{1}
This squashes all the changes and keeps the date and and message of the final commit on the branch.Weirdly, although the "-c" and "-C" flags for the fixup operations sound like they should correspond to the same flags for "git commit", which grabs the date along with the message from the specified commit, the fixup flags only affect the message and not the date.
It would be nice if it worked the same for rebase as for commit. Then "fixup -C" would essentially correspond to Mercurial's "fold".
Also yeah, I would expect the fixup -c and -C flags to be more aligned with commit in a rebase.
I left Google a few months back to work on another project. I missed the internal version control so much that I dropped that other project and am now focused http://twigg.vc It's very similar to what's used at Meta and Google. Atomic commits, linear history, auto rebase, code review compatible with stacked commits.
Perhaps not an option if you need to release the work in branch 1 before the work in branch 2 is ready/reviewed/etc.
Just use git rebase --update-refs ...
I don't usually want to rewrite history. I just want the target branch with all my commits on top (I usually squash the feature branch into one commit anyway). I have yet to run into a situation where this isn't good enough.
If the branch diverges so much and has so many commits that this simpler approach doesn't work, that might not be a git problem, but a project management one. It's still always nice to know git has tools to get me out of a jam.
This seems like a house of cards whose juice isn’t worth the squeeze. But I would love to split up my PRs into smaller pieces.
I just would hate to waste time with an incomprehensibly goofed git history because I forgot a command.
Sounds like using --update-refs would let you do all that in a single operation, but you still need to force-push and don't maintain an explicit merge/conflict resolution history, both of which could be considered sub-optimal for collaborative scenarios.
This is functionally the same as rebasing, except that the new changes show up at the tip of the commit chain rather than the base. And because it doesn't rewrite history you don't need to force-push.
Having to rebase again and again is a symptom that a dev branch is living for too long.
And, as others have pointed out, the modern solution is `--update-refs`, so there's no need for complicated workflows any more anyway.
If you mean rebasing as each PR in the stack is merged, most Git platforms have the ability to do that automatically.
A rebase before a merge can always happen, of course. But there are not stacked commits then it is just a standard rebase of your small chunk and that's it. And this will also have a shorter life than a stack so rebases will be rarer and simpler.
This is quite orthogonal to developing parts serially or not, and it is perfectly fine to change or refactor ealier parts when you work on later parts. Development is an iterative process.
It seems to me that "stacked diffs" are an example of looking for a technical solution to process issue.
Stacked diffs seems like a solution to managing high WIP - but the best solution to high WIP is always to lower WIP
Absolutely everything gets easier when you lower your work in progress.
It is entirely viable to never have more than 1 or 2 open pull requests on any particular code repository, and to use continuous delivery practices to keep deploying small changes to production 1 at a time.
That's exactly how I've worked for the past decade or so.
Oh sure, many bad ideas and poor practises such as that one are quite "normal". It's not a recommendation.
But the real problem with this workflow is that neither Github nor Gitlab support it at all. Not even Forgejo does. Which blows my mind because is such an obvious way to work.
As far as I know only Tangled actually supports it, but unfortunately Tangled is tangled up in its own weird federation ideas. There's no way to privately host it at all.