Back to Articles
#git#vcs#tooling#productivity#devops#learning

Why I Use Jujutsu (jj) VCS: Frictionless Version Control with AI Agents

How Jujutsu replaces the fragile git add/commit/push ritual with automatic snapshots, effortless rebasing, instant undo, and non-blocking conflicts when building with AI agents.

12 min read
2,514 words

For years, the standard version control rhythm was practically burned into muscle memory:

git add .
git commit -m "fix: some changes"
git push origin main

When things are going well, this linear loop feels fine. But the moment you need to do something slightly more complex—reordering commits, extracting clean prerequisites out of a fast-moving feature branch, or pausing in the middle of a gnarly merge conflict to build an urgent hotfix—Git begins to feel cumbersome.

To be completely honest, before running any complex git rebase -i, I used to duplicate the entire repository folder as a manual backup.

Why? Because Git gives you powerful recovery mechanisms, but they are split across the working tree, index, stash, reflog, and object database. Recovering an accidentally discarded uncommitted working-tree change is far harder than recovering a completed commit, and a detached-HEAD interactive rebase gone wrong can quickly turn into an unwanted forensics session. I find this friction especially noticeable when pairing with AI coding agents, where the working tree changes rapidly and where I often want to checkpoint, rearrange, or discard generated work without wrestling with intermediate Git index states.

Then I adopted Jujutsu (jj), a modern, Git-compatible version control system developed by Martin von Zweigbergk at Google.

jj doesn’t just tweak Git syntax; it fundamentally repairs the underlying data model. Here is how jj eliminates version control friction and transforms everyday developer velocity.


The Bridge: Seamless Git Coexistence (--colocate)

One of the biggest hesitations developers have with alternative version control tools (like Mercurial, Darcs, or Pijul) is ecosystem lock-in. We rely on GitHub, pull request reviews, GitHub Actions, pre-commit hooks, and IDE integrations.

jj avoids this problem completely by acting as a first-class frontend for existing Git repositories. You don’t have to convert your team or change your remotes. You can run:

# In your existing git repository:
jj git init --colocate

This command creates a .jj/ folder right alongside your existing .git/ folder. Both systems share Git-backed commit storage, but Jujutsu maintains its own operation log and repository metadata. In particular, Jujutsu maps Git branches to Jujutsu bookmarks, and Git often sees a detached HEAD while jj is actively managing the workspace. Your coworkers, CI/CD runners, and GitHub CLI (gh) see regular Git commits and branches. But inside your terminal, you get all of jj’s modern superpowers.


1. Beyond the Staging Area: The Working Copy as a Live Commit

In Git, making a commit is a two-step ceremony:

  1. Copy files from your working tree into the intermediate index / staging area (git add).
  2. Snapshot the index into a commit object (git commit).

Git’s staging index is a distinctive strength when you want to craft granular commits out of arbitrary working-tree combinations using git add -p. But for many everyday, high-velocity workflows, it introduces an extra layer of state to manage. jj simplifies this: the working-copy commit @ is the direct unit you edit, while tools like jj split and jj squash let you organize and refine changes cleanly after the fact.

Continuous Snapshots in @

In jj, there is no staging area. Your working copy is already a commit, denoted by @.

By default, whenever you run a jj command after modifying files in your editor, jj automatically snapshots the working copy into @. (You can also enable continuous filesystem monitoring via Watchman, but snapshotting on command invocation is the baseline model.) You never have to git add.

flowchart LR
    subgraph GitFlow["Working in Git"]
        direction TB
        G1["Working Directory"] -->|git add| G2["Staging Area / Index"]
        G2 -->|git commit| G3["Permanent Commit"]
    end

    subgraph JJFlow["Working in Jujutsu (jj)"]
        direction TB
        J1["Working Copy (@)<br/><i>Snapshotted on jj command</i>"] -->|jj describe -m '...'| J2["Working Copy (@)<br/><i>Has commit description</i>"]
        J2 -->|jj new| J3["Fresh Working Copy (@)<br/><i>Ready for new code</i>"]
    end

When you are ready to annotate your changes, you can describe the current commit:

jj describe -m "feat(auth): implement token refresh logic"

To start your next task, create a new working-copy commit on top of it:

jj new

jj new immediately creates a clean, empty working-copy commit @ on top of your previous work. Your previous work is already committed, safe, and part of your repository history.

Even more conveniently, jj provides a single command that combines both steps:

jj commit -m "feat(auth): implement token refresh logic"

This describes the current working-copy commit and immediately opens a fresh, empty working copy on top of it.

What if You Need Partial Commits?

