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
| Argument | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional, repeatable) | fileset | all files | Only list entries these match. Several are unioned. A bare path is a cwd-relative prefix, so dir lists everything under dir/ (source). |
-r, --revision REVSET | revset | @ | The commit whose tree is listed. It must resolve to exactly one commit (source). |
-T, --template TEMPLATE | template | templates.file_list | How to render each entry. Every 0-argument method of TreeEntry is a keyword (source). |
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):
| Keyword | Type | Value |
|---|---|---|
path | RepoPath | Repo-relative path. Printing it directly gives the slash-separated repo path; .display() gives the cwd-relative one. |
normal_value | Option<TreeValue> | The resolved value (prints as its object ID), or none for a conflict. |
removed_values | List<Option<TreeValue>> | The negative (removes) terms of the simplified merge; empty when resolved, and absent terms are none. |
added_values | List<Option<TreeValue>> | The positive (adds) terms of the simplified merge; usually one resolved value, and absent terms are none. |
conflict | Boolean | True when the merged value isn't resolved. |
conflict_side_count | Integer | Sides after simplifying the merge; 1 when resolved. |
file_type | String | file, symlink, tree, git-submodule or conflict (source). |
executable | Boolean | For 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
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Commit | read | Resolved from -r; only its tree is used. | commits |
| Trees | read | Every directory the matcher doesn't rule out is loaded through the store; file contents are never read (source). | trees |
| Conflicts | read | A conflicted tree is walked as a Merge<Tree>, resolving trivial entries as it goes. | conflicts |
| Filesets and matchers | read | The positional arguments become a Matcher (source). | upstream docs |
| Revset engine | read | Resolves -r to one commit (source). | revsets |
| Templates | read | Renders each TreeEntry. | upstream docs |
| Working copy | read, may snapshot | Snapshotted 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_helpersnapshots 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 undohas 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):
-
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'ssame_changerule (source). -
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. -
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.
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
| Message | When | Source |
|---|---|---|
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 error | An 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 error | A 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