Back to Field Notes
#git#vcs#tooling#productivity#devops#learning

Why I Use Jujutsu (jj) VCS: Killing Git Anxiety for Solo Developers

How Jujutsu transformed my workflow from the fragile git add/commit/push ritual into fearless rebasing, effortless undo, and non-blocking conflicts.

10 min read
2,214 words

For years, my version control rhythm as a solo developer 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 messy feature branch, or pausing in the middle of a gnarly merge conflict to build an urgent hotfix—Git begins to feel like a high-wire act with no safety net.

To be completely honest, before running any complex git rebase -i, I used to make a panic-copy of the entire repository folder on my desktop.

Why? Because Git’s working directory and staging index are fragile intermediate states. One wrong move during a detached-HEAD interactive rebase, an inadvertent git reset --hard, or a failed git stash pop, and hours of uncommitted work or carefully crafted diffs could vanish into the depths of git reflog—or disappear completely.

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 why jj completely eliminated my Git anxiety and transformed how I write software every day.


The Bridge: Seamless Git Coexistence (--colocated)

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 --colocated

This command creates a .jj/ folder right alongside your existing .git/ folder. Both systems track the exact same repository storage. 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. The Death of the Staging Area (git add)

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

For a solo developer, the staging area is almost always pure overhead. We edit files, run git add . or git add -A, and commit. The staging area only exists because Git cannot easily express partial commits or snapshot a working directory without a temporary scratchpad.

Working Copy as a Live Commit (@)

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

Whenever you save a file in your editor, jj automatically snapshots the change into @. You never have to git add.

Working in Git:                      Working in Jujutsu (jj):
[Working Dir]                        [Working Copy (@) - auto-snapshotted]
      │                                       │
      ▼ (git add)                             ▼ (jj describe -m "feat: login")
[Staging Area / Index]               [Working Copy (@) has a message!]
      │                                       │
      ▼ (git commit)                          ▼ (jj new)
[Permanent Commit]                   [Fresh Working Copy (@) ready for new code]

When you are ready to annotate your changes, you simply 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.

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 Git, you’re forced to use the notoriously clunky git add -p to stage hunks, commit, and hope you didn’t leave unstaged syntax errors behind.

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

jj split

jj opens your interactive diff viewer (such as Meld, Difftastic, or a built-in terminal UI), lets you select the exact files and hunks that belong in the first commit, prompts for a commit message, and cleanly leaves the remaining changes in the subsequent 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:

A (main) ──► B (core deployment logic) ──► C (more changes) ──► 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...
# jj automatically snapshots your edits into D!

# Return to the tip of your feature stack
jj new C

When you edit D, Jujutsu automatically recalculates and rebases both B and C on top of your updated D in the background. You never have to manually run git rebase --onto or repeatedly resolve downstream commits that were already in harmony.


3. Killing Git Anxiety: 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 terrifying.
  • If an interactive rebase goes sideways or overwrites files, recovering lost work can require hours of forensic reconstruction.

This is why developers copy entire directories to .bak before touching git rebase.

jj solves this by treating every single command 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 working copy state—is instantly rewound to exactly how it looked one second before you ran the command.

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 zero fear. You will never need to create a backup copy of a repository directory again.


4. Non-Blocking, First-Class Merge Conflicts

In Git, a merge conflict is a catastrophic event for 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).

While you are staring at conflict markers, your entire repository is paralyzed.

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 mess, or create a separate git worktree.

How Jujutsu Handles Conflicts

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

A commit with a conflict is completely valid in jj’s graph. When a rebase or merge causes a conflict, jj records the two-sided or three-sided 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 a 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, push your commit to GitHub, and take your lunch break. 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 edit mzvwlxor
# Use jj's built-in 3-way resolver or your editor
jj resolve

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

If you amend or rebase your commits, move the bookmark to your new commit and push again:

jj bookmark set feat/pyinfra-helper -r @
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)
Commit Message git commit -m "msg" jj describe -m "msg" followed by jj new
Start Next Task git checkout -b <name> jj new (optionally jj bookmark create <name>)
Undo Last Command 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> (cascades automatically)
Partial Commits git add -p jj split
Merge Conflicted State Locks working tree Saved in commit; run jj new to keep working
Push to GitHub PR git push -u origin branch jj bookmark create <name> && jj git push -b <name>

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 been working as a solo developer feeling like Git was making you work for it rather than working for you, give jj a try. Run jj git init --colocated in one of your projects—and experience what version control feels like when you no longer have anything to fear.