jj / How jj file show works

How jj file show works

jj file show prints the contents of files in one commit. A single literal path is looked up directly; anything else walks the tree like jj file list. Each file is written to stdout as stored, and a conflicted file is written with conflict markers.

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. 7. Errors and edge cases
  8. 8. Example output

In short: jj resolves -r (default @) and parses the required filesets. If they are one literal path that names a file, it prints just that, and a missing path is an error. Otherwise it streams tree.entries_matching(matcher) and prints every file under the match, warning about literal paths that named nothing. Before each file it renders -T or templates.file_show, which is empty by default. Nothing is written: no transaction, no operation.

The command is cmd_file_show, and the per-file output is write_tree_entries. The help text: "Print contents of files in a revision. If the given path is a directory, files in the directory will be visited recursively" (source).

1. Command-line surface

ArgumentTypeDefaultMeaning
FILESETS (positional, repeatable, required)filesetnone; at least one is neededThe paths to print. Several are unioned. A bare path is a cwd-relative prefix, so a directory prints every file under it (source).
-r, --revision REVSETrevset@The commit to read from. It must resolve to exactly one commit (source).
-T, --template TEMPLATEtemplatetemplates.file_showRendered before each file's contents. Every 0-argument method of TreeEntry is a keyword (source).

FileShowArgs

The positional arguments go through parse_file_patterns, which unions each argument parsed with parse_maybe_bare (cli_util.rs, fileset.rs), so globs and operators work. Upstream's fileset docs cover the language.

2. Configuration

templates.file_show supplies the template when -T is absent, and the output is labelled file_show for colour rules (source). Its built-in value is the empty string, so by default the contents are printed with nothing before them (templates.toml):

[templates]
file_show = ''

Setting it to something like '"--- " ++ path ++ "\n"' puts a header line before each file. The keywords are the same six TreeEntry methods jj file list uses, listed on its page.

KeyDefaultEffect
templates.file_show''The per-file header template.
ui.conflict-marker-style"diff"How a conflicted file is printed: diff, snapshot or git (default, docs). Read once when the workspace loads (source).
revset-aliases, fileset-aliasesnoneApply when resolving -r and parsing the paths.
ui.pager, ui.paginateless -FRXK (:builtin on Windows), autoThe pager is requested once a path has been found to print (fast path, slow path).

3. End-to-end flow

jj file show: call flow cmd_file_show loads the workspace through the shared lifecycle, resolves -r (default @) to one commit, takes its tree and parses the filesets. It picks the template from -T or templates.file_show. get_single_path then checks whether the fileset is one literal path. If so, the fast path looks the path up with tree.path_value: absent is the error No such path, a directory falls through to the slow path, and anything else becomes a single TreeEntry. The slow path builds a matcher, streams tree.entries_matching and afterwards warns about literal paths that named nothing. Both paths request the pager and hand their entries to write_tree_entries. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) resolve_single_rev(-r or @) → commit.tree() parse_file_patterns(FILESETS) at least one is required by clap template: -T or templates.file_show parse_template, TreeEntry as the context, labelled file_show get_single_path: is the whole expression one literal path pattern? yes for one bare path, file:, cwd:, root: and the like → fast path no for globs, several arguments, or any operator → slow path fast path: tree.path_value(path) absent → Error: No such path (exit 1) a directory → fall through to the slow path otherwise request_pager, one TreeEntry, and return without the unmatched check slow path: to_matcher, request_pager entries_matching(matcher), files only, in path order then print_unmatched_explicit_paths (Warning: No matching entries …) write_tree_entries: per entry, the template, then the materialized contents see the algorithm below

4. Core architecture touchpoints

StructureRead / writeHowExplained on
CommitreadResolved from -r; only its tree is used, including the tree's conflict labels.commits
TreesreadOne path is looked up with path_value, or the matched part of the tree is walked (path_value, walk).trees
File contentsreadEach file's blob is streamed from the store to stdout (materialize_tree_value).backends
ConflictsreadA conflict of files is merged and printed with markers; any other conflict is described (materialize_merge_result).conflicts
Filesets and matchersreadThe paths become a FilesetExpression, and on the slow path a Matcher.upstream docs
Revset enginereadResolves -r to one commit (source).revsets
TemplatesreadRenders the header for each TreeEntry.upstream docs
Working copyread, may snapshotSnapshotted by the shared lifecycle, so jj file show path on @ prints what is on disk, unless the file is ignored or too large.working-copy

5. What it reads, and snapshotting

  • No transaction, no operation of its own. The command only reads the repo it loaded (source).
  • Snapshot: workspace_helper snapshots the working copy first (source). If files changed on disk, that snapshot records its own operation.
  • Bytes as stored. A file is copied in 16 KiB chunks from the store's reader to stdout, with no decoding or end-of-line conversion (copy_async_to_sync). Files are concatenated with nothing between them unless the template adds it.
  • jj undo has nothing to revert, except the snapshot if one was taken.

