In short: jj loads the workspace, which snapshots it, and turns the paths into a matcher. With
--from/--to it resolves each to exactly one commit (default @) and
compares their trees. Otherwise it evaluates -r (default @), refuses a set with
gaps, and compares the merged trees of the roots' parents with the merged trees of the heads. Copy
records join the two sides, then a template or the diff formats print the result. Nothing is written.
The command is cmd_diff. The help text: "Compare
file contents between two revisions", defaulting to -r @
(source). Unlike jj show, it prints no commit header and
its result is always a single diff, however many revisions are selected.
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
-r, --revisions REVSETS (alias --revision, repeatable) | revset | @ | "Show changes in these revisions". Several revisions give their total diff: in a chain A..D, -r B::D equals --from A --to D. Gaps are not supported (source). |
-f, --from REVSET | revset, one commit | @ if only --to is given | "Show changes from this revision". Conflicts with -r (source). |
-t, --to REVSET | revset, one commit | @ if only --from is given | "Show changes to this revision". Conflicts with -r (source). |
FILESETS (positional, repeatable) | fileset | all paths | "Restrict the diff to these paths" (source, source). |
-T, --template TEMPLATE | template | none | Render each changed path with a template; the keywords are the methods of TreeDiffEntry. Conflicts with every format flag and --tool (source). |
Short formats: -s/--summary, --stat, --types, --name-only | flags, at most one | none | Per-path summary, histogram, type letters, or path only (source). |
Long formats: --git, --color-words | flags, at most one | none | A Git-format or word-level diff. One short and one long format can be combined (source). |
--tool NAME | string | none | An external diff command, or :name for a built-in format (source). |
--context N | integer | diff.git.context / diff.color-words.context | Lines of context (source). |
-w/--ignore-all-space, -b/--ignore-space-change | flags | off | Whitespace handling when comparing lines; diff gives them their short names (source, source). |
Global flags such as --at-op and --ignore-working-copy are on the CLI page.
2. Configuration
| Key | Default | Effect |
|---|---|---|
ui.diff-formatter | :color-words | The format when no format flag and no -T is given (source). A :name picks a built-in; anything else is an external tool (source). |
diff.color-words.* | conflict = "materialize", max-inline-alternation = 3, context = 3 | Options for the default format (source). |
diff.git.* | context = 3, show-path-prefix = true | Options for --git (source). |
ui.conflict-marker-style | diff | How conflicted files are written out when diffed (source, source). |
ui.pager, ui.paginate | less -FRXK, auto | Output goes through the pager (source). |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Revset engine | read | Parses -r, --from and --to; evaluates the set, its roots, heads and gaps (source, source). | revsets |
| Filesets | read | The positional paths become the matcher that limits the diff and the copy records. | revsets |
| Commit index | read | Answers the ancestry questions behind roots, heads and the gap check. | index |
| Commits | read | The resolved commits and the roots' parents. | commits |
| Trees | read | The from and to trees, diffed path by path. | trees |
| Copy records | read | From the backend between the two sides; shown as (old => new) (source). | trees |
| Conflicts | read | Several parents or heads are merged into one tree; conflicted files are materialized for the diff. | conflicts |
| Operation log | untouched | No transaction is started. | operations |
5. What it reads, and snapshotting
- Snapshot: yes. It loads the workspace with
workspace_helper(source), which snapshots the working copy first (source), sojj diffshows current edits. That snapshot may record asnapshot working copyoperation;diffitself records nothing. - Reads: the view at the current (or
--at-op) operation, the index, the selected commits and the roots' parents, their trees, file contents for the matched paths, and copy records. - Output: through the pager, requested once both trees and the renderer are ready (source). The unmatched-paths warning goes to stderr after the diff (source).
6. Algorithm and invariants
Diffing a set of revisions
With -r, jj does not diff each revision in turn. It takes the set's roots and heads, and
first checks that nothing between them is missing: roots..heads, the commits reachable from a
head but not from a root (source), minus the set itself, must be
empty (source). Then the parents of the roots, kept in order and
without duplicates, are merged into the "from" tree, and the heads are merged into the "to" tree
(source). One commit gives its own tree; several are merged with
their merge bases and resolved where possible (merge_commit_trees).
So -r X for a single commit is the same diff jj show X prints, and a set is
one combined diff.
Upstream's test_diff_revisions exercises the cases. A single
revision, a merge, a chain (A::C), a chain with an internal merge (B::E),
several roots (C|D|E: both roots have the parent B) and several heads (B|C|D:
the to tree merges C and D) all work. A|C is refused because B is missing, and
C|E because D, E's other parent, is reachable from the head but not from the root.
--from and --to
Here no merging happens: each side must resolve to exactly one commit, and its tree is used as it is
(source). The two commits need not be related, so
jj diff --from main compares main with @ directly, whatever
lies between them. Copy records are fetched for that one pair (source).
-T and the formats
Without -T, the formats are chosen as for jj show: at most one short and one
long format from the flags, else ui.diff-formatter (source;
see jj show). With -T, the renderer
gets an empty format list, so it prints nothing, and the template is applied to each entry of the tree
diff instead (source, source).
The template sees the copy records too, so a rename is one entry with status renamed.
Invariants: exactly two trees are compared, whatever the arguments. The matcher limits both the diff and the copy records. Nothing in the repository changes.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Cannot diff revsets with gaps in., hint Revision <id> would need to be in the set. | The -r set leaves out a commit between its roots and heads, e.g. -r 'A|C'. | diff.rs, test |
Revset `<rev>` didn't resolve to any revisions | --from or --to is empty. | revset_util.rs |
Revset `<rev>` resolved to more than one revision, listing them | --from or --to names several commits. | revset_util.rs |
| A clap "cannot be used with" error | -r with --from/--to; -T with a format flag or --tool; two short or two long formats; -w with -b. | diff.rs, test |
--tool=<name> cannot be used with --<format> | --tool asks for a format whose slot a flag already filled. | diff_util.rs |
Invalid builtin diff format: <name> | --tool=:name or ui.diff-formatter names no built-in format. | diff_util.rs |
The tool `<name>` cannot be used for diff formatting | --tool or ui.diff-formatter names a configured tool whose diff-args is empty. | diff_util.rs |
Failed to generate diff | An external diff tool failed. | diff_util.rs |
Warning: No matching entries for paths: <paths> | An explicit path exists in neither tree. A path in only one of them is fine. The diff is still printed. | cli_util.rs, test |
| (empty output) | The two trees match, or the paths match nothing. | diff_util.rs |
8. Example output
From upstream's test_diff_basic: @ deletes
file1, edits file2, and adds file3 and file4 with the
old contents of file1 and file2, which the Git backend reports as a rename and a
copy. The default :color-words format:
$ jj diff
Modified regular file file2:
1 1: 1
2 : 2
2: 5
3 3: 3
4 : 4
Modified regular file file3 (file1 => file3):
Modified regular file file4 (file2 => file4):
The same diff as a summary (source):
$ jj diff -s
M file2
R {file1 => file3}
C {file2 => file4}
And through the template source.path() ++ ' => ' ++ target.path() ++ ' (' ++ status ++ ")\n"
passed with -T (source):
file2 => file2 (modified)
file1 => file3 (renamed)
file2 => file4 (copied)
A set with a gap, from test_diff_revisions:
$ jj diff --name-only -r 'A|C'
Error: Cannot diff revsets with gaps in.
Hint: Revision 2378873cd201 would need to be in the set.