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