jj / How jj log works

How jj log works

jj log evaluates a revset and draws it as a graph, children before parents. Edges between shown commits are recomputed so that skipped commits become "elided" nodes, branches are grouped together, and each commit is rendered with a template.

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. Edges between shown commits
  8. Ordering and the limit
  9. 7. Errors and edge cases
  10. 8. Example output

In short: jj picks a revset (-r, else revsets.log, narrowed to commits that touch the given paths), evaluates it, and streams it as a graph whose edges are direct, indirect or missing. TopoGroupedGraph reorders that stream so each branch is drawn in one piece, with the branch holding @ first. -n cuts it, and --reversed flips it. Each commit is rendered with templates.log, plus an optional diff. The result is drawn by sapling's renderdag. Nothing is written: no transaction, no operation.

The command is cmd_log. The help text: "Show revision history" (source). It also says spans of revisions that -r leaves out are drawn as a synthetic node labelled "(elided revisions)", and that @, ◆ (immutable) and ○ mark the nodes.

1. Command-line surface

ArgumentTypeDefaultMeaning
-r, --revision REVSETS (repeatable; alias --revisions)revsetrevsets.log if no paths are givenWhich revisions to show. Several are unioned (source).
FILESETS (positional, repeatable)filesetnoneShow only revisions that modify these paths (source).
-n, --limit Nintegerno limit"Applied after revisions are filtered and reordered topologically, but before being reversed" (source).
--reversedflagoffOlder revisions first (source).
-G, --no-graphflagoffA flat list instead of a graph (source).
-T, --template TEMPLATEtemplatetemplates.logHow to render each commit; every 0-argument method of Commit is a keyword. jj log -T with no value lists the built-in templates (source).
-p, --patchflagoffShow each commit's diff under it (source).
--countflagoffPrint the number of commits instead. Conflicts with the diff flags, -G, -p, --reversed and -T (source).
Diff format flags: -s, --stat, --types, --name-only, --git, --color-words, --tool, --context, whitespace flagsflagsnoneThe shared DiffFormatArgs (source). Any of them turns the diff on; -p alone adds the default long format (source).

Global flags such as --at-op and --ignore-working-copy are on the CLI page.

2. Configuration

KeyDefaultEffect
revsets.logbuiltin_log() = present(@) | ancestors(immutable_heads().., 2) | trunk()The revset when neither -r nor paths are given (source, alias).
revsets.log-graph-prioritizepresent(@)Branches drawn first (source).
templates.logbuiltin_log_compactEach commit's text (source, alias).
templates.log_nodebuiltin_log_node, or builtin_log_node_ascii when ui.graph.style starts with asciiThe node symbol (source): @, ◆, × (conflict), ○, and ~ for elided nodes (source); ASCII uses @, +, x, o (source).
ui.graph.stylecurvedOne of ascii, ascii-large, curved, square (default, source).
ui.log-synthetic-elided-nodestrueDraw an indirect edge as a "(elided revisions)" node rather than a dotted line (source).
ui.log-word-wrapfalseWrap each commit's text to the width left beside the graph (source).
ui.diff-formatter:color-wordsThe diff shown by -p (source).

3. End-to-end flow

jj log: call flow After the shared lifecycle, cmd_log picks the revset: the union of -r, or revsets.log, or all(), intersected with a file filter when paths are given. It evaluates it. With --count it prints the size and stops. Otherwise, by default, it streams the revset as a graph, groups branches with TopoGroupedGraph, cuts to the limit and optionally reverses, then renders each commit with its template, diff and node symbol and adds it to the graph renderer. With --no-graph it streams the commits in index order instead. Finally it warns about unmatched paths and arguments that look like revsets. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) choose the revset -r given: union of them · neither -r nor paths: revsets.log · paths only: all() paths given: intersect with a File filter (commits that modify them) evaluate() against the index --count: print the number, stop uses the size estimate when it's exact stream_graph() edges: direct, indirect, missing TopoGroupedGraph prioritize log-graph-prioritize branches take(-n), then reverse if --reversed per commit: template, diff, node symbol add_node to the renderdag graph --no-graph: stream() index order; take(-n), then reverse template and diff at full width warnings: paths no commit had · a lone path that might have meant a revset graph -G

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Revset enginereadParses and evaluates the revset; the file filter uses the changed-path index if there is one, else diffs each commit against its parents (source).revsets
Commit indexreadWalks commits in index order and computes graph edges (source).index
CommitsreadEach shown commit is loaded for its template. In graph mode up to the store's concurrency limit load at once, still in graph order (source).commits
TreesreadOnly for -p/diff flags, and to check explicit paths exist (source).trees
ViewreadVisible heads, bookmarks and @ feed the revset and the template.view
Operation loguntouchedNo 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 @ shows current edits. That snapshot may record a snapshot working copy operation; log itself records nothing.
  • Reads: the view at the current (or --at-op) operation, the index, every shown commit, and trees only when a diff or explicit path needs them.
  • Output: through the pager, requested before anything is written (source). Warnings go to stderr.

6. Algorithm and invariants

Edges between shown commits

