jj / How jj operation log works

How jj operation log works

jj operation log (alias jj op log) prints the operation history: every operation reachable from the current one, newest first, drawn as a graph of operations and their parents.

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. The walk
  8. The graph
  9. --limit and --reversed
  10. Operation diffs
  11. 7. Errors and edge cases
  12. 8. Example output

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

FlagMeaning
-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 TEMPLATERender 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

SettingDefaultEffect
templates.op_logbuiltin_op_log_compactEach 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_nodebuiltin_op_log_node, or the ASCII variant when ui.graph.style starts with asciiThe node symbol: @ for the current operation, otherwise ○ (o in ASCII) (source, source, source).
templates.commit_summaryThe commit lines of an operation diff, labelled op_log and commit (source, source).
revsets.op-diff-changes-inmutable() | immutable_heads()Which changed commits an operation diff lists (source); see elision.
ui.graph.style, ui.log-word-wrap, ui.pagerGraph 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

jj operation log: call flow After the shared lifecycle, cmd_op_log checks whether the working copy is writable. If it is, it loads the repo at the head operation and snapshots the working copy. If --at-op names another operation or --ignore-working-copy is given, it loads only the workspace and resolves the operation without loading the repo. do_op_log then parses the op_log and op_log_node templates and, with --op-diff or a diff format, prepares the per-operation diff. walk_ancestors streams the operations newest first, take applies --limit, and each operation is rendered either into the graph, optionally reversed with reverse_graph, or as a flat list. The optional operation diff is printed under each operation. Shared lifecycle: parse args, load config (CLI page) is_working_copy_writable: no --at-op (or --at-op=@) and no --ignore-working-copy yes: workspace_helper load the repo at the head op, snapshot no: load_workspace, resolve_operation the repo itself is never loaded parse templates.op_log (or -T) and templates.op_log_node labelled op_log, operation (and node) --op-diff, -p or a diff format: build the per-operation show closure merge the op's parents, load both repos, call show_op_diff walk_ancestors([current_op]).take(--limit) a lazy stream: operations load only as they are printed Graph: one direct edge per parent --reversed collects all, then reverse_graph --no-graph: a flat list --reversed collects all, then reverses

4. Core architecture touchpoints

PieceRole here
CommandHelper::resolve_operationWith --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_ancestorsStreams the operation and all its ancestors, each parent loaded from the op store when reached.
OperationByEndTimeOrders operations by end time, then by the operation itself to stay consistent with equality.
dag_walk_async::topo_order_reverse_lazyThe lazy reverse topological walk (see the walk).
graph::reverse_graphFor --reversed in graph mode: flips every edge and the node order.
merge_operations and show_op_diffThe 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

SituationBehaviour
Several op heads, no --at-opConcurrent 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 operationFor example Operation ID "foo" is not a valid hexadecimal prefix (source, test, with jj log).
-T with no valueA clap error, with a hint listing the defined template aliases (test).
A merge operation with --op-diffNo diff is printed under it (source).
A cycle in the operation graphA 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.