Posted on :: Tags: ,

In the previous article I told about my history with different version control systems and how I ended up using Jujutsu. This part will focus on why I think it is an important improvement over the git's status-quo and why I use it daily.

The way of Git

Let's start by defining the aforementioned status-quo. Typical user sees interaction with Git as moving files between 3 stages:

  1. Files are edited in the file system. Git doesn't know anything about them. Some of the commands can compare the changed parts against other stages (think git status or git diff).
  2. Some [parts] of the files are added to the "staging area" using git add [--interactive] command. These changes are not yet part of the tracked history, but Git knows about them.
  3. Proper content-snapshot with identifier and metadata is injected into the repository using git commit command.

Linus called this system "the stupid content tracker" because at its core it doesn't try to do anything smart. It doesn't try to understand the content, it doesn't try to understand the changes, it doesn't try to understand the history. It just stores the snapshots and the metadata. In the grand scheme of things, it is up to the user to make sense of it. And to be effective with git, users have to make sense of it.

Install Jujutsu

Jujutsu, a VCS which I started to use recently, makes some very different choices. Let's see how it handles this. I will be talking about the concepts and showing the commands. If you want to follow along, you can install it and try it out in parallel.

On macOS with Homebrew it is as simple as brew install jj. You can find instructions for other platforms on the official site.

Jujutsu allows you to work with arbitrary git repositories. You can use both git and jj at the same time. Adding jujutsu support to repo is as simple as running jj git init --colocate in it. Jujutsu will detect existing git repository and will configure itself to transparently sync with it.

Everything's a part of "change"

Git, as I shown above, expects user to work on files and then to "commit" their snapshot to the repository. Thus, the verb became a noun and we say that Git-repository stores commits.

Jujutsu uses a different terminology: repository stores changes. At any point in time, whenever you edit, add or remove a file, these modifications become a part of the "current change". You never need to commit them explicitly.

As soon as we connected jj to the repository it has created a new empty change with no description. We can check this by running jj status (or jj st) command.

Important bits here:

  • nonwtwyp is an id of the change. it would remain static no matter how we change the contents in the future.
  • 126fcd52 is a content-hash which corresponds to the id of the git commit. As soon as we add a single new letter to the files it will change.
  • (empty) means that this change is allocated, but it doesn't have any content yet. Again, as soon as we modify a single letter in files, this marker will disappear.
  • (no description set) means that we didn't bother to set the description of the change yet (it corresponds to the "commit message" in git).

Jujutsu doesn't require you to set descriptions of changes until you want to make them public. In Jujutsu it is perfectly fine to have multiple non-described changes. I still prefer to set descriptions early on, but it is not a requirement.

The command to set the description is jj describe. This command does not create a new change. It modifies the description of current change. You can call it several times and it will be modifying description of the same change.

How do you start a new change? Well, you just use jj new for that.

Branches don't have names

Let's take a look at the log of our changes. We'll use jj log for that.

The thing I'd like to focus on is the main marker, which stayed at exactly the same place where it was in this git repository initially. Branches in jujutsu do not have names, which might be contra-intuitive at first. Previously, we created a single branch of changes starting from git's "main" branch, but we can create more like this: jj new main and they wouldn't be lost, as Jujutsu's log would give us access to all of them without any issues. Strictly speaking, this is exactly what was done as part of jj git init under the hood: it created one empty change for us automatically.

And, as we can create multiple independent branches of changes from any point of the tree, Jujutsu can not meaningfully make assumptions about moving the marker.

In previous paragraphs I said "marker", but the proper Jujutsu term for this is "bookmark". Literally, the "main" is a bookmark in tree, which can be referenced by name, and moved as we need (just as we move a bookmark, when we read the book).

So, whenever we're ready to tell: "this is, what everyone should consider main from now on!", we just type jj bookmark move main --to=@. At this moment, git's "main" branch would start to point at this commit ("current" commit has special id @) and the whole branch would become visible to the git. Other branches we created would be mostly hidden from git tools (but the commits would still be stored in git's repository).

The next step would be able to push the bookmark to the upstream repository (such as github) using jj git push command. Except that we still have some undescribed changes. It is not prohibited to have that in git, but it would definitely look strange. Let's play nice and that.

One way to do it is to use jj describe k. A lot of jj's commands accept ‌[REVSETS] argument which allows to specify the change-id, which is the target of command. In this case, change-id is kqvmuvwn, but we can use k as it was hilighted in the log. Jujutsu always highlights shortest non-ambiguous prefix of the change-id in the log.

Another approach is more generic. We can use jj edit k to resume work on the change and just call jj describe without arguments while we do it.

Whether we modify the description or the contents of the change it's content-hash would change, but it's change-id would not. And, as descendent commits are linked via change-ids and bookmarks are attached to change-ids as well, the branch would maintain it's shape after the changes. So, now we should finally be able to push our changes.

Conflicts happen

In the previous section we were modifying the change in the middle of the branch. How would Jujutsu react if we introduced the conflict? Let's check!

You see that the upper change is marked with a red "conflict" word now, but Jujutsu still allows us to work on whatever we want. It doesn't force us to resolve conflict now. The most interesting part is, that we do not have to resolve the conflict at the point where it was introduced.

I'll create a new change at the top of the branch: jj new m and will edit the code to resolve the conflict there. Take a look at the log now:

We have intermediate change in "conflicted" state, but above it the branch is clean again. And we could leave it at that, but let's apply the fix to the broken change instead to get a clean history. We will use jj squash --to v command for that:

Squash gathers the modifications from the current change and makes them a part of the specified change instead. Done. Conflict is gone. New empty change is automatically created for us. We can continue working on the branch.

One more thing…

jj squash without --to argument applies modifications to the parent change. It can be used to emulate a staging area of git. The basic idea is that you keep the parts which you're sure about in one change and experimental modifications in the next one. And as soon as you're sure in the quality of your experimental code you squash it into the proper change. A lot of people consider this to be the recommended flow for Jujutsu.

Merge is not special

Until this point I was talking about linear changes and diverging branches, but nothing about merges. Two reasons: a). I wanted to lay the groundwork; b). There isn't much to talk about :)

In Jujutsu, one doesn't make a merge of branches. Instead, one makes a change with several parents: jj new rev1 rev2 rev3, where rev1, etc. are either change-ids or branch-names (or some other interesting things we did not talk about yet).

Resulting change would reside on it's own anonymous branch, as usually and you'll need to assign some bookmark to it to make it visible in git.

Thus, equivalent of git merge would look like this:

jj new main feature/name
jj describe -m 'Merge "name" feature'
jj bookmark move main --to=@

It is more verbose, but gives you more control over the process.

Conclusion

I hope I managed to spark your interest in jujutsu. This post is definitely not an exhaustive guide, but I wanted to give you a taste of what it is like to work with it.

If you want to learn more, I encourage you to check out:


Discussion


Subscribe via RSS or follow me on Mastodon to avoid missing updates.