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 copyroot()- 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 setexplicitly - 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
--forceflag needed) - Missing author/committer blocks git push - use
jj metaedit --authorto fix jj describe --reset-authoris deprecated - usejj metaedit --update-author(v0.34.0+)signing.sign-allconfig removed - usesigning.behaviorinstead (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 undoliberally - 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 pushwill 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
- Official Jujutsu Documentation (jj-vcs.github.io)
- Jujutsu Tutorial (jj-vcs.github.io)
- Configuration Reference (jj-vcs.github.io)
- CLI Reference (jj-vcs.github.io)
- Jujutsu GitHub Repository (github.com)
- Git Comparison (jj-vcs.github.io)
- Revset Reference (jj-vcs.github.io)
- Changelog (jj-vcs.github.io)
- Steve Klabnik's jj Tutorial (steveklabnik.github.io)