6.3 KiB
Resolving Conflicts
jj treats conflicts as first-class objects: a commit can contain a conflict, and that conflict persists through rebases until someone resolves it. This is the headline mental-model difference from git, where a working tree must be conflict-free before committing.
For an agent, this changes the rhythm:
- A
jj rebaseorjj squashthat produces a conflict does not fail. It succeeds; the resulting commit is just marked conflicted. - You can keep working — even keep rebasing the conflicted commit further — and resolve it later.
jjwon't let you push a conflicted commit to a remote, so conflicts must be resolved beforejj git push.
Detecting conflicts
jj st # Shows "There are unresolved conflicts" if any
jj resolve --list # Lists all conflicted paths in @
jj resolve --list -r <change-id> # Lists conflicts in a specific change
jj log # Conflicted changes are shown with a "conflict" marker
Conflict marker formats
jj supports three marker styles (set via ui.conflict-marker-style):
diff(modern default) — shows base→side as a diffsnapshot— full content of each side, like git's<<<<<<<stylegit— legacy git-compatible 3-way
Check your style:
jj config get ui.conflict-marker-style
Example: diff style
<<<<<<< Conflict 1 of 1
%%%%%%% Changes from base to side #1
-base content here
+side 1 content here
+++++++ Contents of side #2
side 2 content here
>>>>>>> Conflict 1 of 1 ends
- Side #1 = "ours" (the destination — typically your current branch/change)
- Side #2 = "theirs" (the source — typically what's being rebased/merged in)
- The
%%%%%%%block shows what side #1 changed from the common base; side #2's content is shown directly.
Example: snapshot style
<<<<<<< Conflict 1 of 1
+++++++ Contents of side #1
side 1 content here
%%%%%%% Contents of base
base content here
+++++++ Contents of side #2
side 2 content here
>>>>>>> Conflict 1 of 1 ends
Both styles delimit conflict regions identically. To resolve: replace the entire <<<<<<< ... >>>>>>> block with the desired final content.
Five resolution paths (agent-friendly)
jj resolve with no --tool launches an interactive merge tool and will hang in non-interactive agent environments. Use one of these instead:
1. Edit conflict markers directly (most common)
Read the file, replace each <<<<<<< block with the resolved content, save. Then snapshot:
jj st # Triggers snapshot; should report no remaining conflicts
This is the canonical agent path. Works for any merge complexity.
2. Take one side wholesale: :ours / :theirs
For conflicts where one side simply wins (e.g., a rebase brings in unrelated formatting changes you don't want):
jj resolve --tool :ours # Keep side #1; drop side #2's conflicting changes
jj resolve --tool :theirs # Keep side #2; drop side #1's conflicting changes
# Scope to specific paths
jj resolve --tool :ours path/to/file.rs
These built-in tools are non-interactive and safe to use in agent contexts. They apply to all conflicts in @ (or the paths given).
3. Restore from a specific version: jj restore --from
When neither side is right but a different revision has the content you want:
jj restore --from <change-id> path/to/file.txt
This pulls the file contents from <change-id> into the working copy, replacing conflict markers entirely.
4. Abandon a hopelessly conflicted change
If the conflicted change wasn't worth keeping:
jj abandon <change-id>
Descendants are rebased onto the parent. Their own conflicts (if any) remain to be resolved separately.
5. Resolve at the ancestor, let jj re-propagate
If a rebase produced the same conflict in many descendants, don't resolve each descendant individually. Resolve once at the change where the conflict first appeared, and jj will re-propagate the resolution forward:
# Find where the conflict first appeared
jj log -r 'conflicts()'
# Edit that change directly
jj edit <ancestor-change-id>
# ... fix conflict markers ...
jj st # Snapshot the resolution
jj edit @- # Move back to where you were
Descendants pick up the resolution automatically.
Verifying a resolution
After editing, don't trust jj st alone:
# 1. Status should report no conflicts
jj st
# 2. Grep for residual markers (catches typos that left markers in place)
git grep -E '^(<{7}|>{7}|\\%{7}|\\+{7})' || echo 'no residual conflict markers'
# 3. Run the project's tests/linter — markers may be syntactically valid in some
# languages (e.g. inside strings) and parse-tests catch what eyeballing misses.
In a colocated repo, git grep is allowed (read-only). In a non-colocated repo, use grep -rE or rg.
Pushing conflicted commits is blocked
jj git push -b main
# Refused: branch main contains conflicts
You'll need to resolve before pushing. There's no --force-push-conflicts option — this is by design.
Anti-patterns
- ❌
jj resolve(no--tool) — interactive; will hang the agent. Always pass--tool :ours/--tool :theirsor edit markers manually. - ❌ Resolving descendants individually after a rebase — wasteful and error-prone; resolve at the ancestor (path 5).
- ❌ Relying on
jj stto verify resolution — also grep for residual markers and run tests. - ❌ Trying to
git checkoutto "drop" a conflict in a colocated repo — corrupts jj state. Usejj abandonor resolve. - ❌
jj squashto "hide" a conflict into another commit — the conflict propagates; it doesn't disappear.
Quick reference
| Action | Command |
|---|---|
List conflicts in @ |
jj resolve --list |
| List conflicts everywhere | jj log -r 'conflicts()' |
| Resolve manually | Edit file, then jj st |
| Take "ours" (side #1) | jj resolve --tool :ours [paths...] |
| Take "theirs" (side #2) | jj resolve --tool :theirs [paths...] |
| Restore from another rev | jj restore --from <change-id> <path> |
| Drop the change | jj abandon <change-id> |
| Find conflict's ancestor | jj log -r 'conflicts() ~ ::conflicts()-' |
| Check marker style | jj config get ui.conflict-marker-style |