6. Algorithm and invariants

Choosing a path. get_single_path returns a path only when the whole expression is one FilePath or PrefixPath pattern (source, the same split as as_path). On that fast path, the path's merged value decides (source):

  1. Absent: the command fails with No such path. Unlike the slow path, there is no warning-and-continue.
  2. A directory (is_tree: every present term is a tree, source): nothing is printed yet, and the slow path runs with the prefix matcher, which yields every file under it. A bare . at the workspace root takes this route too, since the root path's value is the whole tree.
  3. Anything else, a file, symlink, submodule or conflict: one entry is printed, and the command returns.

The slow path is the same walk as jj file list: depth-first, name order (subtrees are read concurrently but yielded in that order), directories pruned by the matcher, and trees never yielded as entries (details). Afterwards print_unmatched_explicit_paths warns about each literal path that is absent from the tree (source).

Printing an entry. write_tree_entries renders the template first, then materializes the value and dispatches on it (source, MaterializedTreeValue):

How jj file show prints one entry For each entry, write_tree_entries formats the template to stdout, then calls materialize_tree_value. A resolved file is copied byte for byte to stdout. A conflict made only of files and absences is merged by materialize_merge_result and written with conflict markers in the configured style. Any other conflict is written as a description of its terms. A symlink or Git submodule gives the warning that the path exists but is not a file. A read the backend reports as access denied gives a warning too. Absent values and trees are never passed in, and panic if they are. for each entry: 1. template.format(entry) → stdout (empty by default) 2. materialize_tree_value( store, path, value, tree.labels()) 3. match the result stdout: contents stderr: warnings a warning doesn't stop the others File → copy_async_to_sync(reader, stdout) FileConflict → materialize_merge_result, ui.conflict-marker-style OtherConflict → "Conflict: Removing … Adding …" (describe) Symlink, GitSubmodule → Warning: … exists but is not a file. AccessDenied → Warning: … exists but access is denied Absent, Tree → panic (filtered out before this point) Outlined: the common case. The header is written before the contents, so a symlink still gets its header line.

A conflict counts as a file conflict when every term is a file or absent; its contents are read and simplified with their labels (source). The merge is attempted again with the store's merge options, so a conflict that now resolves is printed as plain text; otherwise the markers use the configured style, a marker length long enough not to clash with the contents, and the files' own line endings (source). These are the same markers jj writes into the working copy; the conflicts page explains them.

Invariants: nothing is written anywhere but stdout and stderr. Files are printed in depth-first name order. Contents come from the commit's tree, never directly from disk. A missing single path fails before anything is printed; on the slow path the warning comes after all output.

7. Errors and edge cases

MessageWhenSource
Error: No such path: <path> (exit 1)The only argument is a literal path, and it is absent from the tree.show.rs
Warning: No matching entries for paths: <paths>On the slow path, a literal path is absent. Everything else is still printed, and the command succeeds.cli_util.rs
Warning: Path '<path>' exists but is not a file.A matched entry is a symlink or a Git submodule. Its header is still printed.show.rs
Warning: Path '<path>' exists but access is denied: <err>The backend refused to read the file.show.rs, conflicts.rs
Usage error (exit 2)No path was given; FILESETS is required.show.rs
Revset `…` resolved to more than one revision (or to none)-r isn't exactly one commit.revset_util.rs
Fileset or template parse errorA path isn't a valid fileset or lies outside the workspace, or the template doesn't parse against TreeEntry.cli_util.rs, show.rs
Backend errorA tree or file can't be read for another reason. The command stops after what was already printed.show.rs

8. Example output

From upstream's test_show. @- has file1 containing a. @ changes it to b and adds dir/file2 (c) and file3 (d):

$ jj file show file1 -r @-
a

$ jj file show file1
b

$ jj file show dir
c

$ jj file show nonexistent
Error: No such path: nonexistent

A header template, and several files through a directory and a glob:

$ jj file show -T '"--- " ++ path ++ "\n"' .
--- dir/file2
c
--- file1
b
--- file3
d

$ jj file show 'file*'
b
d

$ jj file show file1 non-existent
b
Warning: No matching entries for paths: non-existent

A conflict. A new commit changes file1 to c and is rebased onto the commit where it is a, so the default diff style prints a diff and a snapshot:

$ jj file show file1
<<<<<<< conflict 1 of 1
%%%%%%% diff from: rlvkpnrz fc7b369e (parents of rebased revision)
\\\\\\\        to: qpvuntsm eb7b8a1f (rebase destination)
-b
+a
+++++++ kmkuslsw f74f80c5 (rebased revision)
c
>>>>>>> conflict 1 of 1 ends

A symlink gets its header but no contents, and a warning on stderr (test_show_symlink):

$ jj file show -T '"--- " ++ path ++ " [" ++ file_type ++ "]\n"' .
--- dir/file2 [file]
c
--- file1 [file]
a
--- symlink1 [symlink]
Warning: Path 'symlink1' exists but is not a file.