Skip to content

Instantly share code, notes, and snippets.

@christianromney
Last active November 4, 2025 04:21
Show Gist options
  • Save christianromney/27fd1fca9e5f24ef24d9ed6c9eddda50 to your computer and use it in GitHub Desktop.
Save christianromney/27fd1fca9e5f24ef24d9ed6c9eddda50 to your computer and use it in GitHub Desktop.
Jujutsu VCS Tutorial - Complete Guide with Cheatsheet and Speaker Notes

Jujutsu VCS Tutorial

Tutorial Overview

Jujutsu (jj) is a modern version control system that layers powerful features on top of Git. Think of it as Git with a better command-line interface and workflow - you keep your existing Git repositories and remotes, but gain features like automatic rebasing, first-class conflicts, and an operation log that makes mistakes reversible.

This tutorial teaches you jj in the context of GitHub-based development. You’ll learn how jj’s design makes common Git workflows simpler and safer, particularly around history editing and managing stacks of changes for pull requests.

Target Audience: Developers with intermediate to advanced Git experience, working with GitHub

What You’ll Learn:

  • Core jj concepts and how they differ from Git
  • Daily workflow: creating, editing, and navigating changes
  • Managing bookmarks (branches) and syncing with remotes
  • Stacking changes for better pull request workflows
  • Safe history editing and conflict resolution

1. Setup & Installation

Let’s get jj installed and configured. The setup process will feel familiar if you’ve used Git before.

A. Installation

On macOS, install jj via Homebrew:

brew install jujutsu

B. Essential Configuration

Configure your identity for commits. Note that jj uses --user instead of Git’s --global:

jj config set --user user.name "Your Name"
jj config set --user user.email "[email protected]"

C. Commit Signing Setup

Many organizations require signed commits for security. Jujutsu supports both GPG and SSH signing - choose whichever you already have configured.

For GPG:

# List available GPG keys and fingerprints
gpg --list-secret-keys --keyid-format=long

# Configure jj to use GPG
jj config set --user signing.backend "gpg"
jj config set --user signing.behavior "own"
jj config set --user signing.key "4ED556E9729E000F"  # Replace this sample with your key fingerprint

For SSH (simpler option):

