A Short Guide to Jujutsu (Jj) for Git Users

Jujutsu (jj) is a modern, Git-compatible version control system from Google designed to be simple and powerful. It aims to make complex workflows effortless, especially cleaning up commit history. If you’ve ever felt that Git was hard or required too much discipline, jj might be for you.

The jj Philosophy

  • Effortless History: With jj, you don’t need perfect commits from the start. Work freely, and clean up your history later with powerful commands like jj split, jj squash, and jj edit.
  • No Staging Area: The working copy is always “live.” There is no need for git add or git stash. To work on something new, you just check out a different revision.
  • Free Checkpointing: jj records every operation. You can undo almost anything with the magic jj undo command.

1. Installation and Configuration

First, install jj on your system.

  • Linux/macOS (Homebrew):
    1
    
    brew install jj
  • Windows (Scoop/Winget):
    1
    2
    3
    
    scoop install jj
    # OR
    winget install --id Jujutsu.Jujutsu
  • NixOS:
    1
    2
    3
    
    environment.systemPackages = with pkgs; [ jujutsu ];
    # OR
    nix profile install nixpkgs#jujutsu
  • For other methods, see the official installation guide.

After installing, configure your user details. This is a one-time setup.

1
2
jj config set --user user.name "Your Name"
jj config set --user user.email "you@example.com"

2. Creating Repositories

  • Clone a Git repo: This is the most common way to start. It creates a jj repo that is linked to the Git remote.
    1
    
    jj git clone https://github.com/user/repo.git
  • Use jj in an existing Git repo:
    1
    2
    
    cd my-existing-git-repo
    jj git init --colocate
  • Create a new repo:
    1
    
    jj git init my-new-repo

3. Git User’s Cheat Sheet: Common Operations

If you’re used to Git, here’s how to translate common commands to the jj way of thinking.

Git Command Jujutsu (jj) Equivalent Notes
git status jj st Shows modified files in the working copy.
git log jj log Shows the revision history. Much more powerful!
git add . (Not needed) Changes are automatically part of the working-copy revision.
git commit -m "..." jj describe -m "..." Describes the current revision. No staging needed.
git checkout -b my-feature jj new -m "my-feature" Creates a new revision for you to work on.
git checkout main jj new main Creates a new empty change on top of main to start new work.
git commit --amend jj describe -m "new msg" or just edit files Amend the message with describe, or edit files and they are auto-amended.
git rebase -i HEAD~3 jj log, then jj edit, jj squash, jj split, jj rebase jj provides dedicated, safer commands for each interactive rebase action.
git stash (Not needed) Just create a new revision with jj new. Your old work is safe.
git pull --rebase jj git fetch then jj rebase -d main@origin Explicitly fetch and then rebase your stack of changes.
git push origin my-feature jj git push --bookmark my-feature Pushes the revision pointed to by the bookmark.

4. The Core Workflow: Your Working Copy is a Commit

The biggest mental shift from Git is that your working copy is always part of a commit. In jj, there is a special “working-copy commit,” often shown with a ~ in the log. It has no commit message and is where your uncommitted changes live.

When you run jj new, you create a new, empty working-copy commit. When you run jj describe, you give the current working-copy commit a message and “close” it, and jj creates a new, empty working-copy commit for you to continue working.

  1. Start a new change: Use jj new to create a new, empty revision to work on. Your changes will be automatically saved to it.
    1
    2
    
    jj new
    # Now, edit your files
  2. Describe your change: When you’re ready, describe the revision. You can do this at any time.
    1
    
    jj describe -m "My new feature"
  3. Check your status: Get in the habit of running these commands all the time to see what’s happening.
    1
    2
    3
    
    jj st      # See modified files
    jj log     # See the history of revisions
    jj diff    # See code changes in the current revision
  4. Push a change: after cleanup, move your bookmark and push
    1
    2
    
    jj bookmark move master --to=@
    jj git push --bookmark master

5. Understanding Revision IDs vs. Commit IDs

This is a crucial concept to grasp. Run jj log to see them.

  • Revision ID (e.g., wuloypwt): A local, permanent ID for a set of changes. It never changes, even if you edit the commit. It’s how you refer to revisions in jj commands.
  • Commit ID (e.g., 182d3ce4): This is the Git SHA. It represents the entire state of the worktree. It will change every time you edit the revision (e.g., by rebasing or amending the message). This is what you see in GitHub.

6. Navigating and Editing History (The Magic)

jj makes history manipulation safe and easy. There is no need for git stash.

  • Edit a past revision: Just use jj edit with the revision ID. Your working copy will update to that revision, and you can start making changes.
    1
    
    jj edit <revision-id> # e.g., jj edit wuloypwt
    Use @ for the current revision, @- for its parent, and @+ for a child.
  • Fixing Mistakes:
    • The magic undo button: This reverts the last operation (a commit, a rebase, etc.). You can even run it multiple times.
      1
      
      jj undo
    • The operation log: To go further back in time, view the log of all operations and restore to a previous state.
      1
      2
      
      jj op log
      jj op restore <operation-id>
  • Cleaning up commits:
    • Split a commit: If a commit does too much, split it into smaller ones.
      1
      
      jj split
    • Combine commits: To merge a revision into its parent, use jj squash.
      1
      
      jj squash

