In short: jj loads the workspace with the usual snapshot. It then takes a second snapshot whose
"start tracking" matcher is your paths instead of snapshot.auto-track, and with
--include-ignored whose "force tracking" matcher is your paths too. That snapshot updates
only the working copy's own tree state. It records an operation, track paths, that
changes no commits. The working-copy commit picks the files up at the next command's
snapshot; upstream calls this out as a TODO (GH #8298).
The command is cmd_file_track. Its help text points
out that by default new files are tracked automatically, so the command matters only when
snapshot.auto-track is narrowed (source). The
opposite command is jj file untrack.
Snapshots in general are described on the
working-copy page.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional) | filesets | required | The paths to track, relative to the current directory. A directory tracks everything under it (source). |
--include-ignored | bool | false | Also track paths that are ignored or larger than snapshot.max-new-file-size (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
snapshot.auto-track | all() | Which new files every snapshot tracks on its own. jj file track is for files outside it. Its paths are evaluated from the repo root, while jj file track's are relative to the current directory (default, test). |
snapshot.max-new-file-size | 1MiB | New files larger than this stay untracked unless --include-ignored is given. 0 means no limit (default, source). |
.gitignore, .git/info/exclude, core.excludesFile | — | Ignored files stay untracked unless --include-ignored is given. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
Working copy (TreeState) | snapshotted, saved | Gains the newly tracked files. Saved with the track paths operation's ID (source). | working-copy |
SnapshotOptions | built | start_tracking_matcher and force_tracking_matcher decide which new files become tracked (source). | working-copy |
| File and tree objects | written | The snapshot writes the new files' contents and trees to the store. | trees |
| Operation | write | track paths, whose view is unchanged. | operations |
| Working-copy commit | not written here | Rewritten by the next command's snapshot, which also rebases descendants. | commits |
5. Transaction and operation
- Transaction: a raw
Transaction(into_inner()), committed directly withtx.commitrather than throughtx.finish, so there is noNothing changed.check (source). - Operation description:
track paths. Its view equals the previous one, since nothing in the repo changed. Before it, the load may record asnapshot working copyoperation for edits made since the last command. - Commits: none in this command. The
rebase_descendantscall has nothing to rebase (source). - Working copy: locked for the second snapshot, then released with the new operation's ID, so the next command doesn't consider it stale (source).
- The real change lands in the next command's
snapshot working copyoperation, so that is whatjj undowould undo first.
6. Algorithm and invariants
Two snapshots, two matchers
Loading the workspace takes the normal snapshot, whose "start tracking" matcher is
snapshot.auto-track, and keeps its stats (source).
The command's own snapshot swaps that matcher for your paths. With --include-ignored, it
also sets them as the "force tracking" matcher, which overrides both the ignore rules and the size
limit (source, field docs).
Files that are already tracked are always snapshotted, whatever the matchers say.
Why the commit changes later
locked_wc().snapshot() returns a new tree, but the command ignores it: it never rewrites
the working-copy commit or calls set_wc_commit
(source). The working copy's tree state now includes the files,
and it is saved as up to date with the track paths operation. On the next command,
snapshot_working_copy finds that tree differs
from the working-copy commit's and rewrites the commit, printing
Rebased N descendant commits onto updated working copy. if it has descendants.
Upstream's test marks this with TODO: fix GH #8298 so tracking is immediate
(source).
Warnings
print_track_snapshot_stats combines the two
snapshots' stats. A path the second snapshot skipped only because it isn't one of yours, which it
reports as "not auto-tracked", takes the first snapshot's reason instead, so a file left out for size
is still reported as too large. Only too-large files are printed, under
Refused to snapshot some files:; the hint lists them and suggests
jj file track --include-ignored (source,
hint).
Invariants: no file is ever untracked by this command. Without
--include-ignored, ignored and oversized files stay untracked. No commit is rewritten
during the command itself.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
| (no output) | The usual result. Paths that are ignored, or already tracked, change nothing and print nothing. | test |
Warning: Refused to snapshot some files: then <path>: <size> (<n> bytes); the maximum size allowed is … | A named file is larger than snapshot.max-new-file-size, without --include-ignored. | cli_util.rs |
Hint: This is to prevent large files from being added by accident. … | Follows that warning, with track-specific options. | cli_util.rs |
| (no output, no effect) | The path is outside the workspace's sparse patterns, so it isn't on disk to snapshot. Upstream notes that a warning would help. | test |
Concurrent working copy operation. Try again. | The working copy changed between loading and locking. | cli_util.rs |
clap: the following required arguments were not provided: <FILESETS>... | No paths given. | track.rs |
8. Worked example
From upstream's test_track_ignored, with
snapshot.auto-track = 'none()'. file1 and file1.bak exist, and
*.bak is ignored. jj file list prints nothing, and jj new --no-edit
adds a child on top of @:
jj file track file1 prints nothing. The next jj file list prints
(source):
file1
Rebased 1 descendant commits onto updated working copy.
The rebase message comes from that command's snapshot, not from jj file track. Later in
the test, jj file track file1.bak without --include-ignored prints nothing
and leaves the ignored file untracked. test_track_ignored_with_flag
shows --include-ignored tracking file2.ignored, and
test_track_large_file_with_flag shows the size warning for
the file left out, with its hint.