I’ve started using Jujutsu recently and was surprised at how low friction it was to switch. If you’re like the author and keep hearing about it without giving it a shot, I suggest you just sit down and try it – it’s a lot less effort than you might expect.
I really like all the concepts and have only heard good things, so I tried it but wasn't able to figure out how to use it as effectively as I can use Git. Specifically, I use VS Code and do a lot of stuff with the IDE's builtin support for selecting specific parts of files to stage, and I was hoping to be able to do something similar for jj split. I asked the Jujutsu Kaizen devs on Discord and they said that isn't currently implemented. They did also mention that VisualJJ might have more what I want, but I'm reluctant to switch to JJ only to have to rely on a closed-source tool.
Are there others who've previously made heavy use of VS Code's builtin Git staging support and have successfully migrated to JJ? Anything I'm missing?
As far as I know right now, no editors have great built-in support. As a heavy CLI user of (previously) git and now jj, selecting changes graphically is genuinely the one thing I’m envious of. The TUI that jj uses for interactive changes, `scm-record`, is fine but not great. It gets the job done but it could be so much more.
Getting really good diff and conflict editor support into VS Code, Zed, et al is going to be a huge win when it comes.
> The TUI that jj uses for interactive changes, `scm-record`, is fine but not great.
The change selection TUI is one of the things that I'm happiest with in jj over the equivalent in git. It's a huge quality of life improvement over git's version.
Could it be even better? Probably... but compared to `git add -p`... it is already way better.
Right, but the only time I've ever used `git add -p` was just to try it out years ago and be like "yep that sucks" and go back to using Magit (and then later VS Code). Those are my actual baselines.
Totally fair, I stubbornly used `git add -p` anyways because the terminal is where I want that tool to live, but I entirely understand why other people have different habits here.
JJ can be configured to use meld for what I think you mean, although I'm not quite au fait enough with version control to be sure. Not as frictionless as being able to do it in VSC but meld is nice and open source.
One of my favorite features of jj is file watching. Once set up, jj will snapshot the repo on every filesystem event. This means on every file save, you get a git commit! It provides arbitrarily fine-grained commit history, and works across all tools (not just your IDE). I set it up in the config and it has "just worked" ever since.
The result is that `jj evolog -p` will show detailed history of your actions. But all but the most recent commit are hidden, so neatly tucked away behind the same change as usual.
Another favorite is git no longer yelling at me and having meltdowns about switching branches - "files would be overwritten, please stash". This never happens in jj, by design. It's nicer than "auto-stash" options of recent git versions.
It's not a commit per change - all changes made since the last commit are in a new commit. You then usually do one of two things:
- Decide your changes are perfect, so add a commit message to this one and then create a new one on to to carry on
- Decide you only want some of them so use `jj split -i` to select which ones you want and then it creates two commits - the stuff you want in a new named commit, and the stuff you didn't in a new working copy commit. This is the JJ workflow equivalent to `git add -p` adding to the staging area then committing
It is, however, a commit object per change because of how jj tracks the evolution of commits. Those intermediate/ephemeral commits will not be able to be GC'd from git object storage until they are either abandoned in jj or are removed from the jj op log. The latter of which AFAIK does not happen automatically, not even on a time/age basis.
I am an extremely fervent believer in jj and use it exclusively since December '24, but I think it's useful to be accurate as possible for these kinds of trade offs. I don't use watchman snapshots specifically because of this downside.
Ok, so I have to admit I started skimming soon, because after explanation of `jj new`, I thought this is just `git commit --allow-empty`. Oh, and you can specify the message! Add `-m` and you are done.
Then it's a series of either git ammends or `git checkout -b` etc.
Now, since there is so much high praise in this comment and sibling comments, what am I really missing? From the post it just seems like the person hates branches for an unspecified reason.
Here's my workflow, of the past 15 years:
- git checkout main
- git pull
Do some changes. Do some more changes. Now:
- git checkout -b <feature-name>
- git status
- gvim into every file and then :Gvdiffsplit, select what I want to stage for each file
- git push # open PR if I think it's ready
For the remaining changes not in the commit, I either create a separate commit, or discard.
An honest question of curiosity, how does jj improve this workflow?
- While I'm working on something I can do `jj desc` and start writing the commit message. Every edit is automatically being added to this change.
- My work tree is dirty and I quickly want to switch to a clean slate. In Git: (1) either do `git stash` where I'm definitely is going to forget about it or (2) do `git commit -a -m wip && git switch -c some-random-branch-name`. In jj: `jj new @-`. That's it! If I run `jj log` then my previous change shows up. No need to come up with arbitrary names. It's so refreshing to move changes around.
- I'm working on a stack of changes and sometimes need to make edits to different parts. In Git (1): Each change is its own branch and I need to switch around and do a bunch of rebases to keep them in sync. In Git (2): I have one branch with multiple commits. I make changes towards the final state and then do `git rebase -i` to move them upwards to where they belong. Biggest downside: I'm not actually testing the changes at the point where they end up and I'm not guaranteed it makes sense. In jj: I do `jj new <CHANGE>` to make changes further up in the stack. Once I'm happy with it I do `jj squash` and every dependent change is automatically rebased on top.
- And finally: I can solve merge conflicts when I want to! If any rebasing leads to a merge conflict I don't have to deal with it right away.
That exact workflow? It won't improve much. But it tends to change peoples' workflows.
For example: let's say you have a few feature branches in flight at the same time, and you want to make a change to one of them to address some PR feedback. In your git workflow, that presumably means something like `git stash; git checkout feature-name; vim-and-actually-make-the-change; git add -up; git commit; git push; git checkout whatever-you-were-doing-before; git stash pop`, ± a `--amend` depending on your PR philosophy. A common workflow in jj is to instead sit on top of a merge commit with all the feature branches at once, and handle this like `vim-and-actually-make-the-change; jj squash -i --into feature-name; jj git push`. You can do something like the latter in git too, of course, it just tends to get hairy relatively quickly with rebases and merge conflicts.
If you already have 15 years of muscle memory for efficient use of git it's probably not that valuable to you. AFAIU jj's goal is to allow people to be as effective in a workflow like you describe without having to stumble through all of git to find the happy path building that muscle memory.
jj will start recording all of your changes as a real commit that it constantly rewrites as you edit files. You can write the log message for it up front or later. Think of it as an index that every change automatically gets staged to, except it’s just a commit, so instead of two concepts that work differently, you just have one. And you get the benefits of real commits, so for instance you can’t accidentally lose anything you staged that isn’t committed.
When you would create a branch and then stage changes with Git, what you would do in jj is split. The changes you don’t pick end up as the next auto-updating commit, and the changes you do pick use the log message you already set when you were working on them. So if you’ve got a bunch of changes you want to record as a series of commits, you just split however many times you want.
You don’t have to think about branching while you are doing this. The HEAD of master doesn’t automatically move when you are making these changes, so the effect is that you’re working on an anonymous branch already without having to create one. You can give it a name whenever you want by setting a bookmark. Or if you decide that the changes you make need to go into two branches, then you can add two bookmarks.
For instance, if you have:
A (master) => [changes]
Then [changes] is already a commit. Suppose you realise that you have fixed a bug and added a feature, but you want these as separate pull requests. You’d split, giving the bug fix a log message:
A (master) => B (bug fix) => [changes]
Then you’d give the feature a log message:
A (master) => B (bug fix) => C (feature)
Even though we started on master and made a bunch of commits without even thinking about branches, we haven’t changed master at all. So in effect, it’s like B and C are on some anonymous branch that was transparently created for you.
Now you want to open the pull requests, so you add a bookmark for B and a bookmark for C, and push them to your remote. B and C show up as branches that you can open pull requests for.
So your workflow is basically the same as it is now, there’s just fewer moving parts for you to think about as you work, and fewer concepts for newbies to learn.
It may not make it fundamentally better, but it might make it easier and more pleasant. That’s what jj does for me; my basic workflow would work in git too, but it’s just nicer and easier. Which I would not have guessed, as I was very comfortable with git before I switched.
In your case, jj’s ability to slice and dice commits is really nice; jj split is built in and works similarly, but you also have other tools too.
I started using jujutsu after the last round of blog posts here, and have found it super super useful to my mental model of git and vcs.
Stealing Fintan's `jj tug` alias from this post is something I have already found useful. Highly recommend if anyone is on the edge of trying to just give it a shot!
From git's perspective, jj bookmarks are just regular git branches, so you can just do `jj git push` and open a PR as usual.
However, unlike git, jj bookmarks are pinned to change IDs instead of immutable commit SHA-1s. This means that stacked PRs just work: Change something in the pr-1 bookmark, and all dependent bookmarks (pr-2, pr-3, ...) are automatically updated. A `jj git push --tracked` later and everything is pushed.
And do downstream PRs show just what changed or is the merge target against main which then just keeps accumulating differences?
This is one of the strengths I appreciate about graphite which is that the PRs are always on the preceding branch but it knows that when you go to merge it should actually really retarget and merge against main.
Yeah – the key thing here is that there is work to be done on the server, so JJ likely either needs its own forge or a GitHub App that handles managing PRs for each JJ commit.
I'm a huge fan of the JJ paradigm – this is something I'd love for us to be able to do in the future once one or both of:
- we have more bandwidth to go down this road
- JJ is popular enough that its worthwhile for us to do
That said I'd also love to see if anyone in the community comes up with an elegant GH app for this!!
As a satisfied customer of yours, the prospect of having to give up Graphite is the main thing keeping me from giving jj a try at my day job.
Ironic, since if there are a bunch of people in my boat, the lack of us in jj's user base will make it that much harder for jj to cross the "popular enough to be worth supporting" threshold.
My ideal is really just a version of `gt sync` and `gt submit` that handle updating the Graphite + Github server-side of things let you use `jj` for everything else, I think it could feel super nice. Probably not as simple as my dreams, but hopefully something we can get to with enough interest!
Github and GitLab both allow you to specify a merge target other than main and only show you the differences from the target. If that target is merged into main, they're retargeted to main.
There is definitely room for an improved forge experience that takes advantage of the additional powers of jj, but it's no worse an experience using them today than it is with git.
By any chance did you manage to get branch protection rules working neatly in this paradigm? Ideally I’d like any CI to be re-run as necessary and the branch to be automatically merged if review was approved and its base became master, but I never got a completely hands free setup working. Maybe a skill issue though.
Basically if I have five stacked PRs, and the newest four get an approval, I want everything to stay in place no merges. Then when the base (oldest) PR gets approved, I’d like the PRs to all get merged, separately , one after the other, without further interaction from me.
Does GitHub’s merge queue implementation support that?
One problem remains: jj makes it a breeze to parallelize work, but descendant changes will then end up with multiple parents. But PRs cannot target multiple target branches at once - so you cannot point them at both at once.
I mostly solve this by putting a branch on the merge commit M, then the “real” change R is a child of that. The PR is targeted to merge R into M.
As the parents of M are merged, I rebase the whole stack. When M has a single parent left, I abandon M and retarget the PR to merge R into that parent.
It requires a little babysitting, but the PR shows the diff I want it to.
Gitpatch attempts to build a Git hosting with native support for patches and commit-based review system, where each commit is its own patch. It's also smart to handle force pushes and can update or reorder patches as needed.
I feel like submodules are one of Git's most misused features. They're intended as a method of pinning read-only upstream Git dependencies. And when used for that purpose, they're good at what they do.
I think that people mostly get a bad taste in their mouths because they try to use submodules for building multi-repo workspaces where a developer might need to commit in some/all of the repos. They're a bad fit for that problem, but it's mostly because that's not what they were designed to do.
I'd love to see the jj team tackle case #2, personally. I bet they'd do a pretty good job of it.
> They're intended as a method of pinning read-only upstream Git dependencies. And when used for that purpose, they're good at what they do.
No they are not. In theory they could be good, but the actual implementation falls down in ... let me count the ways:
1. Transitive dependencies. I sure do love that my company's submodule-based repo has 12 copies of one of our dependencies.
2. Checkouts don't work any more. You can't simply `git switch <branch>`, especially if that other branch has a different set of submodules. And good fucking luck if one branch has a submodule and another branch has a not-submodule in the same location.
3. They don't work with worktrees. In theory... maybe. In practice, the documentation says not to try and in my experience it is right!
4. The submodule URLs are now baked into the git repo. This means you can't mirror the repo anymore easily. I've even had cases where I couldn't even clone the repo because the authors had used `ssh://` URLs which required permissions I didn't have. It's insane that the authentication method gets baked into the repo. I have no idea why they implemented it like this.
5. The tooling experience is just way worse. Want to see a diff of everything you've changed? Well you can't. If you've changed anything in a submodule you just get a hash difference, or at best a list of commits (which is better but it's not even the default!).
Before you instinctively reach for the "well obviously it must work like that" part of your brain, take a moment to think if it should work like this. I can think of several ways to do it better (beyond just making the implementation less buggy).
I read that "read-only" as essentially a "lockfile".
Which, this is exactly how pinning dependencies works. However if you are mutating your dependencies frequently and want the reference to them to change at the same time, this is the big pain with submodules- gotta do both yourselves. Not to mention there are now logistical problems to answer as this cannot happen atomically in all scenarios, let alone automatically.
That doesn't help if you actually need those repositories to exist separately.
For instance, consider the problem of having an external Open Source project your company maintains or heavily contributes to, and which also has common libraries shared with internal non-public work. Or, the same problem applies if you have a downstream Open Source project that needs to incorporate and vendor an upstream one but easily contribute changes upstream.
Some folks do this by generating the external repo as a filtered version of an internal monorepo, but that's awful in so many ways, and breaks the ability to properly treat the external repo as a first-class thing that can merge PRs normally. It leads to workflows where people submitting PRs feel like their work gets absorbed internally and maybe eventually spit back out the other end, rather than just being merged.
I tried Jujutsu on a simple repo and it ended up a mess I couldn't fix. Never had that with git. Might be my lack of knowledge but it shouldn't allow this.
Did you reach out to `git` commands to make changes to the repo? If you use jj in a colocated repo you should _only_ use jj to manage the repo to ensure it's kept in-sync with jj's data.
In colocated repos, running both git and jj commands is supported. I use it in release workflows which require git tags, which jj does not support creating. However, jj will pick up ("import") on git-created tags just fine afterwards.
AFAIK, jj runs "import" before and "export" (to git) after every invocation. That means it always has a consistent view.
jj can also handle concurrent edits by itself, think in a repo shared across a network. That said, I wouldn't think concurrent git commands are safe.
No, it was a clean jj init'ed repo. I think I just hadn't grokked it properly. Might give it another shot but as git works well for me right now finding the motivation/time is an issue.
anyone who is using jj, or curious about using it, please do yourselves a favour and check out jjui - its an incredible TUI for jj. Brings it to yet another level.
I used jj for a while and it was so problematic and seemed like nothing added value as compared to git. And now in the world of LLMs it is more difficult to switch to jj.
I actually think jujutsu is _more_ ideal for the agentic era. It makes it so easy to explore directions, experiment, play, backtrack, move commits around, etc.
Yup. Still 0 incentive to try jj. I’m still very much convinced most of the problems solved by jj either do not exist or are already solved by recent features of git.
It’s good alternatives of popular tools exist but git would not be my first bet as a tool that needs fixing…
I have recently posted some critical questions. I didn't do that because I "enjoy being contrarian" (rather me enjoying a discussion). I did that to flesh out, what the benefits of JJ are. I think VCSs are interesting, I think Git has some issues, but in my opinion most problems are caused by a misguided mental models, I haven't/can't tried JJ. Most things I heard were workflows that are claimed are not possible in Git, but are possible and most-times only a few commands. There are other design choices which I disagree with and which might bother me.
("haven't/can't tried JJ" meaning I enjoy running a stable distro, but wouldn't mind compiling it myself. But Rust and Rust software is a fast moving target, so it is a hassle to deal with if you aren't invested in it's ecosystem. Also it violates the GNU standard, which makes it unfriendly for endusers, but this seams to be the norm nowadays.)
And some people just happen to disagree - doesn't automatically mean they just like "being contrarian". I took the "Yup..." to mean "this is what I was expecting, because it agrees with what I have seen before on this topic".
> I always enjoy how on jj articles, 90% of commenters tried it and switched, 10% never bothered to try it, and 0% tried it but decided not to switch.
And some unknown quantity of readers don't see anything compelling enough to either try it and/or comment on it after they have (or have not) tried it.
To be fair, I more or less invited these responses by bringing it up. Organically, complaints are very rare.
Obviously not everyone who tries something is going to switch, even if you take as an axiom that the other thing is objectively better. But I still think it’s notable that—without explicitly prompting that type of response—so few people seem to have negative experiences with jj given how tribal and passionate engineers tend to be over tooling.
I dunno, I've tried it and I think I will stick with Git for a while longer at least. I really don't like the fact that it automatically commits changes in the working tree. Apparently you can turn it off but.. yeah I dunno.
I may change my mind. Especially if they provide a less shit alternative to submodules and LFS. (And I agree this guy is just being contrarian - jj definitely does fix some Git annoyances.)
This was the thing that stopped me from giving jj a shot for the longest time, but it turned out to be a complete non-issue. Definitely don't turn it off!
The "aha" moment you might be missing is that you should consider your latest revision to just be the staging area. `jj commit -i` (shorthand for `jj describe; jj split -i`) is effectively `git add -i; git commit`. If you're worried about accidentally pushing unfinished work, don't be! It won't let you push changes without a message by default, and you update bookmarks (e.g., branch pointers) at your discretion anyway. Both of these mean that `jj git push` isn't going to accidentally push a bunch of in-flight work.
Think of it less like jj commits everything by default, and more like your working copy gets the benefits of change tracking and being able to take part in repo operations even when you haven’t taken a moment to make a commit.
Say I check out a branch (or bookmark or whatever). I compile it. Some stuff doesn't work. I add some debug printfs. Compile it again. Ok I'm done now.
In git I can just revert all the changes and I haven't modified anything important. In `jj` won't I have actually added all of those debug printfs to the top commit of that branch? Now I have to manually revert the edit?
As I understand it, the answer is "aha, but you just have to remember to `jj new` before you do any edits. The problem is I'm very sure I will constantly forget to do that. I guess you could say Git is opt-in to modifying commits whereas jj is opt-out, and I think I prefer opt-in.
I have very little jj experience but does that sound accurate? (Genuine question; I would love something better than Git.)
You're reading an extremely biased sample of experiences. It's probably the opposite: 90% haven't tried it, 9% tried and didn't see any reason to switch, and around 1% have switched and won't shut up about it. For an advanced git user, it doesn't offer all that much. I used it for a couple of weeks and can't say that it saved me any time or any amount of work; it was either zero, or so close to zero that I wasn't able to notice it.
When Linus and his lieutenants switch over and recommend it as loudly as some do here, then I'll take another look. Very unlikely IMHO.
> For an advanced git user, it doesn't offer all that much.
As a (former) advanced git user, nothing could be further from the truth. We are always the most passionate cohort of jj users in these threads. This is almost certainly because jj unlocks a bunch of additional abilities and workflows that we've been torturing out of git (or given up on, or didn't even conceive was possible) for years.
On the flip side if all you ever do it git pull, git commit, git push, jj is probably not going to offer you much.
>For an advanced git user, it doesn't offer all that much.
arrogant, and completely absurdly wrong. I've used Git for 20 years. `jj` the single best improvement to my development workflow in... well, since adopting Git.
> I used it for a couple of weeks and can't say that it saved me any time or any amount of work
I would bet 5 figures that's a lie.
> When Linus and his lieutenants switch over and recommend it as loudly as some do here, then I'll take another look. Very unlikely IMHO.
So despite all this chest puffing, an appeal to authority would tip the scales for you?
There’s no need to be antagonistic. We’re all friends here :)
I am an unabashed jj evangelist and I don’t think they’re lying when they say it didn’t save them any time. Adoption costs might be small but they’re not zero. Some workflows are easy with either tool. And some people just don’t “get” it and struggle to adapt to a different mental model. That’s okay!
> So despite all this chest puffing, an appeal to authority would tip the scales for you?
I think GP was simply saying this would be a clear sign to them that if it’s mature enough to handle kernel developers’ needs, that’s a good sign it’s worth the effort to switch. It definitely would be! I can’t wait to hear if and when kernel developers start switching; it will be a huge positive indicator.
I mean, undo alone is a killer JJ feature. Sure, you can always somehow undo any git operation if you dig deep enough, but the ease of use on the JJ side without question.
The oplog is absolutely amazing. Having such a comprehensive safety net means you can have absolutely zero fear from doing absolutely anything to your repo, because you can always return the repo itself back to a known-good state. It's git's reflog on steroids.