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
| Argument | Type | Default | Meaning |
|---|---|---|---|
-r, --revision REVSETS (repeatable; alias --revisions) | revset | revsets.log if no paths are given | Which revisions to show. Several are unioned (source). |
FILESETS (positional, repeatable) | fileset | none | Show only revisions that modify these paths (source). |
-n, --limit N | integer | no limit | "Applied after revisions are filtered and reordered topologically, but before being reversed" (source). |
--reversed | flag | off | Older revisions first (source). |
-G, --no-graph | flag | off | A flat list instead of a graph (source). |
-T, --template TEMPLATE | template | templates.log | How 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, --patch | flag | off | Show each commit's diff under it (source). |
--count | flag | off | Print 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 flags | flags | none | The 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
| Key | Default | Effect |
|---|---|---|
revsets.log | builtin_log() = present(@) | ancestors(immutable_heads().., 2) | trunk() | The revset when neither -r nor paths are given (source, alias). |
revsets.log-graph-prioritize | present(@) | Branches drawn first (source). |
templates.log | builtin_log_compact | Each commit's text (source, alias). |
templates.log_node | builtin_log_node, or builtin_log_node_ascii when ui.graph.style starts with ascii | The node symbol (source): @, ◆, × (conflict), ○, and ~ for elided nodes (source); ASCII uses @, +, x, o (source). |
ui.graph.style | curved | One of ascii, ascii-large, curved, square (default, source). |
ui.log-synthetic-elided-nodes | true | Draw an indirect edge as a "(elided revisions)" node rather than a dotted line (source). |
ui.log-word-wrap | false | Wrap each commit's text to the width left beside the graph (source). |
ui.diff-formatter | :color-words | The diff shown by -p (source). |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Revset engine | read | Parses 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 index | read | Walks commits in index order and computes graph edges (source). | index |
| Commits | read | Each 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 |
| Trees | read | Only for -p/diff flags, and to check explicit paths exist (source). | trees |
| View | read | Visible heads, bookmarks and @ feed the revset and the template. | view |
| Operation log | untouched | No 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 asnapshot working copyoperation;logitself 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).
Ordering and the limit
-
Group branches.
TopoGroupedGraphis 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). -
Prioritize. Every commit of
revsets.log-graph-prioritizethat 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, whichTopoGroupedGraphrequires (source). -
Limit, then reverse.
take(N)is applied to the grouped stream, not its input, for the same reason (source).--reversedcollects those nodes and flips every edge (reverse_graph), so commits whose parents were cut off become roots. -
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_nodedraws the row (source) with renderdag'sGraphRowRenderer(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
| Message | When | Source |
|---|---|---|
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 aliases | jj 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 errors | Bad -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
│
~