Jujutsu: working with conflicts and remote repositories
Published: 2026-10-05
This is a follow-up to my previous post about learning jj, an alternative approach to Git version control. While learning, I had only worked with jj in repositories with clear linear Git histories, and was still trying to understand how it all interacts with branch based Git remotes. Over the last couple of weeks I’ve tackled using jj in real and more complicated projects and I’ve gotten a good feel understanding the major advantages it provides beyond Git’s capabilities.
Branching out and handling conflicts
Branching, I think, is one of the most useful concepts that jj has redefined. Using jj, it’s possible to keep multiple branches in sync at the same time while being able to handle any conflicts that arise gracefully.
Branching builds on jj's concept of a change, which is essentially a fixed identifier for a Git commit. They function like a Git commit and are backed by a Git commit, but are mutable. This works because a change's underlying immutable Git commit can be removed an reassigned. Branches in jj then become changes that share a common ancestor within the tree. Given two branches, editing or squashing down to a common ancestor will automatically rebase any file changes up to both heads. Behind the scenes, new Git commits are being generated and reassigned to changes within each branch.
This is great for trying out different ideas at the same time. And while it’s possible to do this with Git, the DX in Git is considerably more clunky incentivizing a more linear history. Git does this for a good reason, it avoids commit conflicts. Conflicts can be a pain to puzzle out in Git, but jj has a very smart way of handling them!
jj is able to silently rebase multiple branches at the same time because conflicts do not block other operations. Instead, they become part of the code, and can be resolved at a later point. (It’s a lot like handling errors as values in Go or Rust.) The jj CLI very helpfully suggests a great workflow for handling conflicts one at a time:
# Creates a new branch to deal with the conflict.
jj new <REVSET_WITH_CONFICT>;
# Pulls up a great interactive TUI to select which changes should be canonical.
jj resolve;
# Moves changes down to the conflicted parent
# and rebases all descendants automatically.
# If you start with the conficts lower in the tree, there's a good chance
# that it will resovle conflicts higher up as well!
jj squash;
# Repeat each step working your way up the tree until the conflicts are gone.
jj rebase -r @ --onto <REVSET_WITH_CONFLICT>;
This fundamental paradigm shift of how conflicts are handled is what sold me on using jj. Conflicts aren’t as scary anymore and feel like they’re part of the natural process. Which, in turn, makes me feel like I can experiment more whet trying to solve a problem.
Pushing up to remotes
Pushing and updating to Git remotes isn’t as clunky as I thought it was, but still requires more commands than a typical Git workflow.
# Same command as the previous post to create and push a bookmark to a remote
jj b set -r @- "new-feat"
# The bookmark is automatically tracked when pushed via jj
jj git push -b "new-feat"
I learned that bookmarks, unlike Git branch references, don’t move with each new change. They have to be explicitly reassigned to a new change ID. Pushing to a remote without reassigning a bookmark will just return with a “no changes” message from the remote.
jj b set -r @- "new-feat"
# The bookmark flag can be omitted since it already tracked
jj git push
Finally, I’ve also figured out a way to continue drafting and submitting PR’s specifically using Forgejo CLI. All it requires is an extra flag to give the API a clear reference to base the PR on. As mentioned in my previous post, bookmarks in jj don’t seem to assign origins to the underlying Git branches locally, so this is a good way to avoid having to mess with the git backend using Git directly.
fj pr create "feat: new feature" --head new-feat
Fetching and updating references from a remote origin
I like to keep my Git history linear, and most merges I do for my own projects are squash merges. So, once a PR is merged, there’s some clean up to do on the local branch.
Thankfully, jj offers a streamlined DX for managing just that:
# Pulls new remote references from origin and reassigns any tracked bookmarks
jj git fetch
# Deletes the local bookmark and branch
jj abandon main..new-feat
# Pushes the branch deletion onto the origin
jj git push --deleted
Final thoughts
Jujutsu is great; it’s not without its tradeoffs, but I think the tradeoffs are well worth it. I used to rely heavily on tools like lazygit to perform and visualize more complex Git operations. But I feel like I don’t need a similar utility for jj because it empowers you by automating parts of the Git process and introducing new ways to incrementally fix any conflicts that naturally arise.
I think I can conclusively say that I’m adding jj as a permanent member of my tool belt. Give it a try yourself: build something, create some conflicts, and see if the workflow gels with you!