jj / How jj file untrack works

How jj file untrack works

jj file untrack removes paths from the working-copy commit while leaving the files on disk. The paths must already be ignored, or the next snapshot would simply add them back.

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. 7. Errors and edge cases
  8. 8. Worked example

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

ArgumentTypeDefaultMeaning
FILESETS (positional)filesetsrequiredPaths to untrack. Every tracked file they match is removed from @, so a directory untracks everything under it (source).

FileUntrackArgs

2. Configuration

KeyDefaultEffect
.gitignore, .git/info/exclude, core.excludesFile—What counts as ignored. A path that isn't ignored makes the command fail.
snapshot.auto-trackall()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-size1MiBAlso 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

jj file untrack: call flow cmd_file_untrack loads the workspace through the shared lifecycle, which snapshots it. It parses the paths and the auto-track matcher, starts a raw transaction and locks the working copy, getting the working-copy commit. It builds a tree without the matching paths and rewrites the working-copy commit with it, then resets the working copy to the new commit without touching files. It snapshots again: if the snapshot brings back any of the paths, it fails because they are not ignored; if it differs for another reason, it resets again. It rebases descendants, updates the Git index in a colocated workspace, commits "untrack paths", releases the working copy, and prints warnings. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) matcher, auto-track options; start_transaction().into_inner(); lock the working copy → @ tree without the matching paths → rewrite_commit(@).set_tree(…) locked_wc().reset(new @): tree state now matches the new commit; files on disk untouched snapshot again with the auto-track options a path came back error "is not ignored"; the transaction is dropped, nothing recorded other paths differ concurrent edits: reset again so they aren't mixed in rebase_descendants; colocated: update the Git index; tx.commit("untrack paths"); finish then warn about unmatched paths and print the snapshot's stats no difference

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Working-copy commitrewrittenSame parents, change ID and description; the matched paths are gone from its tree (source).commits
TreeswrittenBuilt from @'s tree with each matching path removed with set_or_remove(path, None).trees
Working copy (TreeState)reset, snapshotted, savedreset updates jj's record of the working copy to the new commit "without touching the files in the working copy" (source).working-copy
DescendantsrewrittenRebased onto the new @, so they lose the files too. A descendant that modified one of them gets a conflict instead.transactions
Git index (colocated)writeIntent-to-add entries are updated for the tree change, and refs are exported (source).—
View and operationwritewc_commit_ids, heads and bookmarks follow; one operation, untrack paths.operations

5. Transaction and operation

  • Transaction: a raw Transaction committed with tx.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, with Rebased 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 undo brings the paths back into @.

6. Algorithm and invariants

  1. Remove. For every entry of @'s tree matching the filesets, set it to absent, then write the tree (source).
  2. Rewrite. rewrite_commit(@).set_tree(new_tree) (source).
  3. 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).
  4. 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).
  5. 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

MessageWhenSource
'<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:

jj file untrack: refused, then accepted With .gitignore containing *.bak, jj file untrack file1 file1.bak fails because file1 is not ignored, and nothing changes. jj file untrack file1.bak succeeds: the working-copy commit no longer contains file1.bak, file2.bak is still tracked, and both files remain on disk. jj file untrack target fails because target/file2 and one other file are not ignored, until target/ is added to .gitignore, after which it succeeds. untrack file1 file1.bak Error: 'file1' is not ignored. @ unchanged: file1, file1.bak, file2.bak, target/file2, target/file3 untrack file1.bak (no output) @ now: file1, file2.bak, target/file2, target/file3 on disk: file1.bak and file2.bak both still exist untrack target Error: 'target/file2' and 1 other files are not ignored. add target/ to .gitignore, run it again: (no output) @ now: file1, file2.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.