In short: jj loads the repository at a "from" operation and at a "to" operation, merges the from-side
commit index into an uncommitted transaction on the to-side so both sides' commits can be looked up,
and compares the two views. Commits are matched up through the predecessor records the operations
stored, falling back to change ids, and filtered by the revsets.op-diff-changes-in
revset. Working-copy commits, local bookmarks and tags, and remote bookmarks and tags are compared by
name. It never writes a new operation head.
The command is cmd_op_diff. The help text: "Compare
changes to the repository between two operations" (source). It is
registered as Diff(OperationDiffArgs) in the operation group
(source). The comparison itself is
show_op_diff, which jj operation show and
jj operation log --op-diff also call.
1. Command-line surface
| Flag | Meaning |
|---|---|
--operation, --op OP | "Show repository changes in this operation, compared to its parent". Defaults to @, the current operation. The from side is all of its parents (source, source). |
-f, --from OP | The from operation. Conflicts with --operation; defaults to @ when only --to is given (source, source). |
-t, --to OP | The to operation, with the same rules (source). |
-G, --no-graph | List the changed commits flat instead of as a graph (source). |
-p, --patch | Show a patch for each changed commit. "If the previous version has different parents, it will be temporarily rebased to the parents of the new version" (source). |
Diff format flags (--summary, --stat, --git, …) | The shared DiffFormatArgs (source); see jj diff. Any of them turns diffs on, -p or not (source). |
--show-changes-in REVSETS | "Show only changed revisions matching the given revset expression", defaulting to revsets.op-diff-changes-in (source). |
The global flags are on the CLI page.
2. Configuration
| Setting | Effect |
|---|---|
revsets.op-diff-changes-in | Which changed commits are listed; the rest are counted as elided. Default mutable() | immutable_heads() (source, source). |
templates.op_summary | The "From operation" and "To operation" lines: short id, end time (or root()), first line of the description (source, source). |
templates.commit_summary | Each commit line, labelled op_diff and commit (source). |
ui.graph.style, ui.diff-formatter, ui.pager | Graph style, default diff format, and pager; the command requests the pager (source, source). See jj log. |
3. End-to-end flow
4. Core architecture touchpoints
| Piece | Role here |
|---|---|
resolve_single_op | Resolves each operation argument against the current operation; see operation expressions. |
RepoLoader::merge_operations | No from-ops gives the root repo, one gives that op's repo. Several are merged in a transaction written as "merge N operations" and left unpublished (source). The CLI wrapper is passed no ui, so it prints nothing about rebased descendants (source). |
MutableRepo::merge_index | Adds the from-side commits to the to-side index, so revsets over both sides evaluate in one repo. |
accumulate_predecessors | Collects the predecessor records operations store, from the to-op back to the from-op, resolving chains of rewrites. |
diff_named_ref_targets, diff_named_remote_refs, diff_named_commit_ids | Walk two sorted name maps together and yield the names whose values differ; a missing side is absent or None. |
default_ignored_remote_name | With the Git backend, the git pseudo-remote, whose refs are skipped. |
5. What it reads, and snapshotting
It goes through workspace_helper, so the working copy is snapshotted first unless
--ignore-working-copy is given (source); the snapshot
is then the default to-operation. It reads the operation objects and views of both sides from the op
store, both commit indexes, and the commits it prints. With a diff format it also reads the trees and
file contents of those commits. Nothing is published: the transaction on the to-repo is dropped, and a
merged from-operation is written to the op store but not made an op head
(source, source).
6. Algorithm and invariants
Classifying commits
compute_operation_commits_diff takes the visible heads
on each side. Newly hidden commits are to_heads..from_heads, newly visible ones
from_heads..to_heads (source). Newly hidden commits in
the filter are grouped by change id, keeping one per change ("Just pick one if diverged"), and all of them
start out abandoned (source). Each newly visible commit in the filter
gets its predecessors from the operations' records if there are any, otherwise from a hidden commit with
the same change id, otherwise none; those predecessors stop counting as abandoned
(source). Whatever remains is shown as abandoned
(source). The commits and their predecessors are then loaded
concurrently, "since there may be many of them", up to the store's concurrency limit
(source).
| Kind | Shown as |
|---|---|
| New commit | + commit |
| Rewritten commit | + commit, then - predecessor for each predecessor (a squash has several) |
| Abandoned commit | - commit |
ModifiedChange, write_modified_change_summary.
Predecessors come from operations in one pass when the to-op's parents are exactly the from-ops; otherwise jj walks back from the from-op to the common ancestor and forward again, and the result is "lossy" if the two have no single common ancestor (source).
The graph
The changed commits are evaluated as a revset in the merged repo and drawn topologically. Edges to
commits that are not in the set are dropped "to keep the graph concise", and every node is
○ (source). With --no-graph they are
printed flat, in the revset's own order (source).
Elision
The filter is resolved separately against each side's repo (source).
Commits outside it are only counted, with count_estimate
(source), and summarized as
(Elided N newly added and M newly removed revisions), where a count is N+ or
some when the estimate is not exact (source).
Patches
For a new or rewritten commit, the patch is an inter-diff from its first predecessor only, so a squash's diff stays readable; upstream marks this as "technically wrong". A new commit has no predecessor and shows its whole patch. An abandoned commit shows its own patch, not a reverse one (source).
Working copies, bookmarks and tags
Each changed name prints + with the to-side target, then - with the from-side
target: a commit, (absent), or, for a conflicted target, one (added) or
(removed) line per side of the conflict (source).
Remote refs also show tracked or untracked; refs on the git
remote are skipped, since they "should typically be covered in local branches"
(source). Sections with no changes are not printed.
7. Errors and edge cases
| Situation | Behaviour |
|---|---|
--op with --from or --to | Rejected by clap (source). |
| An operation argument matches no operation, or several | For example No operation ID matching "…" or The "…" expression resolved to more than one operation (source). |
| Invalid filter revset | Invalid `--show-changes-in` expression: …, or a config error Invalid `revsets.op-diff-changes-in` (source). |
| The filter names something missing on one side | "Warning: Could not resolve revset expression for elision", with the hint (Use --show-changes-in=all() to see all changes). The commits section is skipped; the other sections still print (source, test). |
| The same operation on both sides | Only the two header lines (test). |
| The to-op is a merge operation | One "From operation" line per parent, compared against their merge (source). |
8. Example output
After writing a file and running jj new, upstream's test diffs the snapshot operation with a
Git-format patch (source):
$ jj op diff --op @- -p --git
From operation: f63ee16f9553 (2001-02-03 08:05:07) add workspace 'default'
To operation: 494872ff8b1e (2001-02-03 08:05:08) snapshot working copy
Changed commits:
○ + qpvuntsm 6b57e33c (no description set)
- qpvuntsm/1 e8849ae1 (hidden) (empty) (no description set)
diff --git a/file b/file
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/file
@@ -0,0 +1,1 @@ a
+a
Changed working copy default@:
+ qpvuntsm 6b57e33c (no description set)
- qpvuntsm/1 e8849ae1 (hidden) (empty) (no description set)
Bookmark changes, from the same test file (source):
Changed local bookmarks:
bookmark-1:
+ pukowqtp 0cb7e07e bookmark-1 | Commit 1
- (absent)
Changed remote bookmarks:
bookmark-1@origin:
+ tracked pukowqtp 0cb7e07e bookmark-1 | Commit 1
- untracked pukowqtp 0cb7e07e bookmark-1 | Commit 1
See also jj evolog, which follows predecessors for one change, and operations.