NexusCS

Jujutsu (jj)

Version Control
Jujutsu (jj) - Git-compatible version control with automatic snapshots, no staging area, and first-class conflicts. Modern DVCS for Git users seeking better UX.
featured

Getting started

Introduction

Jujutsu (jj) is a powerful DVCS with a git-compatible backend. No staging area, automatic snapshots, and conflicts never block operations.

jj version
# jj 0.38.0

Installation

# Rust/Cargo
cargo install jj-cli --locked

# macOS
brew install jj

# Arch Linux
pacman -S jujutsu

Initial setup

# Initialize with git backend
jj git init

# Clone existing repo
jj git clone <url>

# Configure user
jj config set --user user.name "Name"
jj config set --user user.email "email@example.com"

# Disable pager (optional)
jj config set --user ui.paginate never

Core concepts

Key differences from git

Concept Git jj
Staging Has index None - auto snapshots
Working copy Not a commit Is a commit (@)
Branches Move automatically Must manually set
Conflicts Block operations Never block
Rebasing Can fail Always succeeds
Force push Explicit --force Automatic

Important symbols

  • @ - Current working copy commit (NOT same as git HEAD)
  • @- - Parent of working copy
  • @+ - Child of working copy
  • root() - Root commit (always zzzzzzzz 00000000)
  • Change IDs use letters k-z (disjoint from commit IDs)
  • Changing description updates commit ID but not change ID

Viewing status and history

Status

jj st                    # View current status
jj st --no-pager         # Without pager

Log

jj log                   # View commit history
jj log --limit N         # Show N most recent
jj log -r @              # Current working copy
jj log -r @-             # Parent of working copy
jj log -r <revset>       # Specific revisions

Diffs

jj diff                  # Working copy changes
jj diff -r <revision>    # Specific revision changes
jj show <revision>       # Show commit details

Creating changes

New changes

jj new                       # Create child of current
jj new -m "description"      # With description
jj new <revision>            # From specific parent
jj new rev1 rev2             # Merge commit
jj new -B @                  # Create BEFORE current
jj new -B @ -m "msg"         # Before with message
jj new --no-edit             # Create without moving @

Describing changes

jj describe                  # Opens editor
jj describe -m "message"     # Inline message

Setting author/committer

# Update author to configured user (current change)
jj metaedit --update-author

# Update author for specific revision
jj metaedit -r <revision> --update-author

# Set author explicitly
jj metaedit --author "Name <email@example.com>"

# Set author for specific revision
jj metaedit -r <revision> --author "Name <email@example.com>"

# Update author and timestamp together
jj metaedit --update-author --update-author-timestamp

# Example: Fix revision from error message
jj metaedit -r xvqzyrly --author "Your Name <you@example.com>"

# Force rewrite (updates committer info too)
jj metaedit --force-rewrite

⚠️ Note: Use jj metaedit (not jj describe) for author changes. jj describe --reset-author is deprecated since v0.34.0.

Abandoning changes

jj abandon                   # Abandon current
jj abandon <revision>        # Abandon specific

Squash workflow (recommended)

Squash pattern

This is the preferred workflow by jj's creator:

# 1. Describe the work
jj describe -m "feature description"

# 2. Create new empty change on top
jj new

# 3. Make changes, then squash into parent
jj squash                    # Squash all changes
jj squash <file>             # Squash specific file
jj squash -i                 # Interactive (TUI)

# 4. Abandon scratch space if needed
jj abandon

Why squash workflow?

  • Keeps working copy clean
  • Natural separation of concerns
  • Easy to organize changes after the fact
  • Interactive mode for fine-grained control

Edit workflow

Edit pattern

Alternative workflow for direct editing:

# 1. Create new change
jj new -m "feature"

# 2. Make changes directly

# 3. Insert change before current if needed
jj new -B @ -m "earlier change"

# 4. Navigate between changes
jj edit <revision>   # Edit specific revision
jj next --edit       # Move to child and edit
jj prev --edit       # Move to parent and edit

When to use edit workflow

  • Prefer squash for most cases
  • Use edit when working on existing commits
  • Good for fixing up specific revisions

Bookmarks (branches)

Creating bookmarks

