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
| Argument | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional, repeatable, required) | fileset | none; at least one is needed | The 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 REVSET | revset | @ | The commit to read from. It must resolve to exactly one commit (source). |
-T, --template TEMPLATE | template | templates.file_show | Rendered before each file's contents. Every 0-argument method of TreeEntry is a keyword (source). |
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.
| Key | Default | Effect |
|---|---|---|
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-aliases | none | Apply when resolving -r and parsing the paths. |
ui.pager, ui.paginate | less -FRXK (:builtin on Windows), auto | The pager is requested once a path has been found to print (fast path, slow path). |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Commit | read | Resolved from -r; only its tree is used, including the tree's conflict labels. | commits |
| Trees | read | One path is looked up with path_value, or the matched part of the tree is walked (path_value, walk). | trees |
| File contents | read | Each file's blob is streamed from the store to stdout (materialize_tree_value). | backends |
| Conflicts | read | A conflict of files is merged and printed with markers; any other conflict is described (materialize_merge_result). | conflicts |
| Filesets and matchers | read | The paths become a FilesetExpression, and on the slow path a Matcher. | upstream docs |
| Revset engine | read | Resolves -r to one commit (source). | revsets |
| Templates | read | Renders the header for each TreeEntry. | upstream docs |
| Working copy | read, may snapshot | Snapshotted 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_helpersnapshots 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 undohas 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):
- Absent: the command fails with
No such path. Unlike the slow path, there is no warning-and-continue. - 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. - 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):
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
| Message | When | Source |
|---|---|---|
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 error | A 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 error | A 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.