stream_graph() walks the evaluated set in index order with RevsetGraphWalk, skipping transitive edges (source). A parent in the set gives a direct edge. A parent outside it is walked through until the walk reaches commits in the set, giving one indirect edge to each nearest such ancestor, once even when several outside parents lead to it (source). A path that never comes back into the set gives a missing edge. When a commit has an indirect edge, any edge whose target is also reachable through its other edges is dropped (source), so a commit isn't linked to its grandparent as well as its parent.

cmd_log then turns these into renderer edges, keyed by (commit ID, is_synthetic) (source). Direct stays direct. Indirect becomes a direct edge to a synthetic node for that target, drawn as ~ (elided revisions) with the elided label (source); with ui.log-synthetic-elided-nodes = false it stays a dotted indirect edge. Missing edges collapse into one, drawn as ~ below the node. graphlog.rs maps them to renderdag's parent, ancestor and anonymous edges (source).

How skipped commits become elided nodes Left: a history where merge, at @, has parents main 2 and side 2; main 2 is on main 1 and side 2 is on side 1; both are on initial, which is on the root. The revset picks merge, main 2, side 2 and initial. Right: what jj log draws. Merge has direct edges to main 2 and side 2. Each of those has an indirect edge to initial, so each gets its own elided node between it and initial. Initial's parent, the root, is outside the set, so its missing edge is drawn as a tilde. history; highlighted: -r '@ | @- | subject(initial)' merge @ main 2 main 1 side 2 side 1 initial root what jj log draws merge @ main 2 elided side 2 elided initial ~ missing edge (root not shown) dashed nodes: indirect edges as synthetic nodes

Ordering and the limit

  1. Group branches. TopoGroupedGraph is a depth-first walk from the heads built on Kahn's algorithm (source): a commit is emitted once all its children have been, and at a merge the last parent's branch comes first. A new head is only taken when nothing is ready, choosing one that unblocks a waiting fork point (source). The input is read lazily (source).
  2. Prioritize. Every commit of revsets.log-graph-prioritize that is also in the shown set is queued first (source). The intersection is taken against the query resolved at its own visibility, so the prioritized commit is always in the input, which TopoGroupedGraph requires (source).
  3. Limit, then reverse. take(N) is applied to the grouped stream, not its input, for the same reason (source). --reversed collects those nodes and flips every edge (reverse_graph), so commits whose parents were cut off become roots.
  4. Render. Each commit's template and diff are written into a buffer at the width left beside the graph (source, LogContentFormat), the node template picks the symbol, and add_node draws the row (source) with renderdag's GraphRowRenderer (source).

With --no-graph none of the grouping happens: commits come in index order, newest first, so the limit can pick different commits than in graph mode (see the example).

Invariants: a child is always printed before its parents (after them with --reversed). Every commit in the set is printed exactly once, unless cut by -n. Nothing in the repository changes.

7. Errors and edge cases

MessageWhenSource
Warning: No matching entries for paths: <paths>A literal path existed in none of the printed commits' trees.log.rs
Warning: The argument "." is being interpreted as a fileset expression, but this is often not useful because all non-empty commits touch '.'. If you meant to show the working copy commit, pass -r '@' instead.jj log . at the workspace root, as a Mercurial user might type.log.rs
Warning: The argument "X" is being interpreted as a fileset expression. To specify a revset, pass -r "X" instead.One path and no -r, nothing matched, and the argument also parses as a revset.log.rs
error: a value is required for '--template <TEMPLATE>' but none was supplied, with a hint listing template aliasesjj log -T with no value.test
A clap "cannot be used with" error--count with -T, -G, -p, --reversed or a diff flag.log.rs
Revset or template parse errorsBad -r, config revsets, or -T.cli_util.rs
(empty output)The revset is empty; not an error.log.rs

Edge cases: a path argument also drops empty commits, so jj log root:. differs from jj log (source). A merge counts as modifying a path only if it differs from the merge of its parents (source). --count trusts the engine's size estimate when it's exact or already above the limit, and otherwise counts the stream (source).

8. Example output

From upstream's test_log_limit: a, then b on it, c on a, and d (@) merging c and b.

$ jj log -T description --limit=3
@    d
├─╮
│ ○  b
○ │  c
├─╯

$ jj log -T description --limit=2
@    d
├─╮
│ ○  b

$ jj log -T description --limit=2 --no-graph
d
c

$ jj log -T description --limit=3 --reversed
○  c
│ ○  b
├─╯
@  d

In graph mode the second parent's branch, b, comes right after the merge; without the graph it's index order, so c. Reversed, a was cut by the limit, so b and c are roots. With a path, from the same test:

$ jj log -T description --limit=1 b c
○  c
│
~
Warning: No matching entries for paths: b

Elided revisions, from test_elided (the diagram above):

$ jj log -T 'description ++ "\n"' -r '@ | @- | subject(initial)'
@    merge
├─╮
│ ○  side bookmark 2
│ │
│ ~  (elided revisions)
○ │  main bookmark 2
│ │
~ │  (elided revisions)
├─╯
○  initial
│
~