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