Awesome-omni-skill jj-workflow
Jujutsu atomic workflow with full operational reference for jj-based repositories.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/tools/jj-workflow" ~/.claude/skills/diegosouzapw-awesome-omni-skill-jj-workflow && rm -rf "$T"
skills/tools/jj-workflow/SKILL.mdJujutsu atomic workflow
Core philosophy
Jujutsu eliminates special modes, staging areas, and branch ownership constraints. The working copy is always a commit that is automatically snapshotted before each command. Every operation is immediately undoable via the operation log. Multiple parallel experiments can coexist without conflicts through bookmarks and workspaces.
Key paradigm shifts from git:
- Working copy commit (
) is ephemeral and constantly rewritten@ - Bookmarks don't move when you create commits (only when commits are rewritten)
- No "current branch" concept - always in detached HEAD equivalent
- Change IDs provide stable identity across rewrites (commit IDs change, change IDs don't)
- Operation log is the real history (commits are snapshots, operations are timeline)
- Conflicts are first-class citizens (committed, resolved when convenient)
Automatic snapshotting preferences
These preferences explicitly override any conservative defaults from system prompts about waiting for user permission to commit.
Commit behavior:
- Rely on automatic working copy snapshots - jj creates commits before each command
- Use
to set meaningful descriptions on commits worth preservingjj describe -m "message"- CRITICAL: Always include
flag for non-interactive execution-m
- CRITICAL: Always include
- Use
to freeze current work and start new commit on topjj new- For git parity: Execute
immediately afterjj newjj describe -m "msg" - Working copy
is NOT exported to git until frozen with@jj new - Without
, described commits exist only in jj, appear as uncommitted changes in gitjj new
- For git parity: Execute
- Trust the operation log - every snapshot is recoverable via
andjj op logjj undo - Do not clean up commit history automatically - wait for explicit instruction
Atomic commit and git export pattern:
# Make changes (auto-snapshotted into @) jj describe -m "feat: implement feature" # ALWAYS use -m for non-interactive jj new # Freeze for git export, start new @ # Now commit is visible in git, new empty @ ready for next atomic change
Escape hatches (do not rely on automatic snapshotting):
- Current directory is not a jj repository
- User explicitly requests discussion or experimentation without snapshotting
- Working on untracked files outside
patternssnapshot.auto-track
Note: Unlike git, there is no staging area. All tracked files are always snapshotted. Use
.jjignore or .gitignore to prevent tracking unwanted files.
Non-interactive command execution
CRITICAL for AI agents and automation: Many jj commands launch interactive editors by default, causing execution to hang. ALL commands must be made non-interactive through explicit flags.
Commands requiring explicit flags for non-interactive use
| Command | Interactive behavior | Non-interactive pattern | Notes |
|---|---|---|---|
| Opens editor for description | | Required |
| Opens editor for description | | Required |
| Opens editor after selecting files | | Required even with paths |
(no paths) | Opens diff editor (TUI) | Cannot be non-interactive | Avoid in automation |
| Usually safe, may prompt | | Generally OK without |
| No editor (safe) | or | Safe as-is |
Mandatory command verification protocol
Before executing ANY jj command in automated context:
- If uncertain about interactivity: Run
FIRSTjj [subcommand] --help - Check help output for:
flag (indicates editor can be launched)-m, --message <MESSAGE>- Keywords: "editor", "interactive", "TUI", "prompt"
- Provide required parameters: Always via command-line flags, never rely on prompts
Common gotchas and solutions
requires jj split
even with explicit paths:-m
# WRONG - will hang waiting for editor after file selection jj split file.txt # CORRECT - fully non-interactive jj split file.txt -m "refactor: extract file.txt changes" # WRONG - multiple files but still hangs jj split file1.txt file2.txt # CORRECT - paths selected, description provided jj split file1.txt file2.txt -m "feat: add new files"
Parameterless
cannot be made non-interactive:jj split
# This ALWAYS launches diff editor (TUI) - unavoidable jj split # For automation, use path-based splitting instead: jj split <specific-paths> -m "description"
without jj describe
always opens editor:-m
# WRONG - launches editor jj describe # CORRECT - non-interactive jj describe -m "feat: implement feature" # WRONG - even with -r, launches editor jj describe -r <commit> # CORRECT - always include -m jj describe -r <commit> -m "updated description"
Git parity and the jj new
requirement
jj newCritical understanding: jj working copy
@ exists only in .jj/ metadata until explicitly frozen.
The problem:
- Described commits in jj
are NOT automatically exported to git@ - From git's perspective,
appears as uncommitted working directory changes@ - This breaks git-based tooling, CI/CD, and collaboration workflows
The solution - atomic commit pattern with immediate git export:
# Make changes (auto-snapshotted into @) jj describe -m "feat: implement feature X" # Described but jj-only jj new # Freeze @ → git commit, create new @ # Now the commit is visible to both jj and git # New empty @ is ready for next atomic change
Without
:jj new
jj describe -m "feat: done" # jj shows: @ xyz123 feat: done # git shows: modified working directory (xyz123 doesn't exist in git) jj git push --bookmark main # Fails or pushes incomplete state
With
:jj new
jj describe -m "feat: done" jj new # jj shows: @ abc456 (empty) (no description set) # ○ xyz123 feat: done # git shows: commit xyz123 "feat: done" (HEAD detached) jj git push --bookmark main # Works correctly, xyz123 is a real git commit
When to use
:jj new
- After every
for atomic commitsjj describe -m "message" - When preparing commits for git operations (push, PR creation, etc.)
- When you want to checkpoint work and start the next logical change
When
is optional:jj new
- During rapid experimentation where git visibility doesn't matter
- When using jj-only features (workspaces, operation log recovery)
- Will eventually need it before any git interaction
Escape hatches for interactive operations
If interactive command unavoidable:
- Warn user that manual interaction required
- Provide exact command for user to run
- Document why automation cannot handle it
- Never execute commands with
or-i
flags in automation--interactive
Foundation: Atomic commit workflow
Working copy commit behavior
The working copy is always the
@ commit:
- All file changes automatically amend
without explicit commands@
is rewritten in place as you work (new commit ID, same change ID)@- Use
to add description when changes represent cohesive unitjj describe -m "message"- Required:
flag for non-interactive execution (never omit it)-m
- Required:
- Use
to freezejj new
and create new empty@
on top@- Git export: Frozen commits become visible in git;
remains jj-only until frozen@ - Atomic workflow:
→jj describe -m "msg"
→ commit exported to git, ready for next changejj new
- Git export: Frozen commits become visible in git;
- Use
to movejj commit
changes into its parent (alternative to@
)jj new
Critical for git parity: Without
jj new, your described working copy commit exists only in jj's .jj/ directory.
From git's perspective, these changes remain uncommitted in the working directory.
Execute jj new after each jj describe -m "msg" to maintain git/jj synchronization.
Organizing changes:
- Let related changes accumulate in
@ - Use
when changes diverge into separate concernsjj split <paths> -m "message"- Required:
flag even when providing paths (common mistake to omit)-m
- Required:
- Use
to move changes between commitsjj squash - Use
to automatically distribute fixes to appropriate ancestorsjj absorb
File state awareness:
- Run
to see what's in currentjj status@ - Run
to review changes injj diff@ - No staging area to check - working copy state is commit state
Bookmark management
Bookmarks are named pointers that don't move automatically with new commits.
Core behavior:
- Bookmarks stay on their target when you create new commits (unlike git branches)
- Bookmarks only move when commits are rewritten (rebase, squash, abandon)
- Update bookmarks explicitly:
jj bookmark set <name> -r <commit> - Create bookmarks for important points:
jj bookmark create <name> -r <commit> - Always work in "detached HEAD" state - this is normal in jj
Naming conventions:
- Main bookmarks:
,main
,betastaging - Feature work:
(e.g.,issue-N-descriptor
)issue-42-add-auth - Experiments:
(e.g.,exp-N-description
)exp-1-refactor-parser - Archives:
archive/old-bookmark-name
Integration with issue tracking:
- When work diverges from current bookmark's purpose, create new bookmark
- Example: working near
but fixing unrelated bug →issue-42-authjj bookmark create issue-58-logging
Default bias: bookmarks are cheap, use them liberally to mark important commits.
Operation log and recovery
Every jj operation is atomic and recorded in the operation log.
Core commands:
- undo last operation (any operation, not just commits)jj undo
- view complete operation historyjj op log
- restore repo to exact prior statejj op restore <id>
- see what an operation changedjj op show <id>
Recovery patterns:
- Mistake in last operation:
jj undo - Mistake several operations ago:
, thenjj op logjj op restore <id> - Want to undo operation N but keep N+1:
to N-1, manually redo N+1jj op restore - Concurrent operations created divergence: inspect with
, resolve withjj logjj bookmark set
Key insight: Operation log is your safety net, not backup branches. Delete bookmarks freely - commits remain in operation log.
Conflict management
Conflicts are first-class citizens, committed and resolved when convenient.
Conflict workflow:
- Operations never fail due to conflicts - conflicts are committed with marker
- Continue working on other commits while conflicts exist
- View conflicted commits:
jj log -r 'conflict()' - Resolve when ready:
, fix files,jj new <conflicted-commit>
resolution backjj squash - Or resolve in place:
, fix files (automatically amends)jj edit <conflicted-commit>
Conflict tools:
- launch merge tool for each conflictjj resolve
- see all conflicts in current commitjj resolve --list- Edit conflict markers directly in files or use merge tools
Never blocked by conflicts - they're just another commit state.
Parallel experimentation with bookmarks
Start with bookmarks in single workspace. Graduate to separate workspaces only when needed.
Starting experiments from main
# Create experiment bookmarks from main jj bookmark create exp-1-nix-flakes -r main jj bookmark create exp-2-home-manager -r main jj bookmark create exp-3-unified-config -r main # Start working on experiment 1 jj new exp-1-nix-flakes # @ is now a new commit on top of exp-1-nix-flakes # Make changes (auto-snapshotted into @) # Describe when @ represents cohesive unit jj describe -m "[exp-1] feat(nix): migrate to flakes - part 1" jj new # Freeze that commit, create new @ on top # Continue building commit chain jj describe -m "[exp-1] feat(nix): migrate to flakes - part 2" jj new # Switch to experiment 2 (same workspace) jj new exp-2-home-manager jj describe -m "[exp-2] feat(home): initial home-manager setup" jj new
Viewing and comparing experiments
Query experiments using revsets:
# View all experiments jj log -r 'main.. & (exp-1-nix-flakes:: | exp-2-home-manager:: | exp-3-unified-config::)' # Commits in experiment 1 only jj log -r 'main..exp-1-nix-flakes' # Total diff of experiment jj diff -r 'main..exp-1-nix-flakes' # Compare experiments - unique to exp-1 jj log -r '(main..exp-1-nix-flakes) ~ (main..exp-2-home-manager)' # Compare experiments - shared commits jj log -r '(main..exp-1-nix-flakes) & (main..exp-2-home-manager)' # Count commits in experiment jj log -r 'main..exp-1-nix-flakes' --no-graph --template 'commit_id ++ "\n"' | wc -l
Checkpoint and push experiments
# Point bookmark to latest work (typically @- not @) jj bookmark set exp-1-nix-flakes -r @- # Push to remote for backup/collaboration jj git push --bookmark exp-1-nix-flakes # Creates branch on GitHub for PR or review
Advantages of bookmark-only experiments
- Minimal overhead (no separate directories)
- Fast switching between experiments
- Operation log tracks everything
- No stale workspace issues
- Lower disk space usage
Disadvantages:
- Working tree changes when switching (like git checkout)
- Cannot run tests in parallel
- Cannot compare files side-by-side easily
Graduating to workspaces
Create separate workspaces when experiments need simultaneous file access.
When to create workspaces
Create workspace when you need:
- Long-running builds/tests in each experiment simultaneously
- Side-by-side file comparison in editors
- Different sparse patterns per experiment
- Avoid working-tree thrashing from frequent switches
Keep bookmark-only when:
- Quick exploration or prototyping
- Comparing implementations conceptually
- Don't need files from multiple experiments on disk
Creating workspaces for experiments
# Graduate experiment 1 from bookmark to workspace jj workspace add ../myproject-exp-1 -r exp-1-nix-flakes # Workspace created with working-copy commit (exp1@) starting from exp-1-nix-flakes cd ../myproject-exp-1 # Now exp1@ is your working copy # Files are persistent on disk jj describe -m "[exp-1] feat(nix): add flake inputs" jj new # Start long test in workspace nix build .# & # Work on other experiments in parallel cd ../myproject jj new exp-2-home-manager jj describe -m "[exp-2] feat(home): configure programs"
Workspace-specific concepts
Working-copy commits:
- Each workspace has unique working-copy commit tracked in repository view
refers to current workspace's working-copy commit@
refers to workspace named "exp1" working-copy commitexp1@
shows all workspace working-copy commitsjj log -r 'working_copies()'
Stale workspaces:
- Workspace becomes stale when its working-copy commit is rewritten from another workspace
- Example: rebase
from primary workspace → exp1 workspace becomes staleexp1@ - Warning appears when running commands in stale workspace
- Fix with
to update files to new commitjj workspace update-stale
Cross-workspace operations:
- Can work near same bookmark from multiple workspaces (no exclusive ownership)
- Can read/query any commit from any workspace
- Rewriting another workspace's working-copy commit makes it stale
- Safe:
(create new commit on top)jj new exp1@ - Risky:
(edit directly, will cause staleness if files change)jj edit exp1@
Workspace lifecycle
# List all workspaces jj workspace list # Create workspace jj workspace add <path> -r <starting-commit> # Remove workspace (files remain on disk) jj workspace forget <workspace-name> rm -rf <workspace-path> # Workspace naming convention myproject/ # Primary workspace myproject-exp-1/ # Experiment 1 workspace myproject-exp-2/ # Experiment 2 workspace
Experiment lifecycle management
Scale to arbitrary number of experiments with minimal complexity.
Naming conventions
Bookmarks:
exp-{number}-{short-description}- Examples:
,exp-1-refactor-parser
,exp-2-add-asyncexp-3-optimize-cache
Workspaces (when needed):
{repo}-exp-{number}- Examples:
,myproject-exp-1myproject-exp-2
Commit descriptions:
- Active:
[exp-N] type: description - Review:
[exp-N:review] type: description - Archived:
[exp-N:archived] type: description - Winner:
[exp-N:winner] type: description
Revset aliases for scaling
Add to
~/.jjconfig.toml:
[revset-aliases] # All experiments (commits ahead of main) 'exps()' = 'main.. & ~main' # Specific experiment range 'exp(x)' = 'main..x' # Compare two experiments (what's unique in first) 'exp_diff(x, y)' = '(main..x) ~ (main..y)' # Shared commits between experiments 'exp_shared(x, y)' = '(main..x) & (main..y)' # All workspace working copies 'wcs()' = 'working_copies()' # Experiment workspace working copies (exclude primary) 'exp_wcs()' = 'working_copies() & ~default@'
Usage:
jj log -r 'exps()' # All experiments jj log -r 'exp(exp1@)' # Experiment 1 commits jj log -r 'exp_diff(exp1@, exp2@)' # Unique to exp1 jj log -r 'exp_shared(exp1@, exp2@)' # Shared between experiments
Experiment registry
Maintain
docs/experiments.md in repository:
# Experiments Registry ## Active ### exp-1-refactor-parser - Status: Active - Bookmark: `exp-1-refactor-parser` - Workspace: `myproject-exp-1` - Goal: Rewrite parser for 10x performance - Created: 2025-10-15 - PR: #123 ### exp-2-add-async-support - Status: Active - Bookmark: `exp-2-add-async-support` - Workspace: None (bookmark only) - Goal: Add async/await throughout - Created: 2025-10-14 - PR: None yet ## Review ### exp-3-optimize-cache - Status: Ready for review - Bookmark: `exp-3-optimize-cache` - Workspace: `myproject-exp-3` - Goal: LRU cache for 30% speedup - Created: 2025-10-10 - PR: #120 ## Archived ### exp-4-graphql-api - Status: Archived (too complex) - Bookmark: `archive/exp-4-graphql-api` - Workspace: Removed - Goal: GraphQL API layer - Created: 2025-10-01 - Archived: 2025-10-12 - Reason: Scope too large, splitting into smaller experiments ## Integrated ### exp-5-docker-support - Status: Merged to main - Bookmark: Deleted - Goal: Add Docker deployment - Created: 2025-09-20 - Merged: 2025-10-01 - PR: #105
State transitions and lifecycle
# Active → Review jj describe -r 'description(glob:"*[exp-N]*")' -m "[exp-N:review] ..." jj git push --bookmark exp-N # Review → Integrated (via rebase) jj rebase -s 'main..exp-N' -d main jj bookmark set main -r exp-N jj git push --bookmark main # Review → Archived jj bookmark rename exp-N archive/exp-N # Edit docs/experiments.md # Archived → Deleted (preserved in operation log) jj bookmark delete archive/exp-N # Query by state jj log -r 'description(glob:"*[exp-*:review]*")' jj log -r 'description(glob:"*[exp-*:archived]*")'
Periodic cleanup
# List all experiment bookmarks jj bookmark list | grep '^exp-' # View recent activity by bookmark jj log -r 'bookmarks() & description(glob:"exp-*")' \ --template 'bookmarks ++ " " ++ committer.timestamp() ++ "\n"' # Archive stale experiments jj bookmark rename exp-old archive/exp-old # Cleanup empty commits in experiments jj abandon 'empty() & exps()' # View operation log for recovery if needed jj op log --limit 50 jj op restore <op-id> # Restore deleted experiment
Experiment dependencies
Stack experiments when one depends on another:
# Experiment 2 builds on experiment 1 jj bookmark create exp-1 -r @ jj new exp-1 jj describe -m "[exp-2] feat: builds on exp-1" jj bookmark create exp-2 -r @ # If exp-1 gets rebased, exp-2 follows automatically jj rebase -r exp-1 -d main # exp-2 rebases automatically (descendant relationship)
Merge experiments when needing multiple:
# Experiment 3 combines exp-1 and exp-2 jj new exp-1 exp-2 # Create merge commit with two parents jj describe -m "[exp-3] feat: combines exp-1 and exp-2" jj bookmark create exp-3 -r @
Cherry-pick specific commits:
# Experiment 4 needs one commit from exp-1 jj new main jj squash --from <specific-commit> --into @ jj describe -m "[exp-4] feat: uses technique from exp-1"
History refinement
Transform experimental development history into clean, reviewable commit sequence.
Principles:
- Operations execute immediately and atomically
- No special modes or interactive editors
- Operation log is safety net (not backup branches)
- Descendants auto-rebase when ancestors change
- Conflicts are committed, not blocking
Incremental cleanup workflow
# Phase 1: Review what needs cleaning jj log -r 'main..@' # Phase 2: Squash fixups jj squash -r 'description(glob:"fixup*") & main..@' jj squash -r 'description(glob:"oops*") & main..@' # Phase 3: Abandon empty and temporary commits jj abandon 'empty() & main..@' jj squash -r 'description(glob:"WIP:*") & main..@' # Phase 4: Reorder if needed jj log -r 'main..@' # Identify order issues jj rebase -r <commit> -d <new-parent> # Move commit jj rebase -r <commit> -A <after> # Insert after jj rebase -r <commit> -B <before> # Insert before # Phase 5: Reword commits jj describe -r <commit> -m "proper conventional commit message" # Phase 6: Verify jj log -r 'main..@' jj diff -r 'main..@' # Should match original total changes
Each step executes immediately. Use
jj undo to back out of any step.
Core operations
Reorder commits:
# Move commit to new parent jj rebase -r <commit> -d <parent> # Insert commit after another jj rebase -r <commit> -A <after-commit> # Insert commit before another jj rebase -r <commit> -B <before-commit> # Move entire subtree jj rebase -s <commit> -d <new-base>
Note on
and -A
: These flags automatically rebase all descendants of the moved commit onto it in a single atomic operation. No second rebase command needed to reconnect the chain.-B
Using change ID prefixes: For faster typing, use unambiguous prefixes of change IDs:
# Full change IDs jj rebase -r tknpxpos -A ynrpuxsz # Prefixes (recommended for interactive use) jj rebase -r tk -A y
Moving ranges vs single commits: Choose between
-r and -s based on what you're moving:
# Move single commit (descendants follow automatically) jj rebase -r <commit> -A <after> # Move commit AND all its descendants as a range/subtree jj rebase -s <first-commit> -d <destination>
Use
-r with -A/-B when moving one commit within a chain. Use -s when you want to move multiple commits together as a unit - the specified commit plus everything built on top of it will move to the new destination. This is the primary way to "slide" a range of commits through history.
Squash commits:
# Squash commit into its parent jj squash -r <commit> # Squash with specific message jj squash -r <commit> -m "combined message" # Squash into specific ancestor jj squash --from <commit> --into <ancestor> # Interactive squash (select hunks) jj squash -i -r <commit> # Squash multiple by pattern jj squash -r 'description(glob:"WIP:*") & main..@'
Drop commits:
# Abandon specific commit (descendants rebase onto parent) jj abandon <commit> # Abandon multiple by pattern jj abandon 'description(glob:"tmp:*") & main..@' # Abandon empty commits jj abandon 'empty() & main..@' # Abandon range jj abandon <start>::<end>
Reword descriptions:
# Reword single commit jj describe -r <commit> -m "new description" # Open editor for description jj describe -r <commit> # Reword multiple by pattern jj describe -r 'description(glob:"WIP*") & main..@' -m "proper: description" # Clear description (for commits that will be squashed) jj describe -r <commit> -m ""
Split commits:
# NON-INTERACTIVE: Split by paths with description (REQUIRED for automation) jj split <paths> -m "description for selected changes" # Specified paths → new @- commit with description # Remaining changes → new @ commit (no description) # Example: Split single file from working copy jj split src/feature.rs -m "feat: implement feature X" # Example: Split multiple files from working copy jj split file1.txt file2.txt -m "docs: add documentation" # Example: Split specific commit (not working copy) jj split -r <commit> <paths> -m "refactor: extract changes" # INTERACTIVE: Split without paths (launches diff editor TUI) jj split -r <commit> # ⚠️ CANNOT be made non-interactive - avoid in automation # INTERACTIVE: Split by paths WITHOUT -m (launches editor for description) jj split <paths> # ⚠️ Will hang waiting for commit message editor # INTERACTIVE: Split with interactive hunk selection (unavoidable TUI) jj split -i -r <commit> # ⚠️ Never use in automated execution # Split current working copy (no -r needed) jj split <paths> -m "description" # Without -r, splits @ commit
CRITICAL:
jj split requires -m "message" flag when providing paths to avoid launching
editor for description. Without -m, the command succeeds in selecting files but then
blocks waiting for interactive description input. This is the most common mistake.
After splitting, remember to freeze commits for git export:
jj split file.txt -m "refactor: extract changes" # Now have: @ (remaining changes, no description) # ○ @- (file.txt changes, described) jj describe -m "refactor: remaining changes" # Describe the new @ jj new # Freeze both commits for git visibility # Both commits now visible in git
Edit commit content:
# Approach 1: Edit in place jj edit <commit> # Make changes (automatically amends commit) jj new @- # Return to previous location # Approach 2: Edit without checkout jj diffedit -r <commit> # Approach 3: Move specific changes jj edit <commit> jj commit <files> # Move some changes to new child jj new @- # Return
Auto-distribute changes with absorb
Automatically move fixes to commits that last touched those lines:
# Make fixes in working copy # Fix bug in file1.txt (last modified by commit A) # Improve file2.txt (last modified by commit B) # Auto-distribute based on blame jj absorb # jj analyzes blame and distributes: # file1.txt fix → commit A # file2.txt improvement → commit B
Most powerful for fixing issues found during review.
Verification workflow
After cleanup:
# View final history jj log -r 'main..@' # Verify total diff unchanged jj diff -r 'main..@' # Test each commit builds for commit in $(jj log -r 'main..@' --no-graph --template 'commit_id ++ "\n"'); do jj new $commit --no-edit nix build .# || echo "Build failed: $commit" done jj new @- # Check for conflicts jj log -r 'conflict() & main..@' # Review operation history jj op log --limit 20
Complete cleanup example
Starting state:
jj log -r 'main..@' # @ mno345 WIP: more fixes # ○ jkl012 fix typo # ○ ghi789 add feature Y # ○ def456 WIP: feature Y work # ○ abc123 add feature X # ○ zzz999 fixup: feature X test # ○ yyy888 temp debug # ○ xxx777 feature X implementation
Goal: 2 clean commits (one per feature)
# Squash feature X commits jj squash --from zzz999 --into abc123 jj abandon yyy888 jj squash -r xxx777 # Into abc123 # Squash feature Y commits jj squash -r def456 # Into ghi789 jj squash -r jkl012 # Into ghi789 jj squash -r mno345 # Into ghi789 # Reword both jj describe -r abc123 -m "feat: implement feature X with tests" jj describe -r ghi789 -m "feat: implement feature Y with error handling" # Verify jj log -r 'main..@' # @ ghi789 feat: implement feature Y with error handling # ○ abc123 feat: implement feature X with tests # Test each jj new abc123 && nix build .# && jj new @- jj new ghi789 && nix build .# && jj new @- # Set bookmark jj bookmark set feature-xy -r @
If any step fails,
jj undo backs out immediately.
Advanced patterns
Integration strategies
Rebase winner onto main (preserves commit history):
# Rebase experiment commits onto main jj rebase -s 'main..exp-1-winner' -d main # Move main bookmark to include these commits jj bookmark set main -r exp-1-winner # Push jj git push --bookmark main
Squash winner into main (clean single commit):
# Create new commit with all experiment changes jj new main jj squash --from 'main..exp-1-winner' --into @ jj describe -m "feat: feature from experiment 1 Squashed from exp-1-winner. Includes: ..." # Move main bookmark jj bookmark set main -r @ # Push jj git push --bookmark main
Merge experiments (when both valuable):
# Create merge commit combining both jj new exp-1-winner exp-2-winner # Two parents # Resolve conflicts if any jj describe -m "feat: merge experiments 1 and 2" # Move main jj bookmark set main -r @ # Push jj git push --bookmark main
Sub-experiments within experiments
# Main experiment jj bookmark create exp-1-parser -r main # Sub-experiment 1a: regex approach jj new exp-1-parser jj describe -m "[exp-1a] feat: regex parser" jj bookmark create exp-1a-regex -r @ # Sub-experiment 1b: PEG approach jj new exp-1-parser # Branch from same point jj describe -m "[exp-1b] feat: PEG parser" jj bookmark create exp-1b-peg -r @ # Compare approaches jj diff -r 'exp-1-parser..exp-1a-regex' jj diff -r 'exp-1-parser..exp-1b-peg' # Choose winner jj rebase -s exp-1b-peg -d main jj bookmark set exp-1-parser -r exp-1b-peg jj bookmark delete exp-1a-regex
Experimental feature flags
Merge experiment to main early behind feature flag:
jj new main jj describe -m "[exp-4] feat: add experimental cache Behind EXPERIMENTAL_CACHE feature flag." # Merge to main jj bookmark set main -r @ jj git push --bookmark main # Continue improving in workspace jj workspace add ../myproject-exp-4-cache -r @ # Iterate on implementation # When stable, remove flag in follow-up commits
Session workflow pattern
Effective jj session:
# Starting work jj git fetch jj log jj new <base> # Or work on existing @ # During work jj describe -m "message" # When purpose clear jj split # When changes diverge jj squash # When changes belong together jj undo # On mistakes # Preparing to push jj log -r 'main..@' jj describe -r <commits> # Clean up descriptions # Squash/split as needed jj bookmark set <name> -r @ jj git push --bookmark <name> # After mistakes jj op log jj op restore <id>
Git colocated mode
Jujutsu operates in colocated mode with existing git repositories, allowing seamless interoperation.
Initializing colocated repository
# In existing git repository cd /path/to/git/repo jj git init --colocate # Verify both .git/ and .jj/ exist ls -la # All git branches become jj bookmarks jj log
Alternative initialization:
# Clone git repo with jj (automatically colocated) jj git clone <url> <directory> # Initialize new jj repo backed by git jj git init --git-repo=.
Colocated workflow
In colocated mode:
- All jj bookmark operations automatically sync to git branches
- Git commands work on same repository - changes appear in
jj log
imports git changes (automatic in colocated mode)jj git import
exports to git refs (automatic in colocated mode)jj git export- Git-specific features (GitHub PRs, etc.) work with jj-managed bookmarks
Workflow:
- Prefer jj commands for all operations
- Git commands work but are less powerful
- After git operations, check
to see imported changesjj log - Operation log tracks git operations as "import git refs"
Note on detached HEAD: jj operates in "detached HEAD" mode (git terminology). This is normal and does not affect jj operations -
jj git push --bookmark <name> works correctly without an attached HEAD. Only attach HEAD if switching to git-native workflows:
# Attach HEAD to branch (only needed when using git commands directly) git checkout main git switch main # Modern git (v2.23+)
jj operations may detach HEAD again. This is expected behavior.
Remote synchronization
# Fetch from remote jj git fetch --all-remotes # Track remote bookmark jj bookmark track <name>@<remote> # Push bookmark jj git push --bookmark <name> # Push all changed bookmarks jj git push --all # Push all tracked bookmarks jj git push --tracked
Log visibility and remote bookmarks
After pushing commits to a remote,
jj log hides them by default to focus on work-in-progress.
Symbols indicating commit state:
- Mutable commit (local, not pushed)○
- Immutable commit (exists on remote)◆
- History truncated (more commits exist but are hidden)~
# After push, log shows only unpushed commits jj git push --bookmark main jj log # Shows @ and maybe a few commits, then ~ # The ◆ symbol marks immutable commits (on remotes) # The ~ symbol indicates hidden history below # See full history including pushed commits jj log -r 'all()' jj log -r '::@' # All ancestors of working copy jj log -r 'main@origin::' # Everything since remote main
Customizing log visibility:
To always show full history, configure default revset in
~/.jjconfig.toml:
[revsets] log = "@ | ancestors(bookmarks() | tags() | remote_bookmarks(), 2)"
Or create an alias for full history:
[aliases] la = ["log", "-r", "all()"]
This default behavior keeps your log focused on uncommitted work while clearly marking the boundary between local changes and pushed commits.
Reverting to git-only operations
Since colocated mode maintains both
.git/ and .jj/, you can revert to git-only operations at any time:
# Option 1: Simply stop using jj commands # Git continues working normally with the .git/ directory # All bookmarks exist as git branches git status git log git checkout <branch> # Option 2: Remove jj entirely (preserves all history in git) rm -rf .jj/ # Repository is now pure git # All jj bookmarks remain as git branches # All commits remain in git history # Operation log is lost, but git reflog provides similar functionality # Option 3: Continue using both tools # jj and git commands can be mixed freely # Use jj for advanced history editing # Use git when needed for specific operations
Key insight: Colocated mode provides a safe, reversible migration path. Your git repository is never at risk - jj adds capabilities without replacing git.
Reference
Essential revset patterns
Query language for selecting commits:
# Symbols @ # Current working-copy commit <workspace>@ # Working-copy commit in named workspace <bookmark> # Commit pointed to by bookmark <bookmark>@<remote> # Remote bookmark position root() # Root commit (all zeros) # Operators A..B # Range: reachable from B but not A A::B # Path: all commits between A and B A | B # Union: commits in either A & B # Intersection: commits in both ~A # Negation: commits not in A A ~ B # Difference: commits in A but not B # Functions mine() # Your commits (by configured email) author("pattern") # Commits by author description(glob:"pattern") # Commits with matching description file("path") # Commits modifying path ~/path # Commits modifying path (shorthand) empty() # Empty commits (no changes) conflict() # Commits with conflicts merge() # Merge commits bookmarks() # All bookmarked commits working_copies() # All working-copy commits # Complex queries main..@ # Commits ahead of main mine() & ~bookmarks() # Your unbookmarked commits description(glob:"WIP*") & main..@ # WIP commits in current branch conflict() & main..@ # Conflicts in current work
Common command patterns
# Create and manage commits jj new <base> # New commit on base jj new <parent1> <parent2> # New merge commit jj describe -m "message" # Describe current commit jj commit # Move @ changes to parent jj split # Split @ interactively jj edit <commit> # Make commit the @ # Move changes between commits jj squash # Move @ into parent jj squash -r <commit> # Squash commit into parent jj squash --from <src> --into <dst> # Move changes between any commits jj squash -i # Interactive squash (choose hunks) jj absorb # Auto-distribute @ to ancestors # Rewrite history jj rebase -r <commit> -d <dest> # Move commit to new parent jj rebase -s <commit> -d <dest> # Move commit and descendants jj rebase -r <commit> -A <after> # Insert after commit jj rebase -r <commit> -B <before> # Insert before commit jj abandon <commit> # Remove commit, rebase descendants # Bookmarks jj bookmark create <name> # Create at @ jj bookmark create <name> -r <commit> # Create at commit jj bookmark set <name> -r <commit> # Move bookmark jj bookmark rename <old> <new> # Rename bookmark jj bookmark delete <name> # Delete bookmark jj bookmark list # List all bookmarks jj bookmark track <name>@<remote> # Track remote bookmark # Workspaces jj workspace add <path> -r <commit> # Create workspace jj workspace list # List workspaces jj workspace forget <name> # Stop tracking workspace jj workspace update-stale # Update stale workspace # History and recovery jj log # View history jj log -r <revset> # View specific commits jj diff -r <commit> # Show changes in commit jj show <commit> # Show commit details jj evolog -r <commit> # Evolution log of change jj op log # Operation log jj op show <id> # Show operation details jj undo # Undo last operation jj op restore <id> # Restore to operation # Git interop jj git init --colocate # Initialize colocated repo jj git clone <url> # Clone git repo with jj jj git fetch # Fetch from remote jj git push --bookmark <name> # Push bookmark jj git push --all # Push all changed bookmarks
Session operation summary
After working session, provide summary:
# Operation-focused (shows actual work including undos) jj op log --limit 20 # Commit-focused (shows commits not yet on bookmarks) jj log -r 'bookmarks()..@' # Combined view for complete picture jj op log --limit N # Where N covers session jj log -r 'main..@' # Commits in current work
Use explicit operation IDs from session start for precise ranges.
Key principles
- Non-interactive execution required: Always use
flag with commands that can prompt (-m
,jj describe
,jj describe -r
); verify unfamiliar commands withjj split <paths>
before executionjj [subcommand] --help - Git parity via jj new: Working copy
is not exported to git until frozen; execute@
afterjj new
to make commits visible in git and maintain synchronizationjj describe -m "msg" - Command verification protocol: When uncertain if a command is non-interactive, run
first and check for--help
flag or interactive keywords-m, --message - Operation log is history (commits are snapshots, operations are timeline)
- Bookmarks don't move automatically (only on commit rewrites)
- Working copy commit
is ephemeral (constantly rewritten)@ - Change IDs provide stability (commit IDs change, change IDs don't)
- No staging area (working copy state is commit state)
- Conflicts are first-class (committed, resolved when convenient)
- No special modes (all operations execute immediately)
- Start with bookmarks (graduate to workspaces when needed)
- Describe atomically (one logical change per description)
- Trust operation log (delete bookmarks freely)
- Use revsets liberally (query, don't manually track)
- Clean up incrementally (jj undo backs out any step)