jj / How jj file delete works

How jj file delete works

jj file delete removes the files matched by some filesets from one revision, and rebases its descendants on top. It never touches the working copy directly; the files go away on disk only because @ changes.

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. What gets deleted
  8. Conflicted files
  9. Descendants
  10. 7. Errors and edge cases
  11. 8. Worked example

In short: jj resolves -r (default @) and refuses it if it is immutable. It takes every entry of the commit's tree that the filesets match, conflicted or not, and sets it to absent in a tree builder. If the new tree equals the old one, it prints Nothing changed. and records nothing. Otherwise it rewrites the commit with the new tree, rebases descendants (or, with --restore-descendants, reparents them so their content stays), and records delete paths in commit <id>.

The command is cmd_file_delete. Its help text: "Delete files from the given revision. Removes all files matched by the given filesets from the specified revision. Descendants are rebased on top of the rewritten commit." (source). It is the Delete variant of FileCommand (source, dispatch).

1. Command-line surface

ArgumentTypeDefaultMeaning
FILESETS (positional)filesetsrequiredFiles or directories to delete. At least one is needed. Completion offers every file in the revision (source).
-r, --revision REVSETrevset@The revision to delete the files in. It must be exactly one commit. Completion offers mutable revisions (source).
--restore-descendantsboolfalse"Preserve the content (not the diff) when rebasing descendants" (source).

FileDeleteArgs. The global flags are on the CLI page.

2. Configuration

The command reads no keys of its own. revset-aliases."immutable_heads()" decides whether the commit may be rewritten (source), and the usual fileset and revset settings shape how the arguments are parsed. Snapshotting the working copy first, and updating it afterwards, follow the working-copy settings described on the working-copy page.

3. End-to-end flow

jj file delete: call flow cmd_file_delete loads the workspace through the shared lifecycle, resolves -r (default @), checks it is rewritable, parses the paths and warns about explicit ones that match nothing. It starts a MergedTreeBuilder from the commit's tree and sets every matching entry to absent, then writes the new tree. If the tree IDs are unchanged it prints "Nothing changed." and returns with no transaction. Otherwise it starts a transaction, rewrites the commit with the new tree, calls rebase_or_reparent_descendants, which rebases descendants or, with --restore-descendants, reparents them, and tx.finish records "delete paths in commit" and the commit's id. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) resolve_single_rev(-r); check_rewritable; parse_file_patterns; to_matcher print_unmatched_explicit_paths: "No matching entries for paths: …" MergedTreeBuilder::new(commit.tree()); for each entries_matching → set_or_remove(path, None) write_tree() tree IDs unchanged? yes → "Nothing changed.", no operation start_transaction rewrite_commit(commit).set_tree(new).write() rebase_or_reparent_descendants rebase, or reparent with --restore-descendants tx.finish(ui, "delete paths in commit <id>"): write the operation, update the working copy (CLI page) no

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Target commitrewrittenA new commit with the new tree. Parents, change ID, description and author are kept (source).commits
Treesread, writtenThe commit's MergedTree is read; MergedTreeBuilder writes new tree objects along each removed path. No file content is written.trees
ConflictsremovedA conflicted entry is replaced by a resolved absent value, so the conflict goes away with the file.conflicts
DescendantsrewrittenRebased, or reparented with --restore-descendants, before tx.finish.transactions
ViewwriteHeads, bookmarks and wc_commit_ids follow the rewritten commits.view
OperationwriteOne operation, or none when nothing matched.operations
Working copyread, then maybe writeSnapshotted first. If @'s tree changed, the deleted files are removed from disk.working-copy

5. Transaction and operation

  • Transaction: one, started only after the new tree is written and found to differ (source).
  • Operation description: delete paths in commit <full hex id>, using the commit's id before the rewrite (source).
  • Commits rewritten: the target, then its descendants through rebase_or_reparent_descendants, which prints Rebased N descendant commits. only when N is above zero, adding (while preserving their content) when reparenting.
  • Nothing matched: no transaction and no operation; upstream's test checks the operation ID is unchanged (test).
  • jj undo restores the previous view, bringing the files back in the target and its descendants.

6. Algorithm and invariants

What gets deleted

tree.entries_matching(matcher) yields every file entry the filesets match, so a directory path deletes everything under it and a pattern such as glob:*.txt deletes every match (source, test). Unlike file chmod, it doesn't look at the value: files, symlinks, submodules and conflicts are all passed to set_or_remove as None, which the builder documents as the way to remove a value (source).

Conflicted files

A conflicted entry is overridden by a resolved, absent value, so the file and its conflict disappear together. In upstream's test, deleting the conflicted file from a merge leaves jj debug tree empty (test).

Descendants

By default rebase_descendants replays each descendant's own diff on the rewritten commit, so a descendant that didn't touch a deleted file loses it too. With --restore-descendants, reparent_descendants keeps each descendant's tree as it was; its diff now re-adds the file. restore uses the same pair.

Invariants: only the target's tree changes, never its identity or metadata. Paths outside the filesets are untouched. If nothing matched, nothing is recorded.

7. Errors and edge cases

MessageWhenSource
Warning: No matching entries for paths: …An explicit path isn't in the commit. Matched paths are still deleted.delete.rs, cli_util.rs, test
Nothing changed.No path matched, so the tree is unchanged.delete.rs, test
Commit … is immutableThe commit is immutable.cli_util.rs
Revset `…` resolved to more than one revision (or to none)-r isn't exactly one commit.revset_util.rs
clap: missing <FILESETS>No paths given.delete.rs

8. Worked example

From upstream's test_file_delete. base adds file, dir/a, dir/b, notes.txt, data.txt and keep; child, the working-copy commit, only adds other. Then jj file delete -r=base file is run with and without --restore-descendants:

jj file delete -r=base file: before and after Before: base adds file and five other files, and child at @ adds other. After a plain delete, base is rewritten without file and child is rebased; it doesn't contain file either. After a delete with --restore-descendants, base is rewritten the same way, but child is reparented with its tree unchanged, so it still contains file and its diff now adds it. before jj file delete -r=base file … --restore-descendants base +file, dir/, *.txt, keep child @ +other base 0f03ba3b file deleted child 69c03f6b @ rebased: +other, no file base 53605aea file deleted child 178385e9 @ reparented: +other, +file

Upstream's snapshot for the plain delete (source):

$ jj file delete -r=base file
Rebased 1 descendant commits.
Working copy  (@) now at: zsuskuln 69c03f6b child | child
Parent commit (@-)      : rlvkpnrz 0f03ba3b base | base
Added 0 files, modified 0 files, removed 1 files

With --restore-descendants the first line becomes Rebased 1 descendant commits (while preserving their content). and there is no Added … removed … line, since @'s tree is unchanged (source).