How I use Jujutsu
About three months ago I started using Jujutsu (JJ), a new Version Control System , for my personal projects. It took me a while to get used to it after more than a decade of using Git , but now I’m quite comfortable with it. Working with Jujutsu requires a shift from the mental model of Git. However, it is not as daunting as it may seem on the first day. This post was originally published on abhinavsarkar.net . Looking back, I don’t actually use all the fancy things JJ provides, and you may not need to either. In this post I list my most used JJ commands and how I use them. It is not meant to be a tutorial, or even a comprehensive list, but it should be enough to get you started. This post assumes that the reader knows how to use Git. JJ uses Git as a backend. This means that you can still use Git commands in your repo, and push them to Git remotes. Your coworkers can keep using Git with shared repos without ever being aware that you use JJ . initializes a new Jujutsu repository. You do this once, and you’re ready to start working. I usually run it with the option, which allows me to use Git commands as well in the same repo. If you want to work in an existing Git repo, you should run it with in the repo directory, to make JJ aware of it. Afterward, you don’t need to use Git commands. clones a Git repo and initializes it as a JJ repo. You can supply the option if you want. configures user settings. You can edit the user-level JJ config file by running . You can also override settings at repo level. For example, to set a different user email for a repo, run . You can also run to list the current config in effect. This is an area where JJ differs a lot from Git. JJ has no staging area, which means that every change you make is automatically and continuously staged. This came as a big surprise to me when I was getting started. If you are planning to use JJ with an existing Git repo, get rid of the untracked files either by committing them, or deleting them, or adding them to . There is literally no concept of untracked files in JJ ; a file is either committed or tracked or ignored. JJ has the concept of commits, same as Git. However, the workflow is different. Since there is no staging area, you start with creating a commit. That’s right! The first thing you do is create a commit, and then fill it by changing your files. Once you are done, you finalize the commit, and move on to a new fresh commit. JJ prefers to call them “changes” instead of commits to distinguish them from Git commits. creates a new change. If you know what your change is about, you can start with a commit message: , but JJ does not mandate it. You can start making changes without worrying about the message. One useful variation that I use a lot is . This creates a new change after the given change but before all the change’s descendants, effectively inserting a new change in the commit tree while simultaneously rebasing all descendant change. Once you are done, you can add a commit message to the current change by running . You can also provide the message inline: . As I mentioned, you don’t need to add a message to start working on a change, but you do need it before you push the change to a Git remote. You can run it any number of times to change the current change’s message. Alternatively, you can run to describe the current commit and start a new one. It is equivalent to running followed by . I use a mix of , and , depending on the situation. Like the command, tells you the state your current change is in. It lists the changed files and their individual statuses (added, modified, etc). This is where JJ really shines compared to Git. Moving commits around or editing them is a massive pain in Git. However, JJ makes it so easy, I do it many times a day. switches you over to the given change so you can modify it further. You use this when you’ve already committed a change but you need to tweak it. By default, you can edit only the changes that haven’t been pushed to the main branch of your repo’s remote. After you edit files, all the descendant changes are automatically rebased if there are no conflicts. simply combines the current change with its parent. It is useful when you commit something, and realize that you forgot to make some small changes. Another use for it is to resolve conflicts: create a new change after the conflicted change, fix the conflict, and squash it to resolve. is the opposite of : you use it to interactively split the current change into two or more changes. Often when I’m working on a feature and I find some unrelated things to fix, such as linter warnings, I go ahead and fix them in the same change. After I’m done with all the work for the feature, I use to split the change into unrelated changes so that the project history stays clean. restores files to how they were before the change, pretty much same as . You can run it in interactive mode by adding the option. You can also restore the files to how they were in a different change by specifying a change ID with the option. moves changes from anywhere to anywhere. You can use it to move individual changes between branches, or rearrange them in the same branch like so: When you move single changes like this, their descendant changes become invalid, but you can move them also in the same way. Or you can move entire branch of changes: It mostly works without any issues, but if there are conflicts, you’ll need to resolve them. I actually use rebase all the time. When I’m working on multiple features, and I find something that is more suited to be done on a different feature branch than I’m currently on, I finish working on the change, and then just move it to the different branch. Another use is to rebase feature branches on the main branch every day, like so: Here, , , , and are shorthand change IDs of the roots of various feature branches. You can also use rebase to splice changes/branches in the middle of other branches using the (after) and (before) options, but I rarely do this. is like except the changes are not moved but copied to the destination. It’s somewhat like . discards a change and rebases all its descendants onto the discarded change’s parent. I use it to get rid of failed experiments or prototypes. is supposed to automatically break the current change and integrate parts of it into ancestor changes at the right places, but I haven’t managed to make it work yet. I need to look more deeply into this. shows the change graph. JJ has a concept of revsets (sets of changes) that has an entire language to specify change IDs. takes an argument that uses the revsets language to choose which changes to show. For example: The revset language is rich and revsets can be used with many JJ commands. You can also create your own aliases for it, as we’ll see in a later section. shows differences between two changes, or in general between two revsets: shows the details of the current change. You can also use to inspect another change without switching to it. I’ve been mentioning branches, but actually JJ does not have branches like Git does. Instead, it has bookmarks, which are named pointers to changes. You can bookmark any change by giving it a name, and you can manipulate the bookmarks. Then to have branches, all you need to do is to point a bookmark to the required tip of the change graph. creates a new bookmark pointing to the current change with the given name. You can use bookmarks to mark the root or the tip of a feature branch, or to mark a milestone you want to return to later. When you rebase a change that a bookmark points to, the bookmark moves with it automatically. To list all existing bookmarks, run . To delete a bookmark you no longer need, run . If the deleted bookmark is tracked as a remote Git branch, the deletion is propagated to the remote as well. Alternatively, you can delete a bookmark only locally by running . You can also move, rename, and set bookmarks, as well as associate/disassociate them with Git remote branches. If you push a change with a bookmark to a Git remote, JJ creates a Git branch with the same name on the remote, but locally it remains a JJ bookmark. JJ tracks each operation in the repository in an immutable log, and provides commands to work with this log. shows a history of all operations performed on the repository. Each operation is assigned a unique ID, and you can see what changed with each operation. You can use the op IDs to restore the whole repo to an earlier state by running . undoes the last operation performed on the repository. Unlike , which modifies history, works on the Jujutsu operations themselves. This means it doesn’t lose any information; it just moves you back one step in the operation history. You can run this repeatedly to move backward in the operation history one step at a time. is the opposite of , that is, it moves you forward in the operation history by one step. It can also be run repeatedly. The operation log along with the undo and redo commands provide a safety net that makes it much easier to experiment with JJ without the fear of losing work. JJ uses Git as its backend, and provides commands to interact with remote Git repos. We already learned about and . We can also push and fetch. pushes your JJ changes to a Git remote. By default, it pushes all tracked bookmarks that have new changes. If you want to push a specific bookmark, you can specify it with . You can also push to all or tracked branches with the and options respectively. When you push, JJ converts the changes into Git commits and creates or updates remote Git branches accordingly. One thing to note is that JJ refuses to push changes that have conflicts or are missing commit messages. fetches changes from a Git remote and updates your local repository. It’s equivalent to . After fetching, you can see the remote changes in your change graph, and you can rebase your local changes on top of them if needed. You can fetch from a specific remote by running , and fetch a particular branch by running . manages your Git remotes. You can add a new remote with , or list existing remotes with . This is similar to but integrated with JJ ; it does not update the remotes of your underlying Git repo. creates a new change that undoes the effects of the specified change, pretty much like . The reverted change remains in the history of the repo. marks conflicts as resolved during a merge. When JJ can’t automatically merge changes (for example, when two changes modified the same lines), it creates a conflicted state in your working directory. After you manually fix the conflicts in your files, you run to tell JJ that the conflicts are resolved and the merge can proceed. JJ then automatically rebases any descendant changes. JJ is highly customizable through its configuration files. You can define custom aliases for commonly used commands and revsets, which can significantly ease up your workflow. These are stored in your JJ config file at the user and/or repo level. Here’s my configuration: You can compose revsets to create new revsets. These are the ones I use: I use the above defined revsets to create some custom commands: I have the default command set to so running only shows me the recent log. My usual workflow is to create a new commit, work on it, describe it, split/squash/rebase as needed, then run . Three months in, JJ has become my primary version control tool. The learning curve was steep, but it was worth it. The ability to freely rearrange changes and experiment without fear has fundamentally changed how I work. I spend less time wrestling with Git and more time actually coding. JJ has plenty of other useful features such as workspaces and the ability to manipulate multiple changes at once that I haven’t explored deeply. There’s a lot more to discover as I continue using it. If you use Git for personal projects and find yourself frustrated with rebasing or commit management, JJ might be worth a try. For further learning, I recommend the Jujutsu for Everyone tutorial , Steve Klabnik’s tutorial and Justin Pombrio’s cheat sheet , and of course, the official documentation . If you have any questions or comments, please leave a comment below. If you liked this post, please share it. Thanks for reading! If you liked this post, please leave a comment . Starting Up Creating Changes Modifying Changes Viewing Changes Managing Branches Managing State Working with Git Other Useful Commands Custom Configuration Revset Aliases Command Aliases : finds nonempty leaf changes that are mutable, have descriptions, and can be pushed to a remote. : finds changes from the default branch or the repository root to the current change, plus ancestors of visible leaf changes from the last 5 days. This gives me a good overview of the state of my repo. : finds all changes from the last month. : shows the recent changes from the default branch to the present, combining the and revsets. : moves the bookmark in the current branch to the closest pushable commit.