Skip to content

Instantly share code, notes, and snippets.

@Neonsy
Last active January 21, 2026 13:17
Show Gist options
  • Select an option

  • Save Neonsy/79a6e7508735ac5e264a6834f45f5509 to your computer and use it in GitHub Desktop.

Select an option

Save Neonsy/79a6e7508735ac5e264a6834f45f5509 to your computer and use it in GitHub Desktop.
JJ VSC Setup Guide (Powershell & ZSH)

Jujutsu (jj) Developer's Manual

Cross-Platform Guide, includes both Windows (PowerShell) and "Ubuntu" (ZSH) setup instructions.


1. The Mental Model: jj vs git

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

2. Installation

Windows (winget)

winget install jj-vcs.jj

Ubuntu/Linux

# Using Homebrew
brew install jj

# Using Cargo (requires Rust >= 1.88)
cargo install --locked --bin jj jj-cli

# Arch Linux
pacman -S jujutsu

3. Initial Configuration

Set your identity (required for commits):

jj config set --user user.name "Your Name"
jj config set --user user.email "your-email@example.com"

JJ Config File Location

  • Windows: %APPDATA%\jj\config.toml
  • Linux/macOS: ~/.config/jj/config.toml

View your config:

jj config list
jj config edit  # Opens in your editor

4. SSH Commit Signing

Generating a Secure SSH Key

If you don't have an SSH key yet, generate a secure Ed25519 key:

ssh-keygen -t ed25519 -C "your-email@example.com"

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\\KEY

Git Config (for co-located repos)

Caution

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 = your-email@example.com
    signingkey = ~/.ssh/id_ed25519.pub

[commit]
    gpgsign = true

[gpg]
    format = ssh

JJ Config (jj-native signing)

Note

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 = "your-email@example.com"

[signing]
behavior = "own"      # Sign only your own commits
backend = "ssh"
key = "~/.ssh/id_ed25519.pub"

Verifying Signatures (allowed_signers)

To verify signatures (including your own), you need an allowed_signers file. Without it, jj shows ⚠️ instead of βœ… because it has no trusted key list.

