just playing with tangled
1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use clap_complete::ArgValueCandidates;
16use clap_complete::ArgValueCompleter;
17use jj_lib::backend::TreeValue;
18use jj_lib::merged_tree::MergedTreeBuilder;
19use jj_lib::object_id::ObjectId as _;
20use tracing::instrument;
21
22use crate::cli_util::print_unmatched_explicit_paths;
23use crate::cli_util::CommandHelper;
24use crate::cli_util::RevisionArg;
25use crate::command_error::user_error;
26use crate::command_error::CommandError;
27use crate::complete;
28use crate::ui::Ui;
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
31enum ChmodMode {
32 /// Make a path non-executable (alias: normal)
33 // We use short names for enum values so that errors say that the possible values are `n, x`.
34 #[value(name = "n", alias("normal"))]
35 Normal,
36 /// Make a path executable (alias: executable)
37 #[value(name = "x", alias("executable"))]
38 Executable,
39}
40
41/// Sets or removes the executable bit for paths in the repo
42///
43/// Unlike the POSIX `chmod`, `jj file chmod` also works on Windows, on
44/// conflicted files, and on arbitrary revisions.
45#[derive(clap::Args, Clone, Debug)]
46pub(crate) struct FileChmodArgs {
47 mode: ChmodMode,
48 /// The revision to update
49 #[arg(
50 long, short,
51 default_value = "@",
52 value_name = "REVSET",
53 add = ArgValueCandidates::new(complete::mutable_revisions),
54 )]
55 revision: RevisionArg,
56 /// Paths to change the executable bit for
57 #[arg(
58 required = true,
59 value_name = "FILESETS",
60 value_hint = clap::ValueHint::AnyPath,
61 add = ArgValueCompleter::new(complete::all_revision_files),
62 )]
63 paths: Vec<String>,
64}
65
66#[instrument(skip_all)]
67pub(crate) fn cmd_file_chmod(
68 ui: &mut Ui,
69 command: &CommandHelper,
70 args: &FileChmodArgs,
71) -> Result<(), CommandError> {
72 let executable_bit = match args.mode {
73 ChmodMode::Executable => true,
74 ChmodMode::Normal => false,
75 };
76
77 let mut workspace_command = command.workspace_helper(ui)?;
78 let commit = workspace_command.resolve_single_rev(ui, &args.revision)?;
79 workspace_command.check_rewritable([commit.id()])?;
80 let tree = commit.tree()?;
81 // TODO: No need to add special case for empty paths when switching to
82 // parse_union_filesets(). paths = [] should be "none()" if supported.
83 let fileset_expression = workspace_command.parse_file_patterns(ui, &args.paths)?;
84 let matcher = fileset_expression.to_matcher();
85 print_unmatched_explicit_paths(ui, &workspace_command, &fileset_expression, [&tree])?;
86
87 let mut tx = workspace_command.start_transaction();
88 let store = tree.store();
89 let mut tree_builder = MergedTreeBuilder::new(commit.tree_id().clone());
90 for (repo_path, result) in tree.entries_matching(matcher.as_ref()) {
91 let mut tree_value = result?;
92 let user_error_with_path = |msg: &str| {
93 user_error(format!(
94 "{msg} at '{}'.",
95 tx.base_workspace_helper().format_file_path(&repo_path)
96 ))
97 };
98 let all_files = tree_value
99 .adds()
100 .flatten()
101 .all(|tree_value| matches!(tree_value, TreeValue::File { .. }));
102 if !all_files {
103 let message = if tree_value.is_resolved() {
104 "Found neither a file nor a conflict"
105 } else {
106 "Some of the sides of the conflict are not files"
107 };
108 return Err(user_error_with_path(message));
109 }
110 for value in tree_value.iter_mut().flatten() {
111 if let TreeValue::File { id: _, executable } = value {
112 *executable = executable_bit;
113 }
114 }
115 tree_builder.set_or_remove(repo_path, tree_value);
116 }
117
118 let new_tree_id = tree_builder.write_tree(store)?;
119 tx.repo_mut()
120 .rewrite_commit(&commit)
121 .set_tree_id(new_tree_id)
122 .write()?;
123 tx.finish(
124 ui,
125 format!(
126 "make paths {} in commit {}",
127 if executable_bit {
128 "executable"
129 } else {
130 "non-executable"
131 },
132 commit.id().hex(),
133 ),
134 )
135}