just playing with tangled
at diffedit3 771 lines 26 kB view raw
1// Copyright 2022-2024 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::io::Write as _; 16use std::process::ExitCode; 17use std::sync::Arc; 18use std::{error, io, iter, str}; 19 20use itertools::Itertools as _; 21use jj_lib::backend::BackendError; 22use jj_lib::fileset::{FilePatternParseError, FilesetParseError, FilesetParseErrorKind}; 23use jj_lib::git::{GitConfigParseError, GitExportError, GitImportError, GitRemoteManagementError}; 24use jj_lib::gitignore::GitIgnoreError; 25use jj_lib::op_heads_store::OpHeadResolutionError; 26use jj_lib::op_store::OpStoreError; 27use jj_lib::op_walk::OpsetEvaluationError; 28use jj_lib::repo::{CheckOutCommitError, EditCommitError, RepoLoaderError, RewriteRootCommit}; 29use jj_lib::repo_path::{FsPathParseError, RepoPathBuf}; 30use jj_lib::revset::{ 31 RevsetEvaluationError, RevsetParseError, RevsetParseErrorKind, RevsetResolutionError, 32}; 33use jj_lib::signing::SignInitError; 34use jj_lib::str_util::StringPatternParseError; 35use jj_lib::working_copy::{ResetError, SnapshotError, WorkingCopyStateError}; 36use jj_lib::workspace::WorkspaceInitError; 37use thiserror::Error; 38 39use crate::formatter::{FormatRecorder, Formatter}; 40use crate::merge_tools::{ 41 ConflictResolveError, DiffEditError, DiffGenerateError, MergeToolConfigError, 42}; 43use crate::revset_util::UserRevsetEvaluationError; 44use crate::template_parser::{TemplateParseError, TemplateParseErrorKind}; 45use crate::ui::Ui; 46 47#[derive(Clone, Copy, Debug, Eq, PartialEq)] 48pub enum CommandErrorKind { 49 User, 50 Config, 51 /// Invalid command line. The inner error type may be `clap::Error`. 52 Cli, 53 BrokenPipe, 54 Internal, 55} 56 57#[derive(Clone, Debug)] 58pub struct CommandError { 59 pub kind: CommandErrorKind, 60 pub error: Arc<dyn error::Error + Send + Sync>, 61 pub hints: Vec<ErrorHint>, 62} 63 64impl CommandError { 65 pub fn new( 66 kind: CommandErrorKind, 67 err: impl Into<Box<dyn error::Error + Send + Sync>>, 68 ) -> Self { 69 CommandError { 70 kind, 71 error: Arc::from(err.into()), 72 hints: vec![], 73 } 74 } 75 76 pub fn with_message( 77 kind: CommandErrorKind, 78 message: impl Into<String>, 79 source: impl Into<Box<dyn error::Error + Send + Sync>>, 80 ) -> Self { 81 Self::new(kind, ErrorWithMessage::new(message, source)) 82 } 83 84 /// Returns error with the given plain-text `hint` attached. 85 pub fn hinted(mut self, hint: impl Into<String>) -> Self { 86 self.add_hint(hint); 87 self 88 } 89 90 /// Appends plain-text `hint` to the error. 91 pub fn add_hint(&mut self, hint: impl Into<String>) { 92 self.hints.push(ErrorHint::PlainText(hint.into())); 93 } 94 95 /// Appends formatted `hint` to the error. 96 pub fn add_formatted_hint(&mut self, hint: FormatRecorder) { 97 self.hints.push(ErrorHint::Formatted(hint)); 98 } 99 100 /// Constructs formatted hint and appends it to the error. 101 pub fn add_formatted_hint_with( 102 &mut self, 103 write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>, 104 ) { 105 let mut formatter = FormatRecorder::new(); 106 write(&mut formatter).expect("write() to FormatRecorder should never fail"); 107 self.add_formatted_hint(formatter); 108 } 109 110 /// Appends 0 or more plain-text `hints` to the error. 111 pub fn extend_hints(&mut self, hints: impl IntoIterator<Item = String>) { 112 self.hints 113 .extend(hints.into_iter().map(ErrorHint::PlainText)); 114 } 115} 116 117#[derive(Clone, Debug)] 118pub enum ErrorHint { 119 PlainText(String), 120 Formatted(FormatRecorder), 121} 122 123/// Wraps error with user-visible message. 124#[derive(Debug, Error)] 125#[error("{message}")] 126struct ErrorWithMessage { 127 message: String, 128 source: Box<dyn error::Error + Send + Sync>, 129} 130 131impl ErrorWithMessage { 132 fn new( 133 message: impl Into<String>, 134 source: impl Into<Box<dyn error::Error + Send + Sync>>, 135 ) -> Self { 136 ErrorWithMessage { 137 message: message.into(), 138 source: source.into(), 139 } 140 } 141} 142 143pub fn user_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError { 144 CommandError::new(CommandErrorKind::User, err) 145} 146 147pub fn user_error_with_hint( 148 err: impl Into<Box<dyn error::Error + Send + Sync>>, 149 hint: impl Into<String>, 150) -> CommandError { 151 user_error(err).hinted(hint) 152} 153 154pub fn user_error_with_message( 155 message: impl Into<String>, 156 source: impl Into<Box<dyn error::Error + Send + Sync>>, 157) -> CommandError { 158 CommandError::with_message(CommandErrorKind::User, message, source) 159} 160 161pub fn config_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError { 162 CommandError::new(CommandErrorKind::Config, err) 163} 164 165pub fn config_error_with_message( 166 message: impl Into<String>, 167 source: impl Into<Box<dyn error::Error + Send + Sync>>, 168) -> CommandError { 169 CommandError::with_message(CommandErrorKind::Config, message, source) 170} 171 172pub fn cli_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError { 173 CommandError::new(CommandErrorKind::Cli, err) 174} 175 176pub fn internal_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError { 177 CommandError::new(CommandErrorKind::Internal, err) 178} 179 180pub fn internal_error_with_message( 181 message: impl Into<String>, 182 source: impl Into<Box<dyn error::Error + Send + Sync>>, 183) -> CommandError { 184 CommandError::with_message(CommandErrorKind::Internal, message, source) 185} 186 187fn format_similarity_hint<S: AsRef<str>>(candidates: &[S]) -> Option<String> { 188 match candidates { 189 [] => None, 190 names => { 191 let quoted_names = names 192 .iter() 193 .map(|s| format!(r#""{}""#, s.as_ref())) 194 .join(", "); 195 Some(format!("Did you mean {quoted_names}?")) 196 } 197 } 198} 199 200impl From<io::Error> for CommandError { 201 fn from(err: io::Error) -> Self { 202 let kind = match err.kind() { 203 io::ErrorKind::BrokenPipe => CommandErrorKind::BrokenPipe, 204 _ => CommandErrorKind::User, 205 }; 206 CommandError::new(kind, err) 207 } 208} 209 210impl From<config::ConfigError> for CommandError { 211 fn from(err: config::ConfigError) -> Self { 212 config_error(err) 213 } 214} 215 216impl From<crate::config::ConfigError> for CommandError { 217 fn from(err: crate::config::ConfigError) -> Self { 218 config_error(err) 219 } 220} 221 222impl From<RewriteRootCommit> for CommandError { 223 fn from(err: RewriteRootCommit) -> Self { 224 internal_error_with_message("Attempted to rewrite the root commit", err) 225 } 226} 227 228impl From<EditCommitError> for CommandError { 229 fn from(err: EditCommitError) -> Self { 230 internal_error_with_message("Failed to edit a commit", err) 231 } 232} 233 234impl From<CheckOutCommitError> for CommandError { 235 fn from(err: CheckOutCommitError) -> Self { 236 internal_error_with_message("Failed to check out a commit", err) 237 } 238} 239 240impl From<BackendError> for CommandError { 241 fn from(err: BackendError) -> Self { 242 match &err { 243 BackendError::Unsupported(_) => user_error(err), 244 _ => internal_error_with_message("Unexpected error from backend", err), 245 } 246 } 247} 248 249impl From<WorkspaceInitError> for CommandError { 250 fn from(err: WorkspaceInitError) -> Self { 251 match err { 252 WorkspaceInitError::DestinationExists(_) => { 253 user_error("The target repo already exists") 254 } 255 WorkspaceInitError::NonUnicodePath => { 256 user_error("The target repo path contains non-unicode characters") 257 } 258 WorkspaceInitError::CheckOutCommit(err) => { 259 internal_error_with_message("Failed to check out the initial commit", err) 260 } 261 WorkspaceInitError::Path(err) => { 262 internal_error_with_message("Failed to access the repository", err) 263 } 264 WorkspaceInitError::Backend(err) => { 265 user_error_with_message("Failed to access the repository", err) 266 } 267 WorkspaceInitError::WorkingCopyState(err) => { 268 internal_error_with_message("Failed to access the repository", err) 269 } 270 WorkspaceInitError::SignInit(err @ SignInitError::UnknownBackend(_)) => user_error(err), 271 WorkspaceInitError::SignInit(err) => internal_error(err), 272 } 273 } 274} 275 276impl From<OpHeadResolutionError> for CommandError { 277 fn from(err: OpHeadResolutionError) -> Self { 278 match err { 279 OpHeadResolutionError::NoHeads => { 280 internal_error_with_message("Corrupt repository", err) 281 } 282 } 283 } 284} 285 286impl From<OpsetEvaluationError> for CommandError { 287 fn from(err: OpsetEvaluationError) -> Self { 288 match err { 289 OpsetEvaluationError::OpsetResolution(err) => user_error(err), 290 OpsetEvaluationError::OpHeadResolution(err) => err.into(), 291 OpsetEvaluationError::OpStore(err) => err.into(), 292 } 293 } 294} 295 296impl From<SnapshotError> for CommandError { 297 fn from(err: SnapshotError) -> Self { 298 match err { 299 SnapshotError::NewFileTooLarge { 300 path, 301 size, 302 max_size, 303 } => { 304 // if the size difference is < 1KiB, then show exact bytes. 305 // otherwise, show in human-readable form; this avoids weird cases 306 // where a file is 400 bytes too large but the error says something 307 // like '1.0MiB, maximum size allowed is ~1.0MiB' 308 let size_diff = size.0 - max_size.0; 309 let err_str = if size_diff <= 1024 { 310 format!( 311 "it is {} bytes too large; the maximum size allowed is {} bytes ({}).", 312 size_diff, max_size.0, max_size, 313 ) 314 } else { 315 format!("it is {}; the maximum size allowed is ~{}.", size, max_size,) 316 }; 317 318 user_error(format!( 319 "Failed to snapshot the working copy\nThe file '{}' is too large to be \ 320 snapshotted: {}", 321 path.display(), 322 err_str, 323 )) 324 .hinted(format!( 325 "This is to prevent large files from being added on accident. You can fix \ 326 this error by: 327 - Adding the file to `.gitignore` 328 - Run `jj config set --repo snapshot.max-new-file-size {}` 329 This will increase the maximum file size allowed for new files, in this repository only. 330 - Run `jj --config-toml 'snapshot.max-new-file-size={}' st` 331 This will increase the maximum file size allowed for new files, for this command only.", 332 size.0, size.0 333 )) 334 } 335 err => internal_error_with_message("Failed to snapshot the working copy", err), 336 } 337 } 338} 339 340impl From<OpStoreError> for CommandError { 341 fn from(err: OpStoreError) -> Self { 342 internal_error_with_message("Failed to load an operation", err) 343 } 344} 345 346impl From<RepoLoaderError> for CommandError { 347 fn from(err: RepoLoaderError) -> Self { 348 internal_error_with_message("Failed to load the repo", err) 349 } 350} 351 352impl From<ResetError> for CommandError { 353 fn from(err: ResetError) -> Self { 354 internal_error_with_message("Failed to reset the working copy", err) 355 } 356} 357 358impl From<DiffEditError> for CommandError { 359 fn from(err: DiffEditError) -> Self { 360 user_error_with_message("Failed to edit diff", err) 361 } 362} 363 364impl From<DiffGenerateError> for CommandError { 365 fn from(err: DiffGenerateError) -> Self { 366 user_error_with_message("Failed to generate diff", err) 367 } 368} 369 370impl From<ConflictResolveError> for CommandError { 371 fn from(err: ConflictResolveError) -> Self { 372 user_error_with_message("Failed to resolve conflicts", err) 373 } 374} 375 376impl From<MergeToolConfigError> for CommandError { 377 fn from(err: MergeToolConfigError) -> Self { 378 match &err { 379 MergeToolConfigError::MergeArgsNotConfigured { tool_name } => { 380 let tool_name = tool_name.clone(); 381 user_error_with_hint( 382 err, 383 format!( 384 "To use `{tool_name}` as a merge tool, the config \ 385 `merge-tools.{tool_name}.merge-args` must be defined (see docs for \ 386 details)" 387 ), 388 ) 389 } 390 _ => user_error_with_message("Failed to load tool configuration", err), 391 } 392 } 393} 394 395impl From<git2::Error> for CommandError { 396 fn from(err: git2::Error) -> Self { 397 user_error_with_message("Git operation failed", err) 398 } 399} 400 401impl From<GitImportError> for CommandError { 402 fn from(err: GitImportError) -> Self { 403 let hint = match &err { 404 GitImportError::MissingHeadTarget { .. } 405 | GitImportError::MissingRefAncestor { .. } => Some( 406 "\ 407Is this Git repository a shallow or partial clone (cloned with the --depth or --filter \ 408 argument)? 409jj currently does not support shallow/partial clones. To use jj with this \ 410 repository, try 411unshallowing the repository (https://stackoverflow.com/q/6802145) or re-cloning with the full 412repository contents." 413 .to_string(), 414 ), 415 GitImportError::RemoteReservedForLocalGitRepo => { 416 Some("Run `jj git remote rename` to give different name.".to_string()) 417 } 418 GitImportError::InternalBackend(_) => None, 419 GitImportError::InternalGitError(_) => None, 420 GitImportError::UnexpectedBackend => None, 421 }; 422 let mut cmd_err = 423 user_error_with_message("Failed to import refs from underlying Git repo", err); 424 cmd_err.extend_hints(hint); 425 cmd_err 426 } 427} 428 429impl From<GitExportError> for CommandError { 430 fn from(err: GitExportError) -> Self { 431 internal_error_with_message("Failed to export refs to underlying Git repo", err) 432 } 433} 434 435impl From<GitRemoteManagementError> for CommandError { 436 fn from(err: GitRemoteManagementError) -> Self { 437 user_error(err) 438 } 439} 440 441impl From<RevsetEvaluationError> for CommandError { 442 fn from(err: RevsetEvaluationError) -> Self { 443 user_error(err) 444 } 445} 446 447impl From<FilesetParseError> for CommandError { 448 fn from(err: FilesetParseError) -> Self { 449 let hint = fileset_parse_error_hint(&err); 450 let mut cmd_err = 451 user_error_with_message(format!("Failed to parse fileset: {}", err.kind()), err); 452 cmd_err.extend_hints(hint); 453 cmd_err 454 } 455} 456 457impl From<RevsetParseError> for CommandError { 458 fn from(err: RevsetParseError) -> Self { 459 let hint = revset_parse_error_hint(&err); 460 let mut cmd_err = 461 user_error_with_message(format!("Failed to parse revset: {}", err.kind()), err); 462 cmd_err.extend_hints(hint); 463 cmd_err 464 } 465} 466 467impl From<RevsetResolutionError> for CommandError { 468 fn from(err: RevsetResolutionError) -> Self { 469 let hint = revset_resolution_error_hint(&err); 470 let mut cmd_err = user_error(err); 471 cmd_err.extend_hints(hint); 472 cmd_err 473 } 474} 475 476impl From<UserRevsetEvaluationError> for CommandError { 477 fn from(err: UserRevsetEvaluationError) -> Self { 478 match err { 479 UserRevsetEvaluationError::Resolution(err) => err.into(), 480 UserRevsetEvaluationError::Evaluation(err) => err.into(), 481 } 482 } 483} 484 485impl From<TemplateParseError> for CommandError { 486 fn from(err: TemplateParseError) -> Self { 487 let hint = template_parse_error_hint(&err); 488 let mut cmd_err = 489 user_error_with_message(format!("Failed to parse template: {}", err.kind()), err); 490 cmd_err.extend_hints(hint); 491 cmd_err 492 } 493} 494 495impl From<FsPathParseError> for CommandError { 496 fn from(err: FsPathParseError) -> Self { 497 user_error(err) 498 } 499} 500 501impl From<clap::Error> for CommandError { 502 fn from(err: clap::Error) -> Self { 503 let hint = find_source_parse_error_hint(&err); 504 let mut cmd_err = cli_error(err); 505 cmd_err.extend_hints(hint); 506 cmd_err 507 } 508} 509 510impl From<GitConfigParseError> for CommandError { 511 fn from(err: GitConfigParseError) -> Self { 512 internal_error_with_message("Failed to parse Git config", err) 513 } 514} 515 516impl From<WorkingCopyStateError> for CommandError { 517 fn from(err: WorkingCopyStateError) -> Self { 518 internal_error_with_message("Failed to access working copy state", err) 519 } 520} 521 522impl From<GitIgnoreError> for CommandError { 523 fn from(err: GitIgnoreError) -> Self { 524 user_error_with_message("Failed to process .gitignore.", err) 525 } 526} 527 528fn find_source_parse_error_hint(err: &dyn error::Error) -> Option<String> { 529 let source = err.source()?; 530 if let Some(source) = source.downcast_ref() { 531 file_pattern_parse_error_hint(source) 532 } else if let Some(source) = source.downcast_ref() { 533 fileset_parse_error_hint(source) 534 } else if let Some(source) = source.downcast_ref() { 535 revset_parse_error_hint(source) 536 } else if let Some(source) = source.downcast_ref() { 537 revset_resolution_error_hint(source) 538 } else if let Some(UserRevsetEvaluationError::Resolution(source)) = source.downcast_ref() { 539 revset_resolution_error_hint(source) 540 } else if let Some(source) = source.downcast_ref() { 541 string_pattern_parse_error_hint(source) 542 } else if let Some(source) = source.downcast_ref() { 543 template_parse_error_hint(source) 544 } else { 545 None 546 } 547} 548 549fn file_pattern_parse_error_hint(err: &FilePatternParseError) -> Option<String> { 550 match err { 551 FilePatternParseError::InvalidKind(_) => None, 552 // Suggest root:"<path>" if input can be parsed as repo-relative path 553 FilePatternParseError::FsPath(e) => RepoPathBuf::from_relative_path(&e.input) 554 .ok() 555 .map(|path| format!(r#"Consider using root:{path:?} to specify repo-relative path"#)), 556 FilePatternParseError::RelativePath(_) => None, 557 FilePatternParseError::GlobPattern(_) => None, 558 } 559} 560 561fn fileset_parse_error_hint(err: &FilesetParseError) -> Option<String> { 562 match err.kind() { 563 FilesetParseErrorKind::NoSuchFunction { 564 name: _, 565 candidates, 566 } => format_similarity_hint(candidates), 567 FilesetParseErrorKind::InvalidArguments { .. } | FilesetParseErrorKind::Expression(_) => { 568 find_source_parse_error_hint(&err) 569 } 570 _ => None, 571 } 572} 573 574fn revset_parse_error_hint(err: &RevsetParseError) -> Option<String> { 575 // Only for the bottom error, which is usually the root cause 576 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap(); 577 match bottom_err.kind() { 578 RevsetParseErrorKind::NotPrefixOperator { 579 op: _, 580 similar_op, 581 description, 582 } 583 | RevsetParseErrorKind::NotPostfixOperator { 584 op: _, 585 similar_op, 586 description, 587 } 588 | RevsetParseErrorKind::NotInfixOperator { 589 op: _, 590 similar_op, 591 description, 592 } => Some(format!("Did you mean '{similar_op}' for {description}?")), 593 RevsetParseErrorKind::NoSuchFunction { 594 name: _, 595 candidates, 596 } => format_similarity_hint(candidates), 597 RevsetParseErrorKind::InvalidFunctionArguments { .. } => { 598 find_source_parse_error_hint(bottom_err) 599 } 600 _ => None, 601 } 602} 603 604fn revset_resolution_error_hint(err: &RevsetResolutionError) -> Option<String> { 605 match err { 606 RevsetResolutionError::NoSuchRevision { 607 name: _, 608 candidates, 609 } => format_similarity_hint(candidates), 610 RevsetResolutionError::EmptyString 611 | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. } 612 | RevsetResolutionError::AmbiguousCommitIdPrefix(_) 613 | RevsetResolutionError::AmbiguousChangeIdPrefix(_) 614 | RevsetResolutionError::StoreError(_) 615 | RevsetResolutionError::Other(_) => None, 616 } 617} 618 619fn string_pattern_parse_error_hint(err: &StringPatternParseError) -> Option<String> { 620 match err { 621 StringPatternParseError::InvalidKind(_) => { 622 Some("Try prefixing with one of `exact:`, `glob:` or `substring:`".into()) 623 } 624 StringPatternParseError::GlobPattern(_) => None, 625 } 626} 627 628fn template_parse_error_hint(err: &TemplateParseError) -> Option<String> { 629 // Only for the bottom error, which is usually the root cause 630 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap(); 631 match bottom_err.kind() { 632 TemplateParseErrorKind::NoSuchKeyword { candidates, .. } 633 | TemplateParseErrorKind::NoSuchFunction { candidates, .. } 634 | TemplateParseErrorKind::NoSuchMethod { candidates, .. } => { 635 format_similarity_hint(candidates) 636 } 637 TemplateParseErrorKind::InvalidArguments { .. } | TemplateParseErrorKind::Expression(_) => { 638 find_source_parse_error_hint(bottom_err) 639 } 640 _ => None, 641 } 642} 643 644const BROKEN_PIPE_EXIT_CODE: u8 = 3; 645 646pub(crate) fn handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> ExitCode { 647 try_handle_command_result(ui, result).unwrap_or_else(|_| ExitCode::from(BROKEN_PIPE_EXIT_CODE)) 648} 649 650fn try_handle_command_result( 651 ui: &mut Ui, 652 result: Result<(), CommandError>, 653) -> io::Result<ExitCode> { 654 let Err(cmd_err) = &result else { 655 return Ok(ExitCode::SUCCESS); 656 }; 657 let err = &cmd_err.error; 658 let hints = &cmd_err.hints; 659 match cmd_err.kind { 660 CommandErrorKind::User => { 661 print_error(ui, "Error: ", err, hints)?; 662 Ok(ExitCode::from(1)) 663 } 664 CommandErrorKind::Config => { 665 print_error(ui, "Config error: ", err, hints)?; 666 writeln!( 667 ui.stderr_formatter().labeled("hint"), 668 "For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md." 669 )?; 670 Ok(ExitCode::from(1)) 671 } 672 CommandErrorKind::Cli => { 673 if let Some(err) = err.downcast_ref::<clap::Error>() { 674 handle_clap_error(ui, err, hints) 675 } else { 676 print_error(ui, "Error: ", err, hints)?; 677 Ok(ExitCode::from(2)) 678 } 679 } 680 CommandErrorKind::BrokenPipe => { 681 // A broken pipe is not an error, but a signal to exit gracefully. 682 Ok(ExitCode::from(BROKEN_PIPE_EXIT_CODE)) 683 } 684 CommandErrorKind::Internal => { 685 print_error(ui, "Internal error: ", err, hints)?; 686 Ok(ExitCode::from(255)) 687 } 688 } 689} 690 691fn print_error( 692 ui: &Ui, 693 heading: &str, 694 err: &dyn error::Error, 695 hints: &[ErrorHint], 696) -> io::Result<()> { 697 writeln!(ui.error_with_heading(heading), "{err}")?; 698 print_error_sources(ui, err.source())?; 699 print_error_hints(ui, hints)?; 700 Ok(()) 701} 702 703fn print_error_sources(ui: &Ui, source: Option<&dyn error::Error>) -> io::Result<()> { 704 let Some(err) = source else { 705 return Ok(()); 706 }; 707 ui.stderr_formatter() 708 .with_label("error_source", |formatter| { 709 if err.source().is_none() { 710 write!(formatter.labeled("heading"), "Caused by: ")?; 711 writeln!(formatter, "{err}")?; 712 } else { 713 writeln!(formatter.labeled("heading"), "Caused by:")?; 714 for (i, err) in iter::successors(Some(err), |err| err.source()).enumerate() { 715 write!(formatter.labeled("heading"), "{}: ", i + 1)?; 716 writeln!(formatter, "{err}")?; 717 } 718 } 719 Ok(()) 720 }) 721} 722 723fn print_error_hints(ui: &Ui, hints: &[ErrorHint]) -> io::Result<()> { 724 for hint in hints { 725 ui.stderr_formatter().with_label("hint", |formatter| { 726 write!(formatter.labeled("heading"), "Hint: ")?; 727 match hint { 728 ErrorHint::PlainText(message) => { 729 writeln!(formatter, "{message}")?; 730 Ok(()) 731 } 732 ErrorHint::Formatted(recorded) => { 733 recorded.replay(formatter)?; 734 // Formatted hint is usually multi-line text, and it's 735 // convenient if trailing "\n" doesn't have to be omitted. 736 if !recorded.data().ends_with(b"\n") { 737 writeln!(formatter)?; 738 } 739 Ok(()) 740 } 741 } 742 })?; 743 } 744 Ok(()) 745} 746 747fn handle_clap_error(ui: &mut Ui, err: &clap::Error, hints: &[ErrorHint]) -> io::Result<ExitCode> { 748 let clap_str = if ui.color() { 749 err.render().ansi().to_string() 750 } else { 751 err.render().to_string() 752 }; 753 754 match err.kind() { 755 clap::error::ErrorKind::DisplayHelp 756 | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(), 757 _ => {} 758 }; 759 // Definitions for exit codes and streams come from 760 // https://github.com/clap-rs/clap/blob/master/src/error/mod.rs 761 match err.kind() { 762 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { 763 write!(ui.stdout(), "{clap_str}")?; 764 return Ok(ExitCode::SUCCESS); 765 } 766 _ => {} 767 } 768 write!(ui.stderr(), "{clap_str}")?; 769 print_error_hints(ui, hints)?; 770 Ok(ExitCode::from(2)) 771}