Skip to content

Instantly share code, notes, and snippets.

@cee-dub
Last active August 25, 2026 00:33
Show Gist options
  • Select an option

  • Save cee-dub/c6ff377f55a4f2ce1bafbb7a3a0b1217 to your computer and use it in GitHub Desktop.

Select an option

Save cee-dub/c6ff377f55a4f2ce1bafbb7a3a0b1217 to your computer and use it in GitHub Desktop.
JJ VCS Skill

jj Command Reference (git → jj)

Reference: https://docs.jj-vcs.dev/latest/git-command-table/

Learn about any jj command with jj <cmd> --help.

Many commands (squash, restore, diff, split, log, …) accept trailing file paths to scope their effect to those files. Note jj squash --from <rev> moves all of a revision's content unless you append paths — append them to leave the rest behind.

Most Used

git jj
git status jj st
git diff HEAD jj show
git diff <rev>^ <rev> jj diff -r <rev>
git diff --from A --to B jj diff --from A --to B
git show <rev> jj show <rev>
git log --oneline --graph jj log -r ::@
git log --all jj log -r 'all()' --no-graph
git log -G <pattern> jj log -r 'diff_lines(regex:"<pattern>")'
git blame <file> jj file annotate <file>
git ls-files jj file list

Committing & Describing

Action Command
Commit all changes jj commit -m "<message>"
Squash working copy → parent jj squash
Squash working copy → jj squash --into <rev>
Squash any → jj squash --from <revA> --into <revB>
Squash specific files only jj squash <file>
Edit working copy message jj describe -m "<message>"
Edit parent's message jj describe -m "<message>" @-
Edit any message jj describe -m "<message>" <rev>

Branching & Navigation

git jj
git switch -c topic main jj new main
git checkout <rev> jj new <rev>
git merge A jj new @ A
git stash jj new @-
git stash pop jj squash --from <stashed-change-id>

Undo & Recovery

Action Command
Hard reset working copy jj abandon
Restore a file jj restore <file>
Restore file from revision jj restore --from <rev> <file>
Revert a revision jj revert -r <rev> -B @
Cherry-pick jj duplicate <rev>
Undo last operation jj undo
View operation history jj op log
Restore repo to operation X jj op restore X

Rewriting History

(Squash variants are listed under "Committing & Describing" above.)

Action Command
Split current change jj split -m "<message>"
Split arbitrary change jj split -m "<message>" -r <rev>
Split by file jj split -m "<message>" <file>
Rebase whole branch jj rebase --branch B --onto A
Rebase single revision jj rebase -r A --onto B
Reorder commits jj rebase -r C --before B

Bookmarks (git branches)

git jj
git branch jj bookmark list
git branch <name> jj bookmark create <name> -r <rev>
git branch -f <name> <rev> jj bookmark move <name> --to <rev>
git branch -d <name> jj bookmark delete <name>

Syncing with Remotes

git jj
git fetch jj git fetch
git push jj git push
git push <remote> <bookmark> jj git push --bookmark <name>

Repository Setup

git jj
git init jj git init
git clone <src> <dst> jj git clone <src> <dst>
git remote add <name> <url> jj git remote add <name> <url>

Conflict Resolution

git jj
git merge --abort jj undo
git rebase --abort jj undo
git rebase --continue (not needed — edit & squash)
git mergetool jj new <conflicted-rev>, edit, then jj squash

Misc

git jj
git rev-parse --show-toplevel jj workspace root
git rm --cached <file> jj file untrack <file>

Conflict Resolution in jj

Reference: https://docs.jj-vcs.dev/latest/conflicts/

How Conflicts Work

Unlike Git, jj stores conflicts as logical representations inside commits — not as marker text in files. This means:

  • Rebasing a conflicted commit doesn't produce nested markers.
  • Conflicted commits can be further rebased, merged, or backed out.
  • No --continue workflow. You never get stuck mid-operation.
  • Auto-rebase works through conflicts (descendants rewrite automatically).

Resolution Workflow

Use this workflow for rebase, merge, and stacked-commit conflicts:

  1. Find the earliest conflicted revision with an explicit revset: jj log -r 'unmerged()' -n 20.
  2. Create a working copy on that revision: jj new <conflicted-rev>.
  3. Inspect conflicted files with jj status; edit the materialized conflict markers to the intended final content.
  4. Confirm resolution with jj status.
  5. Squash the working-copy resolution into the conflicted revision: jj squash when the conflicted revision is @-, otherwise jj squash --into <conflicted-rev>.
  6. Inspect the rewritten stack explicitly: jj log -r '<conflicted-rev>::' -n 30. Descendants auto-rebase and may resolve automatically or expose additional conflicts; resolve the nearest remaining child the same way.

