In short: jj resolves -r (default @) and finds the conflicted paths that
match the arguments. With none, it fails with No conflicts found. With
--list, it prints them and stops. Otherwise it refuses an immutable commit, checks that
every chosen conflict is between regular files, and 2-sided unless the tool is :editor, and runs the merge tool on them in
order. The first failure stops it. jj rewrites the commit with the files resolved so far, even if a
later one failed, then reports that error. Descendants are rebased by tx.finish. The
operation is Resolve conflicts in commit <id>.
The command is cmd_resolve. Its help text notes that
you can also resolve conflicts by editing the conflict markers in the file directly
(source). How conflicts are stored is explained on the
conflicts page; this page covers only the command and the
merge tools.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional) | filesets | all files | Only resolve conflicts in these paths (source). |
-r, --revision REVSET | revset | @ | The commit whose conflicts to resolve. It must be exactly one commit (source). |
-l, --list | bool | false | List the conflicts instead of resolving them (source). |
--tool NAME | string | ui.merge-editor | The merge tool to use. :editor opens the conflict markers in your text editor; :ours and :theirs pick side 1 or side 2. Conflicts with --list (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
ui.merge-editor | unset, meaning :builtin | The tool used without --tool. When it's unset jj prints a hint and uses the built-in one. A plain name is looked up as a tool; anything with arguments is run as a command whose arguments are the merge arguments (from_settings, default). |
ui.editor | platform default | The text editor :editor runs. The file's path is appended unless the command uses $path; $line is the line of the first conflict (source). |
merge-tools.<name>.merge-args | empty | Arguments for an external tool, with $base, $left, $right, $output, $marker_length and $path substituted. A tool without them can't be used here (field, check, variables). |
merge-tools.<name>.merge-tool-edits-conflict-markers | false | If true, $output starts with the conflict markers and jj re-parses any left in it. If false, it starts empty and is taken as the full resolution (source). |
merge-tools.<name>.merge-conflict-exit-codes | empty | Exit codes meaning the tool left some conflicts in $output, as conflict markers (source). |
merge-tools.<name>.conflict-marker-style, ui.conflict-marker-style | unset; diff | Marker style for a pre-filled $output. :editor uses ui.conflict-marker-style (source, default). |
jj ships settings for several tools, including meld, kdiff3,
vimdiff, vscode and mergiraf, so a bare name works for those
(merge_tools.toml). The names
:builtin, :editor, :ours and :theirs are built in and
checked before any configured tool (source,
docs).
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commit | rewritten | A new commit whose tree has the resolved files. It keeps its parents, change ID, description and author. | commits |
| Conflicted tree values | read, replaced | Each conflict is a Merge of file IDs. A resolution replaces it with one file, or with new file IDs if conflicts remain. | trees |
| File objects | read, written | The base and both sides are read; the tool's output is written as a new file. | trees |
| Descendants | rewritten | Rebased onto the new commit by tx.finish. | transactions |
View: heads, bookmarks, wc_commit_ids | write | Follow the rewritten commits. | view |
| Operation | write | One operation, even when a later file failed. | operations |
| Working copy | read, then maybe write | Snapshotted first. If @ was rewritten, the resolved files are written to disk. | working-copy |
5. Transaction and operation
- Transaction: one, after the conflict list and before the tool runs.
--list, and errors before any file is resolved, record nothing (source). - Operation description:
Resolve conflicts in commit <full hex id before the rewrite>. It is capitalized, unlike most of jj's (source). - Partial resolution: if an external tool or
:editorfails on the second file or later, the files already resolved are still committed and the operation is recorded. Only then doesjj resolveexit withStopped due to error after resolving <n> conflicts(source, return). - Descendants: the command doesn't rebase them itself;
tx.finishdoes (source). jj undorestores the previous view, bringing back the conflicted commit.
6. Algorithm and invariants
Finding and checking conflicts
tree.conflicts_matching(&matcher) collects the conflicted paths under the arguments
(source). Before any tool runs,
MergeToolFile::from_tree_and_path checks every one
of them, and any failure stops the whole command:
- The value must actually be a conflict, and present.
- Every term must be a regular file or absent. Symlinks, directories and submodules can't be resolved here.
- Unless the tool is
:editor, the conflict must have at most 2 sides after simplifying. Simplifying removes terms that cancel out, so a conflict that only looks larger can still qualify (source, check_two_sided, try_materialize_file_conflict_value). - The executable bit must resolve on its own. If it conflicts, the hint points to
jj file chmod.
An external tool
run_mergetool_external hands
resolve_files_one_by_one a closure. That loop prints
Resolving conflicts in: <path> and calls
run_mergetool_external_single_file for each file in
turn:
- It writes
base_<name>,left_<name>,right_<name>andoutput_<name>into ajj-resolve-temp directory. Left is side 1, right is side 2. All butoutputare read-only (source). outputstarts empty, or with conflict markers if the tool edits markers. The marker length is the minimum unless the tool sees markers or asks for$marker_length; then it is long enough not to clash with the file's content (source).- It runs the tool. A non-zero exit is a failure unless the code is listed in
merge-conflict-exit-codes(source). - An empty or unchanged
outputis a failure. That's how you stop: quit the tool without saving (source). - If the tool edits markers, or exited with a conflict code, jj parses
outputfor markers and may keep a smaller conflict. Otherwiseoutputbecomes the file. A conflict exit code with no markers is reported as invalid (source). - A resolved file keeps the resolved executable bit. A still-conflicted one keeps its executable bits and gets new file IDs (new_tree_value).
The :editor tool
:editor runs ui.editor on the conflict markers, one file at a time through the same
resolve_files_one_by_one loop (source). It is the only
tool that accepts conflicts with more than 2 sides.
edit_conflict_markers_in_text_editor does this for each file:
- It picks a marker length that can't clash with the file's content, and materializes the conflict with markers in
ui.conflict-marker-style(source). - It writes that into a
jj-resolve-temp directory, under the conflicted file's own name so the editor can pick the right syntax highlighting (source). - It opens the editor at the line of the first conflict start marker (source).
- An empty or unchanged file is a failure, as with an external tool (source).
- Otherwise jj parses the file against the conflict's unsimplified terms. Markers left in it are kept as a conflict, possibly a smaller one. The tree value is built the same way as for an external tool (source).
The built-in tools
:builtin opens one terminal UI over all the
conflicted files, where you choose which side's changes to keep. :ours and
:theirs need no UI: pick_conflict_side
sets every file to side 1 or side 2 of its simplified conflict. A side where the file is absent deletes
it (edit_files).
Invariants: only conflicted paths are touched, and only the target commit's tree
changes. Files are handled in the order the tree lists them, and a failure on the first one records nothing. 3-sided and
larger conflicts are only sent to :editor; with any other tool, use :editor or
edit the markers by hand.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No conflicts found at this revision / No conflicts found at the given path(s) | Nothing to resolve. A CLI error, exit status 2. | resolve.rs |
Failed to resolve conflicts, caused by The conflict at "…" has <n> sides. At most 2 sides are supported., hint Edit the conflict markers to resolve this, e.g. with `jj resolve --tool :editor`. | A chosen conflict has more than 2 sides, and the tool isn't :editor. | mod.rs, command_error.rs |
Only conflicts that involve normal files (not symlinks, etc.) are supported. … | A term is a symlink, directory or submodule. | mod.rs |
"…" has conflicts in executable bit, hint Use `jj file chmod` … | The sides disagree on the executable bit. | mod.rs |
The output file is either unchanged or empty after the editor quit … | The tool or :editor was closed without a resolution. | mod.rs |
Tool exited with <status> …; … but did not produce valid conflict markers … | The tool failed, or used a conflict exit code without leaving markers. | external.rs |
Failed to run editor '…'; Editor '…' exited with <status> | :editor couldn't start ui.editor, or it failed. | description_util.rs |
Stopped due to error after resolving <n> conflicts | A later file failed. The earlier resolutions are kept. | mod.rs |
The tool `<name>` cannot be used as a merge tool with `jj resolve`, with a hint to define merge-args | The tool has no merge arguments. | command_error.rs |
Warning: After this operation, some files at this revision still have conflicts: | Conflicts remain in a commit other than @. For @, tx.finish reports them instead. | resolve.rs |
Commit … is immutable | The commit is immutable. --list works on any commit. | cli_util.rs |
clap: --tool cannot be used with --list | Both given. | resolve.rs |
8. Worked example
From upstream's test_resolution. base has
file = base; a and b change it to a
and b; conflict merges them. jj resolve --list prints
file 2-sided conflict. With a merge tool that writes resolution into
$output:
Upstream's snapshot (source):
Resolving conflicts in: file
Working copy (@) now at: vruxwmqv 741263c9 conflict | conflict
Parent commit (@-) : zsuskuln 45537d53 a | a
Parent commit (@-) : royxmykx 89d1b299 b | b
Added 0 files, modified 1 files, removed 0 files
The tool saw an empty output file, as the test checks by dumping it. jj diff
then shows the conflict markers replaced by resolution, and
jj resolve --list fails with No conflicts found at this revision.
Upstream's test_multiple_conflicts_with_error shows a partial
run. The tool resolves file1, then quits on file2 without writing. jj still
rewrites the commit, so file1 is resolved, warns that file2 is still
conflicted, and then exits with:
Error: Stopped due to error after resolving 1 conflicts
Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
Upstream's test_resolve_many_sided_with_text_editor merges three
children of base, so file is a 3-sided conflict in a commit that isn't
@. jj resolve -r conflict --tool :editor opens the markers. If the editor only
changes c to c2, the commit keeps a 3-sided conflict with the new content and jj
warns that the file still has conflicts. If it writes resolution, the conflict is gone.