just playing with tangled
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}