Deep Dive into jj-vcs/jj: Modern Version Control for Engineers
Think of jj as a modern take on version control that aims to address some of the complexities and pain points often associated with Git, while still letting you leverage your existing Git repositories and workflows. It's built on a foundation inspired by Mercurial, known for its simpler data model, but layered with Git compatibility.
Here's how jj can be particularly helpful for a software engineer like myself
Simplified Mental Model
One of the biggest advantages is jj's "always commit" and "no-op merge" philosophy. In jj, every change you make results in a new commit. This means you don't have a separate staging area like Git's index. This can significantly simplify your mental model of what's happening, especially for new users or when dealing with complex changes. You're always working with commits, making it easier to reason about your history.
Mutable History (and safety)
jj makes it easy to rewrite history, but in a controlled and safe way. Operations like amending commits, rebasing, or squashing are often more intuitive and less error-prone than in Git. It does this by keeping track of the "true" history behind the scenes, so you can always revert to previous states of your commits. This is super helpful for cleaning up your commit history before sharing it, leading to cleaner, more understandable pull requests.
Built-in Smart Defaults
Many common Git commands require specific flags or careful sequencing. jj often provides sensible defaults that anticipate what you want to do. For example, jj rebase might automatically rebase your current changes onto the latest version of the main branch without you needing to specify source and destination.
First-Class Conflict Resolution
jj has robust built-in tools for conflict resolution that aim to be more user-friendly than Git's. It provides clear guidance on how to resolve conflicts and helps you apply the resolutions effectively.
Git Compatibility
This is a huge one! You don't have to abandon your existing Git repositories. jj can work directly with Git repositories, allowing you to use jj for your day-to-day work while still pushing to and pulling from remote Git repositories. This makes adoption much smoother.
Scalability
While designed for simplicity, jj is also built with performance in mind and aims to handle large repositories efficiently.
Getting jj up and running is pretty straightforward. Here's how you can typically install it
Using a Package Manager (Recommended)
This is the easiest way for most operating systems.
macOS (Homebrew)
brew install jj
Linux (Nix, Arch Linux, etc.)
Installation methods vary depending on your distribution. For example, with Nix
nix-env -iA nixpkgs.jj
Or check your distribution's package manager for jj or jj-vcs.
Windows (Scoop)
scoop install jj
Building from Source
If jj isn't available via your package manager or you want the absolute latest version, you can build it from source. You'll need Rust and Cargo installed.
cargo install jj
You can initialize a new jj repository or use an existing Git repository.
New Repository
Navigate to your project directory and run
jj init
This will create a .jj directory, similar to how Git creates a .git directory.
Existing Git Repository
Navigate to your existing Git repository and run
jj init --git-repo=.
This command tells jj to initialize its internal state within your existing Git repository, effectively "adopting" it.
Let's look at some common jj commands and how they compare to Git.
Just like git status, jj status shows you what's going on in your workspace.
jj status
You'll see changes in your working copy and any new or modified commits.
In jj, you don't have a separate add step. Changes in your working copy are implicitly part of your "current" commit, and you "record" them into a new commit.
Git
# Make changes to file.txt
git add file.txt
git commit -m "My first commit"
jj
# Make changes to file.txt
jj commit -m "My first commit"
If you modify file.txt again and run jj commit -m "Another change", jj will create a new commit on top of your previous one.
jj has a powerful log command that's often more visually intuitive than git log.
jj log
This will show your commit history, often with a clear graph illustrating parent-child relationships.
This is where jj really shines. Amending a commit is incredibly simple.
Git (to amend the last commit)
# Make changes
git add .
git commit --amend --no-edit
jj (to amend the current commit, which includes changes in your working copy)
# Make changes
jj commit --amend
If you want to amend a previous commit that's not your current one, you can use jj rebase.
Rebasing is a core operation in jj for moving commits around and integrating changes. It's often more straightforward than in Git.
Rebasing your current commit onto the latest main branch
jj rebase -d main
Here, -d specifies the "destination" commit you want to rebase onto. jj automatically understands "main" as a branch.
To discard changes in your working copy for a specific file
Git
git restore file.txt
jj
jj restore file.txt
While jj doesn't have "branches" in the exact same way Git does (it uses a concept called "change IDs"), you can still create new lines of development. The "stacking" of changes is a natural outcome of jj's commit model.
To create a new, empty commit (which you can then build upon)
jj new
This effectively gives you a new "head" to start making changes from, similar to creating a new branch in Git.
Since jj is Git-compatible, you can push and pull from your existing Git remotes.
Pushing to a Git remote
jj git push
Pulling from a Git remote
jj git pull
These commands act as a bridge, synchronizing your jj repository with the Git remote.
From a software engineer's perspective, jj offers a refreshing approach to version control. The simplified mental model, powerful yet safe history manipulation, and seamless Git compatibility make it a compelling alternative for daily development. I find myself spending less time battling the intricacies of Git and more time focusing on writing code. It's especially useful for maintaining a clean and linear history for pull requests, which significantly improves code review efficiency.