DEV Community

Cover image for How jj Fixes the Git Workflow That's Been Wasting Your Time
Alan West
Alan West

Posted on

How jj Fixes the Git Workflow That's Been Wasting Your Time

Every developer has been there. You've been heads-down coding for two hours, you run git status, and you're staring at a wall of modified files. Some changes belong together, others don't. Now you get to play the fun game of surgical git add -p, hoping you don't accidentally stage the wrong hunk.

Then you realize you forgot to create a branch. Or worse, you've been committing to main. Or you made a commit, pushed it, and now you need to edit something three commits back — hello, interactive rebase anxiety.

These aren't edge cases. This is Tuesday.

The Root Cause: Git's Model Is Showing Its Age

Git was designed in 2005 with a specific mental model: you have a working directory, a staging area (the index), and commits. Changes flow in one direction through this pipeline. It works, but it creates friction in places where friction shouldn't exist.

The staging area is the biggest culprit. It's an extra step between "I changed this file" and "this change is recorded." For simple workflows, it's fine. But the moment you need to reorganize changes, split commits, or work on multiple things at once, you're fighting the tool instead of using it.

Interactive rebase is another pain point. It's powerful, but it's also terrifying. One wrong move in that editor and you're digging through the reflog trying to resurrect your work. The number of "how to undo a git rebase" Stack Overflow questions tells you everything you need to know.

Enter jj: A Different Mental Model

Jujutsu (the CLI is called jj) is a version control system created by Martin von Zweigbergk at Google. It's written in Rust and — here's the key part — it's compatible with Git. You can use it on existing Git repos right now. Your team doesn't need to switch anything.

The core insight behind jj is that several of Git's pain points come from the same design decisions. So rather than patching around them, jj rethinks the model.

Let's walk through how jj solves the problems above.

Step 1: Ditch the Staging Area

In jj, your working copy is a commit. Always. Every change you make is automatically part of the current working-copy commit. There's no staging area, no git add, no forgetting to stage something.

# Initialize jj in an existing git repo
jj git init --colocate

# Make some changes to files...
# Check what jj sees:
jj status
# Your changes are already part of the working-copy commit.
# No 'add' step needed.

# Describe what you did (this is like writing a commit message)
jj describe -m "refactor auth middleware to use async handlers"
Enter fullscreen mode Exit fullscreen mode

When you're ready to start new work, you create a new commit on top:

# Start a new change (creates a new empty commit on top)
jj new

# Now your previous work is safely in its own commit,
# and you have a fresh working copy for the next thing.
Enter fullscreen mode Exit fullscreen mode

This feels weird for about ten minutes. Then it feels completely natural. You stop thinking about "staging" and start thinking about "what am I working on right now."

Step 2: Fearlessly Rewrite History

Remember the interactive rebase anxiety? jj has an operation log — a complete, append-only history of every operation you've performed on the repo. Every single one.

# See your operation history
jj op log

# Made a mistake? Undo it. Any operation.
jj op undo

# Want to go back to a specific point?
jj op restore <operation-id>
Enter fullscreen mode Exit fullscreen mode

This changes how you work. You stop being careful and start being fast. Need to edit a commit from five changes back? Just do it:

# Edit an older commit directly — no interactive rebase needed
jj edit <change-id>

# Make your changes... they go directly into that commit.
# When done, go back to where you were:
jj new
Enter fullscreen mode Exit fullscreen mode

No rebase conflicts cascading through a chain of commits. No "detached HEAD" state. jj handles the rebasing of descendant commits automatically.

Step 3: Stop Being Afraid of Conflicts

Here's something that messed with my head at first: in jj, conflicts are stored in commits. A commit can contain conflict markers and that's fine. It's not a broken state that needs to be resolved before you can do anything else.

Why does this matter? Because in Git, a merge conflict stops the world. You can't commit, you can't switch branches, you can't do anything until you resolve it. In jj, you can commit the conflicted state, go work on something else, and come back to resolve it later.

# Rebase and hit a conflict? That's fine.
jj rebase -d main

# jj shows you the conflict but doesn't block you.
# You can resolve it now, or later, or never (if you abandon the change).
jj status  # shows which files have conflicts

# Resolve when you're ready
# Just edit the files and the conflict markers disappear
# from the commit automatically
Enter fullscreen mode Exit fullscreen mode

Step 4: Change IDs vs. Commit IDs

In Git, commits have SHA hashes that change whenever you amend or rebase. This means after a rebase, all your "bookmarks" (branch pointers, mental notes, links in PR descriptions) point to stale hashes.

jj gives every change a stable change ID that persists across rewrites. When you rebase or amend a change, its change ID stays the same even though the underlying commit hash changes.

# jj log shows both change IDs and commit IDs
jj log
# The short alphanumeric prefix (like 'kpqxywon') is the change ID
# It stays stable even as you edit and rebase

# You can always refer to a change by its stable ID
jj show kpqx  # short prefixes work
Enter fullscreen mode Exit fullscreen mode

This is a small thing that makes a surprisingly big difference in practice.

Getting Started Without Disrupting Your Team

The brilliant part of jj is the --colocate workflow. You can use jj on a repo that's also a Git repo. Your coworkers keep using Git. You use jj. The .git directory is shared.

# Clone a repo with jj
jj git clone https://github.com/your-org/your-repo.git
cd your-repo

# Or initialize jj in an existing git checkout
cd existing-git-repo
jj git init --colocate

# Push and pull work through git
jj git fetch
jj git push
Enter fullscreen mode Exit fullscreen mode

You can try jj on a real project today without asking anyone's permission. If you don't like it, just delete the .jj directory and you're back to pure Git.

Prevention: Building Better Habits

Once you've used jj for a week, you'll notice something: you commit more often. Because there's no staging friction and because every operation is undoable, you stop batching up big messy changes.

A few tips that helped me:

  • Use jj new liberally. Finished a logical chunk of work? jj new. Start the next thing fresh. It's free.
  • Describe as you go. jj describe updates the current commit's message. I run it constantly — it's like taking notes on what I'm doing.
  • Don't fear jj squash. Made too many small commits? jj squash folds the current change into its parent. Easy cleanup.
  • Read the operation log when confused. jj op log is your safety net. If something looks wrong, the answer is in there.

Is It Ready?

I won't pretend jj is perfect. It's still a relatively young project, and the ecosystem around it (IDE integrations, CI tooling, hosting platforms) isn't as mature as Git's. Some commands have rough edges, and you'll occasionally hit scenarios where you need to drop back to git for something.

But for the core workflow of writing code, organizing commits, and collaborating through Git remotes — it's genuinely better. The operation log alone is worth the switch. The lack of a staging area removes a whole category of mistakes. And the conflict model means you never get stuck.

If Git's been giving you paper cuts for years, give jj a weekend. Clone a side project, work in it for a few days, and see if the mental model clicks. For me, it clicked fast and I haven't looked back.

Top comments (0)