jj bookmark create <name>              # Create bookmark
jj bookmark set <name>                 # Set to current
jj bookmark set <name> -r <revision>   # Set to specific
jj bookmark set <name> --allow-backwards  # Move backwards

Listing bookmarks

jj bookmark list                       # List all bookmarks

Important note

⚠️ Bookmarks do NOT automatically move (unlike git branches). You must explicitly set them with jj bookmark set.

Rebasing

Rebase commands

jj rebase -r <revision> -d <dest>   # Rebase single revision
jj rebase -b <branch> -d <dest>     # Rebase entire branch
jj rebase -s <revision> -d <dest>   # Rebase with descendants
jj rebase -d <dest>                 # Rebase current (same as -b @)

Sync with remote (git pull --rebase)

jj git fetch
jj rebase -d trunk
# Or for specific remote branch:
jj git fetch
jj rebase -d <remote-branch>

Always succeeds

⚠️ Rebases ALWAYS succeed in jj - conflicts are recorded in commits and don't block operations.

Revsets - Symbols

Basic symbols

@              # Current working copy
@-             # Parent of working copy
@+             # Child of working copy
root()         # Root commit

Operators

x & y          # Intersection (in both)
x | y          # Union (in either)
x-             # Parent of x
x+             # Child of x
::x            # Ancestors of x
x::            # Descendants of x
x..y           # Range from x to y
~x             # Complement (not x)

Functions

all()                    # All visible changes
mine()                    # Changes by current user
trunk()                   # Main/master/trunk branch
git_head()               # Git HEAD
parents(x)                # Parent changes
ancestors(x)              # All ancestors
ancestors(x, depth)       # Ancestors with depth limit
descendants(x)            # All descendants
heads(x)                  # Heads in set
roots(x)                  # Roots in set
author(pattern)           # Changes by author
description(pattern)      # Changes with pattern in description
remote_bookmarks()        # Remote bookmarks

Revset examples

Common patterns

# Show current and recent ancestors
jj log -r '@ | ancestors(remote_bookmarks().., 2) | trunk()'

# Find commits by author and description
jj log -r 'author("Name") & description(print)'

# Rebase all roots of branch
jj rebase -s 'all:roots(trunk..@)' -d trunk

# Show all anonymous branch heads
jj log -r 'heads(all())'

Template customization

jj log -T 'separate(" ", change_id.shortest(8), description.first_line())'

Template keywords: change_id, commit_id, description, author, committer

Methods: .shortest(N), .first_line(), separate(sep, ...)

Conflict resolution

Resolving conflicts

Conflicts NEVER block operations. Rebases always succeed.

jj edit <conflicted-revision>   # Edit directly
jj resolve                      # Interactive resolution
jj resolve <file>               # Resolve specific file

Conflict markers

<<<<<<<
+++++++
    new content snapshot
%%%%%%%
-    old content diff
+    new content diff
>>>>>>>

Different from git's 3-way markers!

Remote operations

Managing remotes

jj git remote add <name> <url>        # Add remote
jj git fetch                          # Fetch from remote
jj git fetch --remote <name>          # Fetch specific remote

Pushing

jj git push                           # Push tracked bookmarks
jj git push -c @                      # Create bookmark and push
jj git push -b <bookmark>             # Push specific bookmark

Force push is automatic

⚠️ Force push happens automatically in jj - no need for --force flag.

Pull request workflow

Creating PRs

# Create PR (creates push-<change_id> branch)
jj git push -c @

Updating PRs

# Update PR (add commits)
jj new -m "address feedback"
jj bookmark set <pr-branch>
jj git push

# Update PR (rewrite)
jj edit <commit>
# make changes
jj git push              # Force push automatic

PR branch naming

jj automatically creates push-<change_id> branches when using jj git push -c @.

Undo and history

Operation log

jj op log                # View operation history
jj op show               # Show operation details
jj op restore <id>       # Restore to specific operation

Undoing changes

jj undo                  # Undo last operation

How it works

Every jj command creates an operation entry. You can view and restore to any previous operation state.

Advanced commands

Absorb

jj absorb                # Auto-distribute changes
jj absorb <path>         # Absorb specific files

Automatically distributes working copy changes to appropriate parent commits.

Gerrit integration

jj gerrit upload -r <revset> --remote-branch <branch>
jj gerrit upload --dry-run
jj config set --user gerrit.default-remote-branch <branch>

Other commands

jj split                 # Split a commit
jj duplicate             # Duplicate a commit
jj parallelize           # Parallelize history

Scripting and Bulk Operations

Getting revision lists

# Get change IDs (one per line)
jj log --no-graph -T 'change_id.short() ++ "\n"' -r 'all()'

# Get commit IDs
jj log --no-graph -T 'commit_id.short() ++ "\n"' -r 'all()'

# Only mutable commits
jj log --no-graph -T 'change_id.short() ++ "\n"' -r 'mutable()'

# Only your commits
jj log --no-graph -T 'change_id.short() ++ "\n"' -r 'mine()'

Bulk author updates

# Update author for all mutable commits
jj log --no-graph -T 'change_id.short() ++ "\n"' -r 'mutable()' | \
  while read change_id; do
    jj metaedit -r "$change_id" --update-author
  done

# Update author for your commits only
jj log --no-graph -T 'change_id.short() ++ "\n"' -r 'mine()' | \
  while read change_id; do
    jj metaedit -r "$change_id" --update-author
  done

Template examples

# Change ID + description
jj log -T 'change_id.short() ++ " " ++ description.first_line()'

# Multiple fields
jj log -T 'change_id.short() ++ " " ++ author.email() ++ "\n"'

# Custom format
jj log -T 'separate(" ", change_id.shortest(), description.first_line())'

⚠️ Tip: Use change_id (not commit_id) for scripting - change IDs remain stable across rewrites.

Git command equivalents

Basic operations

Git Command jj Equivalent
git init jj git init
git clone jj git clone
git status jj st
git log jj log
git diff jj diff
git show jj show

Commits and staging

Git Command jj Equivalent
git add (none - no staging)
git add -p jj squash -i
git commit jj describe (auto-commit)
git commit --amend jj squash or jj edit
git reset jj undo or jj edit

Branches and merging

Git Command jj Equivalent
git branch jj bookmark create/list
git checkout jj edit or jj new
git merge jj new parent1 parent2
git rebase jj rebase

Remote operations

Git Command jj Equivalent
git fetch jj git fetch
git pull --rebase jj git fetch + jj rebase
git push jj git push
git push --force jj git push (auto)
git reflog jj op log

Gotchas and tips

Common gotchas

  • @ is the working copy commit, NOT the same as git's HEAD
  • Bookmarks don't auto-move - must jj bookmark set explicitly
  • No staging/commit cycle - changes are auto-snapshotted on every command
  • Conflicts don't block operations - continue working, resolve later
  • Change IDs persist through rewrites (commit IDs change)
  • Force push happens automatically (no --force flag needed)
  • Missing author/committer blocks git push - use jj metaedit --author to fix
  • jj describe --reset-author is deprecated - use jj metaedit --update-author (v0.34.0+)
  • signing.sign-all config removed - use signing.behavior instead (v0.35.0+)

Best practices

  • Use squash workflow for most tasks
  • Describe commits early and often
  • Don't worry about conflicts - they never block
  • Use revsets for powerful history queries
  • Leverage operation log for fearless experimentation
  • Use jj undo liberally - nothing is lost

Configuration commands

jj config subcommands

jj config list                # List all active settings
jj config list ui             # List settings under ui.*
jj config list --include-defaults  # Show defaults too
jj config list --include-overridden  # Show overridden values
jj config get <NAME>          # Get specific value
jj config set <NAME> <VALUE>  # Set a config value
jj config unset <NAME>        # Remove a config value
jj config path                # Show config file path
jj config edit                # Open config in editor

Config scopes

# User-level (all repos for this user)
jj config set --user user.name "Your Name"
jj config path --user
jj config edit --user

# Repo-level (this repo only)
jj config set --repo ui.default-command "log"
jj config path --repo
jj config edit --repo

# Workspace-level (this workspace only, v0.35.0+)
jj config set --workspace ui.editor "code --wait"
jj config path --workspace

# Inline override (single command)
jj --config ui.color=never log

Scope priority (lowest → highest)

