Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Navigating the history

Alice just got a text from Bob telling her that he pushed another commit. She decides to fetch them:

jj git fetch
@  qrtwnykn alice@local 2025-07-25 20:37:05 c2f4e43e(empty) (no description set)rvpkroku alice@local 2025-07-25 20:37:05 push-rvpkrokuqrxt git_head() b9d02faf
│  WIP add for loop (need to fix syntax)
│   ttqlsyyr bob@local 2025-07-25 20:37:05 main 207a18a9
├─╯  Add submission instructions
  stlxrmun alice@local 2025-07-25 20:37:05 530ad636(empty) Combine code and documentation for hello-world
~

Bob's changes are on a different branch. How can she get access to them? We've already solved this problem in two different ways:

  • Merge the two commits with a new commit that has two parents.
  • Rebase one commit on top of the other, creating a straight line of commits.

Neither of these approaches are suitable here, because we don't want to combine the changes. We want to leave Alice's changes on a separate branch until we decide to go back to them.

The solution is to create a new commit on top of Bob's changes, i.e. the main branch. The command to do that is:

jj new main
@  uumounxy alice@local 2025-07-25 20:52:32 e1bcff24(empty) (no description set)
  ttqlsyyr bob@local 2025-07-25 20:37:05 main git_head() 207a18a9
│  Add submission instructions
│ ○  rvpkroku alice@local 2025-07-25 20:37:05 push-rvpkrokuqrxt b9d02faf
├─╯  WIP add for loop (need to fix syntax)
  stlxrmun alice@local 2025-07-25 20:37:05 530ad636(empty) Combine code and documentation for hello-world
~

This is very similar to creating a merge commit, where we specify the commits to merge as additional arguments to jj new. In this case however, we only specify one parent commit instead of two. Now we can summarize more generally what jj new does: It creates a new commit with its parents specified as additional arguments. If no parents are specified, the working copy is used as the default. (You can even create merge commits with more than two parents!)

Notice that we used to have an empty commit on top of the loop experiment commit, but that is now gone. Jujutsu automatically deletes commits that are empty (no changes and no description). This keeps the log tidy and recreating an empty commit is easy anyway. It also makes jj new suitable for navigating the history. Since empty commits have the same project state as their parent, running jj new some_version is a great way to explore the project at a specific version.

As an example, let's try to look at a relatively old state of our repository. Here's the full log for reference:

@  uumounxy alice@local 2025-07-25 20:52:32 e1bcff24(empty) (no description set)
  ttqlsyyr bob@local 2025-07-25 20:37:05 main git_head() 207a18a9
│  Add submission instructions
│ ○  rvpkroku alice@local 2025-07-25 20:37:05 push-rvpkrokuqrxt b9d02faf
├─╯  WIP add for loop (need to fix syntax)
    stlxrmun alice@local 2025-07-25 20:37:05 530ad636
├─╮  (empty) Combine code and documentation for hello-world
│   uxvvmtos alice@local 2025-07-25 20:37:05 a9946efd
│ │  Add Python script for greeting the world
qxkprvsu bob@local 2025-07-25 20:37:05 9e05718a
├─╯  Document hello.py in README.md
  xsswkrsu alice@local 2025-07-25 20:37:05 695f460b
│  Add project description to readme
  pxvxmtks alice@local 2025-07-25 20:36:59 20c6a3b1
│  Add readme with project title
  zzzzzzzz root() 00000000

Assume we want to look at the commit with the description "Document hello.py in README.md". We could easily do that with it's change ID, i.e. jj new qxkprvsu. To make this more interesting, we'll learn one more way to identify commits: with the description() function. It tells Jujutsu to select the commit that matches the description.

jj new 'description("Document hello.py in README.md")'

Don't forget the outer single quotes, they prevent your shell from interpreting the inner double quotes. There is a powerful system behind selecting commits with functions like that, which we'll learn about in the future.

Let's confirm our maneuver by checking the log:

@  uxuqnppz alice@local 2025-07-25 21:16:00 abba4830(empty) (no description set)  ttqlsyyr bob@local 2025-07-25 20:37:05 main 207a18a9
│ │  Add submission instructions
│ │ ○  rvpkroku alice@local 2025-07-25 20:37:05 push-rvpkrokuqrxt b9d02faf
│ ├─╯  WIP add for loop (need to fix syntax)
│   stlxrmun alice@local 2025-07-25 20:37:05 530ad636
╭─┤  (empty) Combine code and documentation for hello-world
│ │
│ ~
│
  qxkprvsu bob@local 2025-07-25 20:37:05 git_head() 9e05718a
│  Document hello.py in README.md
~

Great! We created a commit on top of an old one from our history. The file hello.py didn't exist at that time, which you can confirm by running ls.

You can go back to the current state of the repository by running jj new main.

Alright, so Alice now has the latest changes from Bob and can continue to do other work. She can always come back to her loop experiment later with jj new, finish the work and then combine it with the main branch.

You've completed Level 1 ! 🎉

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.
  • You can navigate to any branch or past version of your repository with jj new.

It's time to take a break from this book and practice what you've learned on a real project. Level 2 will teach you how to solve everyday problems like merge conflicts, so I recommend you come back for it relatively soon.