
Jj
- 15 installs
- 1 repo stars
- Updated March 1, 2026
- cachemoney/agent-toolkit
A Claude Code skill for jj.
About
Skill: jj. Used during build phase for development. This skill provides essential functionality for the development workflow.
- jj
Jj by the numbers
- 15 all-time installs (skills.sh)
- Ranked #1,631 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cachemoney/agent-toolkit --skill jjAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Last updated | March 1, 2026 |
| Repository | cachemoney/agent-toolkit ↗ |
What it does
A Claude Code skill for jj.
Files
Jujutsu (jj) Version Control
Jujutsu is a Git-compatible VCS with mutable commits, automatic change tracking, and an operation log that makes every action undoable.
Target version: jj 0.36+
Topics
| I need to... | Deep dive |
|---|---|
| Understand how jj relates to Git, or use raw git in a jj repo | git.md |
| Write revset, fileset, or template expressions | revsets.md |
| Push, pull, manage bookmarks, or work with GitHub | sharing.md |
| Split, rebase, squash, or resolve conflicts | history.md |
| Run parallel agents with isolated working copies | workspaces.md |
| Configure jj, set up aliases, or customize diffs | config.md |
Mental Model
The working copy is a commit. No staging area. Every file change is auto-snapshotted into @ when you run any jj command. Instead of "stage → commit," just code and describe.
Change IDs are stable. Commit IDs are not. Every commit has two identifiers:
- Change ID — Stable across rewrites. Letters k–z (e.g.,
tqpwlqmp). Prefer these. - Commit ID — Content hash, changes on any rewrite. Hex digits. This is the Git commit ID in colocated repos.
History is mutable. Commits can be freely rewritten. Descendants auto-rebase. Old versions stay in the operation log.
Bookmarks are not branches. Bookmarks don't advance when new commits are created. They follow rewrites but must be explicitly set before pushing. → Deep dive: sharing.md
Conflicts don't block. jj allows committing conflicted files. Resolve at your convenience by editing conflict markers directly, then verify with jj st. → Deep dive: history.md
Agent Rules
Non-negotiable when operating as an automated agent:
1. Always use `-m` for messages. Never invoke a command that opens an editor. Commands that need -m: jj new, jj describe, jj commit, jj squash. 2. Never use interactive commands. jj split (without file paths), jj squash -i, jj resolve — all hang. Use file-path args or jj restore workflows. 3. Verify after mutations. Run jj st after squash, abandon, rebase, restore, or any destructive op. 4. Use change IDs, not commit IDs. Change IDs survive rewrites. 5. Quote revsets. Always single-quote: jj log -r 'mine() & ::@'.
Agent-Specific Configuration
# agent-jj-config.toml
[user]
name = "Agent"
email = "agent@example.com"
[ui]
editor = "TRIED_TO_RUN_AN_INTERACTIVE_EDITOR"
diff-formatter = ":git"
paginate = "never"Launch with: JJ_CONFIG=/path/to/agent-jj-config.toml <agent-harness> → Deep dive: config.md
Core Workflow
The daily loop: describe → code → new → repeat.
jj describe -m "feat: add user validation"
# make changes — auto-tracked, no `add` needed
jj st && jj diff
jj new -m "feat: add error handling"Curating History
jj squash -m "feat: final clean message" # fold working copy into parent
jj absorb # auto-distribute hunks to right ancestor
jj abandon @ # drop a failed experiment→ Deep dive: history.md
Non-Linear Work
When new work doesn't depend on the current chain, branch off trunk:
# Create sibling from trunk (doesn't move @)
jj new trunk() --no-edit -m "fix: correct timezone handling"
jj edit <bugfix-change-id>
# ... fix the bug ...
# Return to original work
jj log -r 'heads(trunk()..)'
jj edit <feature-change-id>Agent rule: Before creating a new commit, decide if it depends on the current chain. If not, branch off trunk and flag the divergence to the user.
Pushing Changes
jj bookmark set feat -r @
jj git push -b featBookmarks must be set before pushing — they don't auto-advance. → Deep dive: sharing.md
Essential Commands
| Task | Command |
|---|---|
| Check status | jj st |
| View diff / log | jj diff / jj log |
| Describe current commit | jj describe -m "message" |
| Start new work | jj new -m "task description" |
| Edit an older commit | jj edit <change-id> |
| Squash into parent | jj squash |
| Auto-distribute changes | jj absorb |
| Abandon a commit | jj abandon <change-id> |
| Undo last operation | jj undo |
| View operation history | jj op log |
| Restore to earlier state | jj op restore <op-id> |
| Create/move bookmark | jj bookmark create <n> -r @ / jj bookmark set <n> -r @ |
| Push / fetch | jj git push -b <bookmark> / jj git fetch |
For Git translations: references/git-to-jj.md
Recovery
jj undo # undo last op; repeatable
jj op log # full operation history
jj op restore <op-id> # jump to any past state
jj evolog -r <change-id> # see how a change evolvedDetecting a jj Repo
.jj/ directory = jj repo. Both .jj/ and .git/ = colocated repo. Always use jj commands. Git's "detached HEAD" is normal in colocated repos — use jj log for real state.
Common Mistakes
| Mistake | Fix |
|---|---|
Omitting -m on commands | Always pass -m — editor hangs agents |
Using jj split without file paths | Provide paths or use the jj restore workflow |
| Forgetting to set bookmark before push | jj bookmark set <name> -r @ first |
| Using commit IDs instead of change IDs | Change IDs (letters k–z) survive rewrites |
| Unquoted revset expressions | Always single-quote: 'mine() & ::@' |
Confusing :: vs .. operators | :: = ancestry path, .. = range (see revsets.md) |
| Creating workspaces as subdirectories | Must be sibling dirs, not children |
Reference Index
Git Interop:
- references/git-to-jj.md — Git-to-jj command mapping
- references/git-experts.md — Why jj improves on Git
- references/git-compatibility.md — Git interop and colocated repos
Commands:
- references/command-gotchas.md — Flag semantics, quoting, deprecated flags
Revsets & Templates:
- references/revsets.md — Complete revset language spec
- references/filesets.md — Complete fileset language spec
- references/templates.md — Complete template language spec
Sharing:
- references/bookmarks.md — Complete bookmarks reference
- references/github.md — GitHub/GitLab workflow details
History:
- references/conflicts.md — Conflict handling and marker formats
- references/divergence.md — Divergent changes guide
Config:
- references/config-reference.md — Full configuration reference
Workspaces:
- references/parallel-agents.md — Parallel agent setup guide
Jujutsu (jj) Configuration
Configuration reference for jj. Covers config file locations, precedence, agent-specific setup, useful aliases, diff/merge tool configuration, signing, and customization.
Target version: jj 0.36+
For the full configuration reference, see:
- references/config-reference.md — Complete configuration reference (~2100 lines)
Authority: jj official docs (config.md).
Config File Locations and Precedence
Settings load in this order (later overrides earlier):
1. Built-in — compiled into jj, not editable 2. User — jj config edit --user or jj config path --user 3. Repo — jj config edit --repo or jj config path --repo 4. Workspace — jj config edit --workspace or jj config path --workspace 5. Command-line — --config name=value or --config-file path
Authority: jj official docs (config.md, "Config files and TOML").
User config locations (platform-specific, in precedence order):
$HOME/.jjconfig.toml<PLATFORM>/jj/config.toml(preferred)<PLATFORM>/jj/conf.d/*.toml(loaded alphabetically)
Where <PLATFORM> is $XDG_CONFIG_HOME or $HOME/.config on Linux/macOS.
The JJ_CONFIG environment variable overrides all default user config locations. It accepts a path to a TOML file, a directory of TOML files, or multiple paths separated by : (Unix) or ; (Windows).
# Find your config file
jj config path --user
# List all active config with origins
jj config list
# Set a value
jj config set --user ui.pager "less -FRX"Agent-Specific Configuration
Authority: ypares agent-skills (JJ_CONFIG pattern). jj official docs (config.md).
Agents should use a dedicated config file to prevent editor hangs and ensure predictable output:
# agent-jj-config.toml
[user]
name = "Agent"
email = "agent@example.com"
[ui]
editor = "TRIED_TO_RUN_AN_INTERACTIVE_EDITOR"
diff-formatter = ":git"
paginate = "never"Launch with: JJ_CONFIG=/path/to/agent-jj-config.toml <agent-harness>
Key settings for agents:
- `ui.editor` — set to a string that will error clearly if jj tries to open an editor
- `ui.diff-formatter` — use
:gitfor machine-parseable diffs (default:color-wordsis human-oriented) - `ui.paginate` — set to
"never"to prevent pager from blocking
Aliases
Authority: jj official docs (config.md, "Aliases").
[aliases]
# Show recent work on your anonymous bookmark
l = ["log", "-r", "(main..@):: | (main..@)-"]
# Show only your commits
mine = ["log", "-r", "mine()"]
# Show conflicted commits
conflicts = ["log", "-r", "conflicted()"]
# Show empty commits (candidates for cleanup)
empties = ["log", "-r", "empty() & mine()"]
# Quick diff of current change
d = ["diff"]Aliases run a single jj command. For multi-command aliases, use jj util exec:
[aliases]
sync = ["util", "exec", "--", "bash", "-c", """
set -euo pipefail
jj git fetch --all-remotes
jj rebase -s 'all:roots(trunk()..mine())' -o 'trunk()'
""", ""]Revset Aliases
Authority: jj official docs (config.md, revsets.md).
[revset-aliases]
# Customize which commits are immutable
"immutable_heads()" = "builtin_immutable_heads() | release@origin"
# Prevent rewriting other people's commits
"immutable_heads()" = "builtin_immutable_heads() | (trunk().. & ~mine())"Default jj log revset:
[revsets]
log = "main@origin.."Diff Format
Authority: jj official docs (config.md, "Diff format").
[ui]
# Built-in: ":color-words" (default), ":git", ":summary", ":stat", ":types", ":name-only"
diff-formatter = ":git"
# External tool (e.g. difftastic)
diff-formatter = ["difft", "--color=always", "$left", "$right"]
# Or reference a named tool
diff-formatter = "delta"Use an external pager with a formatter:
[ui]
pager = "delta"
diff-formatter = ":git"Merge Tools
Authority: jj official docs (config.md, "3-way merge tools").
[ui]
# Diff editor for jj split, jj squash -i
diff-editor = "meld" # or ":builtin" (default), "meld-3", "diffedit3"
# Merge editor for jj resolve
merge-editor = "meld" # or "kdiff3", "vscode", "vimdiff"Custom merge tool configuration:
[merge-tools.mytool]
program = "/path/to/mytool"
merge-args = ["$left", "$base", "$right", "-o", "$output"]Commit Signing
Authority: jj official docs (config.md, "Commit Signing").
[signing]
behavior = "own" # "drop" | "keep" | "own" | "force"
backend = "ssh" # "gpg" | "ssh" | "none"
key = "ssh-ed25519 AAAAC3..."
# Or path: key = "~/.ssh/id_for_signing.pub"Sign lazily only when pushing (avoids per-commit overhead):
[signing]
behavior = "drop"
backend = "ssh"
key = "ssh-ed25519 AAAAC3..."
[git]
sign-on-push = trueAuto-Track for Remotes
Authority: jj official docs (config.md, "Automatic tracking of bookmarks").
[remotes.origin]
auto-track-bookmarks = "*" # Track all bookmarks (default-like)
[remotes.upstream]
auto-track-bookmarks = "main" # Only track main from upstreamPersonal prefix pattern (avoids tracking coworkers' bookmarks):
[remotes.origin]
auto-track-bookmarks = "alice/*"Template Customization
Authority: jj official docs (config.md, templates.md).
[template-aliases]
# Show shortest unique prefix with minimum 12 chars
'format_short_id(id)' = 'id.shortest(12)'
# Relative timestamps
'format_timestamp(timestamp)' = 'timestamp.ago()'
# Show username instead of full email
'format_short_signature(signature)' = 'signature.email().local()'Conditional Config
Authority: jj official docs (config.md, "Conditional variables").
Apply different settings per repository, hostname, platform, or command:
# Override email for OSS repos
[[--scope]]
--when.repositories = ["~/oss"]
[--scope.user]
email = "oss@example.org"
# Use delta only for diff/show commands
[[--scope]]
--when.commands = ["diff", "show"]
[--scope.ui]
pager = "delta"Or split into conf.d/ files with top-level --when:
# In $XDG_CONFIG_HOME/jj/conf.d/work.toml
--when.repositories = ["~/work"]
[user]
email = "me@work.com"Code Formatting with jj fix
Authority: jj official docs (config.md, "Code formatting").
[fix.tools.prettier]
command = ["prettier", "--write", "--stdin-filepath=$path"]
patterns = ["glob:'**/*.{js,ts,jsx,tsx}'"]
[fix.tools.rustfmt]
command = ["rustfmt", "--emit", "stdout"]
patterns = ["glob:'**/*.rs'"]
enabled = false # Enable per-repo with: jj config set --repo fix.tools.rustfmt.enabled trueCommon Mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Config not taking effect | Wrong precedence level | Check with jj config list; repo overrides user |
| Editor opens unexpectedly | ui.editor not set for agent | Set ui.editor to a non-interactive string |
| Pager blocks execution | Pager waiting for input | Set ui.paginate = "never" |
| Bookmarks not tracking | auto-track-bookmarks too restrictive | Check remotes.<name>.auto-track-bookmarks pattern |
jj fix not running | Tool not enabled | Set fix.tools.<name>.enabled = true per-repo |
| Dotted TOML keys break | Mixed with headings incorrectly | Put dotted keys before the first [heading] |
Git Interop
How jj relates to Git — the colocated repo model, concept mapping, when to use raw git, and safety at the boundary.
Target version: jj 0.36+
For full references, see:
- references/git-to-jj.md — Complete command translation table
- references/git-experts.md — Why jj improves on Git workflows
- references/git-compatibility.md — Feature support matrix and colocated repo details
The Big Picture
jj uses Git as its storage backend. Every jj repo has a Git repo inside it — either visible (colocated) or hidden. Your commits are real Git commits. Your remotes are real Git remotes. Collaborators don't need to know you're using jj.
This means jj inherits Git's network layer, authentication, .gitignore support, and compatibility with forges like GitHub/GitLab. It replaces Git's CLI and workflow model, not its data model.
Colocated Repos
A colocated repo has both .jj/ and .git/ in the same directory. This is the default when you run jj git init or jj git clone.
Why colocated matters:
- Build tools, CI, and IDEs that expect
.git/work normally - You can mix
jjandgitcommands (with care) jjauto-syncs with.git/on every command — bookmarks, commits, and refs stay in sync
"Detached HEAD" is normal. Git will report detached HEAD in colocated repos. This is expected — jj has no concept of a "current branch." Use jj log for the real state, not git log.
# Check colocation status
jj git colocation status
# Convert between modes
jj git colocation enable # make colocated
jj git colocation disable # hide .git inside .jj/Non-Colocated Repos
Created with jj git init --no-colocate or jj git clone --no-colocate. The Git repo lives inside .jj/repo/store/git. You must use jj git import and jj git export to sync manually. Tools like gh CLI need GIT_DIR=.jj/repo/store/git.
Prefer colocated repos unless you have a specific reason not to.
Concept Mapping
| Git concept | jj equivalent | Key difference |
|---|---|---|
Staging area (git add) | No equivalent — working copy auto-commits | Use jj split / jj squash to move changes between commits |
| Branches | Bookmarks | Don't auto-advance on new commits; must be set explicitly |
HEAD / current branch | @ (working copy commit) | Always exists, always points to a real commit |
| Detached HEAD | Normal state | jj doesn't track a "current bookmark" |
| Reflog | Operation log (jj op log) | Tracks entire repo state, not per-ref |
git stash | jj new @- | Old working copy stays as a sibling commit |
git commit --amend | jj squash or jj describe | Descendants auto-rebase |
git rebase -i | jj rebase -r, jj squash --into | No interactive mode needed; each operation is atomic |
git cherry-pick | jj duplicate | |
git revert | jj revert | |
git worktree | jj workspace | Native support, no Git worktrees involved |
| Merge conflicts block work | Conflicts can be committed | Resolve later; conflicts are data, not errors |
Setting Up
# New repo (colocated by default)
jj git init
# Clone from remote
jj git clone https://github.com/org/repo.git
# Add jj to an existing Git repo
cd existing-git-repo
jj git init
# Non-colocated (Git hidden inside .jj/)
jj git init --no-colocateAfter jj git init in an existing Git repo, all Git history is available to jj immediately. Existing branches become bookmarks.
When to Use Raw git
jj handles most daily work. Use raw git for features jj doesn't support:
| Feature | jj support | What to do |
|---|---|---|
| Submodules | No | Use git submodule commands |
| Git LFS | No | Use git lfs commands |
| Annotated tags | No (lightweight only) | Use git tag -a |
.gitattributes | No | Edit the file directly; jj won't interpret it |
| Pre-commit hooks | No | Run git hook tools directly, or use pre-commit run --all-files |
| Partial/shallow clones | Limited | Use git clone --depth then jj git init |
git bisect (legacy) | Use jj bisect | jj has native bisect support |
After any mutating `git` command in a colocated repo, run any `jj` command (even jj st) to re-sync state. jj auto-imports on every command.
Mixing Commands Safely
Safe to mix (colocated repos):
- Read-only
gitcommands (git log,git show,git diff,git blame) — always safe git fetch— safe, but preferjj git fetchsince it auto-tracks bookmarksgit stash— works but unnecessary;jj new @-is the jj idiom
Use with care:
git commit,git rebase,git merge— work but can cause divergent change IDs or bookmark conflicts. Prefer jj equivalents.git switch/git checkout— may be needed before mutating git commands since jj leaves Git in detached HEAD state
Avoid:
git push— usejj git push; rawgit pushdesyncs bookmark trackinggit reset— usejj abandon,jj restore, orjj op restoreinstead
Recovery from Mixed-Command Issues
If mixing jj and git commands causes confusion (divergent change IDs, bookmark conflicts):
# See what happened
jj op log
# Undo the last jj operation (includes the auto-import of git changes)
jj undo
# Or restore to a known-good state
jj op restore <op-id>Import and Export
In colocated repos, import/export is automatic on every jj command. In non-colocated repos, you manage it:
# Import changes made in Git into jj
jj git import
# Export changes made in jj to Git
jj git exportWhat gets synced:
- Commits (both directions)
- Branch/bookmark pointers (both directions)
- Tags (import only — jj reads Git tags)
- Remote tracking refs (import only)
What doesn't sync:
- Git's staging area (ignored by jj)
- Git merge conflict state (ignored)
- Unfinished
git rebasestate (ignored)
Git Auth
jj delegates all network operations to Git. Your existing Git authentication (SSH keys, credential helpers, .netrc, GIT_ASKPASS) works unchanged. If git push works, jj git push works.
Agent Rules at the Git Boundary
1. Always prefer `jj` commands over git equivalents in jj repos 2. Use `jj git push`, never raw git push 3. After raw `git` mutations, run jj st to re-sync 4. Don't panic at "detached HEAD" — it's normal in colocated repos 5. For unsupported features (submodules, LFS, hooks), use git directly and document why
History Rewriting and Investigation
Rewriting history and investigating past changes in jj. Covers squashing, absorbing, rebasing, splitting commits (agent-safe), conflict resolution, investigating history, and cleanup.
Target version: jj 0.36+
For full references, see:
- references/conflicts.md — Conflict handling details and marker formats
- references/divergence.md — Divergent changes guide
Curating Commits
Squash: Fold Changes Into Parent
jj squash moves changes from the working-copy commit into its parent. This is the primary tool for folding work together.
Authority: jj official docs. jujutsu-skill (squash workflow).
# Squash all changes from @ into @-
jj squash
# Squash with a new message for the combined commit
jj squash -m "feat: complete user validation"
# Squash specific files only
jj squash src/auth.rs src/auth_test.rs
# Squash from a specific source into a specific destination
jj squash --from <change-id> --into <target-change-id>Agent rule: Always use -m when squashing — omitting it opens an editor.
After squashing, @ becomes empty (all changes moved to parent). Either start new work with jj new or abandon the empty commit.
Absorb: Auto-Distribute Changes
jj absorb automatically distributes each hunk in the working copy to the ancestor commit that last modified those lines. It's like smart squash — hunks go to the right place without manual targeting.
Authority: jj official docs (git-experts.md — absorb section).
# Absorb all working-copy changes into appropriate ancestors
jj absorb
# Preview what would happen (dry run not available — use jj undo to reverse)
jj absorb
jj log -r 'ancestors(@, 5)' # verify the result
jj undo # if it wasn't rightWhen to use absorb vs squash:
- absorb — working copy has changes to lines touched by different ancestor commits
- squash — all changes belong in one parent commit, or you need explicit control
Rebase: Move Commits
jj rebase moves commits to a new parent. Descendants are automatically rebased. Conflicts are recorded, not blocking.
Authority: jj official docs. ypares working-with-jj.
# Rebase current commit and descendants onto trunk
jj rebase -s @ -o trunk()
# Rebase a single commit (not its descendants)
jj rebase -r <change-id> -o trunk()
# Rebase a bookmark's branch onto trunk
jj rebase -b my-feature -o trunk()Flag meanings:
| Flag | What it rebases |
|---|---|
-s <rev> | The revision AND all its descendants |
-r <rev> | Only that single revision (descendants rebase onto its parent) |
-b <rev> | The entire branch containing rev (all commits not on destination) |
-o <dest> | Destination (what to rebase onto). Use -o, not deprecated -d |
After rebase: always jj st to check for conflicts. If conflicts appear, see the Conflict Resolution section.
Splitting Commits
Splitting divides one commit into multiple focused commits. This is common after making a large change that should be multiple atomic commits.
Authority: edmundmiller jj-history-investigation. jujutsu-skill (split warning).
Agent-Safe Splitting
jj split without file paths is interactive and will hang. Two safe approaches:
Approach 1: Split by file paths (non-interactive)
# Split specific files out of a commit — the named files go into the first
# commit, everything else stays in the second
jj split -r <change-id> src/auth.rs src/auth_test.rs -m "feat: add auth module"This works when the split boundary aligns with file boundaries.
Approach 2: The restore workflow (any boundary)
When you need to split within a file or the boundary is complex, use jj restore to carve out changes:
# 1. Create a new commit on top of the one to split
jj new <change-id> -m "part 2: error handling"
# 2. Restore (copy) only the files you DON'T want in part 2
# This effectively removes them from the new commit
jj restore --from <change-id>- src/validation.rs
# 3. Now squash what remains of the original into a focused message
jj describe -r <change-id> -m "part 1: input validation"
# 4. Verify both commits
jj show <change-id>
jj show @The restore workflow gives you full control without any interactive prompts.
Splitting Immutable Commits
Commits with descendants or in shared history are immutable by default. Override with --ignore-immutable:
jj edit <change-id> --ignore-immutable
jj split --ignore-immutable src/module.rs -m "refactor: extract module"Authority: edmundmiller jj-history-investigation (immutability override section).
When `--ignore-immutable` is safe:
- The commits are local-only (not pushed)
- You own all descendant commits
- No collaborators are affected
What happens: all descendant commits are rewritten with new commit IDs. Change IDs stay the same. Bookmarks on affected commits follow the rewrite.
Conflict Resolution
jj records conflicts in commits instead of blocking operations. A rebase or merge that produces conflicts still succeeds — the conflicted state is stored and you resolve it when ready.
Authority: jj official docs (conflicts.md). steveklabnik jujutsu-tutorial (conflicts chapter).
Identifying Conflicts
# Check current status for conflicts
jj st
# Find all conflicted commits in your branch
jj log -r 'conflicts() & trunk()..@'
# Show what files are conflicted in a specific commit
jj show <change-id>The Resolution Workflow
Method 1: Edit directly (simple conflicts)
# Edit the conflicted commit
jj edit <conflicted-change-id>
# Open the conflicted file — it contains conflict markers
# Edit the file to resolve, removing all markers
# jj auto-snapshots when done
# Verify
jj st # should show no conflictsMethod 2: New commit + squash (complex conflicts, safer)
# Create a child commit to work in
jj new <conflicted-change-id>
# Edit files to resolve conflicts
# Verify with jj diff
# Squash the resolution into the conflicted parent
jj squashMethod 2 is safer because if the resolution goes wrong, you can jj abandon @ to start over without affecting the conflicted commit.
Authority: steveklabnik jujutsu-tutorial (conflicts chapter — new+squash workflow).
Understanding Conflict Markers
jj uses a diff-based marker format that's different from Git's:
<<<<<<< conflict 1 of 1
%%%%%%% diff from base to side 1
unchanged line
-removed in side 1
+added in side 1
+++++++ side 2 content
side 2 full content here
>>>>>>> conflict 1 of 1 ends%%%%%%%— A diff to apply (shows what one side changed)+++++++— A snapshot (shows the full content of the other side)- Resolution: apply the diff mentally to the snapshot, or write the correct combined result
Alternative styles can be set via ui.conflict-marker-style in config:
"snapshot"— shows full content of each side (no diffs)"git"— Git-compatible<<<</====/>>>>markers (2-sided only)
Authority: jj official docs (conflicts.md — conflict marker styles).
Agent Conflict Resolution Rules
1. Never use `jj resolve` — it opens an interactive merge tool 2. Edit conflict markers directly in the file 3. Remove ALL marker lines (<<<<<<<, >>>>>>>, %%%%%%%, +++++++) 4. Verify with `jj st` — output should show no conflict warnings 5. Auto-rebase propagation — resolving a parent conflict automatically re-resolves descendants that inherited it
Handling Divergent Changes
A divergent change occurs when multiple visible commits share the same change ID. This can happen when a hidden predecessor becomes visible again, or when two processes amend the same change simultaneously.
Authority: jj official docs (guides/divergence.md).
Divergent changes show in jj log with a /0, /1 offset and a "divergent" label:
@ mzvwutvl/0 ... (divergent)Resolution Strategies
# Strategy 1: Abandon the unwanted version
jj abandon <unwanted-commit-id>
# Strategy 2: Give one version a new change ID (keep both)
jj metaedit --update-change-id <commit-id>
# Strategy 3: Squash them together
jj squash --from <source-commit-id> --into <target-commit-id>Note: When referring to divergent commits, use their commit ID or change ID with offset (mzvwutvl/0), since the change ID alone is ambiguous.
Investigating History
Viewing Commits
# Show a specific commit's changes
jj show <change-id>
# Show with file statistics
jj show <change-id> --stat
# View diff of a specific commit
jj diff -r <change-id>
# View diff of a specific file in a commit
jj diff -r <change-id> src/main.rs
# View file content at a specific revision (without switching)
jj file show -r <change-id> src/main.rsTracking Line History
# Who last changed each line (like git blame)
jj file annotate src/main.rs
# Find the commit that touched a specific file
jj log -r 'files("src/main.rs")'
# Find commits containing specific diff text
jj log -r 'diff_lines("TODO")'
# Search commit messages
jj log -r 'description(substring-i:"auth")'Authority: edmundmiller jj-history-investigation (annotate and investigation techniques).
Viewing Commit Evolution
jj evolog shows how a single change evolved over time — every rewrite, amend, and squash:
# See all versions of a change
jj evolog -r <change-id>
# With patches to see what changed between versions
jj evolog -r <change-id> -pOperation Log
The operation log records every jj operation, enabling full undo:
# View operation history
jj op log
# Undo the last operation
jj undo
# Restore repo to a specific past state
jj op restore <op-id>jj undo can be repeated to step further back. jj op restore jumps directly to any point.
Abandoning and Cleanup
Abandoning Commits
jj abandon removes a commit. Its descendants are rebased onto its parent:
# Abandon a specific commit
jj abandon <change-id>
# Abandon multiple commits
jj abandon <id1> <id2> <id3>
# Abandon a range
jj abandon 'empty() & trunk()..@'Cleaning Up Empty Commits
After squashing or rebasing, empty commits may remain. Find and remove them:
# Find empty commits in your branch
jj log -r 'empty() & trunk()..@'
# Abandon all empty commits in your branch
jj abandon 'empty() & mine() & trunk()..@'Caution: Some empty commits are intentional (e.g., merge commits, placeholder commits). Check before bulk-abandoning.
Reverting Changes
To undo a commit's changes without removing it from history:
# Create a new commit that reverses the changes
jj revert -r <change-id>Verification Checklist
After any major history rewrite (split, rebase, large squash):
1. No conflicts: jj log -r 'conflicts() & trunk()..@' — should return nothing 2. No unintended empties: jj log -r 'empty() & trunk()..@' — review any results 3. Commits are focused: jj show <id> --stat for each rewritten commit 4. Messages are clear: jj log -r 'trunk()..@' — check descriptions 5. Bookmarks correct: jj bookmark list — verify positions 6. Status clean: jj st — no unexpected state
Common Mistakes
- Using `jj split` without file paths — hangs waiting for interactive input. Always provide paths or use the restore workflow.
- Using `jj squash -i` — interactive, hangs in agent environments. Use
jj squash(all) orjj squash <paths>(specific files). - Forgetting `-m` on squash — opens an editor. Always
jj squash -m "message". - Not checking for conflicts after rebase — rebase succeeds even with conflicts. Always
jj stafterward. - Using `--ignore-immutable` on shared history — rewrites commit IDs, breaking collaborators. Only use on local-only commits.
- Abandoning merge commits carelessly — merge commits may carry resolution data. Check with
jj showfirst. - Not using `jj undo` for recovery — if a split, squash, or rebase goes wrong,
jj undoimmediately reverses it. Don't try to manually fix broken state.
jj
A consolidated skill for Jujutsu (jj), a Git-compatible version control system. Covers the mental model, agent-specific rules, daily workflows, revsets/filesets/templates, bookmarks and sharing, history rewriting, workspaces for parallel agents, and configuration.
Structure
SKILL.md— Router: mental model, agent rules, core workflow, essential commands, and routing to deep divesrevsets.md— Revsets, filesets, and templates (the three query languages)sharing.md— Bookmarks, remotes, pushing, pulling, and GitHub/GitLab PR workflowshistory.md— History rewriting and investigation (squash, absorb, rebase, split, conflicts)workspaces.md— Workspaces for parallel agents (isolated working copies)config.md— Configuration and customization (precedence, aliases, diff/merge tools, signing)
References
references/git-to-jj.md— Git-to-jj command mapping tablereferences/git-experts.md— Why jj improves on Git for power usersreferences/command-gotchas.md— Flag semantics, quoting, deprecated flagsreferences/revsets.md— Complete revset language specreferences/filesets.md— Complete fileset language specreferences/templates.md— Complete template language specreferences/bookmarks.md— Complete bookmarks referencereferences/github.md— GitHub/GitLab workflow detailsreferences/git-compatibility.md— Git interop and colocated reposreferences/conflicts.md— Conflict handling and marker formatsreferences/divergence.md— Divergent changes guidereferences/config-reference.md— Full configuration referencereferences/parallel-agents.md— Parallel agent setup guide
Attribution & License
This skill synthesizes guidance from:
- Jujutsu — the jj VCS itself. Official documentation used for reference material. Licensed under Apache-2.0.
- Steve Klabnik's Jujutsu Tutorial — narrative tutorial providing mental model and conceptual grounding.
- jujutsu-skill by Dan Verbraganza — agent-specific workflow patterns and environment rules. Licensed under MIT.
- dot-claude jj-workflow by TrevorS — concise AI-focused daily workflow patterns. Licensed under ISC.
- agent-skills working-with-jj by Yves Parès — version-aware (0.36.x) command syntax,
JJ_CONFIGagent configuration pattern. Licensed under MIT. - jjtask by Alexander Ryzhikov — anti-patterns and gotchas for agent use. Licensed under MIT.
- sgai by Sandgarden — Git-to-jj command mapping table (synthesized, not copied). Licensed under modified MIT.
- dotfiles jj-history-investigation by Edmund Miller — history investigation techniques. Licensed under MIT.
Bookmarks
Introduction
Bookmarks are named pointers to revisions (just like branches are in Git). You can move them without affecting the target revision's identity. Bookmarks automatically move when revisions are rewritten (e.g. by jj rebase). You can pass a bookmark's name to commands that want a revision as argument. For example, jj new main will create a new revision on top of the main bookmark. Use jj bookmark list to list bookmarks and jj bookmark <subcommand> to create, move, or delete bookmarks. There is currently no concept of an active/current/checked-out bookmark.
Mapping to Git branches
Jujutsu maps its bookmarks to Git branches when interacting with Git repos. For example, jj git push --bookmark foo will push the state of the foo bookmark to the foo branch on the Git remote. Similarly, if you create a bar branch in the backing Git repo, then a subsequent jj git import will create a bar bookmark (reminder: that import happens automatically in [colocated workspaces][colocated-workspaces]).
Remotes and tracked bookmarks
Jujutsu records the last seen position of a bookmark on each remote (just like Git's remote-tracking branches). This record is updated on every jj git fetch and jj git push of the bookmark. You can refer to the remembered remote bookmark positions with <bookmark name>@<remote name>, such as jj new main@origin. jj does not provide a way to manually edit these recorded positions.
A remote bookmark can be associated with a local bookmark of the same name. This is called a tracked remote bookmark (which maps to a Git remote branch when using the Git backend). When you pull a tracked bookmark from a remote, any changes compared to the current record of the remote's state will be propagated to the corresponding local bookmark, which will be created if it doesn't exist already.
!!! note "Details: how fetch pulls bookmarks"
Let's say you run jj git fetch --remote origin and, during the fetch, jj determines that the remote's main bookmark has been moved so that its target is now ahead of the local record in main@origin.
jj will then update main@origin to the new target. If main@origin is tracked, jj will also apply the change to the local bookmark main. If the local target has also been moved compared to main@origin (probably because you ran jj bookmark set main), then the two updates will be merged. If one is ahead of the other, then that target will become the new target. Otherwise, the local bookmark will become conflicted (see the "Conflicts" section below for details).
Most commands don't show the tracked remote bookmark if it has the same target as the local bookmark. The local bookmark (without @<remote name>) is considered the bookmark's desired target. Consequently, if you want to update a bookmark on a remote, you first update the bookmark locally and then push the update to the remote. If a local bookmark also exists on some remote but points to a different target there, jj log will show the bookmark name with an asterisk suffix (e.g. main*). That is meant to remind you that you may want to push the bookmark to some remote.
If you want to know the internals of bookmark tracking, consult the [Design Doc][design].
Terminology summary
- A remote bookmark is a bookmark ref on the remote.
jjcan find out its
actual state only when it's actively communicating with the remote. However, jj does store the last-seen position of the remote bookmark; this is the commit jj show <bookmark name>@<remote name> would show. This notion is completely analogous to Git's "remote-tracking branches".
- A tracked (remote) bookmark is defined above. You can make a remote bookmark
tracked with the `jj bookmark track` command, for example.
- A tracking (local) bookmark is the local bookmark that
jjtries to keep
in sync with the tracked remote bookmark. For example, after jj bookmark track mybookmark --remote=origin, there will be a local bookmark mybookmark that's tracking the remote mybookmark@origin bookmark. A local bookmark can track a bookmark of the same name on 0 or more remotes.
The notion of tracked bookmarks serves a similar function to the Git notion of an "upstream branch". Unlike Git, a single local bookmark can be tracking remote bookmarks on multiple remotes, and the names of the local and remote bookmarks must match.
Manually tracking a bookmark
To track a bookmark permanently use jj bookmark track <bookmark name> --remote=<remote name>. It will now be imported as a local bookmark until you untrack it or it is deleted on the remote.
Example:
$ # List all available bookmarks, as we want our colleague's bookmark.
$ jj bookmark list --all
$ # Find the bookmark.
$ # [...]
$ # Actually track the bookmark.
$ jj bookmark track <bookmark name> --remote=<remote name> # Example: jj bookmark track my-feature --remote=origin
$ # From this point on, <bookmark name> will be imported when fetching from <remote name>.
$ jj git fetch --remote <remote name>
$ # A local bookmark <bookmark name> should have been created or updated while fetching.
$ jj new <bookmark name> # Do some local testing, etc.Untracking a bookmark
To stop following a remote bookmark, you can jj bookmark untrack it. After that, subsequent fetches of that remote will no longer move the local bookmark to match the position of the remote bookmark.
Example:
$ # List all local and remote bookmarks.
$ jj bookmark list --all
$ # Find the bookmark we no longer want to track.
$ # [...]
# # Actually untrack it.
$ jj bookmark untrack <bookmark name> --remote=<remote name> # Example: jj bookmark untrack stuff --remote=origin
$ # From this point on, this remote bookmark won't be imported anymore.
$ # The local bookmark (e.g. stuff) is unaffected. It may or may not still
$ # be tracking bookmarks on other remotes (e.g. stuff@upstream).Listing tracked bookmarks
To list tracked bookmarks, you can jj bookmark list --tracked or jj bookmark list -t. This command omits local Git-tracking bookmarks by default.
You can see if a specific bookmark is tracked with jj bookmark list --tracked <bookmark name>.
Automatic tracking of bookmarks & auto-track-bookmarks option
There are two situations where jj tracks bookmarks automatically. jj git clone automatically sets up the default remote bookmark (e.g. main@origin) as tracked. When you push a local bookmark, the newly created bookmark on the remote is marked as tracked.
By default, every other remote bookmark is marked as "not tracked" when it's fetched. If desired, you need to manually jj bookmark track them. This works well for repositories where multiple people work on a large number of bookmarks.
The default can be changed by setting the config remotes.<name>.auto-track-bookmarks = "*". Then, jj git fetch tracks every newly fetched bookmark with a local bookmark. Branches that already existed before the jj git fetch are not affected. This is similar to Mercurial, which fetches all its bookmarks (equivalent to Git's branches) by default. Similarly, all newly created local bookmarks will be marked as "tracked", preparing them to be pushed with the next jj git push command. See "Automatic tracking of bookmarks" for details.
Bookmark updates
Currently Jujutsu automatically updates local bookmarks when these conditions are met:
- When a commit has been rewritten (e.g, when you rebase) bookmarks and the
working-copy will move along with it.
- When a commit has been abandoned, all associated bookmarks will be deleted.
You could describe the updates as following along the change-id of the current bookmark commit, even if it isn't entirely accurate.
Pushing bookmarks: Safety checks
Before jj git push actually moves, creates, or deletes a remote bookmark, it makes several safety checks.
1. jj will contact the remote and check that the actual state of the remote bookmark matches jj's record of its last known position. If there is a conflict, jj will refuse to push the bookmark. In this case, you need to run jj git fetch --remote <remote name> and resolve the resulting bookmark conflict. Then, you can try jj git push again.
If you are familiar with Git, this makes jj git push similar to git push --force-with-lease.
There are a few cases where jj git push will succeed even though the remote bookmark is in an unexpected location. These are the cases where jj git fetch would not create a bookmark conflict and would not move the local bookmark, e.g. if the unexpected location is identical to the local position of the bookmark.
2. The local bookmark must not be conflicted. If it is, you would need to use jj bookmark move, for example, to resolve the conflict.
This makes jj git push safe even if jj git fetch is performed on a timer in the background (this situation is a known issue[^known-issue] with some forms of git push --force-with-lease). If the bookmark moves on a remote in a problematic way, jj git fetch will create a conflict. This should ensure that the user becomes aware of the conflict before they can jj git push and override the bookmark on the remote.
3. If the remote bookmark already exists on the remote, it must be tracked.
[^known-issue]: See "A general note on safety" in <https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease>
Conflicts
Bookmarks can end up in a conflicted state. When that happens, jj status will include information about the conflicted bookmarks (and instructions for how to mitigate it). jj bookmark list will have details. jj log will show the bookmark name with a double question mark suffix (e.g. main??) on each of the conflicted bookmark's potential target revisions. Using the bookmark name to look up a revision will resolve to all potential targets. That means that jj new main will error out, complaining that the revset resolved to multiple revisions.
Both local bookmarks (e.g. main) and the remote bookmark (e.g. main@origin) can have conflicts. Both can end up in that state if concurrent operations were run in the repo. The local bookmark more typically becomes conflicted because it was updated both locally and on a remote.
To resolve a conflicted state in a local bookmark (e.g. main), you can move the bookmark to the desired target with jj bookmark move. You may want to first either merge the conflicted targets with jj new (e.g. jj new main), or you may want to rebase one side on top of the other with jj rebase.
To resolve a conflicted state in a remote bookmark (e.g. main@origin), simply pull from the remote (e.g. jj git fetch). The conflict resolution will also propagate to the local bookmark (which was presumably also conflicted).
Ease of use
The use of bookmarks is frequent in some workflows, for example, when interacting with Git repositories containing branches. To this end, one-letter shortcuts have been implemented, both for the jj bookmark command itself through an alias (as jj b), and for its subcommands. For example, jj bookmark create BOOKMARK-NAME -r@ can be abbreviated as jj b c BOOKMARK-NAME -r@.
[colocated-workspaces]: git-compatibility.md#colocated-jujutsugit-repos [design]: design/tracking-branches.md
jj Command Gotchas
Pitfalls, flag semantics, and version-specific changes that trip up both humans and agents. Targets jj 0.36+.
Flag Semantics: Source Flags
Source flags select what to operate on:
| Flag | Short | Meaning | Example |
|---|---|---|---|
--revision / --revisions | -r | Exactly the named revision(s) | jj log -r xyz |
--source | -s | The revision and all its descendants (REV::) | jj rebase -s xyz -o main |
--from | -f | The contents of a revision (for diff/move operations) | jj squash --from @- --into @ |
--branch | -b | A topological branch relative to the destination | jj rebase -b @ -o main |
Flag Semantics: Destination Flags
Destination flags select where to put the result:
| Flag | Short | Meaning | Example |
|---|---|---|---|
--onto | -o | Place as children of the target | jj rebase -r xyz -o main |
--insert-after | -A | Insert between target and its children | jj rebase -r xyz -A main |
--insert-before | -B | Insert between target and its parents | jj rebase -r xyz -B main |
--to / --into | -t | Move contents into the target revision | jj squash --into abc |
--to and --into are interchangeable. --into reads more naturally with jj squash.
The -r Short Flag Rule
Many commands only accept the short form -r, not --revision or --revisions:
# ✅ Correct
jj log -r xyz
jj desc -r xyz -m "message"
# ❌ Error on many commands
jj log --revisions xyz
jj desc --revision xyzRule: Always use `-r`.
Some commands allow omitting -r entirely when no paths are involved:
jj abandon, jj describe, jj duplicate, jj new, jj show, jj parallelize
Deprecated Flags (v0.36+)
| Old | New | Commands affected |
|---|---|---|
-d / --destination | -o / --onto | rebase, split, revert |
--edit (on describe) | --editor | describe |
The old flags still work with a deprecation warning but will be removed.
Symbol Strictness (v0.32+)
Bare symbols that match multiple change IDs cause an error:
# ❌ Error if "abc" is ambiguous (matches multiple change IDs)
jj log -r abc
# ✅ Explicit prefix query
jj log -r 'change_id(abc)'Use longer prefixes or the change_id() function to disambiguate.
Fileset Defaults (v0.36+)
Path arguments use glob patterns by default:
# Glob match (default) — matches src/foo.rs, src/bar.rs, etc.
jj diff 'src/*.rs'
# Literal path with special characters
jj diff 'cwd:"src/[special].rs"'Watch out for [ brackets in patterns — they're interpreted as character classes, not literals.
Shell Quoting
Revsets contain characters the shell interprets ((), ", *, |, &). Always quote:
# ✅ Single quotes (safest)
jj log -r 'description(substring:"fix")'
# ✅ Double quotes with escaping
jj log -r "description(substring:\"fix\")"
# ❌ Unquoted — shell breaks it
jj log -r description(substring:"fix")Rule: Always single-quote revset expressions.
--no-edit for Parallel Work
Without --no-edit, jj new moves the working copy to the new commit:
# ❌ B becomes child of A (@ moved to A, then to B)
jj new parent -m "A"
jj new parent -m "B"
# ✅ Both are siblings, children of parent (@ unchanged)
jj new --no-edit parent -m "A"
jj new --no-edit parent -m "B"jj commit vs jj new
Both create new commits, but they differ:
jj commit -m "msg"— Finalizes the working copy with the given message, then creates a new empty working copy on top.jj new -m "msg"— Creates a new empty commit with the message on top of@. The current working copy's description stays as-is.
Most jj workflows prefer: jj describe -m "what I did" → code → jj new -m "next task".
jj split Is Interactive
jj split without file arguments opens an interactive diff selector. This hangs in agent environments.
Agent-safe alternatives:
# Split by file paths (non-interactive)
jj split -r <rev> file1.py file2.py
# Move specific files out using restore
jj new -m "part 2"
jj restore --from @- file1.py file2.pyBookmarks Don't Auto-Advance
Unlike Git branches, jj bookmarks don't move when new commits are created. They do follow when a commit is rewritten (rebased, squashed, etc.), but you must explicitly set them before pushing:
# After creating commits, explicitly set the bookmark
jj bookmark set my-feature -r @
# Then push
jj git push -b my-featureCommon Recovery Commands
| Situation | Command |
|---|---|
| Undo last operation | jj undo |
| View operation history | jj op log |
| Restore to earlier state | jj op restore <op-id> |
| Discard working copy changes | jj restore |
| Discard a commit | jj abandon <rev> |
| Undo specific commit content | jj restore --from @- <paths> |
First-class conflicts
Introduction
Conflicts happen when Jujutsu can't figure out how to merge different changes made to the same file. For instance, this can happen if two people are working on the same file and make different changes to the same part of the file, and then their commits are merged together with jj new (or one is rebased onto the other with jj rebase).
Unlike most other VCSs, Jujutsu can record conflicted states in commits. For example, if you rebase a commit and it results in a conflict, the conflict will be recorded in the rebased commit and the rebase operation will succeed. You can then resolve the conflict whenever you want. Conflicted states can be further rebased, merged, or backed out. Note that what's stored in the commit is a logical representation of the conflict, not conflict markers; rebasing a conflict doesn't result in a nested conflict markers (see technical doc for how this works).
Advantages
The deeper understanding of conflicts has many advantages:
- Removes the need for things like
git rebase/merge/cherry-pick/etc --continue. Instead, you get a single workflow for resolving conflicts: check out the conflicted commit, resolve conflicts, and amend.
- Enables the "auto-rebase" feature, where descendants of rewritten commits
automatically get rewritten. This feature mostly replaces Mercurial's Changeset Evolution.
- Lets us define the change in a merge commit as being compared to the merged
parents. That way, we can rebase merge commits correctly (unlike both Git and Mercurial). That includes conflict resolutions done in the merge commit, addressing a common use case for git rerere. Since the changes in a merge commit are displayed and rebased as expected, evil merges are arguably not as evil anymore.
- Allows you to postpone conflict resolution until you're ready for it. You
can easily keep all your work-in-progress commits rebased onto upstream's head if you like.
and octopus merges become trivial (implementation-wise); some cases that Git can't currently handle, or that would result in nested conflict markers, can be automatically resolved.
- Enables collaborative conflict resolution. (This assumes that you can share
the conflicts with others, which you probably shouldn't do if some people interact with your project using Git.)
For information about how conflicts are handled in the working copy, see here.
Conflict markers
Conflicts are "materialized" using conflict markers in various contexts. For example, when you run jj new or jj edit on a commit with a conflict, it will be materialized in the working copy. Conflicts are also materialized when they are part of diff output (e.g. jj show on a commit that introduces or resolves a conflict).
As an example, imagine that you have a file which contains the following text, all in lowercase:
apple
grape
orangeOne person replaces the word "grape" with "grapefruit" in commit A, while another person changes every line to uppercase in commit B. If you merge the changes together with jj new A B, the resulting commit will have a conflict since Jujutsu can't figure out how to combine these changes. Therefore, Jujutsu will materialize the conflict in the working copy using conflict markers, which would look like this:
<<<<<<< conflict 1 of 1
%%%%%%% diff from: vpxusssl 38d49363 "merge base"
\\\\\\\ to: rtsqusxu 2768b0b9 "commit A"
apple
-grape
+grapefruit
orange
+++++++ ysrnknol 7a20f389 "commit B"
APPLE
GRAPE
ORANGE
>>>>>>> conflict 1 of 1 endsThe markers <<<<<<< and >>>>>>> indicate the start and end of a conflict respectively. The marker +++++++ indicates the start of a snapshot, while the marker %%%%%%% indicates the start of a diff to apply to the snapshot. The \\\\\\\ marker just allows the label to be split across two lines to make it more readable.
Therefore, to resolve this conflict, you would apply the diff (changing "grape" to "grapefruit") to the snapshot (the side with every line in uppercase), editing the file to look like this:
APPLE
GRAPEFRUIT
ORANGEIn practice, conflicts are usually 2-sided, meaning that there's only 2 conflicting changes being merged together at a time, but Jujutsu supports conflicts with arbitrarily many sides, which can happen when merging 3 or more commits at once. In that case, you would see a single snapshot section and multiple diff sections.
Compared to just showing the content of each side of the conflict, the main benefit of Jujutsu's style of conflict markers is that you don't need to spend time manually comparing the sides to spot the differences between them. This is especially beneficial for many-sided conflicts, since resolving them just requires applying each diff to the snapshot one-by-one.
Alternative conflict marker styles
If you prefer to just see the contents of each side of the conflict without the diff, Jujutsu also supports a "snapshot" style, which can be enabled by setting the ui.conflict-marker-style config option to "snapshot":
<<<<<<< conflict 1 of 1
+++++++ rtsqusxu 2768b0b9 "commit A"
apple
grapefruit
orange
------- vpxusssl 38d49363 "merge base"
apple
grape
orange
+++++++ ysrnknol 7a20f389 "commit B"
APPLE
GRAPE
ORANGE
>>>>>>> conflict 1 of 1 endsSome tools expect Git-style conflict markers, so Jujutsu also supports Git's "diff3" style conflict markers by setting the ui.conflict-marker-style config option to "git":
<<<<<<< rtsqusxu 2768b0b9 "commit A"
apple
grapefruit
orange
||||||| vpxusssl 38d49363 "merge base"
apple
grape
orange
=======
APPLE
GRAPE
ORANGE
>>>>>>> ysrnknol 7a20f389 "commit B"This conflict marker style only supports 2-sided conflicts though, so it falls back to the similar "snapshot" conflict markers if there are more than 2 sides to the conflict.
Long conflict markers
Some files may contain lines which could be confused for conflict markers. For instance, a line could start with =======, which looks like a Git-style conflict marker. To ensure that it's always unambiguous which lines are conflict markers and which are just part of the file contents, jj sometimes uses conflict markers which are longer than normal:
<<<<<<<<<<<<<<< conflict 1 of 1
%%%%%%%%%%%%%%% diff from: wqvuxsty cb9217d5 "merge base"
\\\\\\\\\\\\\\\ to: kwntsput 0e15b770 "commit A"
-Heading
+HEADING
=======
+++++++++++++++ mpnwrytz 52020ed6 "commit B"
New Heading
===========
>>>>>>>>>>>>>>> conflict 1 of 1 endsConflicts with missing terminating newline
When materializing conflicts, jj outputs them in a line-based format, with each conflict marker line terminated by a newline character (\n). This format works well for text files that consist of a series of lines, with each line terminated by a newline character as well.
While most text files follow this convention, some do not. Since conflict markers must appear on their own line, when jj encounters a missing terminating newline character in a conflict, it still needs to ensure that there is a newline before the next conflict marker. To do this, it adds an extra newline to each term of the conflict. Then, to compensate for this extra newline, it also omits the terminating newline from the "end of conflict" marker (>>>>>>>).
For instance, if a file originally contained grape with no terminating newline character, and one person changed grape to grapefruit, while another person added the missing newline character to make grape\n, the resulting conflict would look like this (␊ demonstrates the \n character explicitly, note that the last line doesn't have the terminating \n character):
<<<<<<< conflict 1 of 1␊
+++++++ tlwwkqxk d121763d "commit A" (no terminating newline)␊
grapefruit␊
%%%%%%% diff from: qwpqssno fe561d93 "merge base" (no terminating newline)␊
\\\\\\\ to: poxkmrxy c735fe02 "commit B"␊
grape␊
+␊
>>>>>>> conflict 1 of 1 endsTherefore, a resolution of this conflict could be grapefruit\n, with the terminating newline character added.
Handling divergent changes
What are divergent changes?
A [divergent change] occurs when multiple [visible commits] have the same change ID. These changes are displayed with a [change offset] after their change ID and a label of "divergent":
$ jj log
@ mzvwutvl/0 test.user@example.com 2001-02-03 08:05:12 29d07a2d (divergent)
│ a divergent changeNormally, when commits are rewritten, the original version (the "predecessor") becomes hidden and the new commit (the "successor") is visible. Thus, only one commit with a given change ID is visible at a time.
But, a hidden commit can become visible again. This can happen if:
- A visible descendant is added locally. For example,
jj new REVwill make
REV visible even if it was hidden before.
- A visible descendant is fetched from a remote. If the hidden commit was pushed
to a remote, others may base new commits off of them. When their new commits are fetched, their visibility makes the hidden commit visible again.
- It is made the working copy.
jj edit REVwill makeREVand all its
ancestors visible if it wasn't already.
- Some other operations make hidden commits visible. For example, adding a
bookmark to a hidden commit makes it visible with the assumption that you are now working with that commit again.
Divergent changes also occur if two different users or processes amend the same change, creating two visible successors. This can happen when:
- Another author modifies commits in a branch that you have also modified
locally.
- You perform operations on the same change from different workspaces of the
same repository.
- Two programs modify the repository at the same time. For example, you run
jj describe and, while writing your commit description, an IDE integration fetches and rebases the branch you're working on.
[divergent change]: ../glossary.md#divergent-change [visible commits]: ../glossary.md#visible-commits [change offset]: ../glossary.md#change-offset
How do I resolve divergent changes?
When you encounter divergent changes, you have several strategies to choose from. The best approach depends on whether you want to keep the content from one commit, both commits, or merge them together.
Note that revsets must refer to the divergent commit either using its commit ID or using its change ID with a [change offset] like /0 or /1 as shown in the log, since the change ID is ambiguous by itself.
Strategy 1: Abandon one of the commits
If one of the divergent commits is clearly obsolete or incorrect, simply abandon it:
# Abandon the unwanted commit using its commit ID (or change ID with offset)
jj abandon <unwanted-commit-id>
# You can abandon several at once with:
# jj abandon abc def 123
# jj abandon abc::This is the simplest solution when you know which version to keep.
Strategy 2: Generate a new change ID
If you want to keep both versions as separate changes with different change IDs, you can generate a new change ID for one of the commits:
jj metaedit --update-change-id <commit-id>This preserves both versions of the content while resolving the divergence.
Strategy 3: Squash the commits together
When you want to combine the content from both divergent commits:
# Squash one commit into the other
jj squash --from <source-commit-id> --into <target-commit-id>This combines the changes from both commits into a single commit. The source commit will be abandoned.
Strategy 4: Ignore the divergence
Divergence isn't an error. If the divergence doesn't cause immediate problems, you can leave it as-is. If both commits are part of immutable history, this may be your only option.
However, it can be inconvenient since you cannot refer to divergent changes unambiguously using their change ID alone.
Filesets
Jujutsu supports a functional language for selecting a set of files. Expressions in this language are called "filesets" (the idea comes from Mercurial). The language consists of file patterns, operators, and functions.
Quoting file names
Many jj commands accept fileset expressions as positional arguments. File names passed to these commands [must be quoted][string-literals] if they contain whitespace or meta characters. However, as a special case, quotes can be omitted if the expression has no operators nor function calls. For example:
jj diff 'Foo Bar'(shell quotes are required, but inner quotes are optional)jj diff '~"Foo Bar"'(both shell and inner quotes are required)jj diff '"Foo(1)"'(both shell and inner quotes are required)
Glob characters aren't considered meta characters, but shell quotes are still required:
jj diff '~glob:**/*.rs'
[string-literals]: templates.md#stringliteral-type
File patterns
The following patterns are supported. In all cases, we do not mention any shell quoting that might be necessary, and the quotes around "path" are optional if the path has no special characters.
By default, "path" is parsed as a prefix-glob: pattern, which matches cwd-relative path prefix.
cwd:"path": Matches cwd-relative path prefix (file or files under directory
recursively.)
file:"path"orcwd-file:"path": Matches cwd-relative file (or exact) path.glob:"pattern"orcwd-glob:"pattern": Matches file paths with cwd-relative
Unix-style shell [wildcard pattern][glob]. For example, glob:"*.c" will match all .c files in the current working directory non-recursively.
prefix-glob:"pattern"orcwd-prefix-glob:"pattern": Likeglob:, but also
matches path prefix (file or files under directory recursively.) For example, prefix-glob:"*.d" is equivalent to glob:"*.d" | glob:"*.d/**".
root:"path": Matches workspace-relative path prefix (file or files under
directory recursively.)
root-file:"path": Matches workspace-relative file (or exact) path.root-glob:"pattern": Matches file paths with workspace-relative Unix-style
shell [wildcard pattern][glob].
root-prefix-glob:"pattern": Likeroot-glob:, but also matches path prefix
(file or files under directory recursively.)
Glob patterns support case-insensitive matching by appending -i to the pattern name. For example, glob-i:"*.TXT" will match both file.txt and FILE.TXT.
[glob]: https://docs.rs/globset/latest/globset/#syntax
Operators
The following operators are supported. x and y below can be any fileset expressions.
~x: Matches everything butx.x & y: Matches bothxandy.x ~ y: Matchesxbut noty.x | y: Matches eitherxory(or both).
(listed in order of binding strengths)
You can use parentheses to control evaluation order, such as (x & y) | z or x & (y | z).
Functions
You can also specify patterns by using functions.
all(): Matches everything.none(): Matches nothing.
Examples
Show diff excluding Cargo.lock.
jj diff '~Cargo.lock'List files in src excluding Rust sources.
jj file list 'src ~ glob:"**/*.rs"'Split a revision in two, putting foo into the second commit.
jj split '~foo'Git compatibility
Jujutsu has two backends for storing commits. One of them uses a regular Git repo, which means that you can collaborate with Git users without them even knowing that you're not using the git CLI.
See jj help git for help about the jj git family of commands, and e.g. jj help git push for help about a specific command (use jj git push -h for briefer help).
Supported features
The following list describes which Git features Jujutsu is compatible with. For a comparison with Git, including how workflows are different, see the Git-comparison doc.
- Configuration: Partial. The only configuration from Git (e.g. in
~/.gitconfig) that's respected is the following. Feel free to file a bug if you miss any particular configuration options.
- The configuration of remotes (
[remote "<name>"]). Simple fetch refspecs
are respected when branches are not explicitly specified on the CLI. (git is used for remote operations)
core.excludesFile- Authentication: Yes.
gitis used for remote operations under the hood. - Branches: Yes. You can read more about
how branches work in Jujutsu and how they interoperate with Git.
- Tags: Partial. You can check out tagged commits by name (pointed to by
either annotated or lightweight tags). You can also create lightweight tags, but you cannot create annotated tags.
- .gitignore: Yes. Patterns in
.gitignorefiles are supported. So are
ignores in .git/info/exclude or configured via Git's core.excludesFile config. Since working-copy files are snapshotted by almost every jj command, you might need to run jj file untrack to exclude newly ignored files from the working-copy commit. It's recommended to set up the ignore patterns earlier. The .gitignore support uses a native implementation, so please report a bug if you notice any difference compared to git.
- .gitattributes: No. There's #53
about adding support for at least the eol attribute.
- Hooks: No. There's #405
specifically for providing the checks from <https://pre-commit.com>.
- Merge commits: Yes. Octopus merges (i.e. with more than 2 parents) are
also supported.
- Detached HEAD: Yes. Jujutsu supports anonymous branches, so this is a
natural state.
- Orphan branch: Yes. Jujutsu has a virtual root commit that appears as
parent of all commits Git would call "root commits".
- Staging area: Kind of. The staging area will be ignored. For example,
jj diff will show a diff from the Git HEAD to the working copy. There are ways of fulfilling your use cases without a staging area.
- Garbage collection: Yes. It should be safe to run
git gcin the Git
repo, but it's not tested, so it's probably a good idea to make a backup of the whole workspace first. There's no garbage collection and repacking of Jujutsu's own data structures yet, however.
- Bare repositories: Yes. You can use
jj git init --git-repo=<path>to
create a repo backed by a bare Git repo.
- Submodules: No. They will not show up in the working copy, but they will
not be lost either.
- Partial clones: No.
- Shallow clones: Kind of. Shallow commits all have the virtual root commit
as their parent. However, deepening or fully unshallowing a repository is currently not yet supported and will cause issues.
- git-worktree: No. However, there's native support for multiple working
copies backed by a single repo. See the jj workspace family of commands.
- Sparse checkouts: No. However, there's native support for sparse
checkouts. See the jj sparse command.
- Signed commits: Yes.
You can sign commits automatically by configuration, or use the jj sign command.
- Git LFS: No. (#80)
Creating an empty repo
To create an empty repo using the Git backend, use jj git init <name>. This creates a colocated Jujutsu workspace, there will be a .jj directory and a .git directory.
Creating a repo backed by an existing Git repo
To create a Jujutsu repo backed by a Git repo you already have on disk, use jj git init --git-repo=<path to Git repo> <name>. The repo will work similar to a Git worktree, meaning that the working copies files and the record of the working-copy commit will be separate, but the commits will be accessible in both repos. Use jj git import to update the Jujutsu repo with changes made in the Git repo. Use jj git export to update the Git repo with changes made in the Jujutsu repo.
Creating a repo by cloning a Git repo
To create a Jujutsu repo from a remote Git URL, use jj git clone <URL> [<destination>]. For example, jj git clone https://github.com/octocat/Hello-World will clone GitHub's "Hello-World" repo into a directory by the same name.
By default, the remote repository will be named origin. You can use a name of your choice by adding --remote <remote name> to the jj git clone command.
<a name="colocated-jujutsugit-repos"></a>Colocated Jujutsu/Git workspaces
A colocated Jujutsu workspace is a hybrid Jujutsu/Git workspace. This is the default for Git-backed workspace created with jj git init or jj git clone. The Git repo and the Jujutsu workspace then share the same working copy. Jujutsu will import and export from and to the Git repo on every jj command automatically.
This mode is very convenient when tools (e.g. build tools) expect a Git repo to be present.
It is allowed to mix jj and git commands in such a workspace in any order. However, it may be easier to keep track of what is going on if you mostly use read-only git commands and use jj to make changes to the repo. One reason for this (see below for more) is that jj commands will usually put the Git repo in a "detached HEAD" state, since in jj there is not concept of a "currently tracked branch". Before doing mutating Git commands, you may need to tell Git what the current branch should be with a git switch command.
You can undo the results of mutating git commands using jj undo and jj op restore. Inside jj op log, changes by git will be represented as an "import git refs" operation.
You can disable colocation with the --no-colocate flag on the commands jj git init and jj git clone or by setting the configuration git.colocate = false. Much of the repo data will still be stored in the Git repository format, but the Git repository will be hidden inside a sub-directory of the .jj directory. Moreover, unless you explicitly use the jj git import and jj git export commands, that Git repository will either have no branches at all (not even a main branch) or will have branches that are out of date with jj's bookmarks.
Colocation can be disabled because it does have some disadvantages:
- Interleaving
jjandgitcommands increases the chance of confusing branch
conflicts or conflicted (AKA divergent) change ids. These never lose data, but can be annoying.
Such interleaving can happen unknowingly. For example, some IDEs can cause it because they automatically run git fetch in the background from time to time.
- In colocated workspaces with a very large number of branches or other refs,
jj commands can get noticeably slower because of the automatic jj git import executed on each command. This can be mitigated by occasionally running jj util gc to speed up the import (that command includes packing the Git refs).
- Git tools will have trouble with revisions that contain conflicted files.
While jj renders these files with conflict markers in the working copy, they are stored in a non-human-readable fashion inside the repo. Git tools will often see this non-human-readable representation.
- When a
jjbranch is conflicted, the position of the branch in the Git repo
will disagree with one or more of the conflicted positions. The state of that branch in git will be labeled as though it belongs to a remote named "git", e.g. branch@git.
- Jujutsu will ignore Git's staging area. It will not understand merge conflicts
as Git represents them, unfinished git rebase states, as well as other less common states a Git repository can be in.
- Colocated workspaces are less resilient to
concurrency issues if you share the repo using an NFS filesystem or Dropbox. In general, such use of Jujutsu is not currently thoroughly tested.
- There may still be bugs when interleaving mutating
jjandgitcommands,
usually having to do with a branch pointer ending up in the wrong place. We are working on the known ones, and are not aware of any major ones. Please report any new ones you find, or if any of the known bugs are less minor than they appear.
Converting a workspace into a colocated workspace
A Jujutsu workspace backed by a Git repo has a full Git repo inside. Such a workspace can be converted into a colocated workspace using the jj git colocation command.
To check the current colocation status of your workspace:
jj git colocation statusTo convert to a colocated workspace:
jj git colocation enableTo convert to a non-colocated workspace:
jj git colocation disableThe jj colocation enable command automates the following manual process:
# Ignore the .jj directory in Git
echo '/*' > .jj/.gitignore
# Move the Git repo
mv .jj/repo/store/git .git
# Tell jj where to find it (do not use on Windows! See below.)
echo -n '../../../.git' > .jj/repo/store/git_target
# Make the Git repository non-bare and set HEAD
git config --unset core.bare
# Convince jj to update .git/HEAD to point to the working-copy commit's parent
jj new && jj undo!!! warning
On Windows, the echo command will append line endings and cause jj to complain about the contents of git_target.
Instead of the echo -n ... line, use: Set-Content -Path .jj/repo/store/git_target -Value ../../../.git -NoNewLine
Branches
TODO: Describe how branches are mapped
Format mapping details
Paths are assumed to be UTF-8. I have no current plans to support paths with other encodings.
Commits created by jj have a ref starting with refs/jj/ to prevent GC.
Commits with conflicts cannot be represented in Git. They appear in the Git commit as root directories called.jjconflict-base-*/ and .jjconflict-side-*/. Note that the purpose of this representation is only to prevent GC of the relevant trees; the authoritative information is in a non-standard jj:trees commit header. As long as you use jj commands to work with them, you won't notice those paths. If, on the other hand, you use e.g. git switch to check one of them out, you will see those directories in your working copy. If you then run e.g. jj status, the resulting snapshot will contain the contents of the first side of the conflict as well as the .jjconflict-*/ directories, making it look like they were added to your working copy. You will probably want to run jj abandon to get back to the state with the unresolved conflicts.
Change IDs are stored in git commit headers as reverse hex encodings. This is a non-standard header and is not preserved by all git tooling. For example, the header is preserved by a git commit --amend, but is not preserved through a rebase operation. GitHub and other major forges seem to preserve them for the most part. This functionality is currently behind a git.write-change-id-header flag.
Jujutsu for Git Experts
Adapted from the official jj documentation (Apache-2.0).
Colocation — Use Git and jj Side-by-Side
jj and Git repos coexist in the same directory. Use jj for what it does better, fall back to git when you need to. This makes migration gradual — no "big bang" switch required.
No Staging Area
jj replaces Git's index with direct commit manipulation. Instead of git add / git rm --cached, use jj split and jj squash to move work between commits:
# Split specific files into a separate commit
jj split file1 file2
# Move changes in file3 into the parent commit
jj squash file3Moving work-in-progress is as easy as moving finished work.
Safer, Faster History Editing
Amending an older commit in Git takes three steps:
git add file1 file2
git commit --fixup abc
git rebase -i --autosquashIn jj, one command does the same thing and automatically rebases all descendants:
jj squash --into abc file1 file2Undo Is More Powerful Than the Reflog
Git's reflog is per-ref and awkward when multiple refs are involved. jj's operation log records the state of the entire repository after every command.
jj undo— Reverts the last operation. Repeat to keep stepping backward.jj op log -p— Shows operations with diffs.--at-operation ID— Run any command as if the repo were in a previous state.
The Evolution Log (evolog)
Git's reflog shows how refs moved; jj's evolog shows how a single change evolved. Every rewrite is recorded. You can find a previous version of any change and jj restore it (fully or partially) back.
jj absorb — Automatic Fixup Distribution
jj absorb moves each hunk in the working copy into the most recent ancestor commit that last modified those lines. It replaces the git commit --fixup + git rebase --autosquash cycle for the common case.
When multiple ancestor commits touched the same line, jj absorb leaves that hunk in the working copy for you to squash manually.
Git to Jujutsu Command Mapping
Quick reference for translating Git commands to their jj equivalents. For the full official table, see <https://docs.jj-vcs.dev/latest/git-command-table/>.
Setup
| Git | jj | Notes |
|---|---|---|
git init | jj git init | Add --no-colocate to skip .git |
git clone <url> | jj git clone <url> |
Viewing State
| Git | jj | Notes |
|---|---|---|
git status | jj st | |
git diff | jj diff | Working copy vs parent |
git diff <rev>^ <rev> | jj diff -r <rev> | |
git diff --from A --to B | jj diff --from A --to B | |
git log | jj log | |
git log --oneline --graph | jj log -r ::@ | |
git log --all --graph | jj log -r 'all()' | Or jj log -r :: |
git show <rev> | jj show <rev> | |
git blame <file> | jj file annotate <path> | |
git ls-files | jj file list |
Making Changes
| Git | jj | Notes |
|---|---|---|
git add | (automatic) | jj tracks changes automatically |
git commit | jj commit -m "msg" | Or just jj new -m "next task" |
git commit --amend | jj squash | Squash working copy into parent |
git commit --amend --only | jj describe @- -m "msg" | Edit previous commit message |
Navigating
| Git | jj | Notes |
|---|---|---|
git checkout -b topic main | jj new main | |
git switch <branch> | jj new <bookmark> | Or jj edit <rev> |
git stash | jj new @- | Old commit stays as sibling |
History Rewriting
| Git | jj | Notes |
|---|---|---|
git rebase --onto B A | jj rebase -s A -o B | |
git rebase -i | jj rebase -r C --before B | For reordering |
git cherry-pick <rev> | jj duplicate <rev> -o @ | |
git revert <rev> | jj revert -r <rev> -B @ | |
git reset --hard | jj abandon or jj restore | abandon drops commit; restore empties it |
git reset --soft HEAD~ | jj squash --from @- | Keep diff in working copy |
git restore <paths> | jj restore <paths> |
Bookmarks (Branches)
| Git | jj | Notes |
|---|---|---|
git branch | jj bookmark list | Or jj b l |
git branch <name> | jj bookmark create <name> -r <rev> | |
git branch -f <name> <rev> | jj bookmark move <name> --to <rev> | Or jj b m |
git branch -d <name> | jj bookmark delete <name> |
Remotes
| Git | jj | Notes |
|---|---|---|
git push | jj git push | |
git push <remote> <branch> | jj git push -b <bookmark> | |
git push --all | jj git push --all | |
git fetch | jj git fetch | |
git pull | jj git fetch | Then merge/rebase if needed |
git remote add <name> <url> | jj git remote add <name> <url> |
Merging
| Git | jj | Notes |
|---|---|---|
git merge A | jj new @ A | Creates merge commit |
Other
| Git | jj | Notes |
|---|---|---|
git rev-parse --show-toplevel | jj workspace root | |
git rm --cached <file> | jj file untrack <file> | File must match ignore pattern |
Using Jujutsu with GitHub and GitLab Projects
This guide assumes a basic understanding of either Git or Mercurial.
Basic workflow
The simplest way to start with Jujutsu is to create a stack of commits first. You will only need to create a bookmark when you need to push the stack to a remote. There are two primary workflows: using a generated bookmark name or naming a bookmark.
Using a generated bookmark name
In this example we're letting Jujutsu auto-create a bookmark.
# Start a new commit off of the default bookmark.
$ jj new main
# Refactor some files, then add a description and start a new commit
$ jj commit -m 'refactor(foo): restructure foo()'
# Add a feature, then add a description and start a new commit
$ jj commit -m 'feat(bar): add support for bar'
# Let Jujutsu generate a bookmark name and push that to GitHub. Note that we
# push the working-copy commit's *parent* because the working-copy commit
# itself is empty.
$ jj git push --change @- # or -c for shortUsing a named bookmark
In this example, we create a bookmark named bar and then push it to the remote.
# Start a new commit off of the default bookmark.
$ jj new main
# Refactor some files, then add a description and start a new commit
$ jj commit -m 'refactor(foo): restructure foo()'
# Add a feature, then add a description and start a new commit
$ jj commit -m 'feat(bar): add support for bar'
# Create a bookmark so we can push it to GitHub. Note that we created the bookmark
# on the working-copy commit's *parent* because the working copy itself is empty.
$ jj bookmark create bar -r @- # `bar` now contains the previous two commits.
# Set the bookmark to be tracked on the remote.
$ jj bookmark track bar
# Push the bookmark to GitHub (pushes only `bar`)
$ jj git pushWhile it's possible to create a bookmark in advance and commit on top of it in a Git-like manner, you will then need to move the bookmark manually when you create a new commits. Unlike Git, Jujutsu will not do it automatically.
Updating the repository
As of October 2023, Jujutsu has no equivalent to a git pull command (see [issue #1039][sync-issue]). Until such a command is added, you need to use jj git fetch followed by a jj rebase -o $main_bookmark to update your changes.
[sync-issue]: https://github.com/jj-vcs/jj/issues/1039
Working in a Git colocated workspaces
After doing jj git init, which colocates the .jj and .git directories, Git will be in a [detached HEAD state][detached], which is unusual, as Git mainly works with named branches; jj does not.
In a colocated workspace, every jj command will automatically synchronize Jujutsu's view of the repo with Git's view. For example, jj commit updates the HEAD of the Git repository, enabling an incremental migration.
$ nvim docs/tutorial.md
$ # Do some more work.
$ jj commit -m "Update tutorial"
# Create a bookmark on the working-copy commit's parent
$ jj bookmark create doc-update -r @-
$ jj bookmark track doc-update
$ jj git pushWorking in a Jujutsu repository
In a Jujutsu repository, the workflow is simplified. If there's no need for explicitly named bookmarks, you can just generate one for a change. As Jujutsu is able to create a bookmark for a revision.
$ # Do your work
$ jj commit
$ # Push change "mw", letting Jujutsu automatically create a bookmark called
$ # "push-mwmpwkwknuz"
$ jj git push -c mwAddressing review comments
There are two workflows for addressing review comments, depending on your project's preference. Many projects prefer that you address comments by adding commits to your bookmark[^1]. Some projects (such as Jujutsu and LLVM) instead prefer that you keep your commits clean by rewriting them and then force-pushing[^2].
Adding new commits
If your project prefers that you address review comments by adding commits on top, you can do that by doing something like this:
$ # Create a new commit on top of the `your-feature` bookmark from above.
$ jj new your-feature
$ # Address the comments by updating the code. Then review the changes.
$ jj diff
$ # Give the fix a description and create a new working-copy on top.
$ jj commit -m 'address pr comments'
$ # Update the bookmark to point to the new commit.
$ jj bookmark move your-feature --to @-
$ # Push it to your remote
$ jj git pushNotably, the above workflow creates a new commit for you. The same can be achieved without creating a new commit.
!!! warning
We strongly suggest to jj new after the example below, as all further edits still get amended to the previous commit.
$ # Create a new commit on top of the `your-feature` bookmark from above.
$ jj new your-feature
$ # Address the comments by updating the code. Then review the changes.
$ jj diff
$ # Give the fix a description.
$ jj describe -m 'address pr comments'
$ # Update the bookmark to point to the current commit.
$ jj bookmark move your-feature --to @
$ # Push it to your remote
$ jj git pushRewriting commits
If your project prefers that you keep commits clean, you can do that by doing something like this:
$ # Create a new commit on top of the second-to-last commit in `your-feature`,
$ # as reviewers requested a fix there.
$ jj new your-feature- # NOTE: the trailing hyphen is not a typo!
$ # Address the comments by updating the code. Then review the changes.
$ jj diff
$ # Squash the changes into the parent commit
$ jj squash
$ # Push the updated bookmark to the remote. Jujutsu automatically makes it a
$ # force push
$ jj git push --bookmark your-featureThe hyphen after your-feature comes from the revset syntax.
Working with other people's bookmarks
By default, jj git clone imports the default remote bookmark (which is usually main or master), but jj git fetch doesn't import new remote bookmarks to local bookmarks. This means that if you want to iterate or test another contributor's bookmark, you'll need to do jj new <bookmark>@<remote> onto it.
If you want to import all remote bookmarks including inactive ones, set remotes.<name>.auto-track-bookmarks = "*" in the config file. Then you can specify a contributor's bookmark as jj new <bookmark> instead of jj new <bookmark>@<remote>.
You can find more information on that setting [here][auto-bookmark].
Using GitHub CLI
GitHub CLI will have trouble finding the proper Git repository path in jj repos that aren't colocated (see [issue #1008]). You can configure the $GIT_DIR environment variable to point it to the right path:
$ GIT_DIR=.jj/repo/store/git gh issue listYou can make that automatic by installing direnv and defining hooks in a .envrc file in the repository root to configure $GIT_DIR. Just add this line into .envrc:
export GIT_DIR=$PWD/.jj/repo/store/gitand run direnv allow to approve it for direnv to run. Then GitHub CLI will work automatically even in workspaces that aren't colocated so you can execute commands like gh issue list normally.
[issue #1008]: https://github.com/jj-vcs/jj/issues/1008
Useful Revsets
Log all revisions across all local bookmarks that aren't on the main bookmark nor on any remote:
$ jj log -r 'bookmarks() & ~(main | remote_bookmarks())'Log all revisions that you authored, across all bookmarks that aren't on any remote:
$ jj log -r 'mine() & bookmarks() & ~remote_bookmarks()'Log all remote bookmarks that you authored or committed to:
$ jj log -r 'remote_bookmarks() & (mine() | committer(your@email.com))'Log all ancestors of the current working copy that aren't on any remote:
$ jj log -r 'remote_bookmarks()..@'Merge conflicts
For a detailed overview, how Jujutsu handles conflicts, revisit the [tutorial][tut].
[^1]: This is a GitHub-style review, as GitHub currently only is able to compare bookmarks.
[^2]: If you're wondering why we prefer clean commits in this project, see e.g. [this blog post][stacked]
[auto-bookmark]: config.md#automatic-tracking-of-bookmarks [detached]: https://git-scm.com/docs/git-checkout#_detached_head [tut]: tutorial.md#conflicts [stacked]: https://jg.gg/2018/09/29/stacked-diffs-versus-pull-requests/
Using several remotes
It is common to use several remotes when contributing to a shared repository. For example, "upstream" can designate the remote where the changes will be merged through a pull-request while "origin" is your private fork of the project.
$ jj git clone --remote upstream https://github.com/upstream-org/repo
$ cd repo
$ jj git remote add origin git@github.com:your-org/your-repo-forkThis will automatically setup your repository to track the main bookmark from the upstream repository, typically main@upstream or master@upstream.
You might want to jj git fetch from "upstream" and to jj git push to "origin". You can configure the default remotes to fetch from and push to in your configuration file (jj config edit --user|repo|workspace):
[git]
fetch = "upstream"
push = "origin"The default for both git.fetch and git.push is "origin".
If you usually work on a project from several computers, you may configure jj to fetch from both repositories by default, in order to keep your own bookmarks synchronized through your origin repository:
[git]
fetch = ["upstream", "origin"]
push = "origin"Parallel Agents with JJ Workspaces
A guide for running multiple AI agents concurrently using jj workspaces to isolate their working copies.
The Problem
Multiple agents in the same repo fight over the working copy (@). Each agent runs jj restore, jj squash, and jj new operations, causing files to end up in wrong revisions and creating a tangled mess. The fundamental issue: there's only one @ per workspace.
Solution: One Workspace Per Agent
JJ workspaces provide isolated working copies backed by a single shared repo:
- Each workspace has its own
@(working-copy commit) - Changes in one workspace don't affect another's files
- All workspaces share the same revision graph
- Commits made in any workspace are visible to all others
This maps cleanly to parallel agents: assign each agent its own workspace.
Complete Workflow
Step 1: Plan Independent Tasks
Design tasks that touch different files. Tasks that modify the same files will conflict at merge time (which is solvable, but defeats the purpose of parallelization).
Good candidates for parallelization:
- Separate modules or services
- Independent features in different files
- Tests for different components
- Documentation for different areas
Bad candidates:
- Tasks that share config files
- Features that modify the same core module
- Tasks with ordering dependencies
Step 2: Create Workspaces
From the main repo, create a workspace for each agent:
# Create named workspaces as siblings of the main repo
jj workspace add ../workspace-feature-a --name feature-a
jj workspace add ../workspace-feature-b --name feature-b
jj workspace add ../workspace-feature-c --name feature-c
# Verify
jj workspace listWorkspace directories must be siblings of the main repo, not subdirectories. Subdirectories would be tracked by jj, causing circular issues.
Each workspace gets its own .jj/ directory that links back to the main repo's storage.
Step 3: Set Up Task Commits
Create commits for each agent to work on:
# Create task branches from a common parent
jj new -m "feat: implement user service" --no-edit
user_id=$(jj log -r 'latest(description("implement user service"))' --no-graph -T 'change_id.shortest(8)')
jj new -m "feat: implement product service" --no-edit
product_id=$(jj log -r 'latest(description("implement product service"))' --no-graph -T 'change_id.shortest(8)')
jj new -m "feat: implement order service" --no-edit
order_id=$(jj log -r 'latest(description("implement order service"))' --no-graph -T 'change_id.shortest(8)')Step 4: Launch Agents
Each agent needs explicit instructions with absolute paths:
Agent instruction template:
You are working in an isolated JJ workspace for parallel execution.
Working directory: /absolute/path/to/workspace-feature-a
Task change-id: <change-id>
Task description: <what to implement>
Before any work:
cd /absolute/path/to/workspace-feature-a
jj edit <change-id>
After completing work:
jj describe -m "feat: <completed description>"
jj st # verify clean state
CRITICAL:
- All commands MUST run inside your workspace directory
- Use absolute paths for everything
- Do NOT modify files in other workspaces
- Use `jj st` to verify state after mutationsWhy absolute paths? Agents can lose track of cwd. Relative paths like ../workspace-a break if the agent cds somewhere unexpected. Absolute paths are resilient.
Step 5: Monitor Progress
From the main workspace, check all agents' work:
# See working copy status of all workspaces
jj workspace list
# Check a specific workspace's working copy commit
jj log -r 'feature-a@'
# See all workspace commits
jj log -r 'working_copies()'
# Check status from a specific workspace
cd /absolute/path/to/workspace-feature-a && jj stStep 6: Integrate Results
After all agents finish:
# Create a merge commit combining all results
jj new <user-id> <product-id> <order-id> -m "feat: integrate all services"
# Resolve any conflicts
jj st
# Edit conflicted files if any, then verify
jj diffStep 7: Clean Up
# Remove workspace registrations (changes stay in revisions!)
jj workspace forget feature-a
jj workspace forget feature-b
jj workspace forget feature-c
# Delete workspace directories
rm -rf ../workspace-feature-a
rm -rf ../workspace-feature-b
rm -rf ../workspace-feature-cForgetting a workspace does NOT lose any commits. It only removes the working-copy association. All revisions remain in the shared repo.
Stale Workspaces
When you modify a workspace's working-copy commit from another workspace, the affected workspace becomes "stale." This is normal and expected. The agent working in that workspace should run:
jj workspace update-staleThis updates the files on disk to match the current state. If the original operation was lost (e.g., due to jj op abandon), the update creates a recovery commit preserving the working copy's contents.
Conflict Mitigation
Generated files: Ensure .gitignore covers build outputs (__pycache__/, node_modules/, target/, .next/, etc.). Otherwise, each workspace generates its own copies and creates spurious conflicts.
Shared config: If tasks must touch shared files (e.g., a router file, package.json), have one agent own those files and others work around them. Or handle the shared file in a sequential step after parallel work completes.
Lock files: Package manager lock files (package-lock.json, Cargo.lock) are a common conflict source. Consider having only one task add dependencies, or resolve lock conflicts in the integration step.
Decision Checklist
Before using workspaces for parallel agents, verify:
- [ ] 3+ truly independent tasks — fewer tasks don't justify the overhead
- [ ] Tasks touch different files — shared files mean conflicts
- [ ] No ordering dependencies — all tasks can start simultaneously
- [ ] Time savings justify setup — workspace creation, agent instructions, cleanup take real effort
- [ ] Conflict strategy planned — know how you'll handle the integration merge
If any box is unchecked, sequential execution is likely simpler and more reliable.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Agent sees wrong files | Didn't cd to workspace | Ensure cd /absolute/path before every jj command |
| "Workspace not found" errors | Running from wrong directory | Check cwd and use jj workspace list to verify |
| Stale working copy | Commit modified from another workspace | Run jj workspace update-stale |
| Conflicts at merge | Tasks touched same files | Resolve in integration commit; redesign tasks for next time |
| Changes lost after forget | Changes are NOT lost | Commits persist in repo; workspace is just a view |
| Scripts not found | Relative paths in agent instructions | Use absolute paths for all file references |
Revsets
Jujutsu supports a functional language for selecting a set of revisions. Expressions in this language are called "revsets" (the idea comes from Mercurial). The language consists of symbols, operators, and functions.
Most jj commands accept a revset (or multiple). Many commands, such as jj edit <revset> expect the revset to resolve to a single commit; it is an error to pass a revset that resolves to more than one commit (or zero commits) to such commands.
The words "revisions" and "commits" are used interchangeably in this document.
Hidden revisions
Most revsets search only the visible commits. Other commits are only included if you explicitly mention them (e.g. by commit ID, <name>@<remote> symbol, or at_operation() function).
If hidden commits are specified, their ancestors also become available to the search space. They are included in all(), x.., ~x, etc., but not in ..visible_heads(), etc. For example, hidden_id | all() is equivalent to hidden_id | ::(hidden_id | visible_heads()).
Symbols
The @ expression refers to the working copy commit in the current workspace. Use <workspace name>@ to refer to the working-copy commit in another workspace. Use <name>@<remote> to refer to a remote-tracking tag/bookmark.
A full commit ID refers to a single commit. A unique prefix of the full commit ID can also be used. It is an error to use a non-unique prefix.
A full change ID refers to a visible commit with that change ID. A unique prefix of the full change ID can also be used. It is an error to use a non-unique prefix or [a divergent change ID][divergent-change]. To refer to a hidden commit or [divergent change][divergent-change], a [change offset][change-offset] can be added using <change ID>/<offset> syntax.
Use [single or double quotes][string-literals] to prevent a symbol from being interpreted as an expression. For example, "x-" is the symbol x-, not the parents of symbol x. Taking shell quoting into account, you may need to use something like jj log -r '"x-"'.
[divergent-change]: glossary.md#divergent-change [change-offset]: glossary.md#change-offset [string-literals]: templates.md#stringliteral-type
Priority
Jujutsu attempts to resolve a symbol in the following order:
1. Tag name 2. Bookmark name 3. Git ref 4. Commit ID or change ID
To override the priority, use the appropriate revset function. For example, to resolve abc as a commit ID even if there happens to be a bookmark by the same name, use commit_id(abc). This is particularly useful in scripts.
Operators
The following operators are supported. x and y below can be any revset, not only symbols.
Operators are listed in order of binding power from strongest to weakest, e.g. x | y & z is interpreted as x | (y & z) since & has stronger binding power than |. Infix operators of the same binding power are parsed from left to right, e.g. x ~ y & z is interpreted as (x ~ y) & z rather than x ~ (y & z).
As seen above, parentheses can be used to control evaluation order, e.g. (x & y) | z or x & (y | z).
<!-- The following list is based on PRATT in revset_parser.rs. -->
1. * x-: Parents of x, can be empty.
x+: Children ofx, can be empty.
2. * x::: Descendants of x, including the commits in x itself. Equivalent to x::visible_heads() if no hidden revisions are mentioned.
x..: Revisions that are not ancestors ofx. Equivalent to~::x, and
x..visible_heads() if no hidden revisions are mentioned.
::x: Ancestors ofx, including the commits inxitself. Shorthand for
root()::x.
..x: Ancestors ofx, including the commits inxitself, but excluding
the root commit. Shorthand for root()..x. Equivalent to ::x ~ root().
x::y: Descendants ofxthat are also ancestors ofy. Equivalent
to x:: & ::y. This is what git log calls --ancestry-path x..y.
x..y: Ancestors ofythat are not also ancestors ofx. Equivalent to
::y ~ ::x. This is what git log calls x..y (i.e. the same as we call it).
::: All visible commits in the repo. Equivalent toall(), and
root()::visible_heads() if no hidden revisions are mentioned.
..: All visible commits in the repo, but excluding the root commit.
Equivalent to ~root(), and root()..visible_heads() if no hidden revisions are mentioned.
3. * ~x: Revisions that are not in x.
4. * x & y: Revisions that are in both x and y.
x ~ y: Revisions that are inxbut not iny.
5. * x | y: Revisions that are in either x or y (or both).
<!-- The following format will be understood by the web site generator, and will generate a folded section that can be unfolded at will. -->
??? examples
Given this history:
D
|\
B C
|/
A
|
root()Operator x-
D-⇒{C,B}B-⇒{A}A-⇒{root()}root()-⇒{}(empty set)none()-⇒{}(empty set)(D|A)-⇒{C,B,root()}(C|B)-⇒{A}
Operator x+
D+⇒{}(empty set)B+⇒{D}A+⇒{B,C}root()+⇒{A}none()+⇒{}(empty set)(C|B)+⇒{D}(B|root())+⇒{D,A}
Operator x::
D::⇒{D}B::⇒{D,B}A::⇒{D,C,B,A}root()::⇒{D,C,B,A,root()}none()::⇒{}(empty set)(C|B)::⇒{D,C,B}
Operator x..
D..⇒{}(empty set)B..⇒{D,C}(note that, unlikeB::, this includesC)A..⇒{D,C,B}root()..⇒{D,C,B,A}none()..⇒{D,C,B,A,root()}(C|B)..⇒{D}
Operator ::x
::D⇒{D,C,B,A,root()}::B⇒{B,A,root()}::A⇒{A,root()}::root()⇒{root()}::none()⇒{}(empty set)::(C|B)⇒{C,B,A,root()}
Operator ..x
..D⇒{D,C,B,A}..B⇒{B,A}..A⇒{A}..root()⇒{}(empty set)..none()⇒{}(empty set)..(C|B)⇒{C,B,A}
Operator x::y
D::D⇒{D}B::D⇒{D,B}(note that, unlikeB..D, this includesBand excludesC)B::C⇒{}(empty set) (note that, unlikeB..C, this excludesC)A::D⇒{D,C,B,A}root()::D⇒{D,C,B,A,root()}none()::D⇒{}(empty set)D::B⇒{}(empty set)(C|B)::(C|B)⇒{C,B}
Operator x..y
D..D⇒{}(empty set)B..D⇒{D,C}(note that, unlikeB::D, this includesCand excludesB)B..C⇒{C}(note that, unlikeB::C, this includesC)A..D⇒{D,C,B}root()..D⇒{D,C,B,A}none()..D⇒{D,C,B,A,root()}D..B⇒{}(empty set)(C|B)..(C|B)⇒{}(empty set)
Functions
You can also specify revisions by using functions. Some functions take other revsets (expressions) as arguments.
??? note "Function argument syntax"
In this documentation, optional arguments are indicated with square brackets like [arg]. Some arguments also have an optional label which can be used to specify that argument without specifying all previous arguments.
For instance, remote_bookmarks([name_pattern], [[remote=]remote_pattern]) indicates that all of the following usages are valid:
remote_bookmarks()remote_bookmarks("main")remote_bookmarks("main", "origin")remote_bookmarks("main", remote="origin")remote_bookmarks(remote="origin")
parents(x, [depth]):parents(x)is the same asx-.
parents(x, depth) returns the parents of x at the given depth. For instance, parents(x, 3) is equivalent to x---.
children(x, [depth]):children(x)is the same asx+.
children(x, depth) returns the children of x at the given depth. For instance, children(x, 3) is equivalent to x+++.
ancestors(x, [depth]):ancestors(x)is the same as::x.
ancestors(x, depth) returns the ancestors of x limited to the given depth.
descendants(x, [depth]):descendants(x)is the same asx::.
descendants(x, depth) returns the descendants of x limited to the given depth.
first_parent(x, [depth]):first_parent(x)is similar toparents(x), but
for merges, it only returns the first parent instead of returning all parents. The depth argument also works similarly, so first_parent(x, 2) is equivalent to first_parent(first_parent(x)).
first_ancestors(x, [depth]): Similar toancestors(x, [depth]), but only
traverses the first parent of each commit. In Git, the first parent of a merge commit is conventionally the branch into which changes are being merged, so first_ancestors() can be used to exclude changes made on other branches.
reachable(srcs, domain): All commits reachable fromsrcswithin
domain, traversing all parent and child edges. srcs outside domain are not considered even if a parent or child edge would reach into domain.
This is useful for finding all related commits in a branch or feature without traversing outside a defined scope. For example, reachable(@, mutable()) returns the stack of commits you are working on.
connected(x): Same asx::x. Useful whenxincludes several commits.
all(): All visible commits and ancestors of commits explicitly mentioned.
none(): No commits. This function is rarely useful; it is provided for
completeness.
change_id(prefix): Commits with the given change ID prefix. If the specified
change is divergent, this resolves to multiple commits. It is an error to use a non-unique prefix. Unmatched prefix isn't an error.
commit_id(prefix): Commits with the given commit ID prefix. It is an error
to use a non-unique prefix. Unmatched prefix isn't an error.
bookmarks([pattern]): All local bookmark targets. Ifpatternis specified,
this selects the bookmarks whose name match the given string pattern. For example, bookmarks(*push*) would match the bookmarks push-123 and repushed but not the bookmark main. If a bookmark is in a conflicted state, all its possible targets are included.
remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All remote
bookmarks targets across all remotes. If just the name_pattern is specified, the bookmarks whose names match the given string pattern across all remotes are selected. If both name_pattern and remote_pattern are specified, the selection is further restricted to just the remotes whose names match remote_pattern.
For example, remote_bookmarks(*push*, *ri*) would match the bookmarks push-123@origin and repushed@private but not push-123@upstream or main@origin or main@upstream. If a bookmark is in a conflicted state, all its possible targets are included.
Git-tracking bookmarks are excluded by default. Use remote="git" or remote="*" to select bookmarks including @git ones.
tracked_remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All
targets of tracked remote bookmarks. Supports the same optional arguments as remote_bookmarks().
untracked_remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All
targets of untracked remote bookmarks. Supports the same optional arguments as remote_bookmarks().
tags([pattern]): All tag targets. Ifpatternis specified, this selects
the tags whose name match the given string pattern. For example, tags(*v1*) would match the tags v123 and rev1 but not the tag v2. If a tag is in a conflicted state, all its possible targets are included.
remote_tags([name_pattern], [[remote=]remote_pattern]): All remote tags
targets across all remotes. See remote_bookmarks() for arguments.
visible_heads(): All visible heads (same asheads(all())if no hidden
revisions are mentioned).
root(): The virtual commit that is the oldest ancestor of all other commits.
heads(x): Commits inxthat are not ancestors of other commits inx.
Equivalent to x ~ ::x-. Note that this is different from Mercurial's heads(x) function, which is equivalent to x ~ x-.
roots(x): Commits inxthat are not descendants of other commits inx.
Equivalent to x ~ x+::. Note that this is different from Mercurial's roots(x) function, which is equivalent to x ~ x+.
latest(x, [count]): Latestcountcommits inx, based on committer
timestamp. The default count is 1.
fork_point(x): The fork point of all commits inx. The fork point is the
common ancestor(s) of all commits in x which do not have any descendants that are also common ancestors of all commits in x. It is equivalent to the revset heads(::x_1 & ::x_2 & ... & ::x_N), where x_{1..N} are commits in x. If x resolves to a single commit, fork_point(x) resolves to x.
bisect(x): Finds commits in the input set for which about half of the input
set are descendants. The current implementation deals somewhat poorly with non-linear history.
exactly(x, count): Evaluatesx, and errors if it is not of exactly size
count. Otherwise, returns x. This is useful in particular with count=1 when you want to ensure that some revset expression has exactly one target.
merges(): Merge commits.
description(pattern): Commits that have a description matching the given
A non-empty description is usually terminated with newline character. For example, description("") matches commits without description, and description("foo\n") matches commits with description "foo\n".
subject(pattern): Commits that have a subject matching the given [string
pattern](#string-patterns). A subject is the first line of the description (without newline character.)
author(pattern): Commits with the author's name or email matching the given
string pattern. Equivalent to author_name(pattern) | author_email(pattern).
author_name(pattern): Commits with the author's name matching the given
author_email(pattern): Commits with the author's email matching the given
author_date(pattern): Commits with author dates matching the specified [date
pattern](#date-patterns).
mine(): Commits where the author's email matches the email of the current
user. Equivalent to author_email(exact-i:<user-email>)
committer(pattern): Commits with the committer's name or email matching the
given string pattern. Equivalent to committer_name(pattern) | committer_email(pattern).
committer_name(pattern): Commits with the committer's name matching the
given string pattern.
committer_email(pattern): Commits with the committer's email matching the
given string pattern.
committer_date(pattern): Commits with committer dates matching the specified
signed(): Commits that are cryptographically signed.
empty(): Commits modifying no files. This also includesmerges()without
user modifications and root().
files(expression): Commits modifying paths matching the given [fileset
expression](filesets.md).
Paths are relative to the directory jj was invoked from. A directory name will match all files in that directory and its subdirectories.
For example, files(foo) will match files foo, foo/bar, foo/bar/baz. It will not match foobar or bar/foo.
Some file patterns might need quoting because the expression must also be parsable as a revset. For example, . has to be quoted in files(".").
diff_lines(text, [files]): Commits containing diffs matching the given
text pattern line by line.
The search paths can be narrowed by the files expression. All modified files are scanned by default, but it is likely to change in future version to respect the command line path arguments.
For example, diff_lines("*TODO*", "src") will search revisions where "TODO" is added to or removed from files under "src".
conflicts(): Commits that have files in a conflicted state.
divergent(): Commits that are divergent.
present(x): Same asx, but evaluated tonone()if any of the commits
in x doesn't exist (e.g. is an unknown bookmark name.)
coalesce(revsets...): Commits in the first revset in the list ofrevsets
which does not evaluate to none(). If all revsets evaluate to none(), then the result of coalesce will also be none().
working_copies(): The working copy commits across all the workspaces.
at_operation(op, x): Evaluatesxat the specified [operation][]. For
example, at_operation(@-, visible_heads()) will return all heads which were visible at the previous operation.
Since at_operation(op, x) brings all commits that were visible at the operation to the search space, at_operation(op, x) | all() is equivalent to at_operation(op, x) | ::(at_operation(op, x | visible_heads()) | visible_heads()).
[operation]: glossary.md#operation
??? examples
Given this history:
E
| D
|/|
B C
|/
A
|
root()function reachable()
reachable(srcs, domain) finds all commits reachable from srcs by following parent or child edges, but limited to commits within domain.
reachable(E, A..)⇒{E,D,C,B}— All commits after A are reachablereachable(E, B..)⇒{E}— E is isolated: its only edge (to B) leaves the domainreachable(C, B..)⇒{D,C}— C and D are connected; the C→A edge leaves the domainreachable(D, B..)⇒{D,C}— Same result: D reaches C, but D→B leaves the domainreachable(A, A..)⇒{}(empty set) — A is not in domainA.., so it's ignored
function connected()
connected(E|A)⇒{E,B,A}connected(D|A)⇒{D,C,B,A}connected(A)⇒{A}
function heads()
heads(E|D)⇒{E,D}heads(E|C)⇒{E,C}heads(E|B)⇒{E}heads(E|A)⇒{E}heads(A)⇒{A}
function roots()
roots(E|D)⇒{E,D}roots(E|C)⇒{E,C}roots(E|B)⇒{B}roots(E|A)⇒{A}roots(A)⇒{A}
function fork_point()
fork_point(E|D)⇒{B}fork_point(E|C)⇒{A}fork_point(E|B)⇒{B}fork_point(E|A)⇒{A}fork_point(D|C)⇒{C}fork_point(D|B)⇒{B}fork_point(B|C)⇒{A}fork_point(A)⇒{A}fork_point(none())⇒{}
String patterns
Functions that perform string matching support the following pattern syntax (the quotes are optional).
By default, "string" is parsed as a glob: pattern.
exact:"string": Matches strings exactly equal tostring.glob:"pattern": Matches strings with Unix-style shell [wildcard
pattern](https://docs.rs/globset/latest/globset/#syntax).
regex:"pattern": Matches substrings with [regular
expression pattern](https://docs.rs/regex/latest/regex/#syntax).
substring:"string": Matches strings that containstring.
You can append -i after the kind to match case‐insensitively (e.g. glob-i:"fix*jpeg*").
String patterns can be combined by using logical operators (e.g. bookmarks(~glob:"ci/*")):
~x: Matches everything butx.x & y: Matches bothxandy.x ~ y: Matchesxbut noty.x | y: Matches eitherxory(or both).
Date patterns
Functions that perform date matching support the following pattern syntax:
after:"string": Matches dates exactly at or after the given date.before:"string": Matches dates before, but not including, the given date.
Date strings can be specified in several forms, including:
- 2024-02-01
- 2024-02-01T12:00:00
- 2024-02-01T12:00:00-08:00
- 2024-02-01 12:00:00
- 2 days ago
- 5 minutes ago
- yesterday
- yesterday 5pm
- yesterday 10:30
- yesterday 15:30
Aliases
New symbols and functions can be defined in the config file, by using any combination of the predefined symbols/functions and other aliases.
Alias functions can be overloaded by the number of parameters. However, builtin function will be shadowed by name, and can't co-exist with aliases.
For example:
[revset-aliases]
'HEAD' = '@-'
'user()' = 'user("me@example.org")'
'user(x)' = 'author(x) | committer(x)'Built-in Aliases
The following aliases are built-in and used for certain operations. These functions are defined as aliases in order to allow you to overwrite them as needed. See revsets.toml for a comprehensive list.
trunk(): Resolves to the head commit for the default bookmark of the default
remote, or the remote named upstream or origin. This is set at the repository level upon initialization of a Jujutsu repository.
If the default bookmark cannot be resolved during initialization, the default global configuration tries the bookmarks main, master, and trunk on the upstream and origin remotes. If more than one potential trunk commit exists, the newest one is chosen. If none of the bookmarks exist, the revset evaluates to root().
You can override this as appropriate. If you do, make sure it always resolves to exactly one commit. For example:
[revset-aliases]
'trunk()' = 'your-bookmark@your-remote'builtin_immutable_heads(): Resolves to `trunk() | tags() |
untracked_remote_bookmarks(). It is used as the default definition for immutable_heads() below. It is not recommended to redefine this alias. Prefer to redefine immutable_heads()` instead.
immutable_heads(): Resolves to `trunk() | tags() |
untracked_remote_bookmarks() by default. It is actually defined as builtin_immutable_heads()`, and can be overridden as required. See here for details.
immutable(): The set of commits thatjjtreats as immutable. This is
equivalent to ::(immutable_heads() | root()). It is not recommended to redefine this alias. Note that modifying this will not change whether a commit is immutable. To do that, edit immutable_heads().
mutable(): The set of commits thatjjtreats as mutable. This is
equivalent to ~immutable(). It is not recommended to redefined this alias. Note that modifying this will not change whether a commit is immutable. To do that, edit immutable_heads().
visible(): The set of visible commits. Resolves to::visible_heads().
This is equal to all() unless your revset includes hidden revisions.
hidden(): The set of hidden commits. Resolves to~visible(). This is empty
unless your revset includes hidden revisions. Note that this is not the set of all previously visible commits.
Examples
Show the parent(s) of the working-copy commit (like git log -1 HEAD):
jj log -r @-Show all ancestors of the working copy (like plain git log)
jj log -r ::@Show commits not on any remote bookmark:
jj log -r 'remote_bookmarks()..'Show commits not on origin (if you have other remotes like fork):
jj log -r 'remote_bookmarks(remote=origin)..'Show the initial commits in the repo (the ones Git calls "root commits"):
jj log -r 'root()+'Show some important commits (like git log --simplify-by-decoration):
jj log -r 'tags() | bookmarks()'Show local commits leading up to the working copy, as well as descendants of those commits:
jj log -r '(remote_bookmarks()..@)::'Show commits authored by "martinvonz" and containing the word "reset" in the description:
jj log -r 'author(*martinvonz*) & description(*reset*)'