Priority Scope Location
1 (lowest) Built-in Compiled into jj binary
2 User ~/.config/jj/config.toml
3 Repo Outside .jj/ (use jj config path --repo)
4 Workspace Outside .jj/ (use jj config path --workspace)
5 (highest) CLI --config key=value

⚠️ v0.38.0 security change: Repo and workspace configs moved outside .jj/ directory. Legacy .jj/repo/config.toml and .jj/workspace-config.toml are auto-migrated.

Config file locations

Platform paths

Platform User config path
macOS/Linux ~/.config/jj/config.toml
macOS (alt) $XDG_CONFIG_HOME/jj/config.toml
Windows %APPDATA%\jj\config.toml

Use jj config path --user to find the exact path on your system.

⚠️ v0.36.0: macOS legacy path ~/Library/Application Support/jj is no longer read. Use ~/.config/jj/ instead.

Finding config paths

# Show all config file locations
jj config path --user       # User-level config path
jj config path --repo        # Repo-level config path
jj config path --workspace   # Workspace config path

# Discover what's active
jj config list --include-overridden

Minimal config to start

Absolute minimum (required)

# These two are required to describe/push changes
jj config set --user user.name "Your Name"
jj config set --user user.email "you@example.com"

Without user.name and user.email:

  • You can make changes and describe them
  • Commits get placeholder author values
  • jj git push will fail prior to v0.38.0
  • v0.38.0+ removed placeholder support entirely

Recommended starter config

# Identity (required)
jj config set --user user.name "Your Name"
jj config set --user user.email "you@example.com"

# Quality of life
jj config set --user ui.paginate never
jj config set --user ui.editor "vim"
jj config set --user ui.diff-editor ":builtin"
jj config set --user ui.default-command "log"

All config parameters

user.* — Identity

[user]
name = "Your Name"        # Required for push
email = "you@example.com" # Required for push

ui.* — Interface

[ui]
color = "auto"          # auto|always|never|debug
paginate = "auto"       # auto|never
pager = "less -FRX"     # Pager command
editor = "vim"          # Text editor
diff-editor = ":builtin"  # Diff editor (:builtin|meld|etc)
merge-editor = "meld"   # 3-way merge tool
default-command = "log" # Command when running bare `jj`
log-word-wrap = false   # Wrap log content
diff-instructions = true   # Show JJ-INSTRUCTIONS in diffs
conflict-marker-style = "diff"  # diff|snapshot|git

ui.* — Advanced

[ui]
show-cryptographic-signatures = false
bookmark-list-sort-keys = ["name"]
tag-list-sort-keys = ["name"]

[ui.movement]
edit = false   # Default --edit for prev/next

[ui.streampager]
# Builtin pager configuration

signing.* — Commit signing

[signing]
behavior = "keep"   # keep|drop|own|force
backend = "gpg"     # gpg|ssh
key = "your-key-id" # Key identifier or path

Signing behavior values:

