In short: jj resolves the current operation, walks its ancestors lazily in reverse topological order
(ordered by end time where the history forks), and renders each operation with the
templates.op_log template beside a graph node. With --op-diff or a patch flag it
also prints, under each operation, what that operation changed compared to its parent, using the same
code as jj operation diff.
The command is cmd_op_log, and the work is in
do_op_log. The help text: "Show the operation log",
noting that "Like other commands, jj op log snapshots the current working-copy changes and
reconciles divergent operations" (source). It is registered as
Log(OperationLogArgs) in the operation group
(source, dispatch).
1. Command-line surface
| Flag | Meaning |
|---|---|
-n, --limit N | "Limit number of operations to show". "Applied after operations are reordered topologically, but before being reversed" (source). |
--reversed | "Show operations in the opposite order (older operations first)" (source). |
-G, --no-graph | "Show a flat list of operations instead of a graph" (source). |
-T, --template TEMPLATE | Render each operation with this template instead of templates.op_log; all zero-argument methods of the Operation type are keywords (source). |
-d, --op-diff | "Show changes to the repository at each operation" (source). |
-p, --patch | "Show patch of modifications to changes (implies --op-diff)", rebasing a previous version onto the new version's parents first if they differ (source). |
Diff format flags (--summary, --stat, --git, …) | The shared DiffFormatArgs (source). Any format also turns the operation diff on (source); see jj diff. |
--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; two of them,
--at-op and --ignore-working-copy, change how this command loads (see
section 5).
2. Configuration
| Setting | Default | Effect |
|---|---|---|
templates.op_log | builtin_op_log_compact | Each operation's text (default, source). The built-ins are builtin_op_log_compact, _comfortable (compact plus a blank line), _oneline and _redacted (source). |
templates.op_log_node | builtin_op_log_node, or the ASCII variant when ui.graph.style starts with ascii | The node symbol: @ for the current operation, otherwise ○ (o in ASCII) (source, source, source). |
templates.commit_summary | The commit lines of an operation diff, labelled op_log and commit (source, source). | |
revsets.op-diff-changes-in | mutable() | immutable_heads() | Which changed commits an operation diff lists (source); see elision. |
ui.graph.style, ui.log-word-wrap, ui.pager | Graph style, wrapping text to the width left beside the graph, and the pager, which the command requests (source, source, source). See jj log. |
The default text template, builtin_op_log_compact, labels the current operation
current_operation and prints the root operation as its short id and root()
(source). Every other operation gets a first line of short id,
user@host, the workspace name with @ (empty if the operation has none), and its
time range; then the first line of its description; then its attributes, such as args:, one
per line (source). Snapshot operations use the same format
(source). The Operation keywords are
current_operation, description, id, attributes,
snapshot, workspace_name, time, user, root and
parents, plus a deprecated tags (source).
3. End-to-end flow
4. Core architecture touchpoints
| Piece | Role here |
|---|---|
CommandHelper::resolve_operation | With --at-op, resolves that expression; see operation expressions. Otherwise it resolves the op heads, merging several into a new "reconcile divergent operations" operation; see concurrency. |
op_walk::walk_ancestors | Streams the operation and all its ancestors, each parent loaded from the op store when reached. |
OperationByEndTime | Orders operations by end time, then by the operation itself to stay consistent with equality. |
dag_walk_async::topo_order_reverse_lazy | The lazy reverse topological walk (see the walk). |
graph::reverse_graph | For --reversed in graph mode: flips every edge and the node order. |
merge_operations and show_op_diff | The operation diff under each operation, against the merge of its parents. |
5. What it reads, and snapshotting
Without --at-op (or with --at-op=@) and without --ignore-working-copy,
the command goes through workspace_helper: it loads the repo at the head operation, merging
divergent op heads into a new operation if there are several, and snapshots the working copy, which can
write a snapshot operation. That operation is then the current one and is listed first
(source, source).
Otherwise it loads only the workspace and resolves the operation, without loading the repo at it,
"so that the operation history can be inspected even with a corrupted repo state. For example, you can
find the first bad operation id to be abandoned" (source). The help
text recommends --at-op=@ --ignore-working-copy "to inspect the current state without
mutation" (source). Op heads are still merged if
--at-op is absent (source).
Beyond that it reads operation objects from the op store. Only with --op-diff or a diff
format does it read views, commit indexes, commits and, for patches, trees and file contents.
6. Algorithm and invariants
The walk
walk_ancestors wraps each operation in OperationByEndTime and calls
topo_order_reverse_lazy, whose neighbours are the operation's parents
(source). While at most one operation is pending, the walk is
linear: it emits that operation and queues its parents (source).
When several are pending, it pops operations by latest end time until the branches meet again in a single
operation, then sorts that piece topologically and emits it
(source, source). Upstream's
note: this "works so long as the operation history is mostly linear"
(source). Each operation is emitted once, and a cycle panics with
"graph has cycle" (source).
The graph
Each operation's edges are one direct edge per parent id; there are no indirect or missing edges as in
jj log (source).
The text is rendered into a buffer at the width left beside the graph, followed by the operation diff if
any, and then added as a node with the op_log_node symbol
(source). Without --reversed, operations are printed as
the stream yields them.
--limit and --reversed
--limit is a take on the stream, so it cuts the newest-first order
(source). --reversed must then collect every remaining
operation: in graph mode reverse_graph turns parent edges into child edges and reverses the
order; in flat mode the list is simply reversed (source,
source). So -n 3 --reversed shows the three newest
operations, oldest first.
Operation diffs
For each operation, jj loads its parents and merges them with merge_operations, passing the
current workspace's name but no ui, description or arguments, then loads the repo at the merged parent and at
the operation (source). If the operation has more than one parent it
prints nothing, because the merged repo "may have newly rebased commits, which wouldn't exist in the
index" (source); that check comes after the merge. Otherwise it calls
show_op_diff with the graph style unless --no-graph
(source). In the tests the root operation shows no
changes (test).
7. Errors and edge cases
| Situation | Behaviour |
|---|---|
Several op heads, no --at-op | Concurrent modification detected, resolving automatically. on stderr; a "reconcile divergent operations" operation is written and listed first (source, test). |
--at-op that is not a valid operation | For example Operation ID "foo" is not a valid hexadecimal prefix (source, test, with jj log). |
-T with no value | A clap error, with a hint listing the defined template aliases (test). |
A merge operation with --op-diff | No diff is printed under it (source). |
| A cycle in the operation graph | A panic, "graph has cycle" (source). |
8. Example output
After jj git init and one jj describe, from upstream's test
(source):
$ jj op log
@ 7749eb4df93f test-username@host.example.com default@ 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ describe commit e8849ae12c709f2321908879bc724fdb2ab8a781
│ args: jj describe -m 'description 0'
○ f63ee16f9553 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ add workspace 'default'
○ 000000000000 root()
A forked history, after a second describe at --at-op @-, with a template that
prints each operation's parents (source):
@ 7f1c9ff8575c
├─╮ P: 2 7749eb4df93f 3cb69b18c151
○ │ 7749eb4df93f
│ │ P: 1 f63ee16f9553
│ ○ 3cb69b18c151
├─╯ P: 1 f63ee16f9553
○ f63ee16f9553
│ P: 1 000000000000
○ 000000000000
P: 0
With --op-diff, each operation is followed by its changes (source):
@ 7749eb4df93f test-username@host.example.com default@ 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ describe commit e8849ae12c709f2321908879bc724fdb2ab8a781
│ args: jj describe -m 'description 0'
│
│ Changed commits:
│ ○ + qpvuntsm 3ae22e7f (empty) description 0
│ - qpvuntsm/1 e8849ae1 (hidden) (empty) (no description set)
│
│ Changed working copy default@:
│ + qpvuntsm 3ae22e7f (empty) description 0
│ - qpvuntsm/1 e8849ae1 (hidden) (empty) (no description set)
…
See also operations for what an operation records, and jj operation diff.