jj / How jj operation diff works

How jj operation diff works

jj operation diff (alias jj op diff) compares the repository as recorded by two operations: which commits appeared, were rewritten or disappeared, and which working-copy commits, bookmarks and tags moved.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. What it reads, and snapshotting
  6. 6. Algorithm and invariants
  7. Classifying commits
  8. The graph
  9. Elision
  10. Patches
  11. Working copies, bookmarks and tags
  12. 7. Errors and edge cases
  13. 8. Example output

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

FlagMeaning
--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 OPThe from operation. Conflicts with --operation; defaults to @ when only --to is given (source, source).
-t, --to OPThe to operation, with the same rules (source).
-G, --no-graphList the changed commits flat instead of as a graph (source).
-p, --patchShow 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

SettingEffect
revsets.op-diff-changes-inWhich changed commits are listed; the rest are counted as elided. Default mutable() | immutable_heads() (source, source).
templates.op_summaryThe "From operation" and "To operation" lines: short id, end time (or root()), first line of the description (source, source).
templates.commit_summaryEach commit line, labelled op_diff and commit (source).
ui.graph.style, ui.diff-formatter, ui.pagerGraph style, default diff format, and pager; the command requests the pager (source, source). See jj log.

3. End-to-end flow

jj operation diff: call flow After the shared lifecycle, cmd_op_diff loads the workspace and snapshots it, then resolves the operations: with --from or --to, one from-op and one to-op, each defaulting to @; otherwise the to-op is --operation or @ and the from-ops are its parents. merge_operations turns several from-ops into one unpublished merged operation. Both repos are loaded, and the from-repo's index is merged into a transaction started on the to-repo that is never committed. The revset filter is parsed, the header lines printed, and show_op_diff prints changed commits, working copies, local bookmarks and tags, and remote bookmarks and tags. Shared lifecycle: parse args, load config (CLI page) workspace_helper: load the repo at @, snapshot the working copy --from / --to: one op each side each defaults to @ to = --op or @; from = to.parents() a merge operation has several parents merge_operations(from_ops), then load_at both sides several from-ops are merged into one unpublished operation to_repo.start_transaction(); merge_index(&from_repo) both sides' commits are in one index; the transaction is never committed parse the changes-in revset; print "From operation:" / "To operation:" one From line per from-op Changed commits + new, − old or abandoned Changed working copy per workspace name Bookmarks and tags local, then remote

4. Core architecture touchpoints

PieceRole here
resolve_single_opResolves each operation argument against the current operation; see operation expressions.
RepoLoader::merge_operationsNo 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_indexAdds the from-side commits to the to-side index, so revsets over both sides evaluate in one repo.
accumulate_predecessorsCollects 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_idsWalk two sorted name maps together and yield the names whose values differ; a missing side is absent or None.
default_ignored_remote_nameWith 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).

KindShown 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

SituationBehaviour
--op with --from or --toRejected by clap (source).
An operation argument matches no operation, or severalFor example No operation ID matching "…" or The "…" expression resolved to more than one operation (source).
Invalid filter revsetInvalid `--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 sidesOnly the two header lines (test).
The to-op is a merge operationOne "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.