In short: jj loads the workspace, which snapshots it, and resolves --from and
--to to one commit each (whichever is missing defaults to @). It warns about
explicit paths found in none of the four trees involved. Then it rebases --from's tree onto
--to's parents in memory, diffs the two descriptions as a pseudo-file, and diffs the rebased
tree against --to's tree. Nothing is written.
The command is cmd_interdiff. The help text: "Show
differences between the diffs of two revisions". It is "like running jj diff -r on each
change, then comparing those results", and the typical use is checking how a change moved since it was
pushed, jj interdiff --from push-xyz@origin --to push-xyz
(source). Unlike jj diff --from A --to B, which
includes everything that differs between A's and B's parents, interdiff leaves that out.
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
-f, --from REVSET | revset, one commit | @ if only --to is given | "The first revision to compare" (source). |
-t, --to REVSET | revset, one commit | @ if only --from is given | "The second revision to compare" (source). |
At least one of --from and --to is required: they form a clap group that is both multiple and required (source). | |||
FILESETS (positional, repeatable) | fileset | all paths | "Restrict the diff to these paths" (source, source). |
Short formats: -s/--summary, --stat, --types, --name-only | flags, at most one | none | The shared diff format flags, flattened in (source, 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; interdiff gives them their short names (source, source). |
There is no -r and no -T: unlike jj diff,
each side is exactly one revision and the output is always a rendered diff.
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 is given (source, 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 a conflicted file, including one the rebase produced, is 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 | Resolves --from and --to to one commit each (source). | revsets |
| Filesets | read | The positional paths become the matcher that limits the tree diff. | revsets |
| Commits | read | Both commits, their parents and their descriptions. | commits |
| Trees | read | Four trees are loaded: each commit's and each commit's parent tree. | trees |
| Conflicts | read | The rebase is a three-way tree merge; a clash becomes a conflicted file in the "from" side of the diff (source). | conflicts |
| Copy records | not used | An empty set is passed, marked // TODO, so renames show as a delete plus an add (source). | trees |
| 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), so--to @sees current edits. That snapshot may record asnapshot working copyoperation;interdiffitself records nothing. - Reads: the view at the current (or
--at-op) operation, the two commits and their parents, the four trees, and file contents for the matched paths. - Output: the unmatched-paths warning goes to stderr before the pager is requested (source); the diff goes through the pager.
6. Algorithm and invariants
Rebasing --from onto --to's parents
The "from" side of the diff is not --from's tree. It is what --from's tree
would be if --from sat on --to's parents:
rebase_to_dest_parent takes
--to's parent tree and applies the change from --from's parent tree to
--from's tree, as one three-way merge (source). The
"to" side is --to's own tree (source). Whatever the
two parents disagree on is therefore on both sides and cancels out.
There is a fast path: if --from has exactly the same parents as --to, its
tree is used unchanged and no merge runs (source). Then
interdiff shows the same file changes as jj diff --from A --to B, except that
renames are not detected. When the
parents differ, the merge can conflict; the conflict is materialized on the "from" side, labelled with
the three roles above, and the diff shows it being replaced by --to's content (see the
example).
The description pseudo-file
Before the files, the two commit descriptions are diffed under the dummy path
JJ-COMMIT-DESCRIPTION (source).
show_diff_bytes prints nothing if they are equal,
and nothing for the short formats, where a dummy path "wouldn't be useful"; it prints a hunk for
--git and --color-words, and nothing yet for an external --tool
(source). So interdiff -s lists only real files.
Shared with evolog and op log
The same show_inter_diff renders the patches of jj evolog -p, where each
entry is compared with its predecessors (source),
and of jj op log -p / jj op show -p for rewritten commits, which use only the
first predecessor (source).
interdiff always passes exactly one "from" commit (source).
Invariants: exactly two trees are compared. Neither commit is rewritten and nothing is
stored; the rebased tree exists only for the diff. Swapping --from and --to
generally does not just reverse the output, since the rebase always goes onto --to's parents.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
| A clap "required arguments were not provided" error | Neither --from nor --to is given. | interdiff.rs |
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 | Two short or two long formats, or -w with -b. | diff_util.rs |
Warning: No matching entries for paths: <paths> | An explicit path exists in none of the four trees. A file deleted by either commit still counts as found. The diff is still printed. | cli_util.rs, test |
| Conflict markers in the "from" side | The parents differ and --from's change does not apply cleanly to --to's parents. | test |
| A file shown as removed and another as added | A rename: no copy records are passed. | diff_util.rs |
| (empty output) | Both commits make the same change and have the same description, or the paths match nothing. | diff_util.rs |
8. Example output
From upstream's test_interdiff_basic: left adds
file2 containing foo on top of a commit adding file1;
right, with @ on it, adds file2 containing foo and
bar on top of a different commit adding file3. The parents' difference
(file1 against file3) does not appear:
$ jj interdiff --from left
Modified JJ-COMMIT-DESCRIPTION:
1 : add file2 left
1: add file2 right
Modified regular file file2:
1 1: foo
2: bar
The same pair with -s, then --git (source):
$ jj interdiff --from left --to right -s
M file2
$ jj interdiff --from left --to right --git
diff --git a/JJ-COMMIT-DESCRIPTION b/JJ-COMMIT-DESCRIPTION
--- JJ-COMMIT-DESCRIPTION
+++ JJ-COMMIT-DESCRIPTION
@@ -1,1 +1,1 @@
-add file2 left
+add file2 right
diff --git a/file2 b/file2
index 257cc5642c..3bd1f0e297 100644
--- a/file2
+++ b/file2
@@ -1,1 +1,2 @@ foo
foo
+bar
When the rebase conflicts, from test_interdiff_conflicting:
left changes file from foo to bar, while
right's parent has abc and right has def. Moving
left's change onto abc conflicts, and the diff replaces that conflict with
def. The descriptions are both empty, so no description hunk is printed:
$ jj interdiff --from left --to right --git
diff --git a/file b/file
index 0000000000..24c5735c3e 100644
--- a/file
+++ b/file
@@ -1,8 +1,1 @@ def
-<<<<<<< conflict 1 of 1
-%%%%%%% diff from: qpvuntsm d0c049cd (original parents)
-\\\\\\\ to: zsuskuln 0b2c304e (new parents)
--foo
-+abc
-+++++++ rlvkpnrz b23f92c3 (original revision)
-bar
->>>>>>> conflict 1 of 1 ends
+def