Do not use jj resolve -r <ancestor> as the default agent workflow. It rewrites an ancestor immediately and rebases its descendants in the same operation. The built-in :ours and :theirs tools are appropriate only when explicitly choosing an entire side.

There is no --continue, --abort, or special mode.

The default conflict text is not ordinary Git-style “take left/right” text: one side may be a full snapshot and another a diff from the merge base. Read the labels and apply each diff to the snapshot. Use ui.conflict-marker-style = "snapshot" when full content for every side is easier to review.

Understanding Conflict Markers

Default style: "diff"

The default marker style shows one side as a snapshot (full content) and the other as a diff to apply to it:

<<<<<<< conflict 1 of 1
%%%%%%% diff from: vpxusssl "merge base"
\\\\\\\        to: rtsqusxu "commit A"
 apple
-grape
+grapefruit
 orange
+++++++ ysrnknol "commit B"
APPLE
GRAPE
ORANGE
>>>>>>> conflict 1 of 1 ends
Marker Meaning
<<<<<<< Start of conflict
>>>>>>> End of conflict
+++++++ Start of a snapshot (full content)
%%%%%%% Start of a diff to apply
\\\\\\\ Continuation of diff label

How to resolve: Apply the diff (grape → grapefruit) to the snapshot (uppercase side). Result: APPLE / GRAPEFRUIT / ORANGE.

Alternative style: "snapshot"

Shows full content of every side. Set ui.conflict-marker-style = "snapshot":

<<<<<<< conflict 1 of 1
+++++++ rtsqusxu "commit A"
apple
grapefruit
orange
------- vpxusssl "merge base"
apple
grape
orange
+++++++ ysrnknol "commit B"
APPLE
GRAPE
ORANGE
>>>>>>> conflict 1 of 1 ends

Multi-sided Conflicts

When merging 3+ commits, jj produces one snapshot section and multiple diff sections. Resolve by applying each diff to the snapshot one at a time.

Long Conflict Markers

If file contents contain lines that look like markers (e.g., =======), jj uses longer markers (15+ characters) to avoid ambiguity:

<<<<<<<<<<<<<<< conflict 1 of 1
%%%%%%%%%%%%%%% diff from: wqvuxsty "merge base"
\\\\\\\\\\\\\\\        to: kwntsput "commit A"
-Heading
+HEADING
 =======
+++++++++++++++ mpnwrytz "commit B"
New Heading
===========
>>>>>>>>>>>>>>> conflict 1 of 1 ends

Important Considerations

  • Don't panic about conflicts. They're normal commit state in jj — not an error condition. You can keep working, create new commits on top, and resolve later.
  • Do not use jj resolve as the default agent workflow. Resolve from a child working copy as described above; use jj resolve --tool :ours or :theirs only when explicitly choosing an entire side.
  • Undo if needed: jj undo reverses the last operation, allowing you to try again if an undesired state results.
  • Check descendants: After resolving, descendants auto-rebase. Run jj log to verify no remaining conflicts propagated. If there are conflicts, continue at the nearest child.
name jj-vcs
description Use jj (Jujutsu) instead of git for version control. Always activate immediately when the working directory contains a .jj/ directory. Overrides built-in git commit and PR workflows with jj equivalents.
user-invocable true
allowed-tools
Bash(jj *)
Bash(gh pr *)
Bash(gh api *)

jj (Jujutsu) Version Control

jj version 2>/dev/null || echo "WARNING: jj not found on PATH"
jj workspace root 2>/dev/null || echo "WARNING: not inside a jj repository"

Use jj — never git — for all version control here. Learn any command with jj <cmd> --help. Full git→jj table: commands.md. Conflict guide: conflicts.md.