“Wait,” you might ask, “what if I worked on two different things at once and actually want to split them into separate commits?”

In jj, you write your code naturally, and then run:

jj split

jj opens a diff editor (using either Jujutsu’s built-in terminal UI or an external tool like Meld) where you can select the changes that belong in the first commit, prompts for a commit message, and cleanly leaves the remaining changes in the subsequent working-copy commit. No staging index required.


2. Fearless History Reshuffling & Automatic Cascading Rebases

Here is a real scenario I encountered while working on an infrastructure automation project that uses pyinfra:

I was four commits deep into a feature stack:

flowchart LR
    A["A (main)"] --> B["B (core deployment logic)"] --> C["C (more changes)"] --> D["D (deployment tests + helper utility)"]

While polishing commit D, I realized that the helper utility I wrote was actually a generic prerequisite that should have been introduced before commit B, so that other deployment modules could build upon it cleanly.

How this feels in Git:

  1. Run git stash to protect any uncommitted files.
  2. Run git rebase -i HEAD~4.
  3. Split commit D using edit.
  4. Stash, reset HEAD, cherry-pick hunks, create a new commit D_util.
  5. Rearrange the lines in your interactive rebase editor so D_util sits between A and B.
  6. Hope you don’t hit unexpected merge conflicts midway through. If you do, your workspace is thrown into a detached HEAD state.
  7. Run git rebase --continue.

How this feels in Jujutsu:

In jj, commits are first-class revisions identified by short change IDs (like kkmz, yqos, mzvw) that remain stable even when commits are amended or reordered.

To move commit D before commit B, you run a single command:

jj rebase -r D --before B

Alternatively, if you want D positioned right after A:

jj rebase -r D -d A
Before rebase:
◆  D (deployment tests + helper utility)

○  C (more changes)

○  B (core deployment logic)

○  A (main)

After `jj rebase -r D --before B`:
○  C (more changes)            <── automatically updated!

○  B (core deployment logic)   <── automatically updated!

◆  D (deployment tests + helper utility)

○  A (main)

The Superpower: Automatic Cascading Updates

Here is the best part: in jj, modifying an ancestor commit automatically cascades changes down to all descendant commits.

If you notice a bug in commit D (now sitting before B), you don’t need an interactive rebase. You simply edit D:

# Point your working copy directly at commit D
jj edit D

# Make your fixes in your code editor...
# (Snapshotted into D on your next jj command)

# Resume editing at the tip of your feature stack:
jj edit C
# (Or run 'jj new C' if you want to start a fresh change on top of C)

When you edit D, Jujutsu automatically rebases downstream descendants (B and C) on top of your updated D. If the rewritten history cannot be applied cleanly, the affected descendants become conflicted—but the operation still completes rather than halting midway, allowing you to resolve those conflicts when you are ready.


3. Instant Safety Net: The Operation Log (jj undo & jj redo)

In traditional Git, whenever you perform a rebase, reset, or squash, you are operating on a destructive state machine. Yes, git reflog exists, but:

  • It only records updates to local branch references, not working copy files.
  • Deciphering cryptic entries like HEAD@{14}: checkout: moving from feature to main under stress is frustrating.
  • If an interactive rebase goes sideways or overwrites files, recovering lost work can require hours of forensic reconstruction.

This is why developers often resort to manual folder backups before touching complex rebases.

jj solves this by treating every repository-modifying operation as an immutable transaction in an Operation Log.

$ jj op log
@  e6c547a61d80 (2026-09-11 10:42:15) Michael Soliman
  rebase commit kkmzqwup
  91a3b47f201e (2026-09-11 10:40:02) Michael Soliman
  describe commit yqosvznr
  c4f82d61993b (2026-09-11 10:38:22) Michael Soliman
  snapshot working copy

Did you run a rebase that reordered commits in a way that broke your tests? Run:

jj undo

Your repository state—including commit trees, change IDs, and recorded working-copy snapshots—is cleanly restored to its prior state.

Changed your mind again?

jj redo

You can even restore the repository to any point in time from your operation log:

jj op restore <operation-id>

With jj undo, you can experiment with radical branch restructuring, squashes, and splits with complete confidence. You will never need to create manual backup copies of a repository folder again.


4. Non-Blocking, First-Class Merge Conflicts

In Git, a merge conflict interrupts your terminal workflow:

  1. Git halts the merge or rebase.
  2. It writes conflict markers (<<<<<<<, =======, >>>>>>>) directly into your working tree files.
  3. It locks your index (.git/rebase-apply or index locks).

Git’s conflicted index puts that particular worktree into an interrupted operation, making task switching considerably more cumbersome.

