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