Introduction
This is a tutorial for the Jujutsu version control system. It requires no previous experience with Git or any other version control system.
At the time of writing, most Jujutsu tutorials are targeted at experienced Git users, teaching them how to transfer their existing Git skills over to Jujutsu. This blog post is my attempt to fill the void of beginner learning material for Jujutsu. If you are already experienced with Git, I recommend Steve Klabnik's tutorial instead of this book.
I do assume you know how to run commands in the terminal. I will often suggest you run commands to modify files, which only work on Unix-like operating systems like Linux and Mac. If you're on Windows and can't switch to Linux, consider using WSL.
How to read this book
The book is split into levels, which are indicated in the sidebar. The idea is that once you complete a level, you should probably put this book away and practice what you've learned. When you're comfortable with those skills, come back for the next level.
There is one exception to this: If you're here because you need to collaborate with other people, you should push through to the end of level 1 right away.
Here's an overview of the planned levels:
level | description |
---|---|
0 | The bare minimum to get started. This is only enough for the simplest use cases where you're working alone. For example, students who track and submit their homework with a Git repository can get by with only this. |
1 | The bare minimum for any sort of collaboration. Students who are working on a group project and professional software developers need to know this. Going further is highly recommended, but you can take a break after this. |
2 | Basic history navigation and problem solving skills like conflict resolution. Without this knowledge, it's only a matter of time until you have to ask one of your peers to fix something for you. Completing this level is probably comparable to the skill level of the average software developer. |
3 | History rewriting skills. These will allow you to iterate toward a polished version history, which pays dividends long-term. Some projects require you to have these skills in order to meet their quality standards. |
4 | Rounding out the skill set with productivity boosters, advanced workflows, lesser-known CLI functions and a little VCS theory. At the end of this, you can pat yourself on the back and move on to new adventures. |
5 | Additional topics that only come up in specific situations: tags, submodules, workspaces etc. Consider skimming the list of topics and come back once you have an actual need for it. |
Only a few levels are complete right now, the rest are on the way. If you want to be notified when a new level becomes available, subscribe to releases of the GitHub repo. I will "cut a release" every time a new level is complete, causing you to get an email from GitHub. Go to the repo and click on "watch > custom > releases".
Restoring your progress
Throughout the book, you will build an example repository. Later chapters depend on the repo state of previous ones. Losing the state of the example repo can therefore block you from making smooth progress. This might happen for several valid reasons:
- You switch computers or reinstall the OS.
- You intentionally delete it to clean up your home directory.
- You use the example repo for off-road experimentation with Jujutsu.
To solve this problem, there is a script which automates the task of recreating the example repo. It expects the number of the next chapter as the first argument. For example, to set everything up to continue with chapter 4, you can run:
curl https://senekor.github.io/jj-for-everyone/restore_script.sh | bash -s 4
# ^^^
# the next chapter
The script is not complicated, you can verify that it's not doing anything malicious. For convenience, it's included in the expandable text box below. You can also download the script here and then execute it locally once you have inspected it.
Source of restore script
Source of restore script
#!/usr/bin/env bash
set -euo pipefail
if [ "${1:-x}" = "x" ] ; then
echo "Please provide the number of the next chapter as the first argument."
exit 1
fi
chapter="$1"
function success() {
echo "Script completed successfully."
exit 0
}
rm -rf ~/jj-tutorial
rm -rf ~/jj-tutorial-remote
rm -rf ~/jj-tutorial-bob
if [ "$chapter" = 1 ] ; then success ; fi
if [ "$chapter" = 2 ] ; then success ; fi
mkdir ~/jj-tutorial
cd ~/jj-tutorial
jj git init --colocate --quiet
if [ "$chapter" = 3 ] ; then success ; fi
if [ "$chapter" = 4 ] ; then success ; fi
echo "# jj-tutorial" > README.md
jj log -r 'none()' # trigger snapshot
if [ "$chapter" = 5 ] ; then success ; fi
jj describe --quiet --message "Add readme with project title
It's common practice for software projects to include a file called
README.md in the root directory of their source code repository. As the
file extension indicates, the content is usually written in markdown,
where the title of the document is written on the first line with a
prefixed \`#\` symbol.
"
if [ "$chapter" = 6 ] ; then success ; fi
jj new --quiet
if [ "$chapter" = 7 ] ; then success ; fi
mkdir ~/jj-tutorial-remote
cd ~/jj-tutorial-remote
git init --bare --quiet
cd ~/jj-tutorial
jj git remote add origin ~/jj-tutorial-remote
jj bookmark create main --revision @- --quiet
# TODO: fix use of --allow-new.
# The tutorial doesn't actually tell readers to add the --allow-new flag, which
# is because there is no way of explaining it well. It's simply bad UI. Work on
# a better UI is ongoing.
jj git push --bookmark main --allow-new --quiet
cd ~
rm -rf ~/jj-tutorial
jj git clone --colocate ~/jj-tutorial-remote ~/jj-tutorial --quiet
cd ~/jj-tutorial
if [ "$chapter" = 8 ] ; then success ; fi
printf "\nThis is a toy repository for learning Jujutsu.\n" >> README.md
jj describe -m "Add project description to readme" --quiet
jj new --quiet
jj bookmark move main --to @- --quiet
jj git push --quiet
if [ "$chapter" = 9 ] ; then success ; fi
jj config set --repo user.name Alice 2> /dev/null
jj config set --repo user.email alice@local 2> /dev/null
jj describe --reset-author --no-edit --quiet
echo "print('Hello, world!')" > hello.py
jj describe -m "Add Python script for greeting the world
Printing the text \"Hello, world!\" is a classic exercise in introductory
programming courses. It's easy to complete in basically any language and
makes students feel accomplished and curious for more at the same time." --quiet
jj new --quiet
jj git clone --colocate ~/jj-tutorial-remote ~/jj-tutorial-bob --quiet
cd ~/jj-tutorial-bob
jj config set --repo user.name Bob 2> /dev/null
jj config set --repo user.email bob@local 2> /dev/null
jj describe --reset-author --no-edit --quiet
echo "# jj-tutorial
The file hello.py contains a script that greets the world.
It can be executed with the command 'python hello.py'.
Programming is fun!" > README.md
jj describe -m "Document hello.py in README.md
The file hello.py doesn't exist yet, because Alice is working on that.
Once our changes are combined, this documentation will be accurate." --quiet
jj new --quiet
jj bookmark move main --to @- --quiet
jj git push --quiet
cd ~/jj-tutorial
jj bookmark move main --to @- --quiet
jj git fetch --quiet
if [ "$chapter" = 10 ] ; then success ; fi
if [ "$chapter" = 11 ] ; then success ; fi
jj new main@origin @- --quiet
jj describe -m "Combine code and documentation for hello-world" --quiet
jj new --quiet
jj bookmark move main --to @- --quiet
jj git push --quiet
if [ "$chapter" = 12 ] ; then success ; fi
cd ~/jj-tutorial-bob
tar czf submission_alice_bob.tar.gz README.md
echo "
## Submission
Run the following command to create the submission tarball:
~~~sh
tar czf submission_alice_bob.tar.gz [FILE...]
~~~" >> README.md
jj describe -m "Add submission instructions" --quiet
echo "*.tar.gz" > .gitignore
jj file untrack submission_alice_bob.tar.gz
if [ "$chapter" = 13 ] ; then success ; fi
jj new --quiet
jj bookmark move main --to @- --quiet
jj git fetch --quiet
jj rebase --destination main@origin --quiet
jj git push --quiet
if [ "$chapter" = 14 ] ; then success ; fi
cd ~/jj-tutorial
echo "
for (i = 0; i < 10; i = i + 1):
print('Hello, world!')" >> hello.py
jj describe -m "WIP add for loop (need to fix syntax)" --quiet
jj new --quiet
jj git push --change @- --quiet
jj git fetch --quiet
jj new main --quiet
if [ "$chapter" = 15 ] ; then success ; fi
echo "Error: unrecognized chapter."
exit 1
Help make this book better
If you find a typo, you can suggest a fix directly by clicking on the "edit" icon in the top-right corner. If you have general suggestions for improvement, please open an issue. I am also very interested in experience reports, for example:
- Do you have any frustrations with Jujutsu which the tutorial did not help you overcome?
- Was there a section that wasn't explained clearly? (If you didn't understand something, it's probably the book's fault, not yours!)
- Did you complete a level but didn't feel like you had the skills that were promised in the level overview?
- Is there something missing that's not being taught but should?
- Do you feel like the content could be structured better?
Thank you for helping me improve this tutorial!
What is version control and why should you use it?
I will assume you're using version control for software development, but it can be used for other things as well. For example, authoring professionally formatted documents with tools like Typst. The source of this book is stored in version control too!
What these scenarios have in common is that a large body of work (mostly in the form of text) is slowly being expanded and improved over time. You don't want to lose any of it and you want to be able to go back to previous states of your work. Often, several people need to work on the project at the same time.
A general-purpose backup solution can keep a few copies of your files around. A graphical document editor can allow multiple people to edit the text simultaneously. But sometimes, you need a sharper knife. Jujutsu is the sharpest knife available.
Why Jujutsu instead of Git?
Git is by far the most commonly used VCS in the software development industry. So why not use that? Using the most popular thing has undeniable benefits. There is lots of learning material, lots of people can help you with problems, lots of other tools integrate with it etc. Why make life harder on yourself by using a lesser-known alternative?
Here's my elevator pitch:
-
Jujutsu is compatible with Git. You're not actually losing anything by using Jujutsu. You can work with it on any existing project that uses Git for version control without issues. Tools that integrate with Git mostly work just as well with Jujutsu.
-
Jujutsu is easier to learn than Git. (That is, assuming I did a decent job writing this tutorial.) Git is known for its complicated, unintuitive user interface. Jujutsu gives you all the functionality of Git with a lot less complexity. Experienced users of Git usually don't care about this, because they've paid the price of learning Git already. (I was one of these people once.) But you care!
-
Jujutsu is more powerful than Git. Despite the fact that it's easier to learn and more intuitive, it actually has loads of awesome capabilities for power users that completely leave Git in the dust. Don't worry, you don't have to use that power right away. But you can be confident that if your VCS-workflows become more demanding in the future, Jujutsu will have your back. This is not a watered-down "we have Git at home" for slow learners!
Learning Jujutsu instead of Git as your first VCS does have some downsides:
-
When talking about version control with peers, they will likely use Git-centric vocabulary. Jujutsu shares a lot of Git's concepts, but there are also differences. Translating between the two in conversation can add some mental overhead. (solution: convince your peers to use Jujutsu 😉)
-
Jujutsu is relatively new and doesn't cover 100% of the features of Git yet. When you do run into the rare problem where Jujutsu doesn't have an answer, you can always fall back to use Git directly, which works quite seamlessly. Still, having to use two tools instead of one is slightly annoying. I plan to teach these Git features in this book at a higher level. The book should be a one-stop-shop for all Jujutsu users.
-
The command line interface of Jujutsu is not yet stable. That means in future versions of Jujutsu, some commands might work a little differently or be renamed. I personally don't think this should scare you away. Many people including me have used Jujutsu as a daily driver for a long time. Whenever something did change, my reaction was usually: "Great, that was one of the less-than-perfect parts of Jujutsu! Now it's even more intuitive than before!" You can subscribe to release announcements on the GitHub repo (watch > custom > releases). You will get a monthly email with the release notes, including any breaking changes.
Despite some downsides, I think the benefits are well worth it.
Installation and setup
The best installation method depends on your system. I recommend reading the official installation instructions. You can also download a binary directly from the release page or even copy the following commands in your terminal for a completely automatic installation.
mkdir -p ~/.local/bin
curl --silent --location \
https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh |
TARGET=~/.local/bin sh
~/.local/bin/ubi -p jj-vcs/jj
Run jj --version
to verify the installation.
It may be necessary to restart your terminal.
You should tell Jujutsu who you are, so it can track the author of each change.
jj config set --user user.name "Firstname Lastname"
jj config set --user user.email "email.address@provider.com"
If you want shell completions, follow the instructions here. If you don't know what a "shell completion" is, don't worry, it's not important.
Initializing a repository
A "repository" is a directory (folder) where Jujutsu keeps track of all files, including the ones in subdirectories.
A repository usually corresponds to a project, so the version history of unrelated projects are not tied to each other.
To create a repository, make a new folder on your filesystem.
I will assume you use ~/jj-tutorial
, but you can use any location you like.
cd
into that directory and run jj git init --colocate
.
In summary:
mkdir ~/jj-tutorial
cd ~/jj-tutorial
jj git init --colocate
Let's examine our first jj
command.
git
is the subcommand responsible for various Git-specific compatibility features.
One of them is the init
command, which initializes a new repository that's compatible with Git.
I highly recommend always using the --colocate
flag.
It allows third-party tools with Git-integration to work seamlessly.
What does "initializing a repository" mean?
Essentially, Jujutsu creates two directories .git
and .jj
.
These contain all information about the version history.
Why two directories?
The .git
directory contains all the important stuff, stored in a way that is compatible with Git.
The .jj
directory contains additional metadata which enable some of Jujutsu's advanced features.
You should never manipulate files in these directories directly! Their content is a well-structured database. If you corrupt the database format, you might completely brick the repository. We'll talk about a second layer of backup in chapter 7.
Files and directories staring with a dot are hidden by default, but you can verify they were created with ls -a
:
$ ls -a
.git .jj
Inspecting the state of a repository
So, now we've got an empty repository.
Let's take a closer look at it.
The command jj log
shows you a visual representation of your version history.
If you run it, you should see something like this:
@ rnyzwzlp remo@buenzli.dev 2025-07-10 08:52:16 b4cfe153 │ (empty) (no description set) ◆ zzzzzzzz root() 00000000
There's a lot going on already, so let's unpack it one-by-one.
In the leftmost column, there's an @
, a line and a diamond.
The @
sign represents the present state of the files in the repository, also known as the "working copy".
The diamond indicates an earlier state and the line connecting them means one state descends from another.
Older state are at the bottom and more recent ones are at the top.
In this case, the diamond is the "root", an empty state from which all others descend.
It always exists and cannot be modified.
Allow me to introduce a little bit of VCS-lingo.
Until now, I've used the word "state" to refer to the "working copy" (@
sign) and the "root" (diamond).
These states are snapshots of your repository, storing all files in the repo and their content.
The technical term for these snapshots is commit.
The commit is the most important data structure in the repository and I will use that term from now on.
So what's going on to the right of the first column? That's all important metadata about your commits. Let's look at the one of the working copy commit:
rnyzwzlp remo@buenzli.dev 2025-07-10 08:52:16 b4cfe153 (empty) (no description set)
The first word is rnyzwzlp
, a random-looking identification number called the change ID.
Next is the email address you previously configured, indicating that you are the author of this commit.
After that is a timestamp, indicating when the commit was created.
Last on the first line is another random looking word b4cfe153
.
It's also an identification number, but this one is called the commit hash or commit ID.
The change ID is more important than the commit hash, which is reflected in their position in the log output: the important one comes first.
We'll discuss the purpose of these IDs in more detail later.
Note that in the example output I provide in this book, the change IDs and timestamps are not always consistent. While writing the book, I go back and forth, changing stuff and recreating my example repository. This causes change IDs and timestamps to change.
On the second line, the word "(empty)" indicates that none of the files in this commit were changed compared to the previous commit. Lastly, "(no description set)" is self-explanatory and foreshadows the possibility of giving descriptions to commits.
Even though the repository is empty at this point, we already get a decent understanding of what a repository is. It consist of a set of commits which are related to each other, where one commit is said to be the parent or the child of another. These commits store snapshots of the state of your files, as well as relevant metadata like authorship, timestamp and description.
Getting help about using Jujutsu
We've now used two of Jujutsu's commands: jj git init
and jj log
.
There are a lot of commands and flags to modify their behavior.
I will introduce everything you need, but sometimes you may need to refresh your mind.
It can also just be valuable to explore on your own.
You can pass the --help
flag to any command to view contextual documentation.
For example, jj --help
will show you the full list of top-level commands that Jujutsu supports.
jj log --help
will show you specific information about how to use the log
command, i.e. how to modify its behavior according to your needs.
These help pages will feel overwhelming at first. But something always sticks, even if you don't notice at first. If you keep coming back, you will soon become very efficient at finding relevant information this way.
Making changes
Your primary goal is always to work on a project, Jujutsu just helps you manage your work. So let's pretend we're doing some actual work by putting stuff in a file:
echo "# jj-tutorial" > README.md
When you change files in a directory tracked by Jujutsu, what you're conceptually doing is modifying the working copy commit.
Let's see what jj log
has to say now:
@ rnyzwzlp remo@buenzli.dev 2025-07-10 11:42:28 61ab0d8a │ (no description set) ◆ zzzzzzzz root() 00000000
A couple of things have changed:
- The timestamp of the commit was updated.
- The commit hash changed. That's one of the reasons the commit hash is less interesting, it changes every time anything else in the commit changes.
- The commit is not "(empty)" anymore!
The last point means that our new file was successfully recorded in the working copy commit.
Describing changes
You may have noticed that Jujutsu likes to remind you if you haven't given your commits a description yet (also known as a "commit message"). Commit messages are important, because they make it easier for you and others to understand the changes and their motivation later on. This importance lies on a spectrum: The more people work on a project and the longer-lived it is, the more important good commit messages become. For example, the Linux kernel is a multi-decade project with thousands of people working on it together. Linux developers put a lot of thought and effort into good commit messages. On the other extreme of the spectrum may be a student's repository for storing the homework of a single lecture. The project is over within a couple of months and there is little chance of the student ever digging into the history, let alone anyone else. That student probably won't invest much time into good commit messages. If in doubt, err on the side of good commit messages!
You can change the description of the working copy by running jj describe
.
This command will open a text editor - which one depends on your system.
(Your EDITOR environment variable will be respected.)
If you would like to use a specific editor, you can configure Jujutsu to do so.
Let's assume you want to use Visual Studio Code, here's how you do it:
jj config set --user ui.editor "code --wait"
Here's an example description you could use:
Add readme with project title
It's common practice for software projects to include a file called
README.md in the root directory of their source code repository. As the
file extension indicates, the content is usually written in markdown,
where the title of the document is written on the first line with a
prefixed `#` symbol.
JJ: This commit contains the following changes:
JJ: A README.md
JJ:
JJ: Lines starting with "JJ:" (like this one) will be removed.
There is a little bit of structure here that you should follow. The first line of the description is called the subject. Sometimes the subject line is all you need, but to describe your changes in more detail, you can follow it up with a body. Subject and body are separated by an empty line. Both of them should not exceed 72 characters per line.
If you're working on a project with other people, your collaborators might have additional expectations about the content and structure of commit descriptions. Make sure to adhere to existing conventions, consistency is key.
More tips for good commit messages
More tips for good commit messages
Here are some additional conventions that are good practice for any type of project.
Try to keep the subject line below 50 characters
While 72 characters is the hard limit, the subject line usually benefits from being as concise as possible.
It is shown in many places where space is scarce and people want to get a general idea of your changes without reading too much.
If you find yourself exceeding 50 characters regularly, you may be combining multiple unrelated changes into a single commit.
If you put unrelated changes separated commits, finding concise subject lines becomes easier.
However, there are definitely situations where 50 characters is just too restrictive, so don't worry about going above when necessary.
Use imperative mood in the subject line
A common instinct when writing commit messages it to describe what you did in the past tense, e.g. "Fixed bugs and improved code".
Another one is to describe the content of the commit, e.g. "bug fixes and code improvements".
Instead, write the subject line as if giving a command or instruction, e.g. "Fix bugs and improve code".
A simple rule of thumb is that the subject should complete the sentence "If applied, this commit will...".
The resulting history will be more natural to read.
Note that this primarily applies to the subject, the style of the body can be more flexible.
Put yourself in the shoes of the reader
The target audience for your commit messages are future readers of the project history.
They are trying to understand what your changes did and why.
They can always read the content changes directly for how they did that, so the ideal commit message should complement that.
Great candidates include information that's not present in the content itself as well as guidance to understand the changes more quickly.
Creating a new commit
We have now added some changes and a description to a commit.
Let's say we want to make another change to the project which should belong to a different commit.
You can create a new one with the command jj new
and see what happened with jj log
:
@ ptttyorz remo@buenzli.dev 2025-07-10 14:39:33 7553207e │ (empty) (no description set) ○ rnyzwzlp remo@buenzli.dev 2025-07-10 14:21:14 git_head() 88938700 │ Add readme with project title ◆ zzzzzzzz root() 00000000
There are a few things to observe here:
- The new commit is a child of our previous working copy commit.
- The new commit became our working copy, meaning any further file changes will be recorded into the new commit.
- The previous commit is marked with
git_head()
. This marker is not important and you can ignore it. - The previous commit has a different symbol (circle) than the root commit (diamond). This is related to an important feature, which we'll learn about later.
At this point, we've closed the loop. You make changes, give them a description and finish off by creating a new commit that descends from the one you just completed. Rinse and repeat - that's the most basic version-control workflow.
Working with remotes
We now have a commit which we don't want to lose.
The way we're using Jujutsu right now, we don't have a backup at all.
If we delete the directory on disk, the .git
and .jj
subdirectories will be deleted as well and we won't be able to recover any of our work.
We can fix that by duplicating our commit at another location, a so-called remote. Besides providing a backup, sending commits to a remote also allows you to share your work more easily for collaboration.
The most common form of a remote is to host a repository with an online service like GitHub. For maximum realism, it would be good to do that in this tutorial and you certainly can. However, I will explain a simpler approach, which is to just have a second repository in a different location on your filesystem. These two options work pretty much exactly the same way. When you connect to a remote, you just tell Jujutsu where it is. In the case of an online service, that will be a web URL. In the case of a local repository, that will be a filesystem path. There are no other differences, so we're not missing out on any important lessons by avoiding GitHub. At the end of this section, there are a few tips about using GitHub itself.
Initializing the remote
We start by creating a new repository in a different location than our main one. The command is slightly different than the one we used to create our main repository:
mkdir ~/jj-tutorial-remote
cd ~/jj-tutorial-remote
git init --bare # initialize "remote" ("bare") repository
cd ~/jj-tutorial # return to main repo
The difference between a "remote" repository (also known as "bare" repository) and a normal one is irrelevant, don't think too hard about it. If your curiosity is unrelenting, expand the info box below.
You really don't need to know this
You really don't need to know this
git init --bare
is very similar to jj git init
, which we used to create our main repository.
However, instead of a "normal" repository, it creates a "bare" one.
But what's the difference?
Think of a regular repository as consisting of two parts: (1) Jujutsu's internal database stored in the .git
and .jj
directories and (2) all the actual files of your project, which you can modify - your working copy.
The term "copy" is key here, because all the files are also stored in the internal database.
The only reason a copy of the files exists outside the database is so you can read and modify them - "work" with them.
So, "working copy" is a fitting name indeed.
A bare repository is a regular repository without a working copy. Since we will only use the remote repository for sending and receiving commits, we don't need a working copy.
If you inspect the content of the new bare repository, it will look very similar in structure to the content of the .git
directory in our main repository:
ls -lah ~/jj-tutorial/.git
ls -lah ~/jj-tutorial-remote
Connecting to a remote
A repository is connected to a remote by storing its location under a specific name. Remotes can be called anything, but when there is only one, the convention is to call it origin:
jj git remote add origin ~/jj-tutorial-remote
Adding a bookmark
There is one last speed bump before we can send our work to the remote. Remote repositories can receive a lot of commits, not all of which end up being needed in the long run. Therefore, it's desirable that commits which aren't needed anymore can be deleted automatically. How does the remote know which commits to delete and which to keep? With bookmarks!
A bookmark is a simple named label that's attached to a commit. Every commit with such a bookmark label is considered important and won't be deleted automatically.
Let's create a bookmark called main and point it to our completed commit. The name "main" is a convention that represents the primary state of the project. In legacy projects, the name "master" is also still in widespread use.
jj bookmark create main --revision rnyzwzlp # <- substitute your change ID here
The command jj bookmark create
expects a name (main
) and a commit to which the bookmark should point.
We identify the commit by its change ID (--revision rnyzwzlp
).
The flag --revision
can also be abbreviated as -r
.
Let's check the result with jj log
:
@ ptttyorz remo@buenzli.dev 2025-07-10 14:39:33 7553207e │ (empty) (no description set) ○ rnyzwzlp remo@buenzli.dev 2025-07-10 14:21:14 main git_head() 88938700 │ Add readme with project title ◆ zzzzzzzz root() 00000000
Great!
We can see that the bookmark main
is correctly pointing to our most recently completed commit.
Sending commits to a remote
Now that we're connected and have a bookmark, let's finally send our commit to the remote. The technical term for sending commits is "pushing" them. You will often hear phrases like "pushing to the remote" or "pushing to GitHub". The command for pushing a specific bookmark is:
jj git push --bookmark main
Whoops, that didn't work:
Error: Refusing to create new remote bookmark main@origin Hint: Use --allow-new to push new bookmark. Use --remote to specify the remote to push to.
Because jj git push
can also be used to update existing bookmarks, it requires the additional flag --allow-new
or -N
to push completely new ones.
This safety measure prevents you from pushing bookmarks you intended to remain local.
This command should work:
jj git push --bookmark main --allow-new
Cloning an existing remote
To drive home the point that the remote repository functions as a backup, we're now going to completely delete our main repository and restore it from the remote. First, the deletion:
cd ~
rm -rf ~/jj-tutorial
The next step is restoring the repo, but you can also think of it in a different way. Imagine there is an ongoing project and you have just joined the team. In order to contribute changes to the project, you first need to get a copy of it on your own computer. The process to do that with Jujutsu is the exact same process as restoring the project from a backup remote. Here's the command:
jj git clone --colocate ~/jj-tutorial-remote ~/jj-tutorial
The clone
command takes a flag --colocate
just like jj git init
and I recommend you always use it for the same reason.
The last two arguments are (1) the source from which to clone and (2) the destination - where to store the copied repo.
Let's run jj log
in our fresh clone to see if we restored the repo successfully:
cd ~/jj-tutorial
jj log
@ mqksopxt remo@buenzli.dev 2025-07-11 16:57:47 531e57ca │ (empty) (no description set) ◆ rnyzwzlp remo@buenzli.dev 2025-07-11 16:57:47 main git_head() 0112d25d │ Add readme with project title ~
This looks mostly right, but there are little differences.
We can't see the root commit anymore and our commit is marked with a diamond, instead of a circle like before.
As mentioned, the diamond and circle markers are related to a feature we'll learn about later.
For now, we can say that ancestors of diamond commits are hidden by default, because you won't care about them most of the time.
We can tell Jujutsu to show us all commits with jj log --revisions 'all()'
:
@ mqksopxt remo@buenzli.dev 2025-07-11 16:57:47 531e57ca │ (empty) (no description set) ◆ rnyzwzlp remo@buenzli.dev 2025-07-11 16:57:47 main git_head() 0112d25d │ Add readme with project title ◆ zzzzzzzz root() 00000000
Great! Now we can be sure our repository was fully restored from the remote.
Using GitHub
As promised, here are a few tips about using GitHub. If you are not interested in this, feel free to skip to the next chapter, it won't become relevant later.
I want to mention that GitHub is not the only provider of Git-hosting services, but certainly the most popular one. Others include GitLab and Codeberg. Codeberg is a free instance of Forgejo, which is open-source software you can host yourself.
All of these providers work very similarly to what I'm describing below, so you should have no trouble adapting to other providers.
Authenticating with an SSH key
Jujutsu needs to authenticate as your GitHub user in order to send and receive commits on your behalf. It's possible to do that with username and password, but it's very tedious and I don't recommend it at all. If making backup is tedious, you will do it less often. Fewer backups means more risk of losing your work! So let's make the authentication as seamless as possible.
The best authentication method is to use an SSH key. It's more convenient and safer than a password. GitHub has great documentation about how to set that up, so please follow the instructions here:
You can verify the setup with the following command:
ssh -T git@github.com
The expected output is:
Hi user! You've successfully authenticated, but GitHub does not provide shell access.
Creating a new repository on GitHub
Skip ahead if you intend to use an already existing repo.
To create a new repository on GitHub, click here and fill out the form. All you need to do is choose an owner (probably your username) and a repo name. Also check that the visibility matches what you want (can be changed later).
If you already have a local repository with content that you want to push to this new remote, make sure to not initialize the repo with any content.
That means, no template, no README, no .gitignore
and no license.
Finally, click on "Create repository".
Cloning an existing repo
Navigate to the page of the existing repo in the browser. You should see a big green button that says "<> Code". Click on it and select "SSH" in the drop-down (assuming you have set up an SSH key as explained above). Copy the URL that's displayed.
Finally, paste the URL into Jujutsu's clone command:
jj git clone --colocate <COPIED_URL>
Updating bookmarks
We have learned how to push a new bookmark to a remote for the first time. We could do that for every single commit we create and want to push to the remote. However, that would lead to a lot of unnecessary bookmarks lying around. A real mess!
Thankfully, there's a better way. Instead of creating a new bookmark every time, we can move an existing bookmark so it points to another commit.
To try it out, we first need to create a new commit. You can just copy these commands:
printf "\nThis is a toy repository for learning Jujutsu.\n" >> README.md
jj describe -m "Add projcet description to readme"
jj new
Recall how these three commands correspond to our basic VCS-workflow:
- make changes
- describe the changes
- create a new commit
There's also a neat trick here for the describe
command:
If you already have a short message in mind, you can pass it directly with the -m
flag (short for --message
).
This can be faster than opening a separate text editor.
jj log
shows us a new commit on top of the one we pushed to the remote:
@ kxvtnwxr remo@buenzli.dev 2025-07-11 17:44:16 a99737f2 │ (empty) (no description set) ○ txxxnkln remo@buenzli.dev 2025-07-11 17:44:16 git_head() 232d89bf │ Add projcet description to readme ◆ otkvqyur remo@buenzli.dev 2025-07-11 17:44:04 main 28bd8a65 │ Add readme with project title ~
Now we can send this new commit to the remote by first pointing the bookmark at it and then pushing the bookmark again. Let's start by moving the bookmark:
jj bookmark move main --to @-
We could've told Jujutsu where to move the bookmark to using the change ID, i.e. --to txxxnkln
.
Instead, we can use @-
, which is a neat way to refer to the parent of the working copy commit.
There are many such clever ways to refer to commits, but that's a topic for later.
For now, just remember @-
, because that's by far the most useful one.
Let's see what jj log
has to say:
@ myvomxzr remo@buenzli.dev 2025-07-11 17:55:24 51b369c2 │ (empty) (no description set) ○ nrosmspz remo@buenzli.dev 2025-07-11 17:55:24 main* git_head() c59322ad │ Add projcet description to readme ◆ qtzssony remo@buenzli.dev 2025-07-11 17:55:14 main@origin 3812d571 │ Add readme with project title ~
The important thing to notice here is that Jujutsu shows us the discrepancy between the local bookmark main
and its remote counterpart.
main*
is the local bookmark and the star next to its name means it's out-of-sync with the remote.
main@origin
is the location of the bookmark on the remote.
We can fix the situation by pushing the bookmark again:
jj git push
This time, we didn't specify --bookmark main
explicitly.
If you omit the --bookmark
flag on the push
command, Jujutsu tries to be smart about which bookmarks you actually want to push.
That usually works quite well.
If you think it didn't work, just try again with the --bookmark
flag.
You might be wondering:
Since the remote requires a bookmark to receive commits and the main
bookmark is not pointing to the first commit anymore... is that commit now lost or deleted?
Luckily it is not.
Commits store a reference to their parent commit, which is why Jujutsu knows the order in which to draw the commits in the output of jj log
.
So, a commit with a bookmark pointing to it also protects all its ancestors from being deleted.
You made it! At this point, you have all the skills needed for simple solo projects with proper backup. Let's summarize the workflow again:
- make changes
- describe the changes
- create a new commit
- move the bookmark
- push to the remote
Ideally, you can take a little break now and practice what you've learned. Once you feel comfortable with the above, come back quickly for level 1, we're just scratching the surface.
If you need to collaborate with other people, level 1 is just as essential as this one. I encourage you keep going right away! You've earned yourself a quick bathroom break though.
Branching
If you took a break after finishing level 0, remember that you can restore your progress in the example repo in case you lost it.
The previous level only covered situations where you are working on a project on your own. What if several people want to collaborate on the project? Let's simulate such a scenario and see what happens.
Alice and Bob are working on a group project for a computer science class. Their task is to write the classic "Hello, world!" program in Python. Alice and Bob decide to split up the work as such:
- Alice will write the Python program.
- Bob will add documentation to the README.
Let's start with Alice. To role-play as her, we can configure our user for this repository only:
jj config set --repo user.name Alice
jj config set --repo user.email alice@local
jj describe --reset-author --no-edit
Here's the Python program she comes up with:
print('Hello, world!')
That'll do the job just fine.
She adds it to the file hello.py
:
echo "print('Hello, world!')" > hello.py
Happy with her changes, Alice dutifully adds a description to her commit:
jj describe -m "Add Python script for greeting the world
Printing the text \"Hello, world!\" is a classic exercise in introductory
programming courses. It's easy to complete in basically any language and
makes students feel accomplished and curious for more at the same time."
Having completed her work, Alice creates a new commit with jj new
.
Here's how the repository should look at this point:
@ towolxxr alice@local 2025-07-13 12:01:53 06f737ed │ (empty) (no description set) ○ krpmvwkz alice@local 2025-07-13 11:59:11 git_head() f13dc4df │ Add Python script for greeting the world ◆ utlxsmss remo@buenzli.dev 2025-07-13 11:55:15 main 972b9718 │ Add project description to readme ~
Next, we simulate Bob's work. He's working on a different computer than Alice, with a different copy of the repository. Since Bob is working at the same time as her, he doesn't have the commit made by Alice yet. We can simulate that by creating a third repository, which has the same remote as our primary one:
jj git clone --colocate ~/jj-tutorial-remote ~/jj-tutorial-bob
Let's go into that repo, configure our users for the role-play and make sure the log looks the same way as when Alice started her work:
cd ~/jj-tutorial-bob
jj config set --repo user.name Bob
jj config set --repo user.email bob@local
jj describe --reset-author --no-edit
jj log
@ tpuxswrq bob@local 2025-07-13 12:04:19 a0f4c66a │ (empty) (no description set) ◆ utlxsmss remo@buenzli.dev 2025-07-13 11:55:15 main git_head() 972b9718 │ Add project description to readme ~
That looks great. So now Bob is going to do his part the same way Alice did:
echo "# jj-tutorial
The file hello.py contains a script that greets the world.
It can be executed with the command 'python hello.py'.
Programming is fun!" > README.md
jj describe -m "Document hello.py in README.md
The file hello.py doesn't exist yet, because Alice is working on that.
Once our changes are combined, this documentation will be accurate."
jj new
Let's say that Bob was a little faster than Alice, because he was only doing the documentation. Alice was doing actual software engineering, which took a little more time. Therefore, Bob gets to update the main bookmark first:
jj bookmark move main --to @-
jj git push
A little later, Alice is also finished and now she attempts to update the main
bookmark:
cd ~/jj-tutorial
jj bookmark move main --to @-
jj git push
Whoopsie! Alice is met with a scary error message in her terminal:
Changes to push to origin: Move forward bookmark main from cd5f3fff9c7b to e90b597ed78e Error: Failed to push some bookmarks Hint: The following references unexpectedly moved on the remote: refs/heads/main (reason: stale info) Hint: Try fetching from the remote, then make the bookmark point to where ↪ you want it to be, and push again.
Alice is about to panic, so she opens social media and scrolls a little until she finds a cat video. Now that her nerves are calmed (the cat was snuggling with a bunch of baby ducks), she turns her attention back to the terminal and actually reads the error message. She is relieved to find that it actually contains useful information about what went wrong and how to fix it. Following the hint, she first fetches from the remote:
jj git fetch
fetch
is a new command, it's basically the opposite of push
.
While push
sends bookmarks and commits from the local repository to the remote, fetch
downloads bookmarks and commits that someone else (like Bob!) may have pushed from another computer.
Here's the resulting jj log
:
@ towolxxr alice@local 2025-07-13 12:01:53 06f737ed │ (empty) (no description set) ○ krpmvwkz alice@local 2025-07-13 11:59:11 main?? main@git git_head() f13dc4df │ Add Python script for greeting the world │ ◆ tpuxswrq bob@local 2025-07-13 12:09:45 main?? main@origin e64c9ef0 ├─╯ Document hello.py in README.md ◆ utlxsmss remo@buenzli.dev 2025-07-13 11:55:15 972b9718 │ Add project description to readme ~
Oh! That's something we haven't seen before. Our version history is split into two branches. This is not unusual and lies at the core of how Jujutsu enables people to work independently from one another. But we don't know how to deal with this situation yet.
If Jujutsu allowed Alice to push the main
bookmark without knowing about Bob's update, his work would accidentally get deleted.
Remember: The remote only considers commits worth keeping around if they are reachable from a bookmark.
But if the main
bookmark points to Alice's commit, Bob's commit is not reachable anymore, because it is not an ancestor of Alice's commit.
Notice that the main
bookmark appears twice in the log, both times with question marks ??
.
This means Jujutsu isn't sure where the bookmark should point to.
Alice moved it to her commit, while it was moved to Bob's commit on the remote.
Once Alice has decided where it should actually point, she can tell Jujutsu by explicitly moving it there.
The terms "bookmark" and "branch" are often used in very similar situations, which can be confusing. Let's define the difference very precisely and prepare ourselves to recognize potential sources of confusion.
A bookmark is just a named label that's attached to, or points to, a single commit. That's it.
A branch is specified by its tip, which is also a single commit. However, a branch refers to the set of commits that includes the tip and all of its ancestors.
These two concepts can be combined in a phrase like this:
"I pushed my changes to the main
branch."
This may be confusing, because main
is a bookmark, right?
Why are we talking about the "main" branch?
Well, the main
bookmark points to a commit, and that commit is the tip of the branch we mean when we say "the main branch".
There's one more thing to look out for when talking to people who primarily use Git. Git always uses the word branch for both things. The term "bookmark" carries no meaning in Git. So, when Git users say the word "branch", they may be talking about an actual branch, meaning a set of commits with a tip and all its ancestors. They may also be talking about a bookmark, meaning a named label pointing to a single commit, without attaching any significance to the ancestors of that pointed-to commit.
That's all interesting and the tree-shaped graph of our commits looks very pretty, but the teacher wants a version that includes both the program and the documentation. What should Alice and Bob do now? Find out in the following chapters!
Inspecting a commit
Alice has just found out that Bod made changes at the same time as she did.
Before she attempts to somehow create a version that includes both changes, she wants to verify the two changes are compatible.
She therefore decides to check out Bob's changes by using jj show
.
Since the remote bookmark main
is pointing there, she can use that as identification.
jj show main@origin
Commit ID: e64c9ef08548e29adad4e2a0b51d8cb457341cbd Change ID: tpuxswrquvqzkmsxpnkrtzqtpyslqknx Bookmarks: main?? main@origin Author : Bob <bob@local> (2025-07-13 12:09:35) Committer: Bob <bob@local> (2025-07-13 12:09:45) Document hello.py in README.md The file hello.py doesn't exist yet, because Alice is working on that. Once our changes are combined, this documentation will be accurate. Modified regular file README.md: 1 1: # jj-tutorial 2 2: 3 : This is a toy repository for learning Jujutsu. 3: The file hello.py contains a script that greets the world. 4: It can be executed with the command 'python hello.py'. 5: Programming is fun!
There's a lot of useful information here that jj log
doesn't show.
Let's go over it one-by-one:
-
The first two lines are the commit ID and change ID. We've seen them already, but these ones are longer! That's because the output of
jj log
only shows you a prefix of the full ID. The short prefix is usually enough to uniquely identify a commit, but sometimes you want the whole thing. -
Next is a list of bookmarks pointing to the commit.
-
The following two lines are the author and committer information, as well as their timestamps. Author and committer are usually the same, the difference is not important. If you're curious anyway, there's a short explanation in the info box below.
-
Then there's the commit message. Notice that we see the full description here including its body.
jj log
only displays the subject line to save space. -
Lastly, we see a list of files that have changed in this commit as well as the precise changes of their content. The color green means "added" and red means "removed" So we can see that this commit removed the previous line 3 and replaced it with three new lines. The first line is neutral, indicating that it was not changed in this commit.
Alice is pretty sure that Bob's changes are compatible with hers.
He only changed the file README.md
, while Alice didn't do anything besides adding hello.py
.
Her next task is to create a version of the project that combines both changes.
Creating a merge commit
I've strung you along for long enough, let's finally create that version with the combined changes.
There are actually two approaches to achieve this, one of them we'll see later. This time, we're going to create a merge commit. As the name implies, a merge commit merges changes from two (or more!) commits.
Until now, we've created new commits with the command jj new
and merge commits are no different.
We just have to explicitly specific which commits we want to merge as additional arguments.
You can check the log and use the change IDs of the commits or simply exploit that (1) main@origin
is pointing to Bob's commit and (2) Alice's commit is the working copy's parent, i.e. @-
.
A working command is therefore:
jj new main@origin @-
Let's view the result with jj log
:
@ voozptoo alice@local 2025-07-13 12:46:20 0d76b632 ├─╮ (empty) (no description set) │ ○ krpmvwkz alice@local 2025-07-13 11:59:11 main?? main@git f13dc4df │ │ Add Python script for greeting the world ◆ │ tpuxswrq bob@local 2025-07-13 12:09:45 main?? main@origin git_head() e64c9ef0 ├─╯ Document hello.py in README.md ◆ utlxsmss remo@buenzli.dev 2025-07-13 11:55:15 972b9718 │ Add project description to readme ~
Interesting! The resulting merge commit has two parents - precisely the ones we specified. You can confirm that this new commit contains both changes from Alice and Bob:
cat README.md
cat hello.py
Jujutsu tries to be smart about how to combine changes, but not too smart. Combining changes which modify the same part of the project leads to a conflict. Conflicts are not necessarily bad, they are just a signal that you need to combine some changes manually, making sure to preserve the spirit of what each change was trying to achieve individually. How to resolve conflicts is a topic reserved for the next level.
Let's give this merge commit a description and send it to the remote:
jj describe -m "Combine code and documentation for hello-world"
jj new
jj bookmark move main --to @-
jj git push
Phew!
That was intense.
If you run jj log
now, the complicated branching and merging will be hidden by default.
Remember that you can reveal it with jj log --revisions 'all()'
.
Excluding files from version control
Let's check up on Bob:
cd ~/jj-tutorial-bob
Done with his part of the assignment, he's already thinking about the submission.
The teacher expects to receive a tarball called submission_alice_bob.tar.gz
.
After a little trial and error, he comes up with the following command to create the tarball:
tar czf submission_alice_bob.tar.gz README.md
Bob wants to spare Alice the hassle of having to figure that out, in case she ends up making the final submission. He decides to add it to the documentation:
echo "
## Submission
Run the following command to create the submission tarball:
~~~sh
tar czf submission_alice_bob.tar.gz [FILE...]
~~~" >> README.md
jj describe -m "Add submission instructions"
Bob is careful to push only clean commits to the remote shared with Alice.
(Be like Bob!)
He double-checks the content of his commit with jj show
:
Commit ID: 4acd78d8eb750441ac5334ca7a1737ea5130e7a5 Change ID: stqpqnvlrvyttzwmpupvzwlnnkqqpmlp Author : Bob <bob@local> (2025-07-13 12:47:25) Committer: Bob <bob@local> (2025-07-13 12:47:25) Add submission instructions Modified regular file README.md: ... 3 3: The file hello.py contains a script that greets the world. 4 4: It can be executed with the command 'python hello.py'. 5 5: Programming is fun! 6: 7: ## Submission 8: 9: Run the following command to create the submission tarball: 10: 11: ~~~sh 12: tar czf submission_alice_bob.tar.gz [FILE...] 13: ~~~ Added regular file submission_alice_bob.tar.gz: (binary)
Oh no!
It looks like the result of Bob's research was recorded in the commit.
The tarball submission_alice_bob.tar.gz
has no business being tracked in version control.
By default, Jujutsu records every single file in the project into your working copy, which is usually what you want.
But sometimes, you need to tell Jujutsu to ignore certain files and exclude them from being recorded.
For this purpose, Jujutsu reads a special .gitignore
file if it exists in the project.
This file can contain a list of file names, directories, and patterns which describe the set of files that should not be tracked in version control.
To fix the problem, Bob adds a pattern for tarballs to the .gitignore
file:
echo "*.tar.gz" > .gitignore
The star *
symbol is a special glob character which can expand to match any filename, including submission_alice_bob
.
Adding only the precise filename submission_alice_bob.tar.gz
to the .gitignore
file would've worked as well, but the above pattern takes care of any tarball.
Ignoring files globally
Ignoring files globally
Sometimes you might have the same files lying around in several projects that are only intended for your personal use.
In that case, it makes sense to ignore these files globally, not just for a specific project.
It would be a little tedious to add personal files to the .gitignore
of every single project you work on.
One such example is relevant for macOS users:
The Finder application stores hidden files called .DS_Store
in directories opened with it.
These files can end up anywhere and basically never belong in version control.
If you're a macOS user, consider adding .DS_Store
to your global .gitignore
file:
echo ".DS_Store" >> ~/.gitignore
Let's check the commit content again:
Commit ID: 0dfee74b0efa931dd7bb4cc21a7098a942100052 Change ID: stqpqnvlrvyttzwmpupvzwlnnkqqpmlp Author : Bob <bob@local> (2025-07-13 12:47:25) Committer: Bob <bob@local> (2025-07-13 12:48:24) Add submission instructions Added regular file .gitignore: 1: *.tar.gz Modified regular file README.md: ... 3 3: The file hello.py contains a script that greets the world. 4 4: It can be executed with the command 'python hello.py'. 5 5: Programming is fun! 6: 7: ## Submission 8: 9: Run the following command to create the submission tarball: 10: 11: ~~~sh 12: tar czf submission_alice_bob.tar.gz [FILE...] 13: ~~~ Added regular file submission_alice_bob.tar.gz: (binary)
Hmm... the .gitignore
file has joined the party, but the tarball is still recorded in the commit.
The reason is that Jujutsu only considers .gitignore
for files it hasn't already started tracking.
Since Bob added the .gitignore
after the unwelcome file started being tracked, it had no effect.
Luckily, Jujutsu has a command to make it stop tracking already-tracked files:
jj file untrack submission_alice_bob.tar.gz
The untrack
command only works if the file to untrack is already present in .gitignore
.
Otherwise, Jujutsu would immediately start tracking the file again just after untracking it.
Let's check the commit content one more time:
Commit ID: 8d715af92336a29d85f3349cb58070587a267e7b Change ID: stqpqnvlrvyttzwmpupvzwlnnkqqpmlp Author : Bob <bob@local> (2025-07-13 12:47:25) Committer: Bob <bob@local> (2025-07-13 12:48:59) Add submission instructions Added regular file .gitignore: 1: *.tar.gz Modified regular file README.md: ... 3 3: The file hello.py contains a script that greets the world. 4 4: It can be executed with the command 'python hello.py'. 5 5: Programming is fun! 6: 7: ## Submission 8: 9: Run the following command to create the submission tarball: 10: 11: ~~~sh 12: tar czf submission_alice_bob.tar.gz [FILE...] 13: ~~~
Yay! This commit looks nice and tidy.
Rebasing
Now that Bob is happy with his second commit, he tries to push it to the remote:
jj new
jj bookmark move main --to @-
jj git push
As you might've already guessed, Bob gets the same error message as Alice did earlier. He also decides to follow the hint of fetching from the remote (after watching some cat videos):
jj git fetch
Here's what Bob's log looks like now:
@ zrzzqnnn bob@local 2025-07-13 12:49:25 46e265a9 │ (empty) (no description set) ○ stqpqnvl bob@local 2025-07-13 12:48:59 main?? main@git git_head() 8d715af9 │ Add submission instructions │ ◆ voozptoo alice@local 2025-07-13 12:46:54 main?? main@origin b9b41560 ╭─┤ (empty) Combine code and documentation for hello-world │ │ │ ~ │ ◆ tpuxswrq bob@local 2025-07-13 12:09:45 e64c9ef0 │ Document hello.py in README.md ~
It's basically the same situation. Bob's and Alice's changes have branched-off into different directions. To merge them back together, Bob could do the same thing Alice did and create a merge commit. However, Bob doesn't like merge commits. Preferring straight lines in his log, he decides to take a different approach from Alice.
Bob is going to pretend like he made his changes on top of Alice's changes all along. His most recent commit should have Alice's commit as its parent, because that will result in a linear history.
There is a Jujutsu command just for this purpose:
It's called rebase
.
As the name implies, it takes commits from one "base" (some ancestor) and moves them on top of a different "base".
jj rebase
on its own doesn't work though, it needs to know the destination of the operation.
In this case, Bob wants to move his commit on top of the state of the remote main
bookmark, i.e. Alice's commit.
Therefore, he runs the command:
jj rebase --destination main@origin
What does the log say?
@ zrzzqnnn bob@local 2025-07-13 12:50:25 7918cf33 │ (empty) (no description set) ○ stqpqnvl bob@local 2025-07-13 12:50:25 main* git_head() 0b0feeff │ Add submission instructions ◆ voozptoo alice@local 2025-07-13 12:46:54 main@origin b9b41560 │ (empty) Combine code and documentation for hello-world ~
Splendid, that's exactly what Bob wanted.
Jujutsu even figured out that the main
bookmark should probably point to Bob's new commit.
All that's left to do is to rerun:
jj git push
What Bob has just done is a paradigm shift: He has rewritten history. The history used to say that Bob's second commit descended from his first one, but now it says it descended from Alice's commit. The extent of this revisionism is still benign, but as we progress through the book, we will explore the dark arts of history manipulation ever more deeply.
...ahem, where were we?
I've glossed over a small detail.
I said the rebase
command moves commit from a one base to a new one.
What is that previous base?
When using the --destination
flag (or -d
for short), Jujutsu will select the first shared ancestor of your working copy and the new base as the old base to rebase from.
In simple terms, the rebase
command moves only those commits that aren't on the destination branch yet.
In our example, there was only one commit to move.
Creating a merge commit and rebasing are both valid ways of recombining changes that branched-off into different directions. They both have advantages and disadvantages. Some people care more about one aspect than another, so they end up having strong opinions about which approach is best. Here's a hopefully balanced overview of the main trade-off:
advantage | disadvantage | |
---|---|---|
merge | Preserves the history exactly as it happened. | Can result in a tangled, hard-to-read history. |
rebase | Results in an easy-to-read, linear history. | Lies about the order in which things happened. |
Once you have determined the correct opinion about which one is better, please let everybody on the internet know about your important discovery!
Adding more bookmarks
Let's switch back to Alice:
cd ~/jj-tutorial
She heard rumors that next week's assignment is going to be about writing loops in Python. In an attempt to stay ahead of the game, she extends the hello-world program with iteration:
echo "
for (i = 0; i < 10; i = i + 1):
print('Hello, world!')" >> hello.py
Unfortunately, she seems to have made a mistake.
Running python hello.py
prints an error:
File "/home/remo/jj-tutorial/hello.py", line 3
for (i = 0; i < 10; i = i + 1):
^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
Alice doesn't have time to figure out the problem. She decides it's best to wait for the teacher to explain how to do it correctly in next week's lecture. She doesn't just want to throw her changes away though, she wants to keep the experiment around so she can compare it to the correct version later. As usual, she describes her changes and makes a new commit before pushing:
jj describe -m "WIP add for loop (need to fix syntax)"
jj new
Pushing work-in-progress (WIP) changes like this directly to the main bookmark would be a bad idea. Imagine if Bob later created a submission tarball and it accidentally included Alice's incomplete, incorrect code! That would be no good. To avoid that, Alice decides to push her commit to a new bookmark.
What should be the name of that bookmark?
Having a main
bookmark is a strong convention, but for additional ones, anything goes.
A simple approach is to just use a super short description of your changes, like add-for-loop
.
Some people like to prefix their bookmark names with their own name or username, allowing everyone to easily identify who's working on what: alice/add-for-loop
.
Still others include a ticket number from the bug-tracker software in the bookmark name.
These are all fine options, but sometimes you just don't care.
Jujutsu has a handy way to push changes by generating a generic bookmark name for you:
jj git push --change @-
The name of the generated bookmark is push-
, followed by a prefix of the change ID.
It's not very informative, but that's kind of the point.
The content of the commit is always more important than the bookmark.
Note that the --change
flag can be abbreviated as -c
.
You might be noticing a pattern here, many commonly-used flags have these short versions.
From now on, I won't mention them anymore.
You can explore the available flags of any Jujutsu command by calling it with the --help
flag.
Alice just got a text from Bob telling here he pushed another commit. She decides to fetch the new changes and start a new commit on top of them, to make sure she has the latest and greatest of Bob's documentation at her disposal.
jj git fetch
jj new main
jj log
@ zwmnxosp alice@local 2025-07-13 12:55:57 f1ccf84b │ (empty) (no description set) ◆ stqpqnvl bob@local 2025-07-13 12:54:17 main git_head() 5d171946 │ Add submission instructions │ ○ myxzxuqo alice@local 2025-07-13 12:55:50 push-myxzxuqooyln 8c027a05 ├─╯ WIP add for loop (need to fix syntax) ◆ voozptoo alice@local 2025-07-13 12:46:54 b9b41560 │ (empty) Combine code and documentation for hello-world ~
Wonderful. Alice has the latest changes from Bob and can continue to do other work. Her experiment with Python loops is safely stored on the remote with a bookmark. She can always come back to it later, finish the work and then combine it with the main branch. The WIP commit and its bookmark can also be deleted if they're not needed anymore, but that's a topic for another day.
Now you have the basic skills to collaborate on projects with other people. Let's summarize what we've learned:
- A branching history is normal when multiple people work together.
- You can combine changes from a branched history by creating a merge commit or by rebasing one branch of commits on another.
- Files which do not belong in version control can be excluded with
.gitignore
and
jj file untrack
. - Work-in-progress changes can be stored on the remote with additional bookmarks, avoiding chaos on the main branch.
It's time to take a break and practice what you've learned. I still recommend to come back for level 2 relatively soon. It will teach you how to solve everyday problems like conflict resolution.