Suppose a teammate pinged you about an urgent production bug, or you had an idea for an unrelated feature. In Git, you cannot simply git checkout main to work on it:

$ git checkout main
error: you need to resolve your current index first

To switch tasks, you have to either abort the rebase (git rebase --abort), stash your half-resolved state, or manage a separate git worktree.

How Jujutsu Handles Conflicts

In jj, conflicts are first-class objects stored inside the commit itself.

Jujutsu stores a logical representation of the conflict directly in the commit object. A commit with a conflict is completely valid in jj’s graph. When a rebase or merge causes a conflict, jj records the conflict inside that revision, marks the commit as conflicted, and does not halt your workflow.

○  mzvwlxor (conflict)  <── conflict safely contained here!

○  kkmzqwup

○  main

Need to context-switch and work on an urgent hotfix right this second? Just create a new commit off main:

jj new main -m "fix(prod): handle null pointer in payment webhook"

You can write your fix, run your tests, create a bookmark, and push your hotfix to GitHub:

# Create a bookmark on your current working copy (@ is the default target)
jj bookmark create hotfix
jj git push -b hotfix
# (Or push the change directly: jj git push --change @)

The conflict in mzvwlxor isn’t going anywhere, isn’t locking your working copy, and isn’t blocking other branches.

When you are ready to resolve the conflict later, jj materializes conflict markers into the working copy so you can see what collided:

# Resume editing the conflicted revision:
jj edit mzvwlxor

# Resolve conflict markers in your editor, or launch an external 3-way merge tool:
jj resolve

Alternatively, you can start a new revision on top of the conflict (jj new mzvwlxor), edit files to resolve the differences, and fold the fix into the parent commit with jj squash.


5. Working with GitHub: Bookmarks vs Branches

Git conflates two very different concepts into “branches”:

  1. A local line of development.
  2. A remote reference tracking an upstream branch on GitHub.

Because every jj commit is tracked by its unique Change ID, you don’t even need to name a branch while developing locally. You can build entire trees of revisions anonymously.

When you are ready to push your work to GitHub as a Pull Request, you assign a bookmark (Jujutsu’s equivalent to a Git branch name):

# Create a bookmark on your current commit (@)
jj bookmark create feat/pyinfra-helper

# Push it to your Git remote (e.g. GitHub origin)
jj git push -b feat/pyinfra-helper

Jujutsu automatically moves bookmarks when the revisions they point to are rewritten or rebased. However, if you add new child commits on top of a bookmarked revision, jj does not automatically advance the bookmark to the new tip—giving you explicit control over when the PR branch moves forward:

# Advance the bookmark to your new tip (@ is the default target) when ready:
jj bookmark set feat/pyinfra-helper
jj git push

On GitHub, your PR updates seamlessly. To your teammates, you are just a developer submitting clean, well-structured, atomic Git commits.


Quick Reference: Git vs Jujutsu Cheatsheet

Task Git Command Jujutsu (jj) Equivalent
Check Status git status jj status or jj log
Stage Changes git add . (Unnecessary; working copy @ is auto-committed)
Finish Current Change git add . && git commit jj commit -m "msg" (or jj describe + jj new)
Start Next Task git checkout -b <name> jj new (optionally jj bookmark create <name>)
Undo Last Operation git reset --hard / git reflog jj undo
Redo Undone Action (Pray to the reflog gods) jj redo
Reorder Commits git rebase -i HEAD~N jj rebase -r <rev> --before <target>
Edit Old Commit git rebase -i -> edit jj edit <rev> (descendants automatically rebased)
Partial Commits git add -p jj split (opens diff editor)
Conflict Handling Interrupted operation; resolve/continue Conflict recorded in revision; operations continue
Push to GitHub PR git push -u origin branch jj bookmark create <name> && jj git push -b <name> (or jj git push --change @)

Conclusion: From Defensive Version Control to Creative Exploration

Traditional Git forces developers to be defensive. We avoid complex rebases because the cost of failure is high. We hesitate to clean up our commit history because git rebase -i can blow up our afternoon.

Jujutsu removes that cognitive friction:

  • Automatic working copy snapshots eliminate the git add ceremony.
  • The operation log makes any action completely reversible with jj undo.
  • Automatic cascading rebases make reordering and updating commits effortless.
  • First-class conflicts mean a merge conflict never freezes your terminal again.

If you’ve ever felt like Git was making you work for it rather than enabling you to move quickly, give jj a try. Run jj git init --colocate in one of your projects—and experience what version control feels like when you have a truly modern, resilient safety net.