In short: jj loads the workspace, which snapshots it as usual, and locks the working copy. It writes
a new tree for @ without the matching paths and rewrites @ with it. It then
resets the working copy's tree state to that commit without touching any files. A snapshot checks the
result: if any of the paths come back because they aren't ignored, it fails and nothing is recorded.
Otherwise it rebases descendants, updates the Git index in a colocated workspace, and commits the
operation untrack paths.
The command is cmd_file_untrack. Its only argument
is the paths, which "must already be ignored", via .gitignore or, in colocated
workspaces, .git/info/exclude (source). Unlike
jj file track, it rewrites
@ straight away. It only ever changes the working-copy commit.
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional) | filesets | required | Paths to untrack. Every tracked file they match is removed from @, so a directory untracks everything under it (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
.gitignore, .git/info/exclude, core.excludesFile | — | What counts as ignored. A path that isn't ignored makes the command fail. |
snapshot.auto-track | all() | Used for the verifying snapshot. A file outside it isn't added back, so it can be untracked without being ignored (source, test). |
snapshot.max-new-file-size | 1MiB | Also applies to the verifying snapshot, whose stats are printed at the end (source). |
Sparse patterns matter too: a file outside them isn't on disk, so it can't come back and needn't be ignored (test_track_untrack_sparse).
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Working-copy commit | rewritten | Same parents, change ID and description; the matched paths are gone from its tree (source). | commits |
| Trees | written | Built from @'s tree with each matching path removed with set_or_remove(path, None). | trees |
Working copy (TreeState) | reset, snapshotted, saved | reset updates jj's record of the working copy to the new commit "without touching the files in the working copy" (source). | working-copy |
| Descendants | rewritten | Rebased onto the new @, so they lose the files too. A descendant that modified one of them gets a conflict instead. | transactions |
| Git index (colocated) | write | Intent-to-add entries are updated for the tree change, and refs are exported (source). | — |
View and operation | write | wc_commit_ids, heads and bookmarks follow; one operation, untrack paths. | operations |
5. Transaction and operation
- Transaction: a raw
Transactioncommitted withtx.commit, with the working copy locked throughout, so the commit rewrite and the working-copy update land together (start, commit). - Operation description:
untrack paths(source). - Commits rewritten:
@, then its descendants, withRebased N descendant commits.if any (source). - On error: when a path isn't ignored, the lock is dropped before the error returns and the transaction is never committed, so the repo and the saved working-copy state stay as they were (source, test).
jj undobrings the paths back into@.
6. Algorithm and invariants
- Remove. For every entry of
@'s tree matching the filesets, set it to absent, then write the tree (source). - Rewrite.
rewrite_commit(@).set_tree(new_tree)(source). - Reset. Point the working copy at the new commit without changing any file, so the files stay on disk but jj no longer records them as tracked (source).
-
Verify. Snapshot with the normal auto-track options, as the next command would. If
the result differs from the new commit and some of the differing paths match the filesets, those
files would be re-added: fail with
'…' is not ignored.(or'…' and N other files are not ignored.). If it differs only elsewhere, someone changed the working copy meanwhile; reset again so those edits aren't folded into this operation (source). - Finish. Rebase descendants, export to Git if colocated, commit, save the working copy at the new operation, then print warnings (source).
Invariants: files on disk are never deleted or modified. Only the working-copy commit's tree loses paths. A path that a later snapshot would re-add is refused. Paths that aren't tracked are ignored, with a warning for explicit ones.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
'<path>' is not ignored. / '<path>' and <n> other files are not ignored., hint Files that are not ignored will be added back by the next command. Make sure they're ignored, then try again. | A matched file would be re-added by a snapshot. | untrack.rs |
Warning: No matching entries for paths: … | An explicit path isn't in @. The command still succeeds. | untrack.rs |
This command must be able to update the working copy., hint Don't use --at-op. (or --ignore-working-copy, --no-integrate-operation) | Run with one of those global flags. | cli_util.rs |
Concurrent working copy operation. Try again. | The working copy changed between loading and locking. | cli_util.rs |
Warning: Refused to snapshot some files: … | The verifying snapshot left large new files untracked. | untrack.rs |
clap: the following required arguments were not provided: <FILESETS>... | No paths given. | untrack.rs |
8. Worked example
From upstream's test_track_untrack. @ tracks
file1, file1.bak, file2.bak, target/file2 and
target/file3; then .gitignore gets *.bak:
Upstream's snapshot of the first, refused attempt (source):
Error: 'file1' is not ignored.
Hint: Files that are not ignored will be added back by the next command.
Make sure they're ignored, then try again.
The test then checks that jj file list is unchanged. In
test_track_ignored, where @ has a child, untracking
prints Rebased 1 descendant commits. at once, unlike jj file track, whose
rebase shows up on the next command.