# Find your SSH public key
ls ~/.ssh/*.pub
cat ~/.ssh/id_ed25519.pub  # Or your key name

# Configure jj to use SSH
jj config set --user signing.backend "ssh"
jj config set --user signing.behavior "own"
jj config set --user signing.key "~/.ssh/id_ed25519.pub"

Both of these examples use the “own” signing behavior which signs commits you create or edit.

D. Initializing a Co-located Repository

Jujutsu works alongside Git in “co-located” repositories. This means you can use both jj and git commands on the same repo, and they stay synchronized automatically.

jj git init --colocate  # In existing Git repo, or use jj git clone

After running this command, you’ll have both .jj and .git directories. Git will show “detached HEAD” - this is normal and expected with jj. The two systems synchronize on every jj command, so you can use Git tools when needed while primarily working with jj.

2. Core Concepts & Daily Workflow

Now that jj is installed, let’s understand its core concepts. These differ from Git in ways that make common operations simpler and safer.

A. Understanding the Working Copy

The first mind-shift: in jj, your working copy is a commit. There’s no staging area - when you edit files, those changes automatically become part of the current commit (represented by @).

# Make changes to files...
jj status      # Working copy automatically amends!
jj diff        # See what changed in @

Every time you run a jj command, it automatically snapshots your working directory and amends the @ commit. This eliminates the need for git add - your changes are always part of a commit.

B. Understanding Changes vs Commits

This is a critical distinction: jj tracks both change IDs and commit IDs.

  • Change ID: Identifies a logical piece of work. Stays the same when you edit/amend the commit.
  • Commit ID: Identifies a specific snapshot. Changes every time you modify the commit.

Why both? Change IDs let you refer to “that feature I’m working on” across rewrites. Commit IDs identify exact historical snapshots. This means you can use change IDs in your daily workflow without worrying about them changing when you amend commits.

Changes eliminate the need for branches in the way that Git imagines them.

jj log                       # Shows both IDs for each commit
# Example output:
# @  youzwxvz [email protected] 2025-11-03 22:12:08 e35b5e0f
# │  Create jujutsu VCS tutorial outline
# ◆  pswmtnwq [email protected] 2025-09-05 08:04:28 main 7cc5e620
# │  Reorder content and fix headings
#
# First part (youzwxvz) = Change ID (stays stable when you amend)
# Last part (e35b5e0f) = Commit ID (changes every time you modify)

C. Basic Operations & Workflow

The key concept: every jj command auto-amends the working copy commit. This means you’re always building on your current change until you explicitly create a new one.

# Viewing state
jj status                    # See what's changed in working copy
jj diff                      # Show changes in detail
jj log                       # See commit graph with both change and commit IDs

# Making changes
# ... edit files ...
# Changes automatically amend @ on next jj command

# Finishing a change and starting new work
jj new                       # "Finish" current commit, create new empty one on top
jj new -m "message"          # Same, but set message for new commit
jj describe -m "message"     # Set/update commit message for current change
jj commit -m "message"       # Shortcut: describe + new (set message and move forward)

# Navigating between changes
jj edit <change-id>          # Switch working copy to a different change
jj prev                      # Move to parent commit (creates new @ on parent)
jj next                      # Move to child commit (creates new @ on child)
jj prev --edit               # Edit parent directly (like jj edit @-)

# Viewing history
jj evolog                    # Evolution log - see how a change evolved over time
jj interdiff --from @ --to @-  # Compare changes between two commits

# Modifying commits
jj metaedit                  # Modify metadata (author, timestamps, change-id)

# Safety net
jj undo                      # Undo last operation
jj redo                      # Redo operation (after undo)

Think of ~jj new~ as: “I’m done with this change, start a new one”

D. When to Use: jj new vs jj commit vs jj describe

With multiple commands that seem similar, it helps to know when to use each:

~jj new~: Start a new empty commit on top

  • Use when: You want to start fresh work without setting a message yet
  • Result: New empty commit becomes @

~jj describe~: Set/update the commit message for @

  • Use when: You want to add/change the message but keep working on @
  • Result: @ gets a message, stays as working copy

~jj commit -m “message”~: Shortcut for jj describe + jj new

  • Use when: You’re done AND have a message ready
  • Result: @ gets the message and new empty commit created on top
  • Most similar to ~git commit~

~jj commit <files>~: Like jj split but simpler - move specific files to a new commit

  • Use when: You want to commit only certain files
  • Result: Selected files go to @, rest stays in new child commit

~jj prev~ / ~jj next~: Navigate linearly through a stack

  • Use when: Working through a stack of commits sequentially
  • Result: Creates new @ on parent/child (by default) or edits directly (with --edit)
  • Shortcut for jj new <parent> or jj new <child>

E. Working with Files

Jujutsu provides commands for inspecting and managing files, though most file operations happen naturally through editing.

Listing and viewing:

jj file list                 # List all tracked files in @
jj file show <path>          # Show contents of file in @
jj file show <path> -r <rev> # Show file contents in specific revision

Tracking files (usually automatic):

jj file track <path>         # Explicitly mark file as tracked
jj file untrack <path>       # Stop tracking file
# Note: jj auto-tracks new files by default!

When to use ~jj file~ commands:

  • jj file list: See what files are in a commit (like git ls-files)
  • jj file show: View file contents without checking out
  • jj file track/untrack: Rarely needed - use when you want explicit control over tracking

~jj interdiff~ - Compare Two Versions:

One particularly useful command for PR workflows is jj interdiff, which compares what changed between two commits:

jj interdiff --from <old-version> --to <new-version>
# Common use: See what changed after addressing PR feedback
jj interdiff --from @-- --to @   # Compare last two commits

When to use:

  • Reviewing what changed between PR iterations
  • Seeing how a change evolved after feedback
  • Note: For same change across history, use jj evolog -p instead

F. Quick Demo

Show making changes, checking status, using jj new, and viewing the log.

3. History Editing & Rebasing

One of jj’s superpowers is making history editing safe and straightforward. Unlike Git, where history operations can feel risky, jj provides strong guarantees.

A. How jj Makes This Easy

  • All operations logged (jj op log)
  • jj undo reverses last operation
  • Working on your local commits is safe

This means you can confidently rewrite history knowing you can always undo mistakes.

B. Common Operations

Let’s understand when and why you’d use these history editing commands:

  • ~squash~: Combine multiple commits into one. Use when you have “fix typo” or “address review” commits that should be part of the original change.
  • ~rebase -d <dest>~: Move your changes to build on top of a different commit (the “destination”). “Rebasing onto main” means updating your work to start from the latest main branch instead of wherever you originally branched from.
  • ~abandon~: Discard a change you no longer need. Unlike delete, this preserves the descendants by rebasing them onto the abandoned commit’s parent.

Commands:

jj describe             # Change commit message
jj squash               # Squash @ into parent (combine commits)
jj edit <change-id>     # Resume editing an old commit
jj rebase -d main       # Rebase onto main (move changes to build on main)
jj abandon              # Discard a change (preserves descendants)
jj fix                  # Run formatters/linters on commits automatically

~jj fix~ - Automatic Code Formatting:

The jj fix command applies configured formatters (prettier, black, clang-format, etc.) to commits and updates descendants automatically:

jj fix                  # Fix all mutable commits
jj fix -s @             # Fix only current commit
jj fix -s 'main..@'     # Fix all commits in current stack

Configure tools in .jj/config.toml or global config.

C. Demo Examples

Fixing a typo in an earlier commit:

Here’s where jj really shines - you can edit any commit in your history:

jj edit <change-id>     # Switch to that commit
# Fix the typo...
# Descendants automatically rebase on your changes!
jj new                  # Move forward to continue working

When to use squash - combining fixup commits:

If you’ve accumulated small fixup commits, squash combines them into the original change:

# Scenario: You're at @ and realize you need to fix something in @-
# Make the fixes in your current working copy...
jj squash --into @-     # Squash current changes into parent

# Alternative: Already created separate "fix typo" commits? Squash them:
jj squash -r <fixup-change-id> --into <target-change-id>

Note: When you edit an earlier commit, jj automatically rebases all descendants. No manual rebasing needed!

4. Working with Bookmarks

Now let’s talk about how to organize and name your work. Jujutsu uses “bookmarks” which are similar to Git branches but with important differences.

A. What Are Bookmarks?

Bookmarks are jj’s version of Git branches - named pointers to commits.

Think of bookmarks as: Labels you attach to commits to track your work and push to GitHub as branches.

B. How Bookmarks Differ from Git Branches

Understanding these differences helps explain why bookmarks feel more natural in daily use:

Key differences:

  • Automatic movement: Bookmarks automatically follow commits when you rebase or rewrite them. Git branches stay fixed unless you explicitly move them.
  • No “active” bookmark: In Git, you’re always “on” a branch. In jj, there’s no active bookmark - you work with commits directly via change IDs.
  • Conflict handling: Bookmarks can become conflicted (shown with ??) when updated from multiple sources. You resolve these explicitly.

Why this matters: Bookmarks track your intent (which commits belong to which feature) while jj handles the mechanics of keeping them up-to-date.

C. Creating and Managing Bookmarks

jj bookmark list                         # Show all bookmarks
jj bookmark set feature-x -r @           # Create or update bookmark (most common)
jj bookmark create feature-x -r @        # Create NEW bookmark (fails if exists)
jj bookmark move feature-x -r <rev>      # Move existing bookmark
jj bookmark delete old-feature           # Delete local bookmark
jj bookmark track feature-x@origin       # Start tracking <branch>@<remote> with a bookmark

D. Understanding Bookmark Tracking

Tracking is a concept that helps keep your local bookmarks synchronized with remotes.

What does tracking do?

When you track a remote bookmark (like feature-x@origin), jj git fetch will automatically create or update a local bookmark (feature-x) to match the remote.

Without tracking: You can still see and reference feature-x@origin, but fetch won’t automatically create a local feature-x bookmark.

When to track: Track remote bookmarks you’re actively working on and want to stay synchronized with.

E. Which Command to Use?

With multiple bookmark commands available, here’s when to use each:

  • ~set~: Use this most of the time - works for both new and existing bookmarks
  • ~create~: When you want an error if the bookmark already exists (safer but stricter)
  • ~move~: When you need advanced features like --allow-backwards or --from filters

F. Tags vs Bookmarks

Tags serve a different purpose than bookmarks - they mark immutable points in history.

Tags are like bookmarks but immutable - they mark specific points (usually releases):

jj tag list                  # List all tags
# Note: Create tags via git (jj syncs them automatically)
git tag v1.0.0
jj git fetch                 # Import tags from git

Key difference: Tags don’t move when commits are rewritten. Bookmarks follow commits.

5. Syncing with Remotes

With bookmarks in place, let’s connect your local work to GitHub.

A. Fetching Updates

Fetching in jj works similarly to Git, pulling down remote changes:

jj git fetch                 # Pull changes from remote
jj log                       # See remote bookmarks (e.g., main@origin)
jj rebase -d main@origin     # Rebase your work onto latest main

B. Basic Push (without bookmarks)

For quick prototyping, you can push single changes with auto-generated bookmarks:

jj git push --change @ --allow-new   # Pushes current change with generated bookmark name

This is useful for one-off experiments but for real work you’ll want to create explicit bookmarks.

6. Stacked Changes / PR Workflow

One of jj’s most powerful patterns is “stacking” - breaking large features into small, dependent pull requests. Let’s understand why and how.

A. Why Stack Changes?

The problem with large PRs:

  • Hard to review (reviewers lose focus)
  • Risky to merge (many changes at once)
  • Slow feedback cycle (must finish everything before getting feedback)

Stacking solves this by:

  • Breaking large features into small, reviewable chunks
  • Getting feedback on early parts while working on later parts
  • Making each PR focused and easy to understand

Example: Instead of one huge “Add user authentication” PR, stack:

  1. PR 1: Add database schema for users
  2. PR 2: Add authentication API endpoints (builds on PR 1)
  3. PR 3: Add login UI (builds on PR 2)

Each PR can be reviewed and merged independently!

What if your change is already too big? Use jj split to break it apart:

jj split                # Interactively choose which changes go into first commit
# jj opens an editor showing all changes
# Select which hunks belong in the first commit
# Everything else stays in the second commit

After splitting, you have two commits where you had one - perfect for creating separate PRs!

B. Building Dependent PRs

Here’s the workflow for creating a stack:

jj new main                                      # Start from main
# Make changes...
jj new                                            # Start second change
# Make more changes...
jj bookmark set feature-part1 -r @-              # Bookmark first change
jj bookmark set feature-part2 -r @               # Bookmark second change
jj git push --bookmark feature-part1 --allow-new # First time push requires --allow-new
jj git push --bookmark feature-part2 --allow-new

Note the --allow-new flag - this is required the first time you push a new bookmark to protect against typos.

C. Addressing PR Feedback

When you get feedback on a PR in your stack, jj makes it easy to fix:

jj edit <change-id>     # Go back to specific change in stack
# Make fixes...
jj new                  # Move forward
jj git push --bookmark feature-part1  # Update existing bookmark (no --allow-new needed)

All descendant changes automatically rebase on your fixes!

7. Understanding Revsets

Throughout this tutorial we’ve used expressions like @, main, and main@origin. These are “revsets” - jj’s query language for selecting commits.

A. What Are Revsets?

Revsets are jj’s query language for selecting commits. Think of them as “commit selectors.”

Full reference: https://jj-vcs.github.io/jj/latest/revsets/

You’ve already been using simple revsets, but they can be much more powerful.

B. Basic Revset Syntax

Symbols:

  • @ - your working copy commit
  • @- - parent of working copy
  • main - a bookmark/branch
  • main@origin - bookmark on remote

Common Functions:

  • trunk() - main development branches (main, master, trunk)
  • tags() - tagged releases
  • bookmarks() - all local bookmarks
  • remote_bookmarks() - bookmarks on remotes

Examples:

jj log -r @                    # Show working copy
jj log -r @-                   # Show parent
jj log -r main..@              # Commits between main and working copy
jj log -r 'author(alice)'      # Commits by alice

8. Immutable Commits & Safe History

Now that you understand how to work with history, let’s talk about the safety rails that prevent you from breaking shared work.

A. Why This Matters

In Git, it’s easy to accidentally rewrite commits that others have based work on, breaking their repositories. Force pushing can overwrite work. These accidents cause frustration and lost time.

Jujutsu prevents these problems through immutable commits - certain commits are protected from rewriting.

B. What Are Immutable Commits?

Jujutsu protects certain commits from being rewritten:

  • trunk() - main/master branches
  • tags() - tagged releases
  • untracked_remote_bookmarks() - commits pushed to remotes

Configuration example (optional to show):

jj config list | grep immutable

These protections mean:

  • No rewriting shared history - can’t accidentally rebase commits others have
  • No force push needed - jj checks remote state before pushing (like --force-with-lease)
  • Safe by default - protects your team from broken histories

C. When You Need to Update a Pushed Commit

If you need to update a commit you’ve already pushed, jj ensures you do it safely:

jj git push --bookmark my-feature  # Fails if remote diverged
# Must fetch first if remote changed:
jj git fetch
jj rebase -d main@origin          # Resolve conflicts
jj git push --bookmark my-feature  # Now succeeds

Best practice: Don’t rewrite commits others have based work on!

9. Conflict Resolution

Let’s talk about what happens when your changes conflict with others’ changes - an inevitable part of collaborative development.

A. How jj Handles Conflicts Differently

Jujutsu takes a unique approach to conflicts that gives you more flexibility:

Key differences from Git:

  • Conflicts are first-class objects: You can commit with unresolved conflicts and continue working
  • No special commands needed: No git rebase --continue or git merge --continue - just edit the conflict and the change is automatically amended
  • Operations don’t fail: jj rebase succeeds even with conflicts; descendants automatically rebase too

Why this matters: You can keep your work rebased on main without blocking on conflict resolution. Resolve conflicts when you’re ready.

B. Three Ways to Resolve Conflicts

Method 1: Manual editing (simple conflicts)

jj rebase -d main        # May create conflicts - operation still succeeds!
jj status                # Shows conflicted files with conflict markers
# Edit files to resolve (markers: <<<<<<< %%%%%%% +++++++ >>>>>>>)
jj diff                  # Verify resolution
# No special command needed - conflict resolved automatically

Method 2: Using a merge tool (complex conflicts)

jj resolve --list        # List all conflicted files
jj resolve path/to/file  # Opens merge tool for specific file
jj resolve               # Resolve all conflicts interactively, one by one
# Built-in shortcuts: --tool=:ours (use our side) or --tool=:theirs (use their side)

Method 3: View what changed during resolution

jj interdiff --from <before-resolve> --to @  # See what you changed while resolving
# Useful for reviewing your conflict resolution decisions

C. Advanced Features

  • Postpone resolution: Keep commits rebased while deferring conflict fixes
  • Better conflict markers: Shows “diff to apply” rather than just both sides
  • Descendants auto-rebase: When you resolve a conflict, descendants update automatically

D. A Word of Caution

Don’t postpone conflicts indefinitely!

While jj lets you defer resolution, conflicts have real consequences:

  • Tests may fail: Conflicted code won’t compile or run correctly
  • Blocks collaboration: Can’t share conflicted commits with teammates
  • Compounds over time: More rebases = more potential conflicts to untangle
  • Breaks local development: Your working copy may be broken until conflicts are resolved

Best practice: Postpone briefly for convenience (finish current thought, switch tasks), but resolve conflicts before:

  • Pushing to remote
  • Asking for code review
  • Switching to work on dependent changes

10. Advanced Power Features

If you have extra time, here are some advanced features that showcase jj’s power.

A. jj absorb - Automatic Fixup Distribution

Intelligently moves changes from your working copy into the appropriate commits in your stack:

# You've made fixes across multiple files that belong to different commits
jj absorb                # Automatically distributes changes to where they belong
# jj analyzes which commit last touched each line and moves changes there

When to use: After making scattered fixes across a stack of commits - let jj figure out where each change belongs.

B. jj diffedit - Interactive Change Editing

Edit the changes in a commit directly, like using a visual diff editor:

jj diffedit -r <change-id>   # Opens diff editor to modify the commit's changes
# Add/remove hunks, modify lines - more powerful than manual editing

When to use: Surgically edit what a commit changes without touching other commits.

C. jj parallelize - Restructure Commit Relationships

Convert a linear stack into parallel siblings for independent testing:

jj parallelize <revset>      # Makes commits siblings instead of linear ancestors
# Useful for testing independent features in parallel

When to use: You have multiple independent features in a stack that don’t actually depend on each other.

D. jj op log and jj op restore - Time Travel

View and restore any previous state of your repository:

jj op log                    # See every operation you've performed
jj op restore <operation-id> # Go back to any previous state
# More powerful than undo - can restore from any point in history

When to use: Made a complex mistake? Just restore to before you made it!

E. jj duplicate - Copy Commits

Create copies of commits with the same content but different change IDs:

jj duplicate <change-id>           # Duplicate commit onto its current parent
jj duplicate -d main <change-id>   # Duplicate commit onto a different base (main)
# Useful for trying different approaches without losing the original

When to use:

  • Experiment with changes without losing the original
  • Apply the same change to multiple branches
  • Create a backup before risky operations

F. jj bisect - Find When a Bug Was Introduced

Automatically find which commit introduced a bug using binary search:

jj bisect run 'cargo test'         # Automatically test each commit
# jj will binary search through history, running your test command
# Finds the first commit where the test fails

When to use: When you know something broke but don’t know which commit caused it.

11. Wrap-up & Resources

Key Takeaways

  • Working copy is a commit; jj new to start fresh
  • Change IDs stay stable across amendments; commit IDs change
  • Immutable commits prevent rewriting shared history
  • Operation log (jj op log) + jj undo = safety net
  • Colocated repos let you use both jj and git
  • Bookmarks track your work; use jj bookmark set for most cases
  • Stacking changes creates better PRs

Resources

Command Reference Table

CommandDescriptionExample
jj git initInitialize colocated jj+git repojj git init
jj git cloneClone a Git repositoryjj git clone https://github.com/user/repo
jj statusShow working copy changes, auto-snapshots working copyjj status
jj diffShow changes in working copy commitjj diff
jj logDisplay commit graph with change IDsjj log
jj evologShow evolution history of a change (how it was modified over time)jj evolog
jj interdiffCompare changes between two commits (useful for PR iterations)jj interdiff --from @-- --to @
jj newCreate new empty commit on top of current, “finishing” current changejj new
jj new <change-id>Create new commit based on specific revisionjj new main
jj describeSet or change commit message for current or specified commitjj describe -m "feat: add feature"
jj commitShortcut: describe + new (most like git commit)jj commit -m "message"
jj commit <files>Commit only specific files (like split but simpler)jj commit src/*.rs -m "message"
jj metaeditModify commit metadata (author, timestamps, change-id) without changing contentjj metaedit --author "Name <email>"
jj edit <change-id>Make specified commit the working copy, allows resuming work on old commitsjj edit abc123
jj prevMove to parent commit in a stack (creates new @ on parent by default)jj prev
jj nextMove to child commit in a stack (creates new @ on child by default)jj next
jj prev --editEdit parent commit directlyjj prev --edit
jj squashMove changes from working copy into parent commitjj squash
jj splitSplit a commit into two commits interactivelyjj split
jj abandonAbandon current change, removing it from historyjj abandon
jj rebase -d <dest>Rebase current change onto destination commit/bookmarkjj rebase -d main
jj fixApply configured formatters/linters to commitsjj fix -s @
jj file listList tracked files in a revisionjj file list
jj file showShow contents of file in a revisionjj file show path/to/file -r @
jj file trackExplicitly track a filejj file track .gitignore
jj file untrackStop tracking a filejj file untrack temp.txt
jj bookmark createCreate a new bookmark (fails if bookmark already exists)jj bookmark create feature-x -r @
jj bookmark setCreate or update a bookmark to point to a commit (works for new and existing)jj bookmark set feature-x -r @
jj bookmark listList all local and remote bookmarksjj bookmark list
jj bookmark moveMove existing bookmarks with advanced options (supports –allow-backwards, –from)jj bookmark move feature-x -r @
jj bookmark deleteDelete a local bookmarkjj bookmark delete old-feature
jj bookmark trackStart tracking <branch>@<remote> with a bookmarkjj bookmark track feature-x@origin
jj tag listList all tagsjj tag list
jj git fetchFetch changes from Git remotejj git fetch
jj git pushPush bookmarks to Git remote (checks remote state, safe by default)jj git push --bookmark feature-x
jj git push --allow-newPush new bookmark to remote for first time (required for new bookmarks)jj git push --bookmark feature-x --allow-new
jj git push --changePush single change with auto-generated bookmarkjj git push --change @ --allow-new
jj op logShow operation log (history of jj commands run)jj op log
jj op restoreRestore repository to a previous operation statejj op restore <operation-id>
jj undoUndo the last operationjj undo
jj redoRedo an operation (after using undo)jj redo
jj resolveResolve conflicts using a merge tooljj resolve
jj resolve --listList all conflicted filesjj resolve --list
jj absorbAutomatically distribute working copy changes to appropriate commits in stackjj absorb
jj diffeditInteractively edit changes in a commit using a diff editorjj diffedit -r <change-id>
jj parallelizeConvert linear commits to parallel siblingsjj parallelize <revset>
jj duplicateCreate a copy of a commit with the same contentjj duplicate <change-id>
jj bisectFind which commit introduced a bug using binary searchjj bisect run 'cargo test'
jj config set --userSet user-level configurationjj config set --user signing.backend "gpg"
jj signManually sign commitsjj sign

Jujutsu (jj) Command Cheatsheet

Setup & Configuration

brew install jujutsu                        # Install on macOS
jj config set --user user.name "Your Name"
jj config set --user user.email "[email protected]"
jj git init --colocate                      # Initialize in existing Git repo
jj git clone <url>                          # Clone a repository

Viewing State

jj status                    # Show working copy changes
jj diff                      # Show changes in current commit
jj log                       # Show commit graph with change IDs
jj log -r <revset>          # Show specific commits
jj evolog                    # Show how a change evolved over time

Creating & Navigating Changes

jj new                       # Finish current commit, start new one
jj new -m "message"         # Create new commit with message
jj commit -m "message"      # Describe current + new (like git commit)
jj describe -m "message"    # Set/update commit message

jj edit <change-id>         # Switch to editing a different commit
jj prev                      # Move to parent commit
jj next                      # Move to child commit
jj prev --edit              # Edit parent directly

History Editing

jj squash                    # Squash @ into parent
jj squash --into <dest>     # Squash @ into specific commit
jj squash -r <src> --into <dest>  # Squash one commit into another

jj split                     # Split current commit into two
jj rebase -d <dest>         # Rebase onto destination
jj abandon                   # Discard current change
jj fix                       # Apply formatters/linters

Working with Files

jj file list                 # List tracked files
jj file show <path>         # Show file contents
jj file show <path> -r <rev> # Show file at specific revision
jj file track <path>        # Explicitly track file
jj file untrack <path>      # Stop tracking file

Bookmarks (Branches)

jj bookmark list             # List all bookmarks
jj bookmark set <name> -r @  # Create or update bookmark (most common)
jj bookmark create <name>    # Create new bookmark (fails if exists)
jj bookmark move <name> -r <rev>  # Move bookmark
jj bookmark delete <name>    # Delete bookmark
jj bookmark track <name>@<remote>  # Track remote bookmark

Syncing with Remotes

jj git fetch                 # Fetch from remote
jj git push --bookmark <name>  # Push bookmark
jj git push --bookmark <name> --allow-new  # Push new bookmark (first time)
jj git push --change @       # Push with auto-generated bookmark

Conflict Resolution

jj status                    # Shows conflicted files
# Edit files manually to resolve, or:
jj resolve                   # Resolve all conflicts interactively
jj resolve <path>           # Resolve specific file
jj resolve --list           # List conflicted files
jj resolve --tool=:ours     # Use our side
jj resolve --tool=:theirs   # Use their side

Comparing Changes

jj diff                      # Show changes in @
jj diff -r <rev>            # Show changes in specific commit
jj interdiff --from <a> --to <b>  # Compare two versions
jj evolog -p                # Show evolution with diffs

Safety & Undo

jj undo                      # Undo last operation
jj redo                      # Redo after undo
jj op log                    # Show operation history
jj op restore <id>          # Restore to specific operation

Advanced Commands

jj absorb                    # Auto-distribute changes to appropriate commits
jj diffedit -r <rev>        # Interactively edit commit's changes
jj parallelize <revset>     # Convert linear stack to parallel
jj duplicate <change-id>    # Copy commit
jj bisect run '<command>'   # Find bug-introducing commit
jj metaedit                  # Modify commit metadata

Common Revsets

@                            # Current working copy
@-                           # Parent of working copy
main                         # Bookmark/branch named "main"
main@origin                  # Remote bookmark
main..@                      # Commits between main and @
trunk()                      # Main development branches
tags()                       # All tags
bookmarks()                  # All local bookmarks

Key Concepts

  • Working copy is a commit (@) - no staging area
  • Change ID - stable identifier across amendments
  • Commit ID - identifies specific snapshot (changes on amend)
  • Bookmarks - like Git branches but auto-move during rebasing
  • Immutable commits - trunk(), tags(), and pushed commits are protected
  • Co-located repos - both .jj and .git directories, stay synced

Quick Workflow Examples

Starting new work:

jj new main                  # Start from main
# Make changes...
jj commit -m "Add feature"  # Finish and describe

Fixing earlier commit:

jj edit <change-id>         # Switch to that commit
# Make fixes...
jj new                       # Descendants auto-rebase!

Stacked PRs:

jj new main
# Work on part 1...
jj new                       # Start part 2
# Work on part 2...
jj bookmark set part1 -r @-
jj bookmark set part2 -r @
jj git push --bookmark part1 --allow-new
jj git push --bookmark part2 --allow-new

Resources

Jujutsu VCS Tutorial - Speaker Notes

Timing Breakdown

  • Setup & signing: 3-4 min
  • Core concepts & basic operations: 5-6 min
  • History editing: 4-5 min
  • Working with bookmarks: 3-4 min
  • Syncing with remotes: 2 min
  • Stacked changes / PR workflow: 3-4 min
  • Understanding revsets: 2-3 min
  • Immutable commits & safe history: 3-4 min
  • Conflicts: 2-3 min
  • Advanced power features (bonus): 3-5 min
  • Wrap-up: 1-2 min

Total: 28-36 minutes (31-41 minutes with bonus section)

Preparation Recommendations

  1. Have a demo repo ready with some commits
  2. Pre-configure signing keys on the demo machine
  3. Consider having a printed “cheat sheet” handout with the command reference table
  4. Test the full workflow beforehand to keep timing tight
  5. Have jj log output visible throughout to show the change graph evolving
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment