jj / How jj file list works

How jj file list works

jj file list prints the paths in one commit's tree, optionally narrowed by filesets. It walks the tree depth-first in name order, skips directories the matcher rules out, and renders one TreeEntry per file, symlink, submodule or conflicted path.

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 @), parses the positional filesets into a matcher (everything when there are none), and streams tree.entries_matching(matcher). Directories are descended into rather than printed, and a conflicted tree is merged on the fly. Each entry is rendered with -T or templates.file_list. Afterwards, any literal path that named nothing gets a warning. Nothing is written: no transaction, no operation.

The command is cmd_file_list, and the walk is jj-lib's entries_matching, which reads subtrees concurrently but yields entries in path order. The help text: "List files in a revision" (source).

1. Command-line surface

ArgumentTypeDefaultMeaning
FILESETS (positional, repeatable)filesetall filesOnly list entries these match. Several are unioned. A bare path is a cwd-relative prefix, so dir lists everything under dir/ (source).
-r, --revision REVSETrevset@The commit whose tree is listed. It must resolve to exactly one commit (source).
-T, --template TEMPLATEtemplatetemplates.file_listHow to render each entry. Every 0-argument method of TreeEntry is a keyword (source).

FileListArgs

The help calls the positional arguments "prefixes", but they are full fileset expressions: parse_file_patterns returns all() for no arguments and otherwise unions each argument parsed with parse_maybe_bare (cli_util.rs, fileset.rs). Globs such as 'glob:**/*.rs' and operators such as ~ work. Upstream's fileset docs cover the language.

2. Configuration

templates.file_list supplies the template when -T is absent (source). Its built-in value prints the path and a newline, through an alias meant for overriding (templates.toml, alias):

[templates]
file_list = 'format_path(path) ++ "\n"'

[template-aliases]
'format_path(path)' = 'path.display()'

path.display() is relative to the current directory and uses the platform's separator (source, docs), so running in a subdirectory prints paths such as ../README.md. The output is labelled file_list for colour rules. The keywords are the eight methods of TreeEntry (commit_templater.rs, docs):

KeywordTypeValue
pathRepoPathRepo-relative path. Printing it directly gives the slash-separated repo path; .display() gives the cwd-relative one.
normal_valueOption<TreeValue>The resolved value (prints as its object ID), or none for a conflict.
removed_valuesList<Option<TreeValue>>The negative (removes) terms of the simplified merge; empty when resolved, and absent terms are none.
added_valuesList<Option<TreeValue>>The positive (adds) terms of the simplified merge; usually one resolved value, and absent terms are none.
conflictBooleanTrue when the merged value isn't resolved.
conflict_side_countIntegerSides after simplifying the merge; 1 when resolved.
file_typeStringfile, symlink, tree, git-submodule or conflict (source).
executableBooleanFor files, the executable bit; for a conflict of files, the bit if the sides agree after a trivial merge. False for anything else (source, resolve_file_executable).

A TreeValue prints as its full object ID, and has methods of its own: object_id, file_type (file, symlink, tree or git-submodule) and executable, true only for an executable file (source, docs).

revset-aliases apply when resolving -r, and fileset-aliases when parsing the filesets. ui.pager controls paging.

3. End-to-end flow

jj file list: call flow cmd_file_list loads the workspace through the shared lifecycle, resolves -r (default @) to one commit and takes its tree. It parses the filesets into an expression, all() when there are none, and builds a matcher. It picks the template from -T or templates.file_list and parses it with TreeEntry as the context. It requests the pager, streams tree.entries_matching(matcher) and formats a TreeEntry for each path and value. Finally print_unmatched_explicit_paths warns about literal paths that are absent from the tree. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) resolve_single_rev(-r or @) → commit.tree() parse_file_patterns(FILESETS) → all() if empty fileset_expression.to_matcher() template: -T or templates.file_list parse_template with the commit template language, TreeEntry as the context request_pager; tree.entries_matching(matcher) jj-lib: subtrees read concurrently, yielded in path order; matcher.visit prunes, matcher.matches keeps see the algorithm below TreeEntry { path, value? } template.format → stdout a backend error reading a subtree stops the command print_unmatched_explicit_paths tree.path_value(p) for each literal path absent → Warning: No matching entries for paths: … (stderr, exit 0)

4. Core architecture touchpoints

StructureRead / writeHowExplained on
CommitreadResolved from -r; only its tree is used.commits
TreesreadEvery directory the matcher doesn't rule out is loaded through the store; file contents are never read (source).trees
ConflictsreadA conflicted tree is walked as a Merge<Tree>, resolving trivial entries as it goes.conflicts
Filesets and matchersreadThe positional arguments become a Matcher (source).upstream docs
Revset enginereadResolves -r to one commit (source).revsets
TemplatesreadRenders each TreeEntry.upstream docs
Working copyread, may snapshotSnapshotted by the shared lifecycle, so new files on disk show up in @.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 and prints the snapshot stats (source). If files changed on disk, that snapshot records its own operation, so a file you just created is listed in @, and an ignored or too-large one isn't.
  • Only trees. The listing comes from tree objects alone. Blob contents aren't read, so the cost grows with the number of directories visited, not with file sizes.
  • jj undo has nothing to revert, except the snapshot if one was taken.

6. Algorithm and invariants

entries_matching builds an OrderedTreeWalk (source). It schedules a read of the root trees, unless matcher.visit rules out the root, in which case nothing is read and the stream is empty. Then, each time a scheduled read completes, the visitor runs on that directory (source):

  1. Lists the directory's entries in name order. When the directory's trees conflict, entries come from all_tree_entries, which resolves each name trivially using the store's same_change rule (source).
  2. Schedules subdirectories. An entry whose every term is a tree or absent is a directory. It is skipped when matcher.visit(path) says nothing below it can match; otherwise a read of its trees is scheduled, with absent terms becoming empty trees (read_trees, to_tree_merge). A directory that might hold a match is still read, and its files are checked one by one.
  3. Emits the rest if matcher.matches(path). A file, symlink, submodule, or a conflict mixing a tree with a non-tree is emitted as (path, value). In the last case the conflicted path is listed once, and nothing inside the directory is.

The queue runs up to the backend's concurrency() reads at once, starting the ones with the smallest paths first, and holds emitted entries until no pending read could produce an earlier path (source, OrderedTreeWalk). A read that fails is emitted as an error at the directory's path. The stream ends when no reads are pending and every held entry has been yielded.

How jj file list dir walks the tree From upstream's test_file_list, listing the fileset dir. The root level holds conflict-exec-file, conflict-file, dir and exec-file. The three files fail matcher.matches and are skipped; dir passes matcher.visit, so its tree its trees are scheduled for reading. When that read completes, its one entry, dir/file, matches and is emitted. No reads are pending, so it is yielded and the walk ends with one line of output. root trees read → visit dir/ read (scheduled) → visit conflict-exec-file — matches? no, skipped conflict-file — matches? no, skipped dir — visit? yes, schedule read exec-file — matches? no, skipped scheduled: [dir]; emitted: none file — matches? yes → emit "dir/file" no read pending → yield it no work, no held entries stream ends → None Outlined: yielded. A bare "dir" is a prefix pattern, so visit(dir) and matches(dir/file) are true. Without filesets the matcher matches everything, and all four files are yielded in path order.

Unmatched paths. After the listing, every literal path in the fileset (FilePath and PrefixPath patterns, not globs) is looked up with tree.path_value. Those that are absent are joined into one warning (source, as_path, explicit_paths). A path naming a directory isn't absent, so it never warns.

Invariants: output order is path order (depth-first, each directory's entries in name order) however the reads finish, so dir/file sits between conflict-file and exec-file. Directories never appear as entries of their own unless they conflict with a non-directory. Each path appears at most once. The listing doesn't depend on the working copy beyond the snapshot folded into @.

7. Errors and edge cases

MessageWhenSource
Warning: No matching entries for paths: <paths>A literal path in the filesets is absent from the tree. Matching entries are still printed, and the command succeeds.cli_util.rs
Revset `…` resolved to more than one revision (or to none)-r isn't exactly one commit.revset_util.rs
Fileset parse errorAn argument isn't a valid fileset, or names a path outside the workspace.cli_util.rs
Template parse error-T, or the configured template, doesn't parse against TreeEntry.list.rs
Backend errorA subtree can't be read; the error carried in the entry stops the command after the lines already printed.list.rs

8. Example output

From upstream's test_file_list. One side adds dir/file, exec-file, conflict-file and conflict-exec-file, marking the two exec ones executable. A sibling of it, off the root, writes different contents to the two conflict files and marks conflict-exec-file executable. @ is a merge of both. With the default template:

$ jj file list
conflict-exec-file
conflict-file
dir/file
exec-file

With a template using the TreeEntry keywords. Both conflict files are listed once with file_type conflict. executable is true for conflict-exec-file because both sides set the bit:

$ jj file list -T 'separate(" ", path, "[" ++ file_type ++ "]",
    "conflict=" ++ conflict, "executable=" ++ executable) ++ "\n"'
conflict-exec-file [conflict] conflict=true executable=true
conflict-file [conflict] conflict=true executable=false
dir/file [file] conflict=false executable=false
exec-file [file] conflict=false executable=true

Another revision, a filter, and the warning for a path that doesn't exist:

$ jj file list -r 'first_parent(@)'
conflict-exec-file
conflict-file

$ jj file list dir file3
dir/file
Warning: No matching entries for paths: file3

normal_value prints the object ID of a resolved value, here a Git blob (test_file_list_normal_value):

$ jj file list -T 'path ++ " " ++ normal_value ++ "\n"'
file 6b584e8ece562ebffc15d38808cd6b98fc3d97ea