File details:

  • Location: ~/.ssh/allowed_signers (same folder as your keys)
    • Windows: C:\Users\YourName\.ssh\allowed_signers
    • Linux/macOS: ~/.ssh/allowed_signers
  • Extension: None (the file has no .txt or 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 utf8
Ubuntu/Linux (ZSH/Bash)
# Replace YOUR_EMAIL with your jj user.email
echo "YOUR_EMAIL $(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signers

Then 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.


5. Clean Log Template with Signature Status

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 your-email@example.com
β—†  xyz789ab 5e6f7g8h ❌
   Previous commit message
   2026-01-14 10:30:00 other@example.com
Line Content
1 Change ID, Commit ID, Signature βœ…/⚠️/❌, Bookmarks
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 true

This shows [βœ“]/[?]/[x] after each commit ID. Less organized but zero maintenance.


6. Revset Aliases & Auto-Tracking

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/*"

7. Shell Completion & Aliases

Windows (PowerShell)

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 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 jrestore { param($From) jj restore --from $From }
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

# ==============================================================================
# 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 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
}

# ==============================================================================
# REMOTE MANAGEMENT
# ==============================================================================

function jr     { jj git remote add @args }
function jrr    { jj git remote remove @args }
function jrl    { jj git remote list }

# ==============================================================================
# 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
    }
}

# ==============================================================================
# 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

# ==============================================================================
# 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
    }
}

# ==============================================================================
# FILE OPERATIONS
# ==============================================================================

function jfshow { jj file show @args }         # Show file from revision
function jflist { jj file list @args }         # List files in revision

Ubuntu/Linux (Zsh)

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 "$@" }
jdesc()   { jj describe "$@" }
jme()     { jj metaedit "$@" }
ja()      { jj abandon "$@" }
jundo()   { jj undo "$@" }
jrestore(){ jj restore --from "$1" }
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 "$@" }

# ==============================================================================
# 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 "$@" }

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"
}

# ==============================================================================
# REMOTE MANAGEMENT
# ==============================================================================

jr()      { jj git remote add "$@" }
jrr()     { jj git remote remove "$@" }
jrl()     { jj git remote list }

# ==============================================================================
# GIT SYNC
# ==============================================================================

jgf()     { jj git fetch "$@" }

jgp() {
    if [[ -n "$1" ]]; then
        jj git push --bookmark "$1" "${@:2}"
    else
        jj git push "$@"
    fi
}

# ==============================================================================
# OPERATION LOG
# ==============================================================================

jop()     { jj op log "$@" }
jopu()    { jj op undo "$@" }
jopr()    { jj op restore "$@" }

# ==============================================================================
# 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
}

# ==============================================================================
# FILE OPERATIONS
# ==============================================================================

jfshow()  { jj file show "$@" }
jflist()  { jj file list "$@" }

8. Quick Reference Tables

Core & Initialization

Goal Alias Command
Initialize Repo ji [branch] jj git init --colocate + track branch
Pass-through j <args> Run any raw jj command

Navigation & Inspection

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 βœ…/⚠️/❌ signing)
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

History Shaping

Goal Alias Command
New Commit jn Creates child. jn - creates sibling
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

Rebasing

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

Conflict Resolution

Goal Alias Command
Resolve jres Launch merge tool
List Conflicts jresl Show files with conflicts

Bisect

Goal Alias Command
Find Bad Commit jbis Binary search for bad rev

Bookmarks

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
Move jbm <name> [id] Move to @ or specific ID
Remote Delete jbdr <name> Delete from remote

Remotes

Goal Alias Command
Add jr <name> <url> Add remote
Remove jrr <name> Remove remote
List jrl List all remotes

Git Sync

Goal Alias Command
Fetch jgf Fetch all. jgf origin for specific
Push jgp [bookmark] Push all or specific bookmark

Operation Log

Goal Alias Command
View Ops jop Show operation history
Undo Op jopu Undo specific operation
Restore Op jopr <id> Restore to operation state

Maintenance

Goal Alias Command
Garbage Collect jgc Clean unreferenced files
Deep Clean jclean Interactive abandon disconnected commits

Ignore Immutable

Goal Alias Command
Bypass Immutability jjii <args> Run any jj command ignoring immutable

File Operations

Goal Alias Command
Show File jfshow <path> -r <rev> Show file from revision
List Files jflist -r <rev> List files in revision

9. Interactive Split Cheatsheet

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

10. Advanced Tutorial: Replacing Immutable History

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.

Step 1: Duplicate Main

jdup main

Step 2: Bring Up To Date

Option A: Squash specific commit

jsqi xyzabc

Option B: Squash a range

jj squash -r "main@origin..@" --into @

Step 3: Verify the Difference

jd --from main@origin --to @

Step 4: Move the Bookmark

jbm main

Step 5: Push

jgp main

Alternative: Using --ignore-immutable Flag

What are immutable commits?

By default, jj prevents rewriting commits that are ancestors of:

  • trunk() (usually main@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 main

Or 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.


11. Conflict Resolution Workflow

jj allows you to commit conflicts and resolve them later, no "merge in progress" blocking state.

When You Encounter a Conflict:

  1. See what's conflicted:

    jresl  # Lists conflicted files
  2. Launch merge tool:

    jres   # Opens configured merge tool
    # Or for specific file:
    jres path/to/file.txt
  3. Manual resolution (if preferred):

    • Edit the file to remove conflict markers (<<<<<<<, =======, >>>>>>>)
    • jj automatically detects the resolution on next operation
  4. Continue working:

    jdesc -m "Resolved conflicts"

Configure Merge Tool

Add to jj config:

[ui]
merge-editor = "code -w"  # VS Code
# merge-editor = "nvim"   # Neovim
# merge-editor = ":builtin"  # Built-in TUI

12. Feature Bookmark Lifecycle

Create

jn -m "Feature: Login"
jbc feature-login

Work & Push

# Do work...
jdesc -m "Implement login form"
jgp feature-login

Merge & Cleanup

# After PR merged...
jbrr feature-login   # Remove from remote
jbd feature-login    # Remove local bookmark
jclean               # Clean up orphaned commits

13. Useful Revset Expressions

# 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")'

14. Tips & Best Practices

  1. Always start fresh after switching context:

    jn  # Creates a clean child commit
  2. Use descriptive bookmarks:

    jbc yourname/feature-description
  3. Regularly clean up:

    jclean  # Remove disconnected commits
    jgc     # Garbage collect
  4. Signature status is shown inline:

    jl  # Look for βœ… (good), ⚠️ (warning), ❌ (unsigned)
  5. Use the operation log to recover:

    jop         # Find the operation ID
    jopr <id>   # Restore to that state
  6. Preview before destructive operations:

    jd --from <old> --to <new>
  7. 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 .gitignore in this new empty commit, everything (including node_modules) becomes tracked.

    Fix: Delete the massive tracked folder to untrack it, or just jundo.

  8. Restoring ignored files (.env):

    If your .gitignore is only local, and you restore a version where it's missing (or switch to a root sibling), jj might delete previously ignored files like .env because they are technically "untracked and not ignored" in the new state, or simply due to working copy updates.

    Fix: jundo restores them.

  9. Use a Global Gitignore:

    Link a global ignore file in your gitconfig to protect files like **/.env even 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/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment