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
| Argument | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional) | filesets | required | Files or directories to delete. At least one is needed. Completion offers every file in the revision (source). |
-r, --revision REVSET | revset | @ | The revision to delete the files in. It must be exactly one commit. Completion offers mutable revisions (source). |
--restore-descendants | bool | false | "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
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commit | rewritten | A new commit with the new tree. Parents, change ID, description and author are kept (source). | commits |
| Trees | read, written | The commit's MergedTree is read; MergedTreeBuilder writes new tree objects along each removed path. No file content is written. | trees |
| Conflicts | removed | A conflicted entry is replaced by a resolved absent value, so the conflict goes away with the file. | conflicts |
| Descendants | rewritten | Rebased, or reparented with --restore-descendants, before tx.finish. | transactions |
View | write | Heads, bookmarks and wc_commit_ids follow the rewritten commits. | view |
| Operation | write | One operation, or none when nothing matched. | operations |
| Working copy | read, then maybe write | Snapshotted 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 printsRebased 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 undorestores 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
| Message | When | Source |
|---|---|---|
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 immutable | The 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:
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).