Cross-Platform Guide, includes both Windows (PowerShell) and "Ubuntu" (ZSH) setup instructions.
| Concept | Git | jj |
|---|---|---|
| Working Copy | Staged changes before commit | Always a commit (@), saving a file immediately records it |
| History | Mutable (rebase rewrites) | Immutable by default, you create new revisions that replace old ones |
| Branches | git branch |
Bookmarks, jj bookmark |
| Repo Structure | .git/ folder |
Co-located: jj and git share the same .git folder |
| Conflicts | Must resolve before continuing | First-class objects, you can commit conflicts and resolve later |
| Undo | git reflog + manual recovery |
jj undo, universal Ctrl+Z for any operation |
winget install jj-vcs.jj# Using Homebrew
brew install jj
# Using Cargo (requires Rust >= 1.88)
cargo install --locked --bin jj jj-cli
# Arch Linux
pacman -S jujutsuSet your identity (required for commits):
jj config set --user user.name "Your Name"
jj config set --user user.email "[email protected]"- Windows:
%APPDATA%\jj\config.toml - Linux/macOS:
~/.config/jj/config.toml
View your config:
jj config list
jj config edit # Opens in your editorIf you don't have an SSH key yet, generate a secure Ed25519 key:
ssh-keygen -t ed25519 -C "[email protected]"This creates id_ed25519 (private) and id_ed25519.pub (public) in your ~/.ssh folder.
On windows, you can add the key to your SSH agent like this:
ssh-add PATH\\TO\\KEYCaution
The syntax of ~/ probably won't work on Windows. I recommend using the absolute path with \\
Add to your .gitconfig or repo's .git/config:
[user]
name = Your Name
email = [email protected]
signingkey = ~/.ssh/id_ed25519.pub
[commit]
gpgsign = true
[gpg]
format = sshNote
jj has its own signing config separate from Git. For jj to sign commits, add this to your jj config (jj config edit --user):
[user]
name = "Your Name"
email = "[email protected]"
[signing]
behavior = "own" # Sign only your own commits
backend = "ssh"
key = "~/.ssh/id_ed25519.pub"To verify signatures (including your own), you need an allowed_signers file. Without it, jj shows
File details:
- Location:
~/.ssh/allowed_signers(same folder as your keys)- Windows:
C:\Users\YourName\.ssh\allowed_signers - Linux/macOS:
~/.ssh/allowed_signers
- Windows:
- Extension: None (the file has no
.txtor other extension) - Format: One line per trusted key:
email key-type key-content
Create the file:
Windows (PowerShell)
# Replace YOUR_EMAIL with your jj user.email
"YOUR_EMAIL $(Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub)" | Out-File -FilePath "$env:USERPROFILE\.ssh\allowed_signers" -Encoding utf8Ubuntu/Linux (ZSH/Bash)
# Replace YOUR_EMAIL with your jj user.email
echo "YOUR_EMAIL $(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signersThen add to your jj config:
[signing.backends.ssh]
# Windows (use absolute path with double backslashes):
allowed-signers = "C:\\Users\\YourName\\.ssh\\allowed_signers"
# Linux/macOS:
# allowed-signers = "~/.ssh/allowed_signers"💡 Tip: If you use both jj and git commands, configure signing in both places.
The default jj log output is cluttered. This template organizes it into a clean 3-line format with emoji signature indicators.
Add to your jj config (jj config edit --user):
[template-aliases]
'format_sig(sig)' = 'if(sig, if(sig.status() == "good", "✅", "⚠️"), "❌")'
[templates]
log = '''
if(root,
format_root_commit(self),
label(
separate(" ",
if(current_working_copy, "working_copy"),
if(immutable, "immutable", "mutable"),
if(conflict, "conflicted"),
),
concat(
separate(" ",
format_short_change_id(change_id),
format_short_commit_id(commit_id),
format_sig(signature),
bookmarks,
tags,
working_copies,
) ++ "\n",
separate(" ",
if(empty, label("empty", "(empty)")),
if(conflict, label("conflict", "conflict")),
if(description, description.first_line(), description_placeholder),
) ++ "\n",
" " ++ format_timestamp(commit_timestamp(self))
++ " " ++ format_short_signature(author) ++ "\n",
),
)
)
'''Output:
@ abc123de 1a2b3c4d ✅ main feature-branch
Implement new feature
2026-01-15 14:00:00 [email protected]
◆ xyz789ab 5e6f7g8h ❌
Previous commit message
2026-01-14 10:30:00 [email protected]
| Line | Content |
|---|---|
| 1 | Change ID, Commit ID, Signature ✅/ |
| 2 | Commit description |
| 3 | Timestamp + Author email |
Note
Custom templates may need updating when jj releases new versions.
Alternative: Built-in Minimal (no template override)
If you prefer to stay with the default single-line format but just want signature indicators, run:
jj config set --user ui.show-cryptographic-signatures trueThis shows [✓]/[?]/[x] after each commit ID. Less organized but zero maintenance.
Add to your jj config (jj config edit --user):
[revset-aliases]
"wip()" = "description(exact:'') & mine()" # Empty description commits by you
"stale()" = "ancestors(@, 2) & empty()" # Find empty ancestors
[remotes.origin]
auto-track-bookmarks = "*" # Track all bookmarks from origin
# Or use a prefix pattern:
# auto-track-bookmarks = "yourprefix/*"Add to your PowerShell profile (notepad $PROFILE):
# ==============================================================================
# JJ VERSION CONTROL - POWERSHELL PROFILE
# ==============================================================================
# Dynamic Completions (recommended)
$env:COMPLETE = "powershell"
jj | Out-String | Invoke-Expression
Remove-Item Env:\COMPLETE
# Alternative: Standard completions
# jj util completion power-shell | Out-String | Invoke-Expression
# ==============================================================================
# CORE FUNCTIONS
# ==============================================================================
# Initialize jj in an existing Git directory
function ji {
param($Branch = "main")
jj git init --colocate
jj bookmark track $Branch@origin
}
# Pass-through: Run any jj command
function j { jj @args }
# Log commands (signature status shown via custom template: ✅/⚠️/❌)
function jl { jj log @args }
function jla { jj log -r "all()" }
function js { jj status @args }
function jsh { jj show @args } # Show specific revision
# ==============================================================================
# NAVIGATION
# ==============================================================================
function je { jj edit @args } # Edit: switch to commit
function jep { jj edit "@-" } # Edit Parent
function jen { jj edit @args; jj new } # Edit + New child
function jnext { jj next @args } # Move to child revision
function jprev { jj prev @args } # Move to parent revision
# ==============================================================================
# INSPECTION
# ==============================================================================
function jd { jj diff @args }
function jds { jj diff --stat @args }
function jevo { jj evolog @args } # Evolution log
function jidiff { jj interdiff @args } # Compare diffs of two revisions
# ==============================================================================
# ACTIONS
# ==============================================================================
function jn { jj new @args } # New commit
function jci { jj commit @args } # Commit changes (alias: ci)
function jdesc { jj describe @args } # Set commit message
function jme { jj metaedit @args } # Edit metadata only (no content change)
function ja { jj abandon @args } # Abandon commits
function jundo { jj undo @args } # Universal undo
function jredo { jj redo @args } # Redo most recently undone operation
function jrestore { param($From) jj restore --from $From }
function jrevert { jj revert @args } # Apply reverse of revision(s)
function jdup { jj duplicate @args } # Duplicate commit
function jdupe {
# Convert args to string for the success message (defaults to '@' if empty)
$target = if ($args) { "$args" } else { "@" }
# 1. Run duplicate silently by capturing all output
$output = jj duplicate @args 2>&1
# 2. Find the new ID (captures text after "as")
if ("$output" -match 'as\s+([a-z0-9]+)') {
$newId = $Matches[1]
# 3. Switch to the new revision (silencing the edit confirmation too)
jj edit $newId 2>&1 | Out-Null
# 4. Print your custom message
Write-Host "duplicated `"$target`" is now @" -ForegroundColor Green
}
else {
# If the regex fails, it usually means jj returned an error
# So we print the captured output to show you what happened
Write-Error "$output"
}
}
function jabs { jj absorb @args } # Absorb changes into mutable stack
function jfix { jj fix @args } # Apply formatting fixes
function jpar { jj parallelize @args } # Make revisions siblings
function jsimp { jj simplify-parents @args } # Simplify parent edges
function jdiffedit { jj diffedit @args } # Touch up content changes with diff editor
# ==============================================================================
# SPLITTING & SQUASHING
# ==============================================================================
function jsq { jj squash @args } # Squash into parent
function jsqi { param($Target) jj squash --into $Target }
function jsqp { jj squash -i @args } # Interactive squash
function jsplit { jj split @args } # Split commit interactively
# ==============================================================================
# REBASING
# ==============================================================================
function jrb { jj rebase @args } # Rebase
function jrbd { param($Dest) jj rebase -d $Dest } # Rebase to destination
function jrbs { param($Source, $Dest) jj rebase -s $Source -d $Dest }
# ==============================================================================
# BISECT (find bad revisions)
# ==============================================================================
function jbis { jj bisect @args } # Bisect to find bad revision
# ==============================================================================
# IGNORE IMMUTABLE (force modify immutable commits)
# ==============================================================================
function jjii { jj --ignore-immutable @args } # Run jj ignoring immutability
# ==============================================================================
# CONFLICT RESOLUTION
# ==============================================================================
function jres { jj resolve @args } # Launch merge tool
function jresl { jj resolve --list } # List conflicted files
# ==============================================================================
# BOOKMARK MANAGEMENT
# ==============================================================================
function jb { jj bookmark @args }
function jbc { jj bookmark create @args }
function jbd { jj bookmark delete @args }
function jbr { jj bookmark rename @args }
function jbt { jj bookmark track @args }
function jbu { jj bookmark untrack @args }
function jbl { jj bookmark list @args } # List bookmarks
function jbs { jj bookmark set @args } # Set bookmark
function jbm {
param($Name, $Target="@")
jj bookmark move $Name --to $Target --allow-backwards
}
function jbdr {
param($Name, $Remote = "origin")
Write-Host "Deleting bookmark '$Name' from remote '$Remote'..." -ForegroundColor Yellow
git push $Remote --delete $Name @args
}
# ==============================================================================
# TAG MANAGEMENT
# ==============================================================================
function jtag { jj tag @args } # Manage tags
# ==============================================================================
# REMOTE MANAGEMENT
# ==============================================================================
function jr { jj git remote add @args }
function jrr { jj git remote remove @args }
function jrl { jj git remote list }
# ==============================================================================
# REMOTE MANAGEMENT (EXTENDED)
# ==============================================================================
function jgrn { jj git remote rename @args } # Rename remote: jgrn <old> <new>
function jgru { jj git remote set-url @args } # Set URL: jgru <name> <new-url>
# ==============================================================================
# GIT SYNC
# ==============================================================================
function jgf { jj git fetch @args }
function jgp {
param($Bookmark)
if ($Bookmark) {
jj git push --bookmark $Bookmark @args
} else {
jj git push @args
}
}
# ==============================================================================
# GIT SYNC (EXTENDED)
# ==============================================================================
# Base passthrough for any jj git command
function jg { jj git @args }
# Repo Operations
function jclone { jj git clone @args } # Clone repo: jclone <url> [--colocate]
function jginit { jj git init @args } # Init git backing: jginit [--colocate]
function jroot { jj git root @args } # Show .git directory path
# Advanced Fetching
function jgfr { param($Remote) jj git fetch --remote $Remote @args } # Fetch specific remote
function jgfa { jj git fetch --all-remotes @args } # Fetch from all remotes
function jgfp { jj git fetch --prune @args } # Fetch + prune deleted refs
function jgfdr { jj git fetch --dry-run @args } # Preview what would be fetched
# Advanced Pushing
function jgpn { jj git push --allow-new @args } # Push new bookmark first time
function jgpa { jj git push --all @args } # Push ALL bookmarks
function jgpc { jj git push --change @args } # Push specific change ID
function jgpd {
param($Bookmark, $Remote = "origin")
jj git push --bookmark $Bookmark --remote $Remote --delete @args
} # Delete remote bookmark
# Import/Export (for non-colocated repos)
function jimp { jj git import @args } # Import git changes into jj
function jexp { jj git export @args } # Export jj changes to git
# ==============================================================================
# OPERATION LOG
# ==============================================================================
function jop { jj op log @args } # View operation history
function jopu { jj op undo @args } # Undo specific operation
function jopr { jj op restore @args } # Restore to operation state
# ==============================================================================
# CONFIGURATION
# ==============================================================================
function jconfig { jj config @args } # Manage config options
function jc { jj config @args } # Short alias for config
# ==============================================================================
# WORKSPACE MANAGEMENT
# ==============================================================================
function jws { jj workspace @args } # Commands for working with workspaces
function jwsr { jj workspace root @args } # Show workspace root (same as jj root)
# ==============================================================================
# CRYPTOGRAPHIC SIGNING
# ==============================================================================
function jsign { jj sign @args } # Cryptographically sign a revision
function junsign { jj unsign @args } # Drop a cryptographic signature
# ==============================================================================
# SPARSE CHECKOUT
# ==============================================================================
function jsparse { jj sparse @args } # Manage which paths are present in working copy
# ==============================================================================
# FILE OPERATIONS
# ==============================================================================
function jfshow { jj file show @args } # Show file from revision
function jflist { jj file list @args } # List files in revision
function jfchmod { jj file chmod @args } # Change file permissions
function jfforget { jj file forget @args } # Stop tracking a file
# ==============================================================================
# UTILITIES
# ==============================================================================
function jver { jj version @args } # Display version information
function jh { jj help @args } # Print help message
function jutil { jj util @args } # Infrequently used commands
function jrootws { jj root @args } # Show workspace root directory
# ==============================================================================
# MAINTENANCE
# ==============================================================================
function jgc { jj util gc @args }
function jclean {
$revs = 'all() ~ ::(bookmarks() | @) ~ root()'
Write-Host "🔍 Scanning for abandoned commits..." -ForegroundColor Cyan
$count = (jj log -r $revs --no-graph | Measure-Object).Count
if ($count -eq 0) {
Write-Host "✨ Repository is already clean." -ForegroundColor Green
return
}
jj log -r $revs --no-graph
Write-Host "`n⚠️ Found $count disconnected commits." -ForegroundColor Yellow
$confirm = Read-Host "Delete them? (y/n)"
if ($confirm -eq 'y') {
jj abandon -r $revs
Write-Host "✅ Cleaned up." -ForegroundColor Green
} else {
Write-Host "❌ Cancelled." -ForegroundColor Red
}
}Add to your ~/.zshrc:
Warning
Since I'm using Powershell, I have not tested if all of them work
# ==============================================================================
# JJ VERSION CONTROL - ZSH CONFIGURATION
# ==============================================================================
# Dynamic Completions (recommended)
source <(COMPLETE=zsh jj)
# Alternative: Standard completions
# autoload -U compinit
# compinit
# source <(jj util completion zsh)
# ==============================================================================
# CORE FUNCTIONS
# ==============================================================================
# Initialize jj in an existing Git directory
ji() {
local branch="${1:-main}"
jj git init --colocate
jj bookmark track "${branch}@origin"
}
# Pass-through: Run any jj command
j() { jj "$@" }
# Log commands (signature status shown via custom template: ✅/⚠️/❌)
jl() { jj log "$@" }
jla() { jj log -r "all()" }
js() { jj status "$@" }
jsh() { jj show "$@" }
# ==============================================================================
# NAVIGATION
# ==============================================================================
je() { jj edit "$@" }
jep() { jj edit "@-" }
jen() { jj edit "$@" && jj new }
jnext() { jj next "$@" }
jprev() { jj prev "$@" }
# ==============================================================================
# INSPECTION
# ==============================================================================
jd() { jj diff "$@" }
jds() { jj diff --stat "$@" }
jevo() { jj evolog "$@" }
jidiff() { jj interdiff "$@" }
# ==============================================================================
# ACTIONS
# ==============================================================================
jn() { jj new "$@" }
jci() { jj commit "$@" } # Commit changes (alias: ci)
jdesc() { jj describe "$@" }
jme() { jj metaedit "$@" }
ja() { jj abandon "$@" }
jundo() { jj undo "$@" }
jredo() { jj redo "$@" } # Redo most recently undone operation
jrestore(){ jj restore --from "$1" }
jrevert() { jj revert "$@" } # Apply reverse of revision(s)
jdup() { jj duplicate "$@" }
jdupe() {
# 1. Define target for the text message (defaults to "@" if args are empty)
local target="${*:-@}"
# 2. Run duplicate silently, merging stderr into stdout
# --color=never ensures regex matches clean text
local output
output=$(jj duplicate --color=never "$@" 2>&1)
# 3. Regex match looking for "as <id>"
if [[ "$output" =~ "as ([a-z0-9]+)" ]]; then
# Zsh stores capture groups in the $match array
local new_id=$match[1]
# 4. Switch to new ID silently
jj edit "$new_id" > /dev/null 2>&1
# 5. Print success message (using print -P for colors)
print -P "%F{green}duplicated \"$target\" is now @%f"
else
# If match failed, print the error output from jj
print -u2 "$output"
fi
}
jabs() { jj absorb "$@" }
jfix() { jj fix "$@" }
jpar() { jj parallelize "$@" }
jsimp() { jj simplify-parents "$@" } # Simplify parent edges
jdiffedit() { jj diffedit "$@" } # Touch up content changes with diff editor
# ==============================================================================
# SPLITTING & SQUASHING
# ==============================================================================
jsq() { jj squash "$@" }
jsqi() { jj squash --into "$1" }
jsqp() { jj squash -i "$@" }
jsplit() { jj split "$@" }
# ==============================================================================
# REBASING
# ==============================================================================
jrb() { jj rebase "$@" }
jrbd() { jj rebase -d "$1" }
jrbs() { jj rebase -s "$1" -d "$2" }
# ==============================================================================
# BISECT (find bad revisions)
# ==============================================================================
jbis() { jj bisect "$@" }
# ==============================================================================
# IGNORE IMMUTABLE (force modify immutable commits)
# ==============================================================================
jjii() { jj --ignore-immutable "$@" }
# ==============================================================================
# CONFLICT RESOLUTION
# ==============================================================================
jres() { jj resolve "$@" }
jresl() { jj resolve --list }
# ==============================================================================
# BOOKMARK MANAGEMENT
# ==============================================================================
jb() { jj bookmark "$@" }
jbc() { jj bookmark create "$@" }
jbd() { jj bookmark delete "$@" }
jbr() { jj bookmark rename "$@" }
jbt() { jj bookmark track "$@" }
jbu() { jj bookmark untrack "$@" }
jbl() { jj bookmark list "$@" } # List bookmarks
jbs() { jj bookmark set "$@" } # Set bookmark
jbm() {
local name="$1"
local target="${2:-@}"
jj bookmark move "$name" --to "$target" --allow-backwards
}
jbdr() {
local name="$1"
local remote="${2:-origin}"
echo -e "\e[33mDeleting bookmark '$name' from remote '$remote'...\e[0m"
git push "$remote" --delete "$name"
}
# ==============================================================================
# TAG MANAGEMENT
# ==============================================================================
jtag() { jj tag "$@" } # Manage tags
# ==============================================================================
# REMOTE MANAGEMENT
# ==============================================================================
jr() { jj git remote add "$@" }
jrr() { jj git remote remove "$@" }
jrl() { jj git remote list }
# ==============================================================================
# REMOTE MANAGEMENT (EXTENDED)
# ==============================================================================
jgrn() { jj git remote rename "$@" } # Rename: jgrn <old> <new>
jgru() { jj git remote set-url "$@" } # Set URL: jgru <name> <new-url>
# ==============================================================================
# GIT SYNC
# ==============================================================================
jgf() { jj git fetch "$@" }
jgp() {
if [[ -n "$1" ]]; then
jj git push --bookmark "$1" "${@:2}"
else
jj git push "$@"
fi
}
# ==============================================================================
# GIT SYNC (EXTENDED)
# ==============================================================================
# Base passthrough for any jj git command
jg() { jj git "$@" }
# Repo Operations
jclone() { jj git clone "$@" }
jginit() { jj git init "$@" }
jroot() { jj git root "$@" }
# Advanced Fetching
jgfr() { jj git fetch --remote "$1" "${@:2}" } # Specific remote
jgfa() { jj git fetch --all-remotes "$@" } # All remotes
jgfp() { jj git fetch --prune "$@" } # Prune deleted refs
jgfdr() { jj git fetch --dry-run "$@" } # Preview fetch
# Advanced Pushing
jgpn() { jj git push --allow-new "$@" } # Push new bookmark
jgpa() { jj git push --all "$@" } # Push all bookmarks
jgpc() { jj git push --change "$@" } # Push by change ID
jgpd() {
local bookmark="$1"
local remote="${2:-origin}"
jj git push --bookmark "$bookmark" --remote "$remote" --delete "${@:3}"
} # Delete remote bookmark
# Import/Export
jimp() { jj git import "$@" }
jexp() { jj git export "$@" }
# ==============================================================================
# OPERATION LOG
# ==============================================================================
jop() { jj op log "$@" }
jopu() { jj op undo "$@" }
jopr() { jj op restore "$@" }
# ==============================================================================
# CONFIGURATION
# ==============================================================================
jconfig() { jj config "$@" } # Manage config options
jc() { jj config "$@" } # Short alias for config
# ==============================================================================
# WORKSPACE MANAGEMENT
# ==============================================================================
jws() { jj workspace "$@" } # Commands for working with workspaces
jwsr() { jj workspace root "$@" } # Show workspace root (same as jj root)
# ==============================================================================
# CRYPTOGRAPHIC SIGNING
# ==============================================================================
jsign() { jj sign "$@" } # Cryptographically sign a revision
junsign() { jj unsign "$@" } # Drop a cryptographic signature
# ==============================================================================
# SPARSE CHECKOUT
# ==============================================================================
jsparse() { jj sparse "$@" } # Manage which paths are present in working copy
# ==============================================================================
# FILE OPERATIONS
# ==============================================================================
jfshow() { jj file show "$@" }
jflist() { jj file list "$@" }
jfchmod() { jj file chmod "$@" } # Change file permissions
jfforget() { jj file forget "$@" } # Stop tracking a file
# ==============================================================================
# UTILITIES
# ==============================================================================
jver() { jj version "$@" } # Display version information
jh() { jj help "$@" } # Print help message
jutil() { jj util "$@" } # Infrequently used commands
jrootws() { jj root "$@" } # Show workspace root directory
# ==============================================================================
# MAINTENANCE
# ==============================================================================
jgc() { jj util gc "$@" }
jclean() {
local revs='all() ~ ::(bookmarks() | @) ~ root()'
echo -e "\e[36m🔍 Scanning for abandoned commits...\e[0m"
local count=$(jj log -r "$revs" --no-graph | wc -l)
if [[ $count -eq 0 ]]; then
echo -e "\e[32m✨ Repository is already clean.\e[0m"
return
fi
jj log -r "$revs" --no-graph
echo -e "\n\e[33m⚠️ Found $count disconnected commits.\e[0m"
read -q "confirm?Delete them? (y/n) "
echo
if [[ $confirm == "y" ]]; then
jj abandon -r "$revs"
echo -e "\e[32m✅ Cleaned up.\e[0m"
else
echo -e "\e[31m❌ Cancelled.\e[0m"
fi
}| Goal | Alias | Command |
|---|---|---|
| Initialize Repo | ji [branch] |
jj git init --colocate + track branch |
| Pass-through | j <args> |
Run any raw jj command |
| Goal | Alias | Command |
|---|---|---|
| Switch to commit | je <id> |
Makes <id> the new @ |
| Go back one step | jep |
Moves @ to parent |
| Next child | jnext |
Move working copy to child revision |
| Previous parent | jprev |
Move working copy to parent revision |
| View Log | jl |
Shows commit graph (with ✅/ |
| View All | jla |
Shows ALL commits (with signing status) |
| Show Revision | jsh <id> |
Shows specific revision details |
| Diff vs Parent | jd |
Shows working copy changes |
| Diff Summary | jds |
File statistics |
| Evolution Log | jevo |
History of a revision's changes |
| Interdiff | jidiff |
Compare diffs of two revisions |
| Goal | Alias | Command |
|---|---|---|
| New Commit | jn |
Creates child. jn - creates sibling |
| Commit | jci |
Update description and create new change |
| Describe | jdesc -m "msg" |
Set commit message |
| Metaedit | jme |
Edit metadata only (no content) |
| Squash | jsq |
Merge @ into parent |
| Squash Into | jsqi <id> |
Merge @ into specific commit |
| Interactive Squash | jsqp |
Pick specific hunks |
| Split Commit | jsplit |
Split into multiple commits (TUI) |
| Duplicate | jdup <id> |
Clone commit with same parent |
| Duplicate + Edit | jdupe <id> |
Clone commit and edit the new copy |
| Absorb | jabs |
Absorb changes into mutable stack |
| Fix | jfix |
Apply formatting fixes |
| Parallelize | jpar |
Make revisions siblings |
| Undo | jundo |
Reverts last operation |
| Redo | jredo |
Redo most recently undone operation |
| Revert | jrevert |
Apply reverse of revision(s) |
| Simplify Parents | jsimp |
Simplify parent edges |
| Diff Edit | jdiffedit |
Touch up content changes with diff editor |
| Goal | Alias | Command |
|---|---|---|
| Rebase | jrb |
General rebase passthrough |
| Rebase to Dest | jrbd <dest> |
Rebase @ onto destination |
| Rebase Source | jrbs <src> <dest> |
Rebase source tree to destination |
| Goal | Alias | Command |
|---|---|---|
| Resolve | jres |
Launch merge tool |
| List Conflicts | jresl |
Show files with conflicts |
| Goal | Alias | Command |
|---|---|---|
| Find Bad Commit | jbis |
Binary search for bad rev |
| Goal | Alias | Command |
|---|---|---|
| Create | jbc <name> |
Create on current commit |
| Delete | jbd <name> |
Delete locally |
| Rename | jbr <old> <new> |
Rename bookmark |
| Track | jbt <name>@origin |
Track remote bookmark |
| Untrack | jbu <name>@origin |
Stop tracking |
| List | jbl |
List all bookmarks |
| Set | jbs |
Set bookmark |
| Move | jbm <name> [id] |
Move to @ or specific ID |
| Remote Delete | jbdr <name> |
Delete from remote |
| Goal | Alias | Command |
|---|---|---|
| Manage Tags | jtag |
Create, delete, list tags |
| Goal | Alias | Command |
|---|---|---|
| Add | jr <name> <url> |
Add remote |
| Remove | jrr <name> |
Remove remote |
| List | jrl |
List all remotes |
| Goal | Alias | Command |
|---|---|---|
| Rename | jgrn <old> <new> |
Rename remote |
| Set URL | jgru <name> <url> |
Update remote URL |
| Goal | Alias | Command |
|---|---|---|
| Fetch | jgf |
Fetch all. jgf origin for specific |
| Push | jgp [bookmark] |
Push all or specific bookmark |
| Goal | Alias | Command |
|---|---|---|
| Passthrough | jg <args> |
Run any jj git command |
| Clone | jclone <url> |
Clone repository (--colocate optional) |
| Init Git | jginit |
Initialize git backing store |
| Git Root | jroot |
Show path to .git directory |
| Fetch Remote | jgfr <remote> |
Fetch from specific remote |
| Fetch All | jgfa |
Fetch from all remotes |
| Fetch Prune | jgfp |
Fetch and prune deleted remote bookmarks |
| Fetch Dry Run | jgfdr |
Preview what would be fetched |
| Push New | jgpn |
Push new bookmark (first time) |
| Push All | jgpa |
Push ALL local bookmarks |
| Push Change | jgpc <id> |
Push specific change ID |
| Push Delete | jgpd <name> [remote] |
Delete bookmark from remote |
| Import | jimp |
Import git refs into jj |
| Export | jexp |
Export jj changes to git |
| Goal | Alias | Command |
|---|---|---|
| View Ops | jop |
Show operation history |
| Undo Op | jopu |
Undo specific operation |
| Restore Op | jopr <id> |
Restore to operation state |
| Goal | Alias | Command |
|---|---|---|
| Config | jconfig / jc |
Manage config options |
| Goal | Alias | Command |
|---|---|---|
| Workspace | jws |
Commands for working with workspaces |
| Workspace Root | jwsr / jrootws |
Show workspace root directory |
| Goal | Alias | Command |
|---|---|---|
| Sign | jsign |
Cryptographically sign a revision |
| Unsign | junsign |
Drop a cryptographic signature |
| Goal | Alias | Command |
|---|---|---|
| Sparse | jsparse |
Manage which paths are present in working copy |
| Goal | Alias | Command |
|---|---|---|
| Garbage Collect | jgc |
Clean unreferenced files |
| Deep Clean | jclean |
Interactive abandon disconnected commits |
| Goal | Alias | Command |
|---|---|---|
| Bypass Immutability | jjii <args> |
Run any jj command ignoring immutable |
| Goal | Alias | Command |
|---|---|---|
| Show File | jfshow <path> -r <rev> |
Show file from revision |
| List Files | jflist -r <rev> |
List files in revision |
| Chmod | jfchmod |
Change file permissions |
| Forget | jfforget |
Stop tracking a file |
| Goal | Alias | Command |
|---|---|---|
| Version | jver |
Display version information |
| Help | jh |
Print help message |
| Util | jutil |
Infrequently used commands |
When using jsplit (or jj split -i), these keyboard shortcuts apply:
| Key | Action |
|---|---|
? |
Show help |
q |
Quit/cancel |
c |
Confirm and proceed |
↑/k |
Previous item |
↓/j |
Next item |
←/h |
Outer item (fold) |
→/l |
Inner item (unfold) |
Space |
Toggle selection |
Enter |
Toggle + advance |
a |
Invert all selections |
f |
Expand/collapse section |
F |
Expand/collapse all |
e |
Edit commit message |
Problem: main is immutable (◆) because it was pushed. You need to modify it.
Solution: Create a mutable replacement, bring it up to date, then move the bookmark.
jdup mainOption A: Squash specific commit
jsqi xyzabcOption B: Squash a range
jj squash -r "main@origin..@" --into @jd --from main@origin --to @jbm mainjgp mainWhat are immutable commits?
By default, jj prevents rewriting commits that are ancestors of:
trunk()(usuallymain@origin)tags()untracked_remote_bookmarks()
These show as ◆ (diamond) in jj log instead of ○ (circle). The root commit is always immutable.
When to use --ignore-immutable:
Use this flag when you genuinely need to modify a commit that jj considers immutable, for example, fixing a typo in a commit message on main before others have pulled it.
Warning
This allows rewriting any commit and all its descendants without warning. Use wisely, remember jj undo exists.
Example: Fix a typo in an immutable commit's message
# This would normally fail because main is immutable:
jj describe main -m "Fixed commit message"
# Error: Commit abc123 is immutable
# With the flag (or alias), it works:
jjii describe main -m "Fixed commit message"Example: Squash a fix into an already-pushed commit
# You found a bug in an immutable commit and want to amend it:
jjii squash --into mainOr use the flag directly:
jj --ignore-immutable describe main -m "Fixed message"The jjii alias is simply jj --ignore-immutable, so you can use any jj command after it.
jj allows you to commit conflicts and resolve them later, no "merge in progress" blocking state.
-
See what's conflicted:
jresl # Lists conflicted files -
Launch merge tool:
jres # Opens configured merge tool # Or for specific file: jres path/to/file.txt
-
Manual resolution (if preferred):
- Edit the file to remove conflict markers (
<<<<<<<,=======,>>>>>>>) - jj automatically detects the resolution on next operation
- Edit the file to remove conflict markers (
-
Continue working:
jdesc -m "Resolved conflicts"
Add to jj config:
[ui]
merge-editor = "code -w" # VS Code
# merge-editor = "nvim" # Neovim
# merge-editor = ":builtin" # Built-in TUIjn -m "Feature: Login"
jbc feature-login# Do work...
jdesc -m "Implement login form"
jgp feature-login# After PR merged...
jbrr feature-login # Remove from remote
jbd feature-login # Remove local bookmark
jclean # Clean up orphaned commits# Show all your work-in-progress
jl -r "mine() & mutable()"
# Find empty commits
jl -r "empty()"
# Commits between main and current
jl -r "main..@"
# All descendants of main
jl -r "main::"
# Commits with a specific description
jl -r 'description("fix")'
# All commits by author
jl -r 'author("name")'
# Commits touching a file
jl -r 'file("src/main.rs")'
# List all remote bookmarks (after fetch)
jj log -r "remote-bookmarks()"-
Always start fresh after switching context:
jn # Creates a clean child commit -
Use descriptive bookmarks:
jbc yourname/feature-description
-
Regularly clean up:
jclean # Remove disconnected commits jgc # Garbage collect
-
Signature status is shown inline:
jl # Look for ✅ (good), ⚠️ (warning), ❌ (unsigned) -
Use the operation log to recover:
jop # Find the operation ID jopr <id> # Restore to that state
-
Preview before destructive operations:
jd --from <old> --to <new>
-
Beware of creating siblings at root:
If you run
jn -(new sibling) when you are at a child of root, you create a new root commit. If there is no.gitignorein this new empty commit, everything (includingnode_modules) becomes tracked.Fix: Delete the massive tracked folder to untrack it, or just
jundo. -
Restoring ignored files (.env):
If your
.gitignoreis only local, and you restore a version where it's missing (or switch to a root sibling),jjmight delete previously ignored files like.envbecause they are technically "untracked and not ignored" in the new state, or simply due to working copy updates.Fix:
jundorestores them. -
Use a Global Gitignore:
Link a global ignore file in your gitconfig to protect files like
**/.enveven without a local.gitignore. This prevents them from being accidentally deleted or tracked.In
.gitconfig:[core] excludesfile = ~/.gitignore_global
(Windows path example:
C:/Users/YourName/.gitignore_global)In
~/.gitignore_global:**/.env .DS_Store node_modules/
-
Fetching vs Pulling:
Remember jj has no
pullcommand. Use the fetch + rebase pattern:jgf # Fetch updates from origin jrbd main@origin # Rebase local main onto remote
Or configure auto-tracking in your config to simplify this workflow.
-
Use Redo to Restore Undone Operations:
If you accidentally undo an operation or want to restore a previously undone state:
jundo # Undo last operation jredo # Redo the most recently undone operation
-
Commit vs New:
jci(commit) - Updates the description and creates a new change on top (likegit commit)jn(new) - Creates a new, empty change and edits it in the working copy (like staging area)
Use
jciwhen you have changes ready to commit, andjnwhen you want to start working on a new feature. -
Revert vs Undo:
jundo- Reverts the last jj operation (like Ctrl+Z)jrevert- Applies the reverse of the given revision(s) (likegit revert)
Use
jundoto undo a mistake in your workflow, andjrevertto create a new commit that reverses the changes of a previous commit. -
Sign and Unsign Commits:
jsign <rev> # Cryptographically sign a revision junsign <rev> # Drop a cryptographic signature
-
Use Sparse Checkout for Large Repositories:
For very large repositories, use sparse checkout to only checkout the files you need:
jsparse add <path> # Add path to working copy jsparse remove <path> # Remove path from working copy jsparse list # List paths in working copy
-
Check Version and Help:
jver # Display version information jh # Print help message jh <command> # Get help for a specific command
-
Manage Multiple Workspaces:
jws add <path> # Add a new workspace jws list # List all workspaces jws root # Show workspace root directory
-
Use Diff Edit for Precise Changes:
jdiffeditallows you to touch up the content changes in a revision with a diff editor, which is useful for making small adjustments to a commit without rewriting the entire thing. -
Simplify Parents for Cleaner History:
jsimp <rev> # Simplify parent edges for the specified revision(s)
This can help clean up complex merge histories and make the commit graph easier to understand.
-
Use File Operations for Precise Control:
jfchmod <path> <mode> # Change file permissions jfforget <path> # Stop tracking a file
These commands give you fine-grained control over file-level operations in your repository.