Jujutsu: Modern Version Control Beyond Git
On this page 6
Jujutsu vs Git: Why a New VCS?
Git models history as a Directed Acyclic Graph (DAG) of commits. Each commit is immutable once created, identified by a SHA-1 hash, and points to its parent(s). This design excels at tracking a linear progression of changes and merging divergent lines of work efficiently.
Common development workflows, however, often require modifying local history before sharing. Operations like reordering commits, splitting a large change, or amending an earlier commit are not fundamental in Git. Instead, Git implements them as history rewrites.
For example, git rebase creates entirely new commits with new SHA-1 hashes, discarding the originals. This changes the commit identity. Such changes complicate collaboration when shared branches are involved, requiring force pushes and careful coordination to avoid data loss or divergent histories.
Managing several independent but incomplete changes simultaneously is also cumbersome in Git. Developers often use git stash or create many short-lived topic branches. Neither approach naturally supports iterative refinement across multiple related changes without frequent rebasing or complex branch management.
Jujutsu addresses these pain points by adopting a different core philosophy: treat individual changes (commits) as mutable objects in the local repository.
Unlike Git, where a commit’s identity is tied to its content and parentage, Jujutsu separates the concept of a “change” from its specific content hash. This allows direct manipulation of history, such as reordering, editing, or splitting commits, without the mental overhead of “rewriting” history or generating new commit IDs for every local adjustment.
This conceptual shift means operations like amending an ancestor commit, reordering a series of changes, or splitting a single commit into multiple smaller ones are first-class commands. Jujutsu’s internal model tracks these changes as distinct entities, allowing developers to iterate on their work more fluidly. The system manages the underlying commit graph adjustments, making common iterative development steps simpler and safer before publishing work.
The goal is a version control system that aligns more closely with how developers naturally think about evolving their work. Instead of working around Git’s immutable commit model with rebase-heavy workflows, Jujutsu makes local history manipulation a core, intuitive part of the development cycle. This reduces cognitive load and simplifies complex local refactoring tasks.
Jujutsu Installation and Initial Setup
Jujutsu is distributed as a single binary, making its installation straightforward across platforms. The recommended method for most users is through the Rust toolchain, which compiles the latest version directly from source.
To install Jujutsu on Linux or macOS, first ensure you have the Rust toolchain installed. If not, use rustup.rs to set it up.
Once Rust is available, install Jujutsu with Cargo:
cargo install --locked jj
This command compiles and installs the jj binary into your Cargo bin directory, typically ~/.cargo/bin. Confirm this directory is in your system’s PATH to run jj from any location.
For users who prefer not to install the Rust toolchain, pre-built binaries are available for download on Jujutsu’s GitHub releases page.
On Windows, cargo install --locked jj also works if the Rust toolchain is present. Alternatively, Windows users can use scoop for installation:
scoop install jj
After installation, verify Jujutsu is correctly set up and accessible by checking its version. This confirms the jj command is in your PATH and functional.
jj --version
Output similar to the following indicates a successful installation:
jj 0.12.0
Before using Jujutsu, configure your user name and email. These details are associated with the changes you make, similar to Git’s user configuration. Set them globally using jj config set:
jj config set user.name "Your Name"
jj config set user.email "your.email@example.com"
Jujutsu stores this configuration in ~/.jjconfig.toml (or %USERPROFILE%\.jjconfig.toml on Windows). You can inspect the current configuration with jj config list.
To begin a new project, initialize a Jujutsu repository within your project directory. Navigate to your project’s root and run:
jj init
This creates a .jj directory, which holds Jujutsu’s internal state.
If you are starting with an existing Git repository, initialize Jujutsu to overlay it. This allows Jujutsu to manage your commits while still using Git for remote interactions:
jj init --git-repo=.
The --git-repo=. argument tells Jujutsu to use the current directory’s Git repository as its backend. Jujutsu will then manage the Git repository’s working copy and commit history through its own interface, providing its unique workflow advantages on top of Git’s capabilities.
How Jujutsu Manages Commits and Branches
Jujutsu manages changes through a mutable commit graph, where the working copy always points to an editable commit. This differs from Git’s immutable commit history.
To begin work, create a repository:
jj init --git-repo example_repo
cd example_repo
After modifying files, jj status shows the changes associated with the current commit, referred to as @.
echo "First line" > file1.txt
jj status
Working copy changes for commit 23f5b24c6e (empty) (no description set):
A file1.txt
To incorporate these changes into the current commit, use jj commit -i. This command does not create a new commit; it updates the existing working copy commit with the staged changes.
jj commit -i -m "Add file1.txt with initial content"
To start a new line of work, or to create a new, empty commit on top of the current one, use jj new. This command moves the working copy to the newly created child commit.
jj new
echo "Second line" >> file1.txt
jj commit -i -m "Append second line to file1"
Jujutsu’s branches are named pointers to specific commits, similar to Git’s lightweight tags, but designed for active development. They do not define the commit graph’s structure themselves; the commits’ parentage does that.
To create a named branch pointing to the current commit:
jj branch create feature-a
You can view all current branches and their associated commits with jj branch list.
Merging in Jujutsu integrates changes from one commit into another. If you are on a commit and want to merge changes from feature-a into your current working copy commit, use jj merge.
This creates a new merge commit that has both the current commit and feature-a’s target commit as parents, and then moves the working copy to this new merge commit.
# Assume we are on main and feature-a has diverged
# jj checkout main (if not already there)
jj merge feature-a
The merge operation automatically handles conflicts. If conflicts occur, Jujutsu pauses and prompts for resolution, similar to Git. Once resolved, jj commit -i finalizes the merge commit. This model allows for flexible history rewriting and reordering before publishing changes to a remote repository.
Jujutsu’s Powerful Rebase and Amend Features
Git’s model for rewriting history, particularly with rebase and amend, often introduces complexity. This is especially true when conflicts arise or when modifying commits not at the HEAD.
Jujutsu simplifies these operations by treating the commit graph as inherently mutable. Instead of copying commits or replaying patches, Jujutsu directly moves and modifies commits within the graph, making history rewriting a more intuitive process.
Jujutsu’s jj rebase command moves a commit and all its descendants onto a new parent. When conflicts occur during a rebase, Jujutsu does not stop the operation. Instead, it completes the rebase, leaving the resulting conflicts directly in the working copy for resolution.
This allows you to resolve conflicts using standard merge tools and then jj commit the resolution, rather than navigating a multi-stage rebase process.
Consider rebasing a feature branch my-feature onto an updated main branch. First, ensure your working copy points to the head of my-feature. Then, specify the destination commit for the rebase, which is main in this case.
# Assume current working copy is on the 'my-feature' branch,
# which has diverged from 'main'.
jj rebase -d main
This command moves my-feature and any commits built on top of it directly onto the current main commit. If conflicts arise, jj status will indicate them, and you resolve them in your files.
Jujutsu’s jj amend command offers a direct way to modify an existing commit. Unlike Git’s commit --amend which only works on the current HEAD, jj amend can modify any commit, including those deeper in history. This capability is crucial for fixing typos or adding forgotten files to an earlier commit without an interactive rebase.
To amend a commit that is not the working copy’s parent, use jj edit to temporarily move the working copy to that specific commit. After making your changes, jj amend incorporates them into the edited commit. Finally, jj go returns your working copy to its original position.
# Suppose we have commits: A -- B -- C (where C is the current working copy)
# To amend commit B:
jj edit B
# Make necessary file changes here, e.g., fix a typo in a source file
# ...
jj amend
# Commit B is now updated. To return to commit C:
jj go C
This sequence directly modifies commit B, automatically rebasing C (and any subsequent commits) onto the new version of B. Jujutsu handles the graph adjustments automatically, simplifying operations that are cumbersome in Git.
Jujutsu Common Pitfalls and Troubleshooting
Users new to Jujutsu sometimes modify an existing commit without intending to create new work. This happens because Jujutsu’s working copy is always “on” a specific commit, and changes apply directly to that commit. If you begin coding without explicitly creating a new commit, your modifications will alter the current commit.
To start new work on a fresh commit, first use jj new. This command creates an empty commit that is a child of the current working copy commit, and then moves your working copy to this new child. Any subsequent changes you make will then apply to this new commit, keeping your previous commit untouched.
If you have already made changes to the wrong commit, you can restore its original state. First, find the commit ID of the parent you intended to modify, or the state before your changes. Then, use jj restore --changes-from <commit_id>.
Alternatively, if you just want to discard all uncommitted changes on the current commit, use jj restore .. After restoring, run jj new and re-apply your changes.
# Before making changes, create a new commit
jj new
# Now, modify files and add content
echo "new content" > new_file.txt
jj status
Working copy: 5c866d9b3d0a (empty) (no description set)
Parent: 23f5b706c71c root (no description set)
Added 1 files, changed 0 files, removed 0 files
This output shows the working copy is on the new commit 5c866d9b3d0a, which is empty and has 23f5b706c71c as its parent. The changes are staged for the new commit.
A frequent concern arises when a user performs an operation like jj squash or jj restore and immediately regrets it, feeling that work is lost. Jujutsu records every operation in an explicit operation log, making it possible to revert almost any action. This log is an essential safety net.
To review past operations, run jj op log. This command displays a chronological list of all Jujutsu commands executed in the repository, each with a unique operation ID. Find the operation ID corresponding to the state before your unintended action.
Once you have the correct operation ID, use jj undo <operation_id> to revert the repository’s state to that specific point in time. For instance, if you squashed commits by mistake, jj op log will show the squash operation. Find the operation ID before the squash, then jj undo that ID. This will restore the commits to their state prior to the squash.
# Example: Accidentally squashed a commit
jj squash -r my-feature-commit -m "Squashed by mistake"
# Realize the mistake, check operation log
jj op log
@ 5c866d9b3d0a 2023-10-27 10:30:00.123Z (my-user)@my-host
jj squash -r my-feature-commit -m "Squashed by mistake"
...
o 23f5b706c71c 2023-10-27 10:29:00.000Z (my-user)@my-host
jj new
...
In this example, 23f5b706c71c is the operation ID before the squash.
# Undo the squash operation
jj undo 23f5b706c71c
This command reverts the repository to the state it was in after jj new and before the squash, restoring the squashed commit.
When pulling changes from a remote or rebasing, users sometimes encounter merge conflicts that Jujutsu presents as explicit merge commits. Unlike Git, which often leaves the working directory in a conflicted state, Jujutsu creates a merge commit with conflict markers. This can be initially confusing.
Jujutsu’s approach means the repository is always in a valid, albeit conflicted, state. The working copy commit becomes a merge commit with two parents.
To resolve the conflict, edit the files directly to remove the conflict markers (<<<<<<<, =======, >>>>>>>).
After resolving all conflicts in the affected files, simply commit the changes. Since the working copy is already a merge commit, Jujutsu will update that commit with the resolved content. There is no separate “add” step for the resolved files as in Git; modifying the files and then updating the commit is sufficient.
# Pulling changes that introduce a conflict
jj pull origin
# Jujutsu indicates a conflict, e.g., in `file.txt`
# Open `file.txt`, resolve markers:
# <<<<<<<
# your change
# =======
# their change
# >>>>>>>
# After editing `file.txt` to resolve the conflict:
# (no special `jj add` needed for resolutions)
jj status
Working copy: 5c866d9b3d0a (conflicted) (no description set)
Parent: 23f5b706c71c root (no description set)
Parent: a1b2c3d4e5f6 remote-branch (no description set)
Modified 1 files, added 0 files, removed 0 files
The (conflicted) tag indicates the merge commit is not yet fully resolved. Once all markers are removed, jj status will show the commit is no longer conflicted.
Jujutsu: Hands-on Practice for Workflow Mastery
This exercise guides you through a typical development scenario using Jujutsu’s core capabilities. Begin by creating a new repository and an initial file to simulate a project start.
First, make a directory for the project and initialize a Jujutsu repository within it.
mkdir my_script_project
cd my_script_project
jj init
Now, create a basic Python script. This will be the first version of our app.py.
echo 'print("Hello, world!")' > app.py
jj commit -m "Initial script: hello world"
The output from jj commit shows the new commit hash and its parent. Jujutsu automatically stages changes for you.
Next, add a new feature: a function to greet a specific name. This will be a separate commit.
echo -e 'def greet(name):\n print(f"Hello, {name}!")\n\ngreet("Jujutsu user")' >> app.py
jj commit -m "Add greet function"
At this point, you have two commits. View the commit history using jj log.
jj log
You will see something similar to this, with unique commit hashes:
@ aa54a2a7a40c Add greet function
o 7263c940c313 Initial script: hello world
o 000000000000 (empty)
The @ symbol indicates the current working commit.
Suppose you realize the initial script commit, 7263c940c313, should have included a shebang and a docstring. Jujutsu allows direct modification of past commits without requiring a manual rebase dance.
Use jj amend to update the parent of the current commit.
# First, edit the file to add the shebang and docstring
sed -i '1s/^/#!\/usr\/bin\/env python3\n"""A simple greeting application."""\n/' app.py
# Now, amend the parent of the current commit
jj amend --parent
The jj amend --parent command applies the current working directory changes to the parent commit and then automatically re-applies subsequent commits on top. This rewrites the Initial script commit.
After the amendment, the Add greet function commit automatically rebased onto the new version of the Initial script commit. Verify the new history and current changes.
jj log
The original Initial script commit is gone, replaced by a new one with the shebang and docstring. The Add greet function commit now has this new commit as its parent. This operation modified history, a common task in Jujutsu.
To prepare for sharing, you would typically push your changes to a remote. Jujutsu tracks which commits are public (pushed) and prevents accidental rewriting of them, making collaborative workflows safer.
# This command would push all local changes that are not yet public
# jj public-push
This exercise demonstrated rewriting history and automatic rebase in a common scenario. Jujutsu’s mutable history simplifies many iterative development steps that are more complex in Git.
Spotted an error? Tell us via the corrections process — verified reports get fixed and credited.