Key Differences from Git

  • No staging area. jj auto-snapshots the working copy on every command. No git add; files are tracked by default.
  • Change IDs. Every revision has a stable Change ID (e.g. kpqxywon) that survives rebases. Prefer Change IDs over commit hashes.
  • Bookmarks, not branches. jj bookmark replaces git branch. Detached HEAD is the natural state; there is no "current branch."
  • Mutable commits. jj describe, squash, split, diffedit rewrite history without ceremony. No git rebase -i.
  • Conflicts are first-class. Stored as logical state in commits, not markers. Operations never stop mid-way; resolve at leisure. See conflicts.md.
  • Op log & undo. jj op log shows every operation; jj undo reverses the last (repeatable). Safer than git reflog.

Revset Quick Reference

Revset Meaning
@ / @- / @-- Working copy / parent / grandparent
::@ All ancestors of working copy
A..B Ancestors of B that are not A or its ancestors
A::B Revisions connecting A and B
bookmarks() All bookmarked revisions
trunk() Trunk/main bookmark (defined in repo config)
mine() Revisions you authored
description("*<pat>*") Revisions whose description matches
diff_lines("*<pat>*") Revisions whose diff matches

Discover User Config First

The user's jj config can change command meaning, revsets, and output. Check before relying on defaults:

jj config list --user   # aliases, revset-aliases, templates, ui, git settings
jj config list --repo   # trunk() definition, remote tracking

