##// END OF EJS Templates
rhg: sort unsupported extensions in error message...
Raphaël Gomès -
r49842:c75dae6e default
parent child Browse files
Show More
@@ -1,713 +1,716 b''
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::{local_to_utf8, Ui};
3 use crate::ui::{local_to_utf8, Ui};
4 use clap::App;
4 use clap::App;
5 use clap::AppSettings;
5 use clap::AppSettings;
6 use clap::Arg;
6 use clap::Arg;
7 use clap::ArgMatches;
7 use clap::ArgMatches;
8 use format_bytes::{format_bytes, join};
8 use format_bytes::{format_bytes, join};
9 use hg::config::{Config, ConfigSource};
9 use hg::config::{Config, ConfigSource};
10 use hg::exit_codes;
10 use hg::exit_codes;
11 use hg::repo::{Repo, RepoError};
11 use hg::repo::{Repo, RepoError};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
13 use hg::utils::SliceExt;
13 use hg::utils::SliceExt;
14 use std::collections::HashSet;
14 use std::collections::HashSet;
15 use std::ffi::OsString;
15 use std::ffi::OsString;
16 use std::path::PathBuf;
16 use std::path::PathBuf;
17 use std::process::Command;
17 use std::process::Command;
18
18
19 mod blackbox;
19 mod blackbox;
20 mod color;
20 mod color;
21 mod error;
21 mod error;
22 mod ui;
22 mod ui;
23 pub mod utils {
23 pub mod utils {
24 pub mod path_utils;
24 pub mod path_utils;
25 }
25 }
26
26
27 fn main_with_result(
27 fn main_with_result(
28 process_start_time: &blackbox::ProcessStartTime,
28 process_start_time: &blackbox::ProcessStartTime,
29 ui: &ui::Ui,
29 ui: &ui::Ui,
30 repo: Result<&Repo, &NoRepoInCwdError>,
30 repo: Result<&Repo, &NoRepoInCwdError>,
31 config: &Config,
31 config: &Config,
32 ) -> Result<(), CommandError> {
32 ) -> Result<(), CommandError> {
33 check_unsupported(config, repo)?;
33 check_unsupported(config, repo)?;
34
34
35 let app = App::new("rhg")
35 let app = App::new("rhg")
36 .global_setting(AppSettings::AllowInvalidUtf8)
36 .global_setting(AppSettings::AllowInvalidUtf8)
37 .global_setting(AppSettings::DisableVersion)
37 .global_setting(AppSettings::DisableVersion)
38 .setting(AppSettings::SubcommandRequired)
38 .setting(AppSettings::SubcommandRequired)
39 .setting(AppSettings::VersionlessSubcommands)
39 .setting(AppSettings::VersionlessSubcommands)
40 .arg(
40 .arg(
41 Arg::with_name("repository")
41 Arg::with_name("repository")
42 .help("repository root directory")
42 .help("repository root directory")
43 .short("-R")
43 .short("-R")
44 .long("--repository")
44 .long("--repository")
45 .value_name("REPO")
45 .value_name("REPO")
46 .takes_value(true)
46 .takes_value(true)
47 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
47 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
48 .global(true),
48 .global(true),
49 )
49 )
50 .arg(
50 .arg(
51 Arg::with_name("config")
51 Arg::with_name("config")
52 .help("set/override config option (use 'section.name=value')")
52 .help("set/override config option (use 'section.name=value')")
53 .long("--config")
53 .long("--config")
54 .value_name("CONFIG")
54 .value_name("CONFIG")
55 .takes_value(true)
55 .takes_value(true)
56 .global(true)
56 .global(true)
57 // Ok: `--config section.key1=val --config section.key2=val2`
57 // Ok: `--config section.key1=val --config section.key2=val2`
58 .multiple(true)
58 .multiple(true)
59 // Not ok: `--config section.key1=val section.key2=val2`
59 // Not ok: `--config section.key1=val section.key2=val2`
60 .number_of_values(1),
60 .number_of_values(1),
61 )
61 )
62 .arg(
62 .arg(
63 Arg::with_name("cwd")
63 Arg::with_name("cwd")
64 .help("change working directory")
64 .help("change working directory")
65 .long("--cwd")
65 .long("--cwd")
66 .value_name("DIR")
66 .value_name("DIR")
67 .takes_value(true)
67 .takes_value(true)
68 .global(true),
68 .global(true),
69 )
69 )
70 .arg(
70 .arg(
71 Arg::with_name("color")
71 Arg::with_name("color")
72 .help("when to colorize (boolean, always, auto, never, or debug)")
72 .help("when to colorize (boolean, always, auto, never, or debug)")
73 .long("--color")
73 .long("--color")
74 .value_name("TYPE")
74 .value_name("TYPE")
75 .takes_value(true)
75 .takes_value(true)
76 .global(true),
76 .global(true),
77 )
77 )
78 .version("0.0.1");
78 .version("0.0.1");
79 let app = add_subcommand_args(app);
79 let app = add_subcommand_args(app);
80
80
81 let matches = app.clone().get_matches_safe()?;
81 let matches = app.clone().get_matches_safe()?;
82
82
83 let (subcommand_name, subcommand_matches) = matches.subcommand();
83 let (subcommand_name, subcommand_matches) = matches.subcommand();
84
84
85 // Mercurial allows users to define "defaults" for commands, fallback
85 // Mercurial allows users to define "defaults" for commands, fallback
86 // if a default is detected for the current command
86 // if a default is detected for the current command
87 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
87 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
88 if defaults?.is_some() {
88 if defaults?.is_some() {
89 let msg = "`defaults` config set";
89 let msg = "`defaults` config set";
90 return Err(CommandError::unsupported(msg));
90 return Err(CommandError::unsupported(msg));
91 }
91 }
92
92
93 for prefix in ["pre", "post", "fail"].iter() {
93 for prefix in ["pre", "post", "fail"].iter() {
94 // Mercurial allows users to define generic hooks for commands,
94 // Mercurial allows users to define generic hooks for commands,
95 // fallback if any are detected
95 // fallback if any are detected
96 let item = format!("{}-{}", prefix, subcommand_name);
96 let item = format!("{}-{}", prefix, subcommand_name);
97 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
97 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
98 if hook_for_command.is_some() {
98 if hook_for_command.is_some() {
99 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
99 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
100 return Err(CommandError::unsupported(msg));
100 return Err(CommandError::unsupported(msg));
101 }
101 }
102 }
102 }
103 let run = subcommand_run_fn(subcommand_name)
103 let run = subcommand_run_fn(subcommand_name)
104 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
104 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
105 let subcommand_args = subcommand_matches
105 let subcommand_args = subcommand_matches
106 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
106 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
107
107
108 let invocation = CliInvocation {
108 let invocation = CliInvocation {
109 ui,
109 ui,
110 subcommand_args,
110 subcommand_args,
111 config,
111 config,
112 repo,
112 repo,
113 };
113 };
114
114
115 if let Ok(repo) = repo {
115 if let Ok(repo) = repo {
116 // We don't support subrepos, fallback if the subrepos file is present
116 // We don't support subrepos, fallback if the subrepos file is present
117 if repo.working_directory_vfs().join(".hgsub").exists() {
117 if repo.working_directory_vfs().join(".hgsub").exists() {
118 let msg = "subrepos (.hgsub is present)";
118 let msg = "subrepos (.hgsub is present)";
119 return Err(CommandError::unsupported(msg));
119 return Err(CommandError::unsupported(msg));
120 }
120 }
121 }
121 }
122
122
123 if config.is_extension_enabled(b"blackbox") {
123 if config.is_extension_enabled(b"blackbox") {
124 let blackbox =
124 let blackbox =
125 blackbox::Blackbox::new(&invocation, process_start_time)?;
125 blackbox::Blackbox::new(&invocation, process_start_time)?;
126 blackbox.log_command_start();
126 blackbox.log_command_start();
127 let result = run(&invocation);
127 let result = run(&invocation);
128 blackbox.log_command_end(exit_code(
128 blackbox.log_command_end(exit_code(
129 &result,
129 &result,
130 // TODO: show a warning or combine with original error if
130 // TODO: show a warning or combine with original error if
131 // `get_bool` returns an error
131 // `get_bool` returns an error
132 config
132 config
133 .get_bool(b"ui", b"detailed-exit-code")
133 .get_bool(b"ui", b"detailed-exit-code")
134 .unwrap_or(false),
134 .unwrap_or(false),
135 ));
135 ));
136 result
136 result
137 } else {
137 } else {
138 run(&invocation)
138 run(&invocation)
139 }
139 }
140 }
140 }
141
141
142 fn main() {
142 fn main() {
143 // Run this first, before we find out if the blackbox extension is even
143 // Run this first, before we find out if the blackbox extension is even
144 // enabled, in order to include everything in-between in the duration
144 // enabled, in order to include everything in-between in the duration
145 // measurements. Reading config files can be slow if they’re on NFS.
145 // measurements. Reading config files can be slow if they’re on NFS.
146 let process_start_time = blackbox::ProcessStartTime::now();
146 let process_start_time = blackbox::ProcessStartTime::now();
147
147
148 env_logger::init();
148 env_logger::init();
149
149
150 let early_args = EarlyArgs::parse(std::env::args_os());
150 let early_args = EarlyArgs::parse(std::env::args_os());
151
151
152 let initial_current_dir = early_args.cwd.map(|cwd| {
152 let initial_current_dir = early_args.cwd.map(|cwd| {
153 let cwd = get_path_from_bytes(&cwd);
153 let cwd = get_path_from_bytes(&cwd);
154 std::env::current_dir()
154 std::env::current_dir()
155 .and_then(|initial| {
155 .and_then(|initial| {
156 std::env::set_current_dir(cwd)?;
156 std::env::set_current_dir(cwd)?;
157 Ok(initial)
157 Ok(initial)
158 })
158 })
159 .unwrap_or_else(|error| {
159 .unwrap_or_else(|error| {
160 exit(
160 exit(
161 &None,
161 &None,
162 &Ui::new_infallible(&Config::empty()),
162 &Ui::new_infallible(&Config::empty()),
163 OnUnsupported::Abort,
163 OnUnsupported::Abort,
164 Err(CommandError::abort(format!(
164 Err(CommandError::abort(format!(
165 "abort: {}: '{}'",
165 "abort: {}: '{}'",
166 error,
166 error,
167 cwd.display()
167 cwd.display()
168 ))),
168 ))),
169 false,
169 false,
170 )
170 )
171 })
171 })
172 });
172 });
173
173
174 let mut non_repo_config =
174 let mut non_repo_config =
175 Config::load_non_repo().unwrap_or_else(|error| {
175 Config::load_non_repo().unwrap_or_else(|error| {
176 // Normally this is decided based on config, but we don’t have that
176 // Normally this is decided based on config, but we don’t have that
177 // available. As of this writing config loading never returns an
177 // available. As of this writing config loading never returns an
178 // "unsupported" error but that is not enforced by the type system.
178 // "unsupported" error but that is not enforced by the type system.
179 let on_unsupported = OnUnsupported::Abort;
179 let on_unsupported = OnUnsupported::Abort;
180
180
181 exit(
181 exit(
182 &initial_current_dir,
182 &initial_current_dir,
183 &Ui::new_infallible(&Config::empty()),
183 &Ui::new_infallible(&Config::empty()),
184 on_unsupported,
184 on_unsupported,
185 Err(error.into()),
185 Err(error.into()),
186 false,
186 false,
187 )
187 )
188 });
188 });
189
189
190 non_repo_config
190 non_repo_config
191 .load_cli_args(early_args.config, early_args.color)
191 .load_cli_args(early_args.config, early_args.color)
192 .unwrap_or_else(|error| {
192 .unwrap_or_else(|error| {
193 exit(
193 exit(
194 &initial_current_dir,
194 &initial_current_dir,
195 &Ui::new_infallible(&non_repo_config),
195 &Ui::new_infallible(&non_repo_config),
196 OnUnsupported::from_config(&non_repo_config),
196 OnUnsupported::from_config(&non_repo_config),
197 Err(error.into()),
197 Err(error.into()),
198 non_repo_config
198 non_repo_config
199 .get_bool(b"ui", b"detailed-exit-code")
199 .get_bool(b"ui", b"detailed-exit-code")
200 .unwrap_or(false),
200 .unwrap_or(false),
201 )
201 )
202 });
202 });
203
203
204 if let Some(repo_path_bytes) = &early_args.repo {
204 if let Some(repo_path_bytes) = &early_args.repo {
205 lazy_static::lazy_static! {
205 lazy_static::lazy_static! {
206 static ref SCHEME_RE: regex::bytes::Regex =
206 static ref SCHEME_RE: regex::bytes::Regex =
207 // Same as `_matchscheme` in `mercurial/util.py`
207 // Same as `_matchscheme` in `mercurial/util.py`
208 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
208 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
209 }
209 }
210 if SCHEME_RE.is_match(&repo_path_bytes) {
210 if SCHEME_RE.is_match(&repo_path_bytes) {
211 exit(
211 exit(
212 &initial_current_dir,
212 &initial_current_dir,
213 &Ui::new_infallible(&non_repo_config),
213 &Ui::new_infallible(&non_repo_config),
214 OnUnsupported::from_config(&non_repo_config),
214 OnUnsupported::from_config(&non_repo_config),
215 Err(CommandError::UnsupportedFeature {
215 Err(CommandError::UnsupportedFeature {
216 message: format_bytes!(
216 message: format_bytes!(
217 b"URL-like --repository {}",
217 b"URL-like --repository {}",
218 repo_path_bytes
218 repo_path_bytes
219 ),
219 ),
220 }),
220 }),
221 // TODO: show a warning or combine with original error if
221 // TODO: show a warning or combine with original error if
222 // `get_bool` returns an error
222 // `get_bool` returns an error
223 non_repo_config
223 non_repo_config
224 .get_bool(b"ui", b"detailed-exit-code")
224 .get_bool(b"ui", b"detailed-exit-code")
225 .unwrap_or(false),
225 .unwrap_or(false),
226 )
226 )
227 }
227 }
228 }
228 }
229 let repo_arg = early_args.repo.unwrap_or(Vec::new());
229 let repo_arg = early_args.repo.unwrap_or(Vec::new());
230 let repo_path: Option<PathBuf> = {
230 let repo_path: Option<PathBuf> = {
231 if repo_arg.is_empty() {
231 if repo_arg.is_empty() {
232 None
232 None
233 } else {
233 } else {
234 let local_config = {
234 let local_config = {
235 if std::env::var_os("HGRCSKIPREPO").is_none() {
235 if std::env::var_os("HGRCSKIPREPO").is_none() {
236 // TODO: handle errors from find_repo_root
236 // TODO: handle errors from find_repo_root
237 if let Ok(current_dir_path) = Repo::find_repo_root() {
237 if let Ok(current_dir_path) = Repo::find_repo_root() {
238 let config_files = vec![
238 let config_files = vec![
239 ConfigSource::AbsPath(
239 ConfigSource::AbsPath(
240 current_dir_path.join(".hg/hgrc"),
240 current_dir_path.join(".hg/hgrc"),
241 ),
241 ),
242 ConfigSource::AbsPath(
242 ConfigSource::AbsPath(
243 current_dir_path.join(".hg/hgrc-not-shared"),
243 current_dir_path.join(".hg/hgrc-not-shared"),
244 ),
244 ),
245 ];
245 ];
246 // TODO: handle errors from
246 // TODO: handle errors from
247 // `load_from_explicit_sources`
247 // `load_from_explicit_sources`
248 Config::load_from_explicit_sources(config_files).ok()
248 Config::load_from_explicit_sources(config_files).ok()
249 } else {
249 } else {
250 None
250 None
251 }
251 }
252 } else {
252 } else {
253 None
253 None
254 }
254 }
255 };
255 };
256
256
257 let non_repo_config_val = {
257 let non_repo_config_val = {
258 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
258 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
259 match &non_repo_val {
259 match &non_repo_val {
260 Some(val) if val.len() > 0 => home::home_dir()
260 Some(val) if val.len() > 0 => home::home_dir()
261 .unwrap_or_else(|| PathBuf::from("~"))
261 .unwrap_or_else(|| PathBuf::from("~"))
262 .join(get_path_from_bytes(val))
262 .join(get_path_from_bytes(val))
263 .canonicalize()
263 .canonicalize()
264 // TODO: handle error and make it similar to python
264 // TODO: handle error and make it similar to python
265 // implementation maybe?
265 // implementation maybe?
266 .ok(),
266 .ok(),
267 _ => None,
267 _ => None,
268 }
268 }
269 };
269 };
270
270
271 let config_val = match &local_config {
271 let config_val = match &local_config {
272 None => non_repo_config_val,
272 None => non_repo_config_val,
273 Some(val) => {
273 Some(val) => {
274 let local_config_val = val.get(b"paths", &repo_arg);
274 let local_config_val = val.get(b"paths", &repo_arg);
275 match &local_config_val {
275 match &local_config_val {
276 Some(val) if val.len() > 0 => {
276 Some(val) if val.len() > 0 => {
277 // presence of a local_config assures that
277 // presence of a local_config assures that
278 // current_dir
278 // current_dir
279 // wont result in an Error
279 // wont result in an Error
280 let canpath = hg::utils::current_dir()
280 let canpath = hg::utils::current_dir()
281 .unwrap()
281 .unwrap()
282 .join(get_path_from_bytes(val))
282 .join(get_path_from_bytes(val))
283 .canonicalize();
283 .canonicalize();
284 canpath.ok().or(non_repo_config_val)
284 canpath.ok().or(non_repo_config_val)
285 }
285 }
286 _ => non_repo_config_val,
286 _ => non_repo_config_val,
287 }
287 }
288 }
288 }
289 };
289 };
290 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
290 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
291 }
291 }
292 };
292 };
293
293
294 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
294 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
295 {
295 {
296 Ok(repo) => Ok(repo),
296 Ok(repo) => Ok(repo),
297 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
297 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
298 // Not finding a repo is not fatal yet, if `-R` was not given
298 // Not finding a repo is not fatal yet, if `-R` was not given
299 Err(NoRepoInCwdError { cwd: at })
299 Err(NoRepoInCwdError { cwd: at })
300 }
300 }
301 Err(error) => exit(
301 Err(error) => exit(
302 &initial_current_dir,
302 &initial_current_dir,
303 &Ui::new_infallible(&non_repo_config),
303 &Ui::new_infallible(&non_repo_config),
304 OnUnsupported::from_config(&non_repo_config),
304 OnUnsupported::from_config(&non_repo_config),
305 Err(error.into()),
305 Err(error.into()),
306 // TODO: show a warning or combine with original error if
306 // TODO: show a warning or combine with original error if
307 // `get_bool` returns an error
307 // `get_bool` returns an error
308 non_repo_config
308 non_repo_config
309 .get_bool(b"ui", b"detailed-exit-code")
309 .get_bool(b"ui", b"detailed-exit-code")
310 .unwrap_or(false),
310 .unwrap_or(false),
311 ),
311 ),
312 };
312 };
313
313
314 let config = if let Ok(repo) = &repo_result {
314 let config = if let Ok(repo) = &repo_result {
315 repo.config()
315 repo.config()
316 } else {
316 } else {
317 &non_repo_config
317 &non_repo_config
318 };
318 };
319 let ui = Ui::new(&config).unwrap_or_else(|error| {
319 let ui = Ui::new(&config).unwrap_or_else(|error| {
320 exit(
320 exit(
321 &initial_current_dir,
321 &initial_current_dir,
322 &Ui::new_infallible(&config),
322 &Ui::new_infallible(&config),
323 OnUnsupported::from_config(&config),
323 OnUnsupported::from_config(&config),
324 Err(error.into()),
324 Err(error.into()),
325 config
325 config
326 .get_bool(b"ui", b"detailed-exit-code")
326 .get_bool(b"ui", b"detailed-exit-code")
327 .unwrap_or(false),
327 .unwrap_or(false),
328 )
328 )
329 });
329 });
330 let on_unsupported = OnUnsupported::from_config(config);
330 let on_unsupported = OnUnsupported::from_config(config);
331
331
332 let result = main_with_result(
332 let result = main_with_result(
333 &process_start_time,
333 &process_start_time,
334 &ui,
334 &ui,
335 repo_result.as_ref(),
335 repo_result.as_ref(),
336 config,
336 config,
337 );
337 );
338 exit(
338 exit(
339 &initial_current_dir,
339 &initial_current_dir,
340 &ui,
340 &ui,
341 on_unsupported,
341 on_unsupported,
342 result,
342 result,
343 // TODO: show a warning or combine with original error if `get_bool`
343 // TODO: show a warning or combine with original error if `get_bool`
344 // returns an error
344 // returns an error
345 config
345 config
346 .get_bool(b"ui", b"detailed-exit-code")
346 .get_bool(b"ui", b"detailed-exit-code")
347 .unwrap_or(false),
347 .unwrap_or(false),
348 )
348 )
349 }
349 }
350
350
351 fn exit_code(
351 fn exit_code(
352 result: &Result<(), CommandError>,
352 result: &Result<(), CommandError>,
353 use_detailed_exit_code: bool,
353 use_detailed_exit_code: bool,
354 ) -> i32 {
354 ) -> i32 {
355 match result {
355 match result {
356 Ok(()) => exit_codes::OK,
356 Ok(()) => exit_codes::OK,
357 Err(CommandError::Abort {
357 Err(CommandError::Abort {
358 message: _,
358 message: _,
359 detailed_exit_code,
359 detailed_exit_code,
360 }) => {
360 }) => {
361 if use_detailed_exit_code {
361 if use_detailed_exit_code {
362 *detailed_exit_code
362 *detailed_exit_code
363 } else {
363 } else {
364 exit_codes::ABORT
364 exit_codes::ABORT
365 }
365 }
366 }
366 }
367 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
367 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
368
368
369 // Exit with a specific code and no error message to let a potential
369 // Exit with a specific code and no error message to let a potential
370 // wrapper script fallback to Python-based Mercurial.
370 // wrapper script fallback to Python-based Mercurial.
371 Err(CommandError::UnsupportedFeature { .. }) => {
371 Err(CommandError::UnsupportedFeature { .. }) => {
372 exit_codes::UNIMPLEMENTED
372 exit_codes::UNIMPLEMENTED
373 }
373 }
374 }
374 }
375 }
375 }
376
376
377 fn exit(
377 fn exit(
378 initial_current_dir: &Option<PathBuf>,
378 initial_current_dir: &Option<PathBuf>,
379 ui: &Ui,
379 ui: &Ui,
380 mut on_unsupported: OnUnsupported,
380 mut on_unsupported: OnUnsupported,
381 result: Result<(), CommandError>,
381 result: Result<(), CommandError>,
382 use_detailed_exit_code: bool,
382 use_detailed_exit_code: bool,
383 ) -> ! {
383 ) -> ! {
384 if let (
384 if let (
385 OnUnsupported::Fallback { executable },
385 OnUnsupported::Fallback { executable },
386 Err(CommandError::UnsupportedFeature { message }),
386 Err(CommandError::UnsupportedFeature { message }),
387 ) = (&on_unsupported, &result)
387 ) = (&on_unsupported, &result)
388 {
388 {
389 let mut args = std::env::args_os();
389 let mut args = std::env::args_os();
390 let executable = match executable {
390 let executable = match executable {
391 None => {
391 None => {
392 exit_no_fallback(
392 exit_no_fallback(
393 ui,
393 ui,
394 OnUnsupported::Abort,
394 OnUnsupported::Abort,
395 Err(CommandError::abort(
395 Err(CommandError::abort(
396 "abort: 'rhg.on-unsupported=fallback' without \
396 "abort: 'rhg.on-unsupported=fallback' without \
397 'rhg.fallback-executable' set.",
397 'rhg.fallback-executable' set.",
398 )),
398 )),
399 false,
399 false,
400 );
400 );
401 }
401 }
402 Some(executable) => executable,
402 Some(executable) => executable,
403 };
403 };
404 let executable_path = get_path_from_bytes(&executable);
404 let executable_path = get_path_from_bytes(&executable);
405 let this_executable = args.next().expect("exepcted argv[0] to exist");
405 let this_executable = args.next().expect("exepcted argv[0] to exist");
406 if executable_path == &PathBuf::from(this_executable) {
406 if executable_path == &PathBuf::from(this_executable) {
407 // Avoid spawning infinitely many processes until resource
407 // Avoid spawning infinitely many processes until resource
408 // exhaustion.
408 // exhaustion.
409 let _ = ui.write_stderr(&format_bytes!(
409 let _ = ui.write_stderr(&format_bytes!(
410 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
410 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
411 points to `rhg` itself.\n",
411 points to `rhg` itself.\n",
412 executable
412 executable
413 ));
413 ));
414 on_unsupported = OnUnsupported::Abort
414 on_unsupported = OnUnsupported::Abort
415 } else {
415 } else {
416 log::debug!("falling back (see trace-level log)");
416 log::debug!("falling back (see trace-level log)");
417 log::trace!("{}", local_to_utf8(message));
417 log::trace!("{}", local_to_utf8(message));
418 // `args` is now `argv[1..]` since we’ve already consumed
418 // `args` is now `argv[1..]` since we’ve already consumed
419 // `argv[0]`
419 // `argv[0]`
420 let mut command = Command::new(executable_path);
420 let mut command = Command::new(executable_path);
421 command.args(args);
421 command.args(args);
422 if let Some(initial) = initial_current_dir {
422 if let Some(initial) = initial_current_dir {
423 command.current_dir(initial);
423 command.current_dir(initial);
424 }
424 }
425 let result = command.status();
425 let result = command.status();
426 match result {
426 match result {
427 Ok(status) => std::process::exit(
427 Ok(status) => std::process::exit(
428 status.code().unwrap_or(exit_codes::ABORT),
428 status.code().unwrap_or(exit_codes::ABORT),
429 ),
429 ),
430 Err(error) => {
430 Err(error) => {
431 let _ = ui.write_stderr(&format_bytes!(
431 let _ = ui.write_stderr(&format_bytes!(
432 b"tried to fall back to a '{}' sub-process but got error {}\n",
432 b"tried to fall back to a '{}' sub-process but got error {}\n",
433 executable, format_bytes::Utf8(error)
433 executable, format_bytes::Utf8(error)
434 ));
434 ));
435 on_unsupported = OnUnsupported::Abort
435 on_unsupported = OnUnsupported::Abort
436 }
436 }
437 }
437 }
438 }
438 }
439 }
439 }
440 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
440 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
441 }
441 }
442
442
443 fn exit_no_fallback(
443 fn exit_no_fallback(
444 ui: &Ui,
444 ui: &Ui,
445 on_unsupported: OnUnsupported,
445 on_unsupported: OnUnsupported,
446 result: Result<(), CommandError>,
446 result: Result<(), CommandError>,
447 use_detailed_exit_code: bool,
447 use_detailed_exit_code: bool,
448 ) -> ! {
448 ) -> ! {
449 match &result {
449 match &result {
450 Ok(_) => {}
450 Ok(_) => {}
451 Err(CommandError::Unsuccessful) => {}
451 Err(CommandError::Unsuccessful) => {}
452 Err(CommandError::Abort {
452 Err(CommandError::Abort {
453 message,
453 message,
454 detailed_exit_code: _,
454 detailed_exit_code: _,
455 }) => {
455 }) => {
456 if !message.is_empty() {
456 if !message.is_empty() {
457 // Ignore errors when writing to stderr, we’re already exiting
457 // Ignore errors when writing to stderr, we’re already exiting
458 // with failure code so there’s not much more we can do.
458 // with failure code so there’s not much more we can do.
459 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
459 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
460 }
460 }
461 }
461 }
462 Err(CommandError::UnsupportedFeature { message }) => {
462 Err(CommandError::UnsupportedFeature { message }) => {
463 match on_unsupported {
463 match on_unsupported {
464 OnUnsupported::Abort => {
464 OnUnsupported::Abort => {
465 let _ = ui.write_stderr(&format_bytes!(
465 let _ = ui.write_stderr(&format_bytes!(
466 b"unsupported feature: {}\n",
466 b"unsupported feature: {}\n",
467 message
467 message
468 ));
468 ));
469 }
469 }
470 OnUnsupported::AbortSilent => {}
470 OnUnsupported::AbortSilent => {}
471 OnUnsupported::Fallback { .. } => unreachable!(),
471 OnUnsupported::Fallback { .. } => unreachable!(),
472 }
472 }
473 }
473 }
474 }
474 }
475 std::process::exit(exit_code(&result, use_detailed_exit_code))
475 std::process::exit(exit_code(&result, use_detailed_exit_code))
476 }
476 }
477
477
478 macro_rules! subcommands {
478 macro_rules! subcommands {
479 ($( $command: ident )+) => {
479 ($( $command: ident )+) => {
480 mod commands {
480 mod commands {
481 $(
481 $(
482 pub mod $command;
482 pub mod $command;
483 )+
483 )+
484 }
484 }
485
485
486 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
486 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
487 app
487 app
488 $(
488 $(
489 .subcommand(commands::$command::args())
489 .subcommand(commands::$command::args())
490 )+
490 )+
491 }
491 }
492
492
493 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
493 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
494
494
495 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
495 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
496 match name {
496 match name {
497 $(
497 $(
498 stringify!($command) => Some(commands::$command::run),
498 stringify!($command) => Some(commands::$command::run),
499 )+
499 )+
500 _ => None,
500 _ => None,
501 }
501 }
502 }
502 }
503 };
503 };
504 }
504 }
505
505
506 subcommands! {
506 subcommands! {
507 cat
507 cat
508 debugdata
508 debugdata
509 debugrequirements
509 debugrequirements
510 debugignorerhg
510 debugignorerhg
511 files
511 files
512 root
512 root
513 config
513 config
514 status
514 status
515 }
515 }
516
516
517 pub struct CliInvocation<'a> {
517 pub struct CliInvocation<'a> {
518 ui: &'a Ui,
518 ui: &'a Ui,
519 subcommand_args: &'a ArgMatches<'a>,
519 subcommand_args: &'a ArgMatches<'a>,
520 config: &'a Config,
520 config: &'a Config,
521 /// References inside `Result` is a bit peculiar but allow
521 /// References inside `Result` is a bit peculiar but allow
522 /// `invocation.repo?` to work out with `&CliInvocation` since this
522 /// `invocation.repo?` to work out with `&CliInvocation` since this
523 /// `Result` type is `Copy`.
523 /// `Result` type is `Copy`.
524 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
524 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
525 }
525 }
526
526
527 struct NoRepoInCwdError {
527 struct NoRepoInCwdError {
528 cwd: PathBuf,
528 cwd: PathBuf,
529 }
529 }
530
530
531 /// CLI arguments to be parsed "early" in order to be able to read
531 /// CLI arguments to be parsed "early" in order to be able to read
532 /// configuration before using Clap. Ideally we would also use Clap for this,
532 /// configuration before using Clap. Ideally we would also use Clap for this,
533 /// see <https://github.com/clap-rs/clap/discussions/2366>.
533 /// see <https://github.com/clap-rs/clap/discussions/2366>.
534 ///
534 ///
535 /// These arguments are still declared when we do use Clap later, so that Clap
535 /// These arguments are still declared when we do use Clap later, so that Clap
536 /// does not return an error for their presence.
536 /// does not return an error for their presence.
537 struct EarlyArgs {
537 struct EarlyArgs {
538 /// Values of all `--config` arguments. (Possibly none)
538 /// Values of all `--config` arguments. (Possibly none)
539 config: Vec<Vec<u8>>,
539 config: Vec<Vec<u8>>,
540 /// Value of all the `--color` argument, if any.
540 /// Value of all the `--color` argument, if any.
541 color: Option<Vec<u8>>,
541 color: Option<Vec<u8>>,
542 /// Value of the `-R` or `--repository` argument, if any.
542 /// Value of the `-R` or `--repository` argument, if any.
543 repo: Option<Vec<u8>>,
543 repo: Option<Vec<u8>>,
544 /// Value of the `--cwd` argument, if any.
544 /// Value of the `--cwd` argument, if any.
545 cwd: Option<Vec<u8>>,
545 cwd: Option<Vec<u8>>,
546 }
546 }
547
547
548 impl EarlyArgs {
548 impl EarlyArgs {
549 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
549 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
550 let mut args = args.into_iter().map(get_bytes_from_os_str);
550 let mut args = args.into_iter().map(get_bytes_from_os_str);
551 let mut config = Vec::new();
551 let mut config = Vec::new();
552 let mut color = None;
552 let mut color = None;
553 let mut repo = None;
553 let mut repo = None;
554 let mut cwd = None;
554 let mut cwd = None;
555 // Use `while let` instead of `for` so that we can also call
555 // Use `while let` instead of `for` so that we can also call
556 // `args.next()` inside the loop.
556 // `args.next()` inside the loop.
557 while let Some(arg) = args.next() {
557 while let Some(arg) = args.next() {
558 if arg == b"--config" {
558 if arg == b"--config" {
559 if let Some(value) = args.next() {
559 if let Some(value) = args.next() {
560 config.push(value)
560 config.push(value)
561 }
561 }
562 } else if let Some(value) = arg.drop_prefix(b"--config=") {
562 } else if let Some(value) = arg.drop_prefix(b"--config=") {
563 config.push(value.to_owned())
563 config.push(value.to_owned())
564 }
564 }
565
565
566 if arg == b"--color" {
566 if arg == b"--color" {
567 if let Some(value) = args.next() {
567 if let Some(value) = args.next() {
568 color = Some(value)
568 color = Some(value)
569 }
569 }
570 } else if let Some(value) = arg.drop_prefix(b"--color=") {
570 } else if let Some(value) = arg.drop_prefix(b"--color=") {
571 color = Some(value.to_owned())
571 color = Some(value.to_owned())
572 }
572 }
573
573
574 if arg == b"--cwd" {
574 if arg == b"--cwd" {
575 if let Some(value) = args.next() {
575 if let Some(value) = args.next() {
576 cwd = Some(value)
576 cwd = Some(value)
577 }
577 }
578 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
578 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
579 cwd = Some(value.to_owned())
579 cwd = Some(value.to_owned())
580 }
580 }
581
581
582 if arg == b"--repository" || arg == b"-R" {
582 if arg == b"--repository" || arg == b"-R" {
583 if let Some(value) = args.next() {
583 if let Some(value) = args.next() {
584 repo = Some(value)
584 repo = Some(value)
585 }
585 }
586 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
586 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
587 repo = Some(value.to_owned())
587 repo = Some(value.to_owned())
588 } else if let Some(value) = arg.drop_prefix(b"-R") {
588 } else if let Some(value) = arg.drop_prefix(b"-R") {
589 repo = Some(value.to_owned())
589 repo = Some(value.to_owned())
590 }
590 }
591 }
591 }
592 Self {
592 Self {
593 config,
593 config,
594 color,
594 color,
595 repo,
595 repo,
596 cwd,
596 cwd,
597 }
597 }
598 }
598 }
599 }
599 }
600
600
601 /// What to do when encountering some unsupported feature.
601 /// What to do when encountering some unsupported feature.
602 ///
602 ///
603 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
603 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
604 enum OnUnsupported {
604 enum OnUnsupported {
605 /// Print an error message describing what feature is not supported,
605 /// Print an error message describing what feature is not supported,
606 /// and exit with code 252.
606 /// and exit with code 252.
607 Abort,
607 Abort,
608 /// Silently exit with code 252.
608 /// Silently exit with code 252.
609 AbortSilent,
609 AbortSilent,
610 /// Try running a Python implementation
610 /// Try running a Python implementation
611 Fallback { executable: Option<Vec<u8>> },
611 Fallback { executable: Option<Vec<u8>> },
612 }
612 }
613
613
614 impl OnUnsupported {
614 impl OnUnsupported {
615 const DEFAULT: Self = OnUnsupported::Abort;
615 const DEFAULT: Self = OnUnsupported::Abort;
616
616
617 fn from_config(config: &Config) -> Self {
617 fn from_config(config: &Config) -> Self {
618 match config
618 match config
619 .get(b"rhg", b"on-unsupported")
619 .get(b"rhg", b"on-unsupported")
620 .map(|value| value.to_ascii_lowercase())
620 .map(|value| value.to_ascii_lowercase())
621 .as_deref()
621 .as_deref()
622 {
622 {
623 Some(b"abort") => OnUnsupported::Abort,
623 Some(b"abort") => OnUnsupported::Abort,
624 Some(b"abort-silent") => OnUnsupported::AbortSilent,
624 Some(b"abort-silent") => OnUnsupported::AbortSilent,
625 Some(b"fallback") => OnUnsupported::Fallback {
625 Some(b"fallback") => OnUnsupported::Fallback {
626 executable: config
626 executable: config
627 .get(b"rhg", b"fallback-executable")
627 .get(b"rhg", b"fallback-executable")
628 .map(|x| x.to_owned()),
628 .map(|x| x.to_owned()),
629 },
629 },
630 None => Self::DEFAULT,
630 None => Self::DEFAULT,
631 Some(_) => {
631 Some(_) => {
632 // TODO: warn about unknown config value
632 // TODO: warn about unknown config value
633 Self::DEFAULT
633 Self::DEFAULT
634 }
634 }
635 }
635 }
636 }
636 }
637 }
637 }
638
638
639 /// The `*` extension is an edge-case for config sub-options that apply to all
639 /// The `*` extension is an edge-case for config sub-options that apply to all
640 /// extensions. For now, only `:required` exists, but that may change in the
640 /// extensions. For now, only `:required` exists, but that may change in the
641 /// future.
641 /// future.
642 const SUPPORTED_EXTENSIONS: &[&[u8]] =
642 const SUPPORTED_EXTENSIONS: &[&[u8]] =
643 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
643 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
644
644
645 fn check_extensions(config: &Config) -> Result<(), CommandError> {
645 fn check_extensions(config: &Config) -> Result<(), CommandError> {
646 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
646 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
647 // All extensions are to be ignored, nothing to do here
647 // All extensions are to be ignored, nothing to do here
648 return Ok(());
648 return Ok(());
649 }
649 }
650
650
651 let enabled: HashSet<&[u8]> = config
651 let enabled: HashSet<&[u8]> = config
652 .get_section_keys(b"extensions")
652 .get_section_keys(b"extensions")
653 .into_iter()
653 .into_iter()
654 .map(|extension| {
654 .map(|extension| {
655 // Ignore extension suboptions. Only `required` exists for now.
655 // Ignore extension suboptions. Only `required` exists for now.
656 // `rhg` either supports an extension or doesn't, so it doesn't
656 // `rhg` either supports an extension or doesn't, so it doesn't
657 // make sense to consider the loading of an extension.
657 // make sense to consider the loading of an extension.
658 extension.split_2(b':').unwrap_or((extension, b"")).0
658 extension.split_2(b':').unwrap_or((extension, b"")).0
659 })
659 })
660 .collect();
660 .collect();
661
661
662 let mut unsupported = enabled;
662 let mut unsupported = enabled;
663 for supported in SUPPORTED_EXTENSIONS {
663 for supported in SUPPORTED_EXTENSIONS {
664 unsupported.remove(supported);
664 unsupported.remove(supported);
665 }
665 }
666
666
667 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
667 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
668 {
668 {
669 for ignored in ignored_list {
669 for ignored in ignored_list {
670 unsupported.remove(ignored.as_slice());
670 unsupported.remove(ignored.as_slice());
671 }
671 }
672 }
672 }
673
673
674 if unsupported.is_empty() {
674 if unsupported.is_empty() {
675 Ok(())
675 Ok(())
676 } else {
676 } else {
677 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
678 // Sort the extensions to get a stable output
679 unsupported.sort();
677 Err(CommandError::UnsupportedFeature {
680 Err(CommandError::UnsupportedFeature {
678 message: format_bytes!(
681 message: format_bytes!(
679 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
682 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
680 join(unsupported, b", ")
683 join(unsupported, b", ")
681 ),
684 ),
682 })
685 })
683 }
686 }
684 }
687 }
685
688
686 fn check_unsupported(
689 fn check_unsupported(
687 config: &Config,
690 config: &Config,
688 repo: Result<&Repo, &NoRepoInCwdError>,
691 repo: Result<&Repo, &NoRepoInCwdError>,
689 ) -> Result<(), CommandError> {
692 ) -> Result<(), CommandError> {
690 check_extensions(config)?;
693 check_extensions(config)?;
691
694
692 if std::env::var_os("HG_PENDING").is_some() {
695 if std::env::var_os("HG_PENDING").is_some() {
693 // TODO: only if the value is `== repo.working_directory`?
696 // TODO: only if the value is `== repo.working_directory`?
694 // What about relative v.s. absolute paths?
697 // What about relative v.s. absolute paths?
695 Err(CommandError::unsupported("$HG_PENDING"))?
698 Err(CommandError::unsupported("$HG_PENDING"))?
696 }
699 }
697
700
698 if let Ok(repo) = repo {
701 if let Ok(repo) = repo {
699 if repo.has_subrepos()? {
702 if repo.has_subrepos()? {
700 Err(CommandError::unsupported("sub-repositories"))?
703 Err(CommandError::unsupported("sub-repositories"))?
701 }
704 }
702 }
705 }
703
706
704 if config.has_non_empty_section(b"encode") {
707 if config.has_non_empty_section(b"encode") {
705 Err(CommandError::unsupported("[encode] config"))?
708 Err(CommandError::unsupported("[encode] config"))?
706 }
709 }
707
710
708 if config.has_non_empty_section(b"decode") {
711 if config.has_non_empty_section(b"decode") {
709 Err(CommandError::unsupported("[decode] config"))?
712 Err(CommandError::unsupported("[decode] config"))?
710 }
713 }
711
714
712 Ok(())
715 Ok(())
713 }
716 }
General Comments 0
You need to be logged in to leave comments. Login now