jj / How jj file track works

How jj file track works

jj file track snapshots the working copy once more, this time also starting to track the paths you name, even if snapshot.auto-track doesn't match them, and, with --include-ignored, even if they are ignored or too large.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. Transaction and operation
  6. 6. Algorithm and invariants
  7. Two snapshots, two matchers
  8. Why the commit changes later
  9. Warnings
  10. 7. Errors and edge cases
  11. 8. Worked example

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

FlagTypeDefaultMeaning
FILESETS (positional)filesetsrequiredThe paths to track, relative to the current directory. A directory tracks everything under it (source).
--include-ignoredboolfalseAlso track paths that are ignored or larger than snapshot.max-new-file-size (source).

FileTrackArgs

2. Configuration

KeyDefaultEffect
snapshot.auto-trackall()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-size1MiBNew 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

jj file track: call flow cmd_file_track loads the workspace with workspace_helper_with_stats, which takes the normal snapshot using snapshot.auto-track and keeps its stats. It parses the paths into a matcher, builds snapshot options that start tracking that matcher, and with --include-ignored force-track it too. It starts a raw transaction, locks the working copy, and snapshots again, which updates only the working copy's tree state. It commits the transaction as "track paths", with no commit changes, and saves the tree state at that operation, then prints any files it still refused. The next command's snapshot sees the tree state differ from the working-copy commit and rewrites that commit. workspace_helper_with_stats: the normal snapshot (auto-track matcher); keep its stats options: start_tracking = your paths (instead of auto-track) --include-ignored: force_tracking = your paths too, so ignores and the size limit don't apply start_transaction().into_inner(); start_working_copy_mutation (lock); snapshot(options) updates the working copy's TreeState only; the working-copy commit is not rewritten here tx.commit("track paths"); locked_ws.finish(op id) an operation with no commit changes; the tree state is saved as belonging to it print_track_snapshot_stats: "Refused to snapshot some files" plus the large-file hint next command: snapshot_working_copy tree state ≠ working-copy commit's tree → rewrite @, rebase descendants, operation "snapshot working copy"

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Working copy (TreeState)snapshotted, savedGains the newly tracked files. Saved with the track paths operation's ID (source).working-copy
SnapshotOptionsbuiltstart_tracking_matcher and force_tracking_matcher decide which new files become tracked (source).working-copy
File and tree objectswrittenThe snapshot writes the new files' contents and trees to the store.trees
Operationwritetrack paths, whose view is unchanged.operations
Working-copy commitnot written hereRewritten by the next command's snapshot, which also rebases descendants.commits

5. Transaction and operation

  • Transaction: a raw Transaction (into_inner()), committed directly with tx.commit rather than through tx.finish, so there is no Nothing 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 a snapshot working copy operation for edits made since the last command.
  • Commits: none in this command. The rebase_descendants call 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 copy operation, so that is what jj undo would 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

MessageWhenSource
(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, then the next command Before: the working-copy commit tracks nothing and has a child. jj file track file1 prints nothing and records the track paths operation; only the working copy's tree state gains file1. The next command, jj file list, snapshots: it rewrites the working-copy commit so it contains file1, rebases the child onto it, prints Rebased 1 descendant commits onto updated working copy, and lists file1. after jj file track file1 after the next command (jj file list) child no files @ commit: no files working copy tree state tracks file1 (saved at "track paths") file1.bak: ignored, untracked child rebased @ rewritten: + file1 operation "snapshot working copy" tree state ≠ @'s tree → rewrite @, rebase descendants

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.