In short: jj resolves -r (default @) and refuses it if immutable. For every
file entry matching the paths, it sets executable on every term of the value: the one
file of a resolved path, or every side and base of a conflict. Symlinks, directories and submodules
are rejected. It rewrites the commit with the new tree, tx.finish rebases descendants,
and the operation is make paths executable in commit <id> or
make paths non-executable ….
The command is cmd_file_chmod. Its help text
contrasts it with POSIX chmod: this one works on Windows, on conflicts, and on any
revision (source). On disk, the bit only matters once the commit
is checked out; see configuration.
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
MODE (positional) | x | n | required | x (alias executable) sets the bit; n (alias normal) clears it. The short names keep clap's error listing short (source). |
FILESETS (positional) | filesets | required | The paths to change. At least one is needed (source). |
-r, --revision REVSET | revset | @ | The commit to change. It must be exactly one commit (source). |
2. Configuration
The command itself reads no keys besides revset-aliases."immutable_heads()", which
decides whether the commit may be rewritten. working-copy.exec-bit-change
(respect, ignore or auto, the default) decides what happens on
disk. With ignore, jj neither reads nor sets the executable permission in the working
copy, so jj file chmod is the way to change the recorded bit. Windows always behaves
that way (docs). Upstream's tests for these
settings are test_chmod_exec_bit_settings and the two that follow.
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. It keeps its parents, change ID, description and author. | commits |
TreeValue::File | read, replaced | Only the executable field changes. File IDs and copy IDs stay the same, so no file content is written (source). | trees |
| Trees | written | New tree objects along each changed path, via MergedTreeBuilder. | trees |
| Descendants | rewritten | Rebased by tx.finish. The bit change is part of the commit's diff, so it carries into descendants that didn't change it. | transactions |
View | write | Heads, bookmarks and wc_commit_ids follow the rewritten commits. | view |
| Operation | write | One operation. | operations |
| Working copy | read, then maybe write | Snapshotted first. If @ changed, the file's permission is updated on disk, subject to working-copy.exec-bit-change. | working-copy |
5. Transaction and operation
- Transaction: one, started after the commit is checked and the paths are parsed (source).
- Operation description:
make paths executable in commit <full hex id>ormake paths non-executable in commit <full hex id>(source). - Commits rewritten: the target, then its descendants in
tx.finish(source). - No unchanged check: unlike
diffeditorrestore, the command doesn't compare trees before rewriting. Whether anything is recorded is left totx.finish, which only skips when the repo has no changes at all (source). - An error aborts everything: the first bad path returns an error before the tree is written, so no other path changes either (source).
jj undorestores the previous view.
6. Algorithm and invariants
tree.entries_matching(matcher) yields every path in the commit's tree that the filesets
match, each with its MergedTreeValue, a Merge of optional tree values
(source). For each one:
-
It checks that every side (each add term) that is present is a
TreeValue::File. Absent sides, as in a modify/delete conflict, are skipped. If a side is something else, the error saysFound neither a file nor a conflictfor a resolved value andSome of the sides of the conflict are not filesfor a conflict (source). -
It then walks every present term, bases included, and sets
executableto the requested value (source). Because every term gets the same value, the executable bit resolves to that value whatever the conflict's shape. Since only adds are checked, a base that isn't a file is left as it is; upstream has a TODO for a test of exactly that case (source). - It stores the value back with
set_or_remove_merge, which accepts the conflict because it keeps the base tree's arity, and unwraps the result (source).
Invariants: file contents and copy IDs never change, only the bit. A conflict stays a conflict with the same terms. Paths outside the filesets are untouched. Either every matched path changes or none does.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Found neither a file nor a conflict at '<path>'. | A matched path is a symlink or submodule. | chmod.rs |
Some of the sides of the conflict are not files at '<path>'. | A conflict side is a directory, symlink or submodule. | chmod.rs |
Warning: No matching entries for paths: … | An explicit path isn't in the commit. The others still change. | chmod.rs, cli_util.rs |
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: invalid value '…' for '<MODE>', listing n, x | An unknown mode. | chmod.rs |
clap: missing <FILESETS> | No paths given. | chmod.rs |
8. Worked example
From upstream's test_chmod_regular_conflict. base
has file; x and n both change it, and
jj file chmod x file -r=x makes it executable in x. The merge
conflict then has a 2-sided conflict whose terms disagree on the bit: x's
side is executable, while the base and n's side are not. Running
jj file chmod x file on the merge changes all three terms:
jj debug tree before and after shows exactly this, with the file IDs unchanged, and
jj file show file prints the same conflict markers both times. A later
jj file chmod n file sets all three back to false. Passing an extra path
that doesn't exist (source):
Warning: No matching entries for paths: nonexistent
Working copy (@) now at: yostqsxw a1b4dce4 conflict | (conflict) conflict
Parent commit (@-) : royxmykx 02247291 x | x
Parent commit (@-) : zsuskuln eb0ba805 n | n
Added 0 files, modified 1 files, removed 0 files
Warning: There are unresolved conflicts at these paths:
file 2-sided conflict including an executable
In test_chmod_file_dir_deletion_conflicts, a file-vs-directory
conflict is refused with Some of the sides of the conflict are not files at 'file'.
A modify/delete conflict is accepted: the absent side is skipped, and the summary becomes
2-sided conflict including 1 deletion and an executable.