This item is a work in progress. It is subject to change radically at any time—up to and including being deleted entirely if I change my mind about it! Because it is a draft, it is not included in the site’s feeds; if you want to watch its progress, you will need to do that very old thing and bookmark this page and return to it over time until it is published.
Given its preliminary, work-in-progress status, I would appreciate it very much if you do not share this to social media, Hacker News, etc.—call it part of the contract for “working with the garage door open”. Thanks!
Assumed audience: People who have worked with Git or other modern version control systems like Mercurial, Darcs, Pijul, Bazaar, etc., and have at least a basic idea of how they work.
Some background: Along with my experiment with Mac-native text editors over a recent extended stretch of time off, I spent some time learning Jujutsu. Jujutsu is a new version control system from a software engineer at Google, which already (though tentatively) has a good future there as a next-gen development beyond Google’s history with Perforce, Piper, and Mercurial. I find it interesting both for the approach it takes and for its careful design choices in terms of both implementation details and user interface. Indeed, I think — hope! — it has a possible future as the next generation of version control.
Watch this space over the next month: I will update it with notes and comments about the experience, as well as expanding on these thoughts. This is a “garden”-style post and will grow organically over time!
The rest of this post is organized into the following overarching sections:
- Usage notes
- Rewiring your Git brain
Jujutsu is one possible answer to a question I first started asking most of a decade ago: What might a next-gen version control system look like — one which actually learned from the best parts of all of this generation’s systems, including Mercurial, Git, Darcs, Fossil, etc.? To answer that question, it is important to have a sense of what those lessons are.
This is trickier than it might seem. Git has substantially the most “mind-share” in the current generation; most software developers learn it and use it not because they have done any investigation of the tool and its alternatives but because it is a de facto standard: a situation which arose in no small part because of its “killer app” in the form of GitHub. Developers who have been around for more than a decade or so have likely seen more than one version control system — but there are many, many developers for whom Git was their first and, so far, last VCS.
TODO: Why is it interesting?
- change as distinct from revision: borrowed from Mercurial
- first-class conflicts: borrowed from Pijul and Darcs
- a reasonable user interface (!!!)
- Pijul-style “we have hard math to make all changes commute”
That is all interesting enough philosophically, but for a tool that, if successful, will end up being one of a software developer’s most-used tools, there is an even more important question: What is it actually like to use?
For all of these kinds of initial notes, I will update them/rewrite them as I figure them out; but I will not do is pretend like they were not issues. At some point I expect the notes throughout to read something like:
- It was not initially clear to me how to see the equivalent of…
Setup is, overall, quite easy:
brew install jj did everything I needed. As with most modern Rust-powered CLI tools, Jujutsu comes with great completions right out of the box. I did make one post-install tweak, since I am going to be using this on existing Git projects: I updated my
~/.gitignore_global to ignore
.jj directories anywhere on disk.1
Using Jujutsu in an existing Git project is also quite easy.2 You just run
jj init --git-repo <path to repo>. That’s the entire flow. After that you can use
jj commands alike on the repository, and everything Just Works™. I have since run
jj init in every Git repository I am actively working on, and have had no issues. It is also possible to initialize a Jujutsu copy of a Git project without having an existing Git repo, using
jj git clone, which I have also done, and which mostly works well. (For where it does not work all that well, see the detailed section on Git interop below!)
Notionally, Jujutsu understands local
.gitignore files and uses them. As of my initial explorations (specifically, as of 2023-07-02), it is tracking files I do not want it to in a
node_modules directory in one project where I am trying it out: there is a bug, plain and simple. I was able to work around it in the end, but it stymied my initial attempts to commit anything there, because I really do not want anything from
node_modules in history.
One of the big things to wrap your head around when first coming to Jujutsu is its approach to its “revsets”, which are the fundamental elements of changes. It takes a somewhat different approach from the other DVCS tools I have used. Specifically: revsets are actually expressions in a functional language “for selecting a set of revisions”. The term and idea are borrowed directly from Mercurial (as is common in many things about Jujutsu, and about which I am quite happy).
The first place you are likely to run into this is in the
log command, since
jj log is likely to be something you do pretty early in trying it out: certainly it was for me. I initially thought that the
jj log only included the information since initializing Jujutsu in a given directory, rather than the whole Git history, which was quite surprising. In fact, the view I was seeing was entirely down to a default behavior of
jj log, totally independent of Git: the specific revset it chooses to display. Per [the tutorial][tutorial]’s note on the
log command specifically:
jj loglists your local commits, with some remote commits added for context. The
~indicates that the commit has parents that are not included in the graph. We can use the
-rflag to select a different set of revisions to list.
To show the full revision history for a given commit, you can use a leading
:, which indicates “parents”. (A trailing
: indicates “children”.) Since
jj log always gives you the identifier for a revision, you can follow it up with
jj log -r :<id>. For example, in one repo where I am trying this, the most recent commit identifier starts with
mwoq (Jujutsu helpfully highlights the segment of the identifier you need to use), so I could write
jj log -r :mwoq, and this will show all the parents of
mwoq. Like Git,
@ is a shortcut for “the current head commit”. Net, the equivalent command for “show me all the history for this commit” is:
$ jj log -r :@
jj log does show by default was still a bit non-obvious to me, even after that. Which remote commits added for context, and why? The answer is in the
help output for
Which revisions to show. Defaults to the
@ | (remote_branches() | tags()).. | ((remote_branches() | tags())..)-if it is not set
This shows a couple other interesting features of
jj’s approach to revsets and thus the
It treats some of these operations as functions (
branches(), etc.). I don’t have a deep handle on this yet, but I plan to come back to it. (There is a whole list here!) This is not a surprise if you think about what “expressions in a functional language” implies… but it was a surprise to me because I had not yet read that bit of documentation.
It makes “operators” a first-class idea. Git has operators, but this goes a fair bit further:
-for the parent and
+for a child, and these stack and compose, so writing
@-+-+is the same as
@as long as the history is linear. (That is an important distinction!)
It supports union
&, and difference
:<id>for ancestors has a matching
<id>:for descendants and
<id1>:<id2>for a directed acyclic graph range between two commits. Notably,
<id1>: & :<id2>.
There is also a
..operator, which also composes appropriately (and, smartly, is the same as
..in Git when used between two commits,
<id1>..<id2>). The trailing version,
<id>.., is interesting: it is “revisions that are not ancestors of
This strikes me as extremely interesting: I think it will dodge a loooot of pain in dealing with Git histories, because it lets you ask questions about the history in a compositional way using normal set logic.
That’s all well and good, but even with reading the operator and function guides, it still took me a bit to actually quite make sense out of the default output. Right now, the docs have a bit of a flavor of explanations for people who already have a pretty good handle on version control systems, and the description of what you get from
jj log is a good example of that. If and as the project gains momentum, it will need other kinds of more-introductory material, but the current status is totally fair and reasonable for the stage the project is at.
I also have yet to figure out how to see the equivalent of
git log’s full commit message; when I
jj log, it prints only the summary line, and the
jj log --help output did not give me any hints about what I am missing! There is a template language for log output, and there are hints here and there in the docs for how it works, but the format is explicitly unstable and intentionally undocumented. Happily, the Git interop means I can just run
git log instead if I need to. TODO: this is the
templates stuff. It’s unstable but worth pointing people to. TODO: share my own tweaks here.
Once a project is initialized, working on it is fairly straightforward, though there are some significant adjustments required if you have deep-seated habits from Git!
Courtesy of the
node_modules issue described above, I was initially not able even to commit the very update to this item where I am writing this sentence using Jujutsu, because I could not figure out how to get it configured with Kaleidoscope, my go-to diff and merge tool. (This turned out to be a quirk with how to launch file diffs; see the appendix if you’re curious.) Once I worked around that, though, I quickly came to see the upside. Most of the time with Git, I am doing one of two things:
- Committing everything that is in my working copy:
git commit -ais an extremely common operation for me.
- Committing a subset of it, not by using Git’s
-pto do it via that atrocious interface, but instead opening Fork and doing it with Fork’s staging UI.
In the first case, Jujutsu’s choice to skip Git’s “index” looks like a very good one. In the second case, I was initially skeptical. Admittedly, my setup woes exacerbated my skepticism. Once I got things working, though, I started to come around. My workflow with Fork looks an awful lot like the workflow that Jujutsu pushes you toward with actually using a diff tool. With Jujutsu, though, any diff tool can work. Want to use Vim? Go for it.
What is more, Jujutsu’s approach to the working copy results in a really interesting shift. In every version control system I have worked with previously (including CVS, PVCS, SVN), the workflow has been some variation on:
- Make a bunch of changes.
- Create a commit and write a message to describe it.
With both Mercurial and Git, it also became possible to rewrite history in various ways. I use Git’s
rebase --interactive command extensively when working on large sets of changes. (I did the same with Mercurial’s history rewriting when I was using it a decade ago.) That expanded the list of common operations to include two more:
- Possibly directly amend that set of changes and/or its description.
- Possibly restructure history: breaking apart changes, reordering them, rewriting their message, changing what commit they land on top of, and more.
Jujutsu flips all of that on its head. A change, not a commit, is the fundamental element of the mental and working model. That means that you can describe a change that is still “in progress” as it were. I discovered this while working on a little example code for a blog post I plan to publish later this month: you can describe the change you are working on and then keep working on it. The act of describing the change is distinct from the act of “committing” and thus starting a new change. This falls out naturally from the fact that the working copy state is something you can operate on directly: akin to Git’s index, but without its many pitfalls. (This simplification affects a lot of things, as I will discuss further below; but it is especially important for new learners. Getting my head around the index was one of those things I found quite challenging initially with Git a decade ago.) When you are ready to start a new change, you use either
jj commit to “finalize” this commit with a message, or
jj new to “Create a new, empty change and edit it in the working copy”. Implied:
jj commit is just a convenience for
jj describe and
jj new. And a bonus: this means that rewording a message earlier in history does not involve some kind of rebase operation; you just
jj describe --revision <target>.
This is also where merging comes in, and it comes with some other frankly astonishing abilities:
-A, --insert-after Insert the new change between the target commit(s) and their children [aliases: after] -B, --insert-before Insert the new change between the target commit(s) and their parents [aliases: before]
You can do this using interactive rebasing with Git (or with history rewriting with Mercurial, though I am afraid my
hg is rusty enough that I do not remember the details). What you cannot do in Git specifically is say “Start a new change at point x” without that being in the middle of a rebase operation, which makes it inherently somewhat fragile. I never use
git reflog so much as when doing interactive rebases! Once I got the hang of this, it basically obviates most of the need for Git’s interactive rebase mode, especially when combined with Jujutsu’s support for “first-class conflicts”. There is still an escape hatch for mistakes, though:
jj op log shows all the operations you have performed on the repo — and frankly, is much more useful and powerful than
git reflog, because it logs all the operations.
This also leads to another significant difference with Git: around breaking up your current set of changes on disk. As I noted above, Jujutsu treats the working copy itself as a commit instead of having an “index” like Git. Git really only lets you break apart a set of changes with the index, using
git add --patch. Jujutsu instead has a
split command, which launches a diff editor and lets you select what you want to incorporate — rather like
git add --patch does. As with all of its commands, though,
jj split works exactly the same way on any commit; the working copy commit gets it “for free”.
Philosophically, I really like this. Practically, it is a slightly bumpier experience for me than the Git approach at the moment. Recall that I do not use
git add --patch directly. Instead, I always stage changes into the Git index using a graphical tool like Fork. That workflow is slightly nicer than editing a diff — at least, as Jujutsu does it today. In Fork (and similar tools), you start with no changes and add what you want to the change set you want. By contrast,
jj split launches a diff view with all the changes from a given commit present: splitting the commit involves removing changes from the right side of the diff so that it has only the changes you want to be present in the first of two new commits; whatever is not present in the final version of the right side when you close your diff editor ends up in the second commit.
If this sounds a little complicated, that is because it is. There are two big downsides to this approach, philosophically elegant though it is. First, I find it comes with more cognitive load. It requires thinking in terms of negation rather than addition, and the “second commit” becomes less and less visible over time as you remove it from the first commit. Second, it requires you to repeat the operation when breaking up something into more than two commits. I semi-regularly take a single bucket of changes on disk and chunk it up into many more than just 2 commits, though! That significantly multiplies the cognitive overhead.
The net is: when I want to break apart changes, at least for the moment I find myself quite tempted to go back to Fork and Git’s index. I do not think this problem is intractable, and I think the idea of
jj split is right. It just — “just”! — needs some careful design work. Preferably, the
split command would make it straightforward to generate an arbitrary number of commits from one initial commit, and it would allow progressive creation of each commit from a “vs. the previous commit” baseline. This is the upside of the index in Git: it does actually reflect the reality that there are three separate “buckets” in view when splitting apart a change: the baseline before all changes, the set of all the changes, and the set you want to include in the next commit. Existing diff tools do not really handle this — other than the integrated index-aware diff tools in Git clients!
- TODO: on
- TODO: on
- TODO: on
Another huge feature of Jujutsu is it support for first-class conflicts. Instead of a conflict resulting in a nightmare that has to be resolved before you can move on, Jujutsu can incorporate both the merge and its resolution (whether manual or automatic) directly into commit history. Just having the conflicts in history does not seem that weird. “Okay, you committed the text conflict markers from git, neat.” But: having the conflict and its resolution in history, especially when Jujutsu figured out how to do that resolution for you, as part of a rebase operation? That is just plain wild.
I was working on a change to a library I maintain3 and decided to flip the order in which I landed two changes to
package.json. Unfortunately, those changes were adjacent to each other in the file and so flipping the order they would land in seemed likely to be non-trivial. It was actually extremely trivial. First of all, the flow itself was great: instead of launching an editor for interactive rebase, I just explicitly told Jujutsu to do the rebases:
jj rebase --revision <source> --destination <target>. I did that for each of the items I wanted to reorder and I was done. (I could also have rebased a whole series of commits; I just did not need to in this case.) Literally, that was it: because Jujutsu had agreed with me that JSON is a terrible format for changes like this and committed a merge conflict, then resolved the merge conflict via the next rebase command, and simply carried on.
One of the really interesting bits about picking up Jujutsu is realizing just how weirdly Git has wired your brain, and re-learning how to think about how a version control system can work. It is one thing to believe — very strongly, in my case! — that Git’s UI design is deeply janky (and its underlying model just so-so); it is something else to experience how much better a VCS UI can be (even without replacing the underlying model!).
In Git, you work with changes by committing them. This took me a fair bit to wrap my head around, but in Jujutsu, “commit” is actually basically just an alias for two other operations:
jj describe and
jj new, in that order.
jj describe lets you provide a descriptive message for any change.
jj new starts a new change. You can think of
jj commit --message "something I did" as being equivalent to
jj describe --message "some I did" && jj new. This falls out of the fact that
jj describe and
jj new are orthogonal operations which are much more capable than
jj describe works on any commit. It just defaults to the commit that is the current working copy. But if you want to rewrite a message earlier in your commit history, that is not a special operation like it is in Git, where you have to run an interactive rebase to do it. You just do
jj describe --revision <ID> --message "new and improved message". That’s it. (How you choose to integrate that into your history is a matter for you and your team to decide; and we are going to want new tooling which actually understands Jujutsu. This will be a recurring theme in this section!)
jj new is the core of creating any new change, and it does not require there to be only a single parent. You can create a new change with as many parents as is appropriate! Is a given change logically the child of four other changes, with identifiers
jj new a b c d. That’s it. One neat consequence that falls out of this:
jj merge is just
jj new with the requirement that it have at least two parents. Another is that while Jujutsu has a
checkout command, it is just an alias for
TODO: replace this with a better recording. I’m leaving it here for my own reference for what to do better next time, as well as the config options I want!
- TODO: in particular: you can
newwith a requirement for parent count
TODO: branch behavior is a bit quirky-feeling at first, and definitely makes interacting with GitHub repos a bit weird
jj git pushand friends do not always seem to work
Jujutsu does this by using
libgit2, so there is effectively no risk of breaking your repo because of a
As noted in my overall write-up, there was a quirk in being able to use Kaleidoscope, my beloved diff-and-merge tool, for the Jujutsu diff editor. However, you can use Kaleidoscope that way, and I wanted to document the appropriate setup here:
Add the following to your Jujutsu config (
jj config edit --user) to configure Kaleidoscope for the various diff and merge operations:
[ui] diff-editor = ["ksdiff", "--wait", "$left", "--no-snapshot", "$right", "--no-snapshot"] merge-editor = ["ksdiff", "--merge", "--output", "$output", "--base", "$base", "--", "$left", "--snapshot", "$right", "--snapshot"]
I will note, however, that I have still not been 100% successful using Kaleidoscope this way. In particular,
jj splitdoes not give me the desired results; it often ends up reporting “Nothing changed” when I close Kaleidoscope.
When opening a file diff, you must Option ⎇-double-click, not do a normal double-click, so that it will preserve the
ksdiffis what makes the resulting diff editable, which is what Jujutsu needs for its just-edit-a-diff workflow. I have been in touch with the Kaleidoscope folks about this, which is how I even know about this workaround; they are evaluating whether it is possible to make the normal double-click flow preserve the
--no-snapshotin this case so you do not have to do the workaround.
Pro tip for Mac users: add
~/.gitignore_globaland live a much less annoyed life. ↩︎
I did have one odd hiccup along the way due to a bug (already fixed, though not in a released version) in how Jujutsu handles a failure when initializing in a directory. While confusing, the problem should be fixed in the next release… and this is what I expected of still-relatively-early software. ↩︎
Yes, this is what I do for fun on my month off. At least: partially. ↩︎