Watch for: custom trunk() and revset-aliases (don't guess unfamiliar revset names); template-aliases/templates (alter log output, push bookmark names, default messages); git.private-commits / git.sign-on-push (may block or require signing on push); revsets.bookmark-advance-to; ui.default-command (bare jj may not be log).

Colocated Repos (.git/ + .jj/)

Both dirs present = colocated. Still use jj only. jj auto-syncs to git refs (jj git import/export) on most operations. Interleaving raw git commands can cause bookmark conflicts.

Start exactly one fresh working-copy change before editing

Before making any code changes, ensure that @ is the one fresh, empty working-copy change for the work. This is required even for small or "quick" edits, but it is a one-time setup step:

  • If @ is already empty, undescribed, and at the intended place in the graph, use it directly. Do not run jj new.
  • Otherwise, run exactly one jj new <base> -m "<intent>" before the first edit. Pick <base> deliberately (usually @ to stack on current work, or trunk() for independent work) and inspect the graph explicitly, for example jj log -r '::@' -n 20; do not rely on bare jj log.
  • jj commit already finalizes the current change and creates the next empty @. After jj commit, edit that new @ directly or describe it with jj describe -m "<intent>"; do not run jj new @ afterward.
  • Never create an empty child from an empty @. There must not be a chain of empty revisions interleaved with topical commits. jj new and the automatic empty change created by jj commit are alternatives, not steps to perform together.

Use jj new to start a fresh change, not jj edit (which moves the working copy into an existing commit and edits it in place). Do not edit directly on top of pre-existing uncommitted changes or on a commit the user is actively working in; carve out your own change. Do not auto-squash or auto-commit finished work into the user's commits unless asked. The dedicated workflows below (e.g. megamerge scratch space and mechanical-transform splits) define their own commit setup.

Working rhythm (commit per topical change; leave the rest for review)

  • Happy with a change? jj commit it — finalizes the current commit and drops you on the one fresh empty working copy for the next topical change. Continue from that @; do not add another empty change with jj new. Describe it with jj describe -m "<intent>" when needed. One commit per coherent unit of work, not one sprawling working copy.
  • Do not auto-squash at task end. Leave the working copy and descendants intact so the user reviews the stack as discrete commits. Squashing/reordering is theirs to do — free later (see the jj squash gotcha below).
  • On resume (new task or context reset), run jj diff early — with jj status and an explicit graph query such as jj log -r '::@' -n 20 — to see what's already in the working copy before touching anything. Bare jj log is subject to user defaults; don't assume it shows the relevant stack.

Committing Changes (OVERRIDES built-in git commit workflow)

When asked to commit, use jj — never git. There is NO staging step.

  1. Understand current state (run in parallel): jj status, jj diff, jj log -r '::@' -n 10 (recent descriptions for style). Use an explicit revset; bare jj log follows user defaults.
  2. Draft a concise (1–2 sentence) message focused on the "why." Don't commit files that likely contain secrets.
  3. jj commit -m "<message>"NEVER append a Co-Authored-By trailer.
  4. jj st to verify (a new empty working copy is expected and normal).

Do NOT use git add/git commit. Do NOT push unless asked. If something goes wrong, jj undo reverses the last operation.

Creating Pull Requests (OVERRIDES built-in git PR workflow)

Use jj for VCS, gh for GitHub.

  1. Run in parallel: jj git fetch, jj st, jj diff, jj log -r 'main..@' -n 100 (the full graph that will land in the PR). Always pass the revset and limit explicitly; do not rely on bare jj log.
  2. Analyze all revisions in the PR (not just the latest); draft title + summary.
  3. Commit any uncommitted changes per the workflow above.
  4. Push, auto-creating a tracked bookmark: jj git push --change @- (@- is the just-committed revision; rerun to move the bookmark forward). For a chosen name use --named <name>=<rev>.
  5. gh pr create --title "title" --body "$(cat <<'EOF' … EOF)".
  6. Return the PR URL.

Working on Top of a Merge ("megamerge") Commit

A merge commit unifies its parents' contents. When the user keeps an integration commit (often bookmarked megamerge) that merges several feature branches, jj new megamerge starts a working copy whose content is the union of all those branches — including changes you just committed on one of the parent branches. The merged files appear with no rebase.

  • Don't rebase to "bring files together." If the file you need lives on branch A and the toolchain (Makefile, deps) lives on branch B, and both are parents of megamerge, jj new megamerge gives you both at once. Rebasing A onto B is the wrong move and rewrites history the user may not want.
  • A just-made working-copy edit on a parent branch is auto-snapshotted into a commit, so it is already part of what jj new megamerge sees — no need to commit-then-rebase first.
  • This new commit is throwaway scratch space (build/validate here). The real work stays on its own branch; megamerge is only an integration view.
  • If the user is "on a branch rather than atop the megamerge" and files seem missing, the fix is jj new megamerge, not reconstructing the files.

Splitting a Mechanical Transform out of a Mixed Commit

jj split <files> cuts at file boundaries; jj split -i --tool <ui> needs an interactive diff editor an agent cannot drive. When the change to isolate is a pure textual transform (e.g. go$(GO), a rename, a reformat) tangled with logic in the same file, reproduce it deterministically instead of picking hunks:

  1. Build a scoped sed/perl that reproduces only the transform. Restrict it (e.g. recipe lines, specific assignments) and exclude lines the logic commit also rewrites, so the two pieces never collide on the same line.
  2. Dry-run first: apply it to the parent's file in a temp copy and diff against the original tip's file. Every residual difference must be a substantive change — proof the transform is neither over- nor under-reaching.
  3. jj new <target>- to start a commit on the target's parent (working copy now holds parent content), apply the transform in place, describe it.
  4. jj rebase -r <target> -d @ to stack the original commit on top. Expect no conflicts (step 1 kept them on disjoint lines).
  5. Verify end state: jj file show -r <target> <file> must be byte-identical to the original tip — the split must not alter the result. Confirm with diff - <saved-original>.

This is fully automatable and self-verifying, unlike any --tool diff editor.

Gotchas Beyond the Basics

  • Conflicts are not a failure state — leaving them is often fine. A conflict is recorded in the commit as logical state (see conflicts.md), travels with the commit, and is resolvable anytime. No need to resolve the moment one appears; keep working, return later (or leave it if it's in the user's territory).
  • Resolve conflicts in a child working copy. Find the earliest conflicted revision with an explicit query such as jj log -r 'unmerged()' -n 20, run jj new <rev>, edit its materialized conflict markers, verify jj status, then jj squash or jj squash --into <rev>. Descendants auto-rebase and may resolve or expose later conflicts; inspect them with an explicit query such as jj log -r '<rev>::' -n 30. Avoid resolving an ancestor directly with jj resolve -r, which rewrites the stack immediately.
  • jj squash --into <earlier> may create descendant conflicts — expected, not a block. Auto-rebase rewrites descendants and may surface a conflict there; the squash still succeeds and the op completes. Don't avoid jj squash reflexively — let it happen, resolve later if/when it matters.
  • Avoid the interactive-editor hang on squash. jj squash opens $EDITOR to combine descriptions when it can't decide non-interactively, which hangs an agent. Steer clear:
    • Both source and destination have descriptions → pass a message: jj squash --from <rev> --into <rev> -m "...".
    • Working copy into a named rev → --use-destination-message (keep target's) or --use-source-message also stays non-interactive.
  • Bookmark-list templates need the self. prefix on methods. In jj bookmark list -T '...', qualify method calls, e.g. self.normal_target().commit_id(). Bare keywords work only for top-level names like name; a bare normal_target() won't resolve.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment