On Jujutsu and Magit

Thursday, February 13, 2025

I’m writing this post after having used Jujutsu1 for a few weeks. This is some kind of “experience report”, as well as yet another2 love letter to Magit.

My background

I think I’d classify myself as a git power user. I’m sure I don’t know everything about the intricacies about the Git command line interface, but I do know my way around it. I like to believe I am one of those guys that colleagues and friends call for help when their local repo inevitably gets borked.

Git is the only VCS I’ve used3 since my beginnings as a young warlock apprentice computer science student, 10 years ago. So although in hindsight I’ve come to understand why people criticize its CLI so much, it’s always felt familiar to me, and it’s the only way I’ve known how to do things for a long time.

In addition to being quite blind used to the quirks of its CLI, my main way of interacting with git has been through Magit, ever since a bearded wizard my now PhD advisor showed it to me about 8 years ago. At the time, I was a convinced Vim user. After trying Magit, I decided it was compelling enough to switch to Emacs4 and never looked back. Yes, Magit is that good.

Discovering JJ

I’ve been reading about jj through blog posts, tutorials, and its official documentation. Everything that I read made me want to learn more and try it. In no particular order:

  • A lot of workflows look simpler than equivalent ones in git, so it feels like there a more ways to “officially” use it depending on your preferred mental model
  • I really like the notion of changeid being invariant to history rewrites5. This leads to forge workflows that are more similar to what Gerrit or Phabricator enable6.
  • First-class conflicts are awesome! Pijul had them as well.
  • The git backend is a must, this is what allows using jj transparently in git repos, and switching to jj full time without bothering your collaborators with your new shiny technology that they don’t really want to learn about. This “backwards compatibility” is a key feature, and I believe it is what makes the difference between a cool experiment, like Pijul, and a real path forward.7
  • Mega-merges look really handy when working on multiple unrelated features at the same time.

So of course I bit the bullet. A few lines of Nix code later, I was running my first jj commands and following along Steve Klabnik’s excellent tutorial. After finishing that, I decided that I’d try using it in my “real-world” repositories, and forbid myself from using the git binary.

And… that’s where things went wrong.

Magit ruined the CLI experience for me

Yeah. In practice, I feel quite frustrated when using jj, not because it feels worse than git (on the contrary, I can confirm what everyone says! It does feel way better than doing everything with the git CLI, it’s a net improvement), but because it feels worse than Magit.

Now, of course a simple CLI cannot really rival with a GUI application. I’m not saying jj on its own should feel like Magit for me to even consider it. I’m saying, I’ll need a Magit equivalent before the jj experience feels better than what I currently have. And that’s okay! It will happen eventually. In fact, there is such a project being developed already, which I tried to load into my configuration, but got an error which I couldn’t find a fix for. The creator of the plugin mentions that it is in “pre-alpha” stage, and that as such, he is not accepting issues or pull requests. As he states, the plugin is

inspired by magit and humbly not attempting to match it in scope

So I’ll keep an eye on it, though it doesn’t seem to have all the features I need yet. Who knows, maybe once I’m done with my thesis8, and the author opens up PRs, I’ll contribute to it?

Of course there are other GUI/TUI options, some of which look quite usable! But they’re far from matching the experience I get within Magit.

Dreaming of the ideal tool/workflow

One of the things I like to do often when writing code is iterating on the patch “as I go”, incrementally:

  1. I write some code, maybe some of it should be part of one commit, maybe some of it should be part of another.
  2. When I’ve written enough code for it to execute meaningfully, I build it, run the executable or some tests to check on the new expected behaviour, fixing it if necessary.
  3. Then I stage some of the changes I just made that I think should be part of a single commit. But I’m probably not done yet! The feature surely needs more polish, so I go back to step 1, write some more code, test it, stage it, rinse and repeat.

With the git CLI, that process would be painful. Each staging operation would need a call to git add -up and its clunky UI9, and each “review” operation would need calls to git diff or git diff --staged, depending on if I want to look at what’s staged already or not. Magit makes it a breeze though! It displays both staged and unstaged changes in its main status buffer, and allows to visually select individual lines of hunks for staging. At the press of a single s key I can stage parts of the code I want.

Within git, this means I’m making heavy use of the distinction between the working directory and the staging area: some code lives in the working directory only for a while, maybe because it belongs to another side commit that I’m planning to complete later. And some code lives in the staging area, where I’m incrementally building up my patch, reading through it between each iteration to check how the patch is looking.

Now, one of the fundamental differences in jj is that there is no staging area. Your working directory is automatically “staged”/snapshotted with every jj command you run, and there’s even an option to snapshot @ (your working copy) on every file modification!

Thankfully, there is a way to get an equivalent workflow. If anything, Jujutsu is more flexible in terms of which workflows it allows. The one I’m using is called the “squash” workflow, because it makes heavy use of the jj squash command. Steve’s tutorial has a whole section about it, so I’m not going to explain how it works in detail, but in a few words, you can use jj squash -i to do the equivalent of git add -p and selectively move some of the changes to the parent10 of the working copy.

So you have:

  • Your working copy @ which would be the equivalent of git’s working directory in this workflow11
  • The change you’re actually building, @-, which is the parent of @.
  • git diff and git diff --staged are replaced by jj diff and jj diff -r @-12 respectively.

In practice it looks like this:

$ jj
@  ulrxurnn user@mail.net 2025-02-13 12:47:52 ff935950
(empty) (no description set) # this is your working copy, no description is needed
○  nlqrkwps user@mail.net 2025-02-13 12:47:36 git_head() f20d4859
(no description set) # this is the change you're incrementally building, your "staging area"
◆  klryslqk user@mail.net 2025-02-11 17:24:13 main 62ff2625
│  feat: my awesome change # this change is "finished" for now

So jj supports what I want, all I’m missing is a great Emacs package that enables this workflow!

Where jj still shines

Magit makes a ton of stuff almost as painless as they are in jj. For instance, whenever I need to rewrite some commit history, I’ll always go for Magit if that’s an option. Complex history rewriting operations are available with a few key strokes, and Magit takes care of building the arcane command line invocations for you. No more typing git commit --fixup rev and git rebase -i --autosquash by hand, and there’s even some very handy shortcuts like the magit-commit-instant-fixup command, bound to F in the commit menu, that basically run both previous commands one after the other, avoiding the need to manually rebase to squash the commit into its relevant change. So all these years I’ve almost been getting jj’s automatic rebasing for free!

But even Magit can’t fix everyting about git: after all, it’s still built upon it. And if there’s one thing where Magit is helpless compared to jj, it’s regarding conflicts. jj has first class conflicts, and doesn’t require you to fix them when they happen: you can delay the resolution indefinitely. That’s not the case in git, and sometimes my super duper cool Magit flow gets interrupted by a pesky conflict that I have to fix immediately.

I’m sure there’s some other stuff where jj is clearly superior, but I certainly haven’t used it enough to know about all of it.

Conclusion

I guess this post is both a way to point the curious reader at a new very cool VCS, as well as a way to show my appreciation for an old tool that I realize I’ve taken for granted for the past 8 years.

Forgetting about my user interface expectations for a second, I think jj is a massive UI improvement over the git CLI. Anyone that is not using a superior GUI can probably benefit from switching to jj right now, and I encourage you to try it!

I’ll keep using jj in small projects where being able to incrementally build up my patches is not as important (for instance, I wrote up this blog post and resurrected my blog setup using jj for all changes!). That way I can follow the development closely, and who knows, maybe I’ll end up itching for that Magit equivalent so much that I’ll start my own little Emacs experiement.

Know more about Jujutsu

In case you landed here without knowing what jj is, here’s a non-exhaustive list of cool resources about it:


  1. If you don’t know what jj is, this post is not for you (yet!). I’ve put together a list of great resources to learn about it↩︎

  2. This is the first time I’m writing about it myself, but you can find magit appreciation all over the internet. ↩︎

  3. Okay, it’s the only VCS I’ve used seriously. I’ve read about and maybe installed Mercurial once or twice, and I was quite hyped when I tried out the first Pijul versions that were released a few years ago, but nothing beyond that. ↩︎

  4. With Vim bindings, of course. My pinky fingers are fine, thank you very much. ↩︎

  5. As opposed to commit ids, which change everytime you rebase a branch or amend a commit ↩︎

  6. Workflows which I believe to be superior to the Pull Request model. ↩︎

  7. I think the people working on Oils would also agree here. ↩︎

  8. Yep, the one I’m currently procrastinating by writing this blog post instead! ↩︎

  9. I’m complaining about it but I’m glad the option to split up a patch is there at all! ↩︎

  10. It squashes into the working copy’s parent by default but you can actually squash into any change/commit with -r my_revision↩︎

  11. Except you get automagic snapshotting of your entire working directory for free with jj↩︎

  12. -r @- is a pain to type compared to --staged, but I’ve seen people alias it to jj pdiff for parent diff. ↩︎