17 Jul 2025
Migrating to jj
I just want to use jj with GitHub, man
Sure, you can do that. Convert an existing git repo with jj git init --colocate
or clone a repo with jj git clone
. Work in the repo like usual, but with no add
needed, changes are staged automatically.
Commit with jj commit
, name the branch you want to push with jj bookmark set NAME
, and then push it with jj git push
.
Get changes from the remote with with jj git fetch
. Switch where you’re working (like checking out a branch) with jj new NAME
. That’s probably all you need to get started, so good luck and have fun!
concepts
Still here? Cool, let’s talk about how jj is different from git. There’s a list of differences from git in the jj docs, but more than specific differences, I found it helpful to think of jj as “what if everything in git was made into a commit”. That includes the staging area and the results of every single jj command. Some interesting effects fall out of treating everything as a commit, including undo, committed conflicts, and change IDs.
While using jj, any command can be rewound using jj undo
because any command creates a new operation in the op log. Any merge conflict can be rebased without resolving it because the conflict itself is stored in the merge commit. A rebase failure doesn’t stop the rebase—your rebase now includes commits with conflicts inside them that you can fix any time later.
Ironically, everything being a commit leads away from commits: how do you talk about a commit both before and after you amended it? You add change IDs. Changes give you a single unchanging name for what you’re doing in a commit that’s been amended, rebased, and then used in a merge.
Once you’ve internalized a model where everything is a commit, and change IDs stick across amends, you can do some wild shenanigans that used to be quite hard with git. Five separate PRs open but you want to work with all of them at once? Easy. Made changes that need to be split into five different new commits across five branches? Also easy.
One other genius concept jj offers is revsets. In essence, it’s a query language for selecting changes, based on name, message, metadata, parents, children, or several other options. Being able to select lists of changes easily is a huge improvement for commands like log or rebase.
further conceptual reading
For more about jj’s design, concepts, and why they are interesting, check out the blog posts What I’ve Learned From JJ and jj init. For a quick reference you can refer to later, there’s a single page summary in the jj cheat sheet PDF.
commands
Now, let’s take a look at the most common jj commands, with a special focus on the way arguments are generally consistent and switches don’t hide totally different additional commands.
jj log
The log command is the biggest consumer of revsets, which are passed using -r
or --revset
. With @
, which is the jj version of HEAD
, you can build a revset for exactly the commits you want to see. The git operator ..
is supported, allowing you to log commits after A and up to B with -r A..B
, but that’s just the start. Here’s a quick list of some useful revsets to give you the flavor:
@-
the parent of the current commitkv+
the first child of the change namedkv
..A & ..B
changes in the intersection ofA
andB
’s ancestors~description(glob:"wip:\*")
changes whose message does not start withwip:
, because tilde negates a revsetheads(::@ & mutable() & ~description(exact:"") & (~empty() | merges()))
the closest “pushable” change, meaning the nearest ancestor of@
that is mutable (by default mutable means “not in the main/trunk branch”), that has some description set, and that either has some changes or is a merge commit. (Some jj merge commits can be empty, if there were no conflicts.)
Using the jj config file, you can give any revset an alias, and then use that alias. I use closest_pushable(@)
quite a bit, especially when naming branches and pushing.
For a full review of everything that’s possible with revsets, check out the revset documentation and the blog post Understanding Revsets for a Better JJ Log Output.
jj commit / new / edit / split
The functionality of git commit
is broken up into three separate jj commands. You use new
to create a new empty child change, defaulting to @
, and edit it. You use edit
to re-open an existing change for amending, and split
to interactively select a diff to break out into a second change. These are all common git workflows, done by using flags or multiple git commands, made direct and straightforward single commands in jj.
jj restore / abandon
What if checkout
with file arguments had a semantic name? You go back to a previous file version using restore
or use abandon
to get files from your immediate parent.
jj bookmark list / set / track
Bookmarks are jj’s equivalent to named git branches, and can be set up to automatically track a branch in a git remote. Compatibility with git branches is nice, but names aren’t required by jj’s model. You can push any change directly with jj git push --change @
, and jj will use the change ID (which stays the same across amends and rebases) as the git branch name. Now you don’t have to think of a good name for your branch before you can work on it (or push it!).
jj git push / fetch
The defaults here are different than you might expect. Unless you configure the git.fetch
and git.push
settings, jj will only push to or fetch from origin
. To operate on another remote, pass --remote NAME
. To operate on all remotes, use glob:*
as the remote name.
jj rebase / absorb / squash
The rebase command works like you would expect, but better. You can rebase a single change to a different place with jj rebase -r id --insert-before A
, or rebase a change and all it’s descendants with jj rebase -s id --insert-after B
. You can even rebase an entire branch automatically with jj rebase -b @ --destination C
, moving every ancestor of @
that is not an ancestor of C
into a new chain of commits descending from C
. I do all of these constantly in git, and it’s much more involved.
The absorb and squash commands are just clear, single commands for the common git operations where you move a diff into a commit or move a diff out of a commit, by change ID and/or filename.
jj undo / restore / op log
The op log is the first half of the big magical-feeling difference from git. Run any jj command, and don’t like the results? You can jj undo
right back to the commits and files you had before. This magic is accomplished by creating a special kind of commit (an operation) every time a jj command is run. Operations are stored in a separate list, and undo
is the same as restoring the parent of the current operation. The full list is available with op log
, which also accepts revsets to filter and select operations.
jj merge (doesn’t exist)
The git rebase and merge commands (also including apply-patch, cherry-pick, and others) are all a bit special because they can create conflicts that have to be resolved before git will allow the commit to be… committed. This is the other half of the magic of jj: your new commit just holds any conflicts inside it. It’s impossible to lose work in a merge disaster because everything is always committed. You can resolve conflicts immediately, after other merges, or never! The results are always immediately stored, no matter how complete or incomplete your resolution is at the time.
Thanks to this feature, you don’t need a dedicated merge command—any new change can have however many parents you want, regardless of conflicts. It’s just as valid to jj new A B C D E
as it is to jj new A
. On top of merging always being possible, you can rebase your work on top of the “megamerge”backwards into any branch as you make new changes. Compared to git, it feels like magic.
further command reading
The previously mentioned jj cheat sheet PDF has a second page, containing a quick summary of each command, what it does, and the arguments it accepts.
workflows
Now that you hopefully have an idea of how to operate jj, let’s look at the commands you need to get work done in jj. One great aspect of jj layering on top of git repos is that the git repo is still there underneath, and you can use any git command exactly like you usually would if there’s missing from your jj workflows.
submit a pull request
The flow to create and send a PR will probably look pretty familiar: use jj git clone
to get a copy of the repo, make your changes, use jj commit
to create your new commits. When you’re ready, use jj bookmark set NAME
to give your changes a name and jj git push
to create a new branch on the remote. Use GitHub.com or gh pr create --head NAME
to open the PR.
If you amend the changes in your PR, you can push updated commits with jj git push
. If you add new changes on top, you’ll need to jj bookmark set NAME
to update the name to the latest change before you jj git push
again. If that gets tedious, there’s a community alias named jj tug
that finds the closest bookmark and moves it to the closest pushable change. We’ll talk about that in the next section, which is about configuring jj.
That’s the whole flow! Congratulations on migrating from git to jj for your everyday work.
work on multiple PRs at once
tk
further workflow reading
The jj docs include a section on using jj with GitHub or GitLab, and there are some great reflections on different workflows in the blog posts Jujutsu VCS Introduction and Patterns, Git experts should try Jujutsu, and jj tips and tricks.
configuration
tk
We are gonna talk so much about configuration.
My jj config https://github.com/indirect/dotfiles/blob/main/private_dot_config/private_jj/config.toml
Thoughtpolice’s jj config thoughtpolice/jjconfig.toml
Pksunkara’s jj config https://gist.github.com/pksunkara/622bc04242d402c4e43c7328234fd01c
git commit —verbose https://jj-vcs.github.io/jj/latest/config/#default-description
counting changes via jj log
https://github.com/jj-vcs/jj/discussions/6683
jj template docs https://jj-vcs.github.io/jj/latest/templates/
jj beyond git
tk
Now that you’ve mastered replacing git with jj, what about the amazing new powers unlocked by jj itself? Well, the biggest power of jj is that you don’t need branches anymore. Create changes, rebase changes, stack five separate changes together and work on top of them while all five of them are reviewed separately. The world is your oyster.
Tangled.sh has shipped jujutsu on tangled, allowing pull requests to be reviewed directly as stacked diffs.
Reorient GitHub Pull Requests Around Changesets Why some of us like “interdiff” code review