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