7. A Concrete Example: From Messy to Clean

Let’s see how this works in practice.

  1. Start a new feature on top of main:

    1
    2
    
    jj new main -m "feat: Add user login button"
    # You add the HTML for the button.
  2. Add the styling, but as a new change:

    1
    2
    
    jj new -m "style: Style the login button"
    # You add the CSS.

    Your log now looks like this (simplified):

    1
    2
    3
    4
    5
    
    o style: Style the login button
    |
    o feat: Add user login button
    |
    o (main branch)
  3. Realize you forgot something in the first commit. No problem! Just edit it directly.

    1
    2
    3
    
    jj edit <revision-id-of-login-button>
    # Add the missing 'aria-label' attribute to the HTML.
    # The change is automatically saved to that revision.
  4. Decide to combine the two commits. A button and its style belong together.

    1
    2
    3
    4
    
    jj new .  # Go back to the top commit
    jj squash
    # The "style" commit is now merged into the "feat" commit.
    # jj will ask you to write a new, combined commit message.

    Your log is now clean:

    1
    2
    3
    
    o feat: Add styled user login button
    |
    o (main branch)
  5. Now you are ready to push.

    1
    2
    
    jj bookmark create login-feature -r @
    jj git push --bookmark login-feature

8. Branching and Remotes

In jj, branches are just pointers (called “bookmarks”) to revisions. They don’t automatically move forward when you create a new commit. You must explicitly create, move, and push them.

  1. Start a new line of work:
    1
    2
    
    # Creates a new, empty revision on top of main
    jj new main
  2. Do your work: Make some edits, then describe the commit.
    1
    2
    3
    
    jj describe -m "A new feature"
    # You might continue with more commits
    jj new -m "Another commit for the feature"
  3. Create a bookmark (branch): Before you push, create a bookmark to give your line of work a name. The -r @ flag means “give the name to the current revision.”
    1
    
    jj bookmark create my-feature -r @
  4. List bookmarks: To see all bookmarks (local and remote), use jj bookmark list.
    1
    
    jj bookmark list
  5. Pull latest changes: First, fetch from the remote. To make jj automatically update your local main bookmark when origin/main changes, you can “track” the remote branch. Then, rebase your stack of changes onto the main branch.
    1
    2
    3
    
    jj git fetch
    jj bookmark track main@origin
    jj rebase -d origin/main
  6. Push your branch: The command for pushing a bookmark is jj git push --bookmark <name>.
    1
    2
    3
    4
    5
    
    # Push a specific bookmark. This is the most common push command.
    jj git push --bookmark my-feature
    
    # Alternatively, push a specific change, which will create a branch for it
    jj git push --change .
  7. Checking out a colleague’s branch: To review or work on someone else’s changes, first fetch the latest from the remote. Then, find the change you want and create a new working copy at that revision.
    1
    2
    3
    4
    5
    6
    
    jj git fetch
    # Find the revision from the remote branch in the log
    jj log -r "all()"
    
    # Check it out. This creates a new working-copy commit at that revision.
    jj new <revision-id-of-their-change>

9. Merging and Tidying Up (Without a PR)

When you own the repository or have direct push access to the main branch (main or master), you can merge your changes and clean up your feature branch directly from the command line, skipping the GitHub pull request process entirely.

  1. Update your local main branch: Make sure you have the latest changes from the remote.
    1
    2
    3
    
    jj git fetch
    jj new main
    jj git pull
  2. Rebase your feature branch: Go to your feature branch and rebase it on top of the latest main.
    1
    2
    
    jj new my-feature
    jj rebase -d main
  3. Merge into main: Move to the main branch and fast-forward it to the tip of your feature branch.
    1
    2
    
    jj new main
    jj rebase -s my-feature  # Fast-forwards main to my-feature
  4. Push main to the remote:
    1
    
    jj git push
    This command will push the updated main branch.
  5. Delete your feature branch: Once merged, you no longer need the bookmark or the remote branch.
    1
    2
    3
    4
    5
    
    # Delete the local bookmark
    jj bookmark delete my-feature
    
    # Delete the remote branch
    jj git push --delete my-feature

10. Important Considerations & Downsides

  • Force-Pushing is Normal: jj works by rewriting history, so it force-pushes changes. This can make reviewing a pull request commit-by-commit on GitHub difficult, as old commits will change.
  • State is Local: The undo history and the status of revisions are stored locally on your machine. If you clone your repo on a second computer, you won’t have the undo history from the first. A revision created on machine A will be “immutable” (you can’t use jj split, etc.) on machine B. Push your branches before switching computers to avoid losing work.
  • Using Git in Parallel: It’s safe to run read-only git commands (like git status). For any action that modifies history (commit, rebase, push), use the jj command to avoid confusion.

Also check out “The jj VCS Workshop” by Jimmy Koppel and of course the jj-vcs site.

0%