Value Meaning
"drop" Never sign commits
"keep" Preserve existing signatures (default)
"own" Sign your own commits
"force" Sign all commits (even others')

git.* — Git integration

[git]
fetch = "origin"          # Default remote for fetch
push = "origin"           # Default remote for push
abandon-unreachable-commits = true
private-commits = ""      # Revset for unfetchable commits
auto-colocate = false     # Colocate jj+git by default

revsets.* and aliases

[revsets]
log = "present(@) | ancestors(immutable_heads().., 2) | trunk()"
short-prefixes = ""       # Prioritize short prefixes

[revset-aliases]
"immutable_heads()" = "builtin_immutable_heads()"
"wip()" = "description(exact:'') & mine()"

[aliases]
l = ["log", "-r", "(main..@):: | (main..@)-"]
st = ["status"]
d = ["diff"]

templates.* — Display

[templates]
log = "builtin_log_compact"
show = "builtin_log_detailed"
op_log = "builtin_op_log_compact"
evolog = "builtin_evolog_compact"
draft_commit_description = ""  # Editor template

[template-aliases]
"format_short_id(id)" = "id.shortest(12)"
"format_timestamp(ts)" = "ts.ago()"
"format_short_signature(sig)" = "sig.email()"

Other settings

[snapshot]
auto-track = "all()"   # File tracking pattern
max-new-file-size = "1MiB"

[merge]
diff-style = "word"     # word|line
resolution-mode = "auto"

[working-copy]
exec-bit-change = "respect"  # respect|ignore

[fsmonitor]
backend = "watchman"    # Optional performance boost

[fix.tools.prettier]
command = ["prettier", "--write"]
patterns = ["glob:**/*.{js,ts}"]

[remotes.origin]
auto-track-bookmarks = "glob:*"

merge-tools.* — External tools

[merge-tools.meld]
program = "meld"
merge-args = ["$left", "$base", "$right", "-o", "$output"]
edit-args = ["$left", "$right"]

[merge-tools.kdiff3]
program = "kdiff3"
merge-args = ["$base", "$left", "$right",
  "-o", "$output", "--auto"]

[merge-tools.vscode]
program = "code"
merge-args = ["--wait", "--merge",
  "$left", "$right", "$base", "$output"]
edit-args = ["--wait", "--diff", "$left", "$right"]

Sample configs (TOML)

Complete user config

# ~/.config/jj/config.toml

[user]
name = "Your Name"
email = "you@example.com"

[ui]
color = "auto"
paginate = "auto"
pager = "less -FRX"
editor = "vim"
diff-editor = ":builtin"
merge-editor = "meld"
default-command = "log"
log-word-wrap = true

[signing]
behavior = "own"
backend = "ssh"
key = "~/.ssh/id_ed25519.pub"

[git]
fetch = "origin"
push = "origin"
private-commits = "description(exact:'wip')"

[revsets]
log = "present(@) | ancestors(immutable_heads().., 2) | trunk()"

[revset-aliases]
"immutable_heads()" = "builtin_immutable_heads()"
"wip()" = "description(exact:'') & mine()"

[aliases]
l = ["log", "-r", "(main..@):: | (main..@)-"]
st = ["status"]
d = ["diff"]
sync = ["git", "fetch"]

[template-aliases]
"format_short_id(id)" = "id.shortest(8)"
"format_timestamp(ts)" = "ts.ago()"

[snapshot]
max-new-file-size = "1MiB"

[remotes.origin]
auto-track-bookmarks = "glob:*"

Repo-level config example

# Set via: jj config edit --repo
# Path: jj config path --repo

[revset-aliases]
# Protect release branches
"immutable_heads()" = """
  builtin_immutable_heads() |
  release@origin |
  tags()
"""

[ui]
default-command = ["log", "-r", "trunk().."]

[fix.tools.rustfmt]
command = ["rustfmt"]
patterns = ["glob:src/**/*.rs"]

[fix.tools.prettier]
command = ["prettier", "--write"]
patterns = ["glob:**/*.{js,ts,tsx,json}"]

[templates]
draft_commit_description = """
JJ: Enter commit description.
JJ: Lines starting with "JJ:" are removed.
"""

Deprecated settings

Deprecated parameters

Deprecated Replacement Version
signing.sign-all signing.behavior v0.35.0
git.auto-local-branch git.auto-local-bookmark v0.18.0
git.auto-local-bookmark remotes.<name>.auto-track-bookmarks v0.36.0
git.push-bookmark-prefix Removed v0.37.0
git.push-new-bookmarks remotes.<name>.auto-track-created-bookmarks v0.36.0
ui.default-description Removed v0.37.0
ui.diff.format Removed v0.37.0
ui.diff.tool Removed v0.37.0
diff.format Removed v0.35.0
core.watchman.register_snapshot_trigger Removed v0.35.0

Deprecated commands

Deprecated Replacement Version
jj describe --reset-author jj metaedit --update-author v0.34.0
jj bookmark track <b>@<r> jj bookmark track <b> --remote=<r> v0.37.0
.jj/repo/config.toml Use jj config path --repo v0.38.0
.jj/workspace-config.toml Use jj config path --workspace v0.38.0

Fixing deprecated warnings

# "signing.sign-all is updated to signing.behavior"
jj config unset --user signing.sign-all
jj config set --user signing.behavior "own"

# "git.auto-local-bookmark is updated to remotes.*.auto-track-bookmarks"
jj config unset --user git.auto-local-bookmark
jj config set --user remotes.origin.auto-track-bookmarks "glob:*"

# "git.push-bookmark-prefix" removed
jj config unset --user git.push-bookmark-prefix

Also see