1use std::error;
16use std::error::Error as _;
17use std::io;
18use std::io::Write as _;
19use std::iter;
20use std::str;
21use std::sync::Arc;
22
23use itertools::Itertools as _;
24use jj_lib::absorb::AbsorbError;
25use jj_lib::backend::BackendError;
26use jj_lib::config::ConfigFileSaveError;
27use jj_lib::config::ConfigGetError;
28use jj_lib::config::ConfigLoadError;
29use jj_lib::config::ConfigMigrateError;
30use jj_lib::dsl_util::Diagnostics;
31use jj_lib::evolution::WalkPredecessorsError;
32use jj_lib::fileset::FilePatternParseError;
33use jj_lib::fileset::FilesetParseError;
34use jj_lib::fileset::FilesetParseErrorKind;
35use jj_lib::fix::FixError;
36use jj_lib::gitignore::GitIgnoreError;
37use jj_lib::op_heads_store::OpHeadResolutionError;
38use jj_lib::op_heads_store::OpHeadsStoreError;
39use jj_lib::op_store::OpStoreError;
40use jj_lib::op_walk::OpsetEvaluationError;
41use jj_lib::op_walk::OpsetResolutionError;
42use jj_lib::repo::CheckOutCommitError;
43use jj_lib::repo::EditCommitError;
44use jj_lib::repo::RepoLoaderError;
45use jj_lib::repo::RewriteRootCommit;
46use jj_lib::repo_path::RepoPathBuf;
47use jj_lib::repo_path::UiPathParseError;
48use jj_lib::revset;
49use jj_lib::revset::RevsetEvaluationError;
50use jj_lib::revset::RevsetParseError;
51use jj_lib::revset::RevsetParseErrorKind;
52use jj_lib::revset::RevsetResolutionError;
53use jj_lib::str_util::StringPatternParseError;
54use jj_lib::trailer::TrailerParseError;
55use jj_lib::transaction::TransactionCommitError;
56use jj_lib::view::RenameWorkspaceError;
57use jj_lib::working_copy::RecoverWorkspaceError;
58use jj_lib::working_copy::ResetError;
59use jj_lib::working_copy::SnapshotError;
60use jj_lib::working_copy::WorkingCopyStateError;
61use jj_lib::workspace::WorkspaceInitError;
62use thiserror::Error;
63
64use crate::cli_util::short_operation_hash;
65use crate::description_util::ParseBulkEditMessageError;
66use crate::description_util::TempTextEditError;
67use crate::description_util::TextEditError;
68use crate::diff_util::DiffRenderError;
69use crate::formatter::FormatRecorder;
70use crate::formatter::Formatter;
71use crate::merge_tools::ConflictResolveError;
72use crate::merge_tools::DiffEditError;
73use crate::merge_tools::MergeToolConfigError;
74use crate::merge_tools::MergeToolPartialResolutionError;
75use crate::revset_util::BookmarkNameParseError;
76use crate::revset_util::UserRevsetEvaluationError;
77use crate::template_parser::TemplateParseError;
78use crate::template_parser::TemplateParseErrorKind;
79use crate::ui::Ui;
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum CommandErrorKind {
83 User,
84 Config,
85 Cli,
87 BrokenPipe,
88 Internal,
89}
90
91#[derive(Clone, Debug)]
92pub struct CommandError {
93 pub kind: CommandErrorKind,
94 pub error: Arc<dyn error::Error + Send + Sync>,
95 pub hints: Vec<ErrorHint>,
96}
97
98impl CommandError {
99 pub fn new(
100 kind: CommandErrorKind,
101 err: impl Into<Box<dyn error::Error + Send + Sync>>,
102 ) -> Self {
103 CommandError {
104 kind,
105 error: Arc::from(err.into()),
106 hints: vec![],
107 }
108 }
109
110 pub fn with_message(
111 kind: CommandErrorKind,
112 message: impl Into<String>,
113 source: impl Into<Box<dyn error::Error + Send + Sync>>,
114 ) -> Self {
115 Self::new(kind, ErrorWithMessage::new(message, source))
116 }
117
118 pub fn hinted(mut self, hint: impl Into<String>) -> Self {
120 self.add_hint(hint);
121 self
122 }
123
124 pub fn add_hint(&mut self, hint: impl Into<String>) {
126 self.hints.push(ErrorHint::PlainText(hint.into()));
127 }
128
129 pub fn add_formatted_hint(&mut self, hint: FormatRecorder) {
131 self.hints.push(ErrorHint::Formatted(hint));
132 }
133
134 pub fn add_formatted_hint_with(
136 &mut self,
137 write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>,
138 ) {
139 let mut formatter = FormatRecorder::new();
140 write(&mut formatter).expect("write() to FormatRecorder should never fail");
141 self.add_formatted_hint(formatter);
142 }
143
144 pub fn extend_hints(&mut self, hints: impl IntoIterator<Item = String>) {
146 self.hints
147 .extend(hints.into_iter().map(ErrorHint::PlainText));
148 }
149}
150
151#[derive(Clone, Debug)]
152pub enum ErrorHint {
153 PlainText(String),
154 Formatted(FormatRecorder),
155}
156
157#[derive(Debug, Error)]
159#[error("{message}")]
160struct ErrorWithMessage {
161 message: String,
162 source: Box<dyn error::Error + Send + Sync>,
163}
164
165impl ErrorWithMessage {
166 fn new(
167 message: impl Into<String>,
168 source: impl Into<Box<dyn error::Error + Send + Sync>>,
169 ) -> Self {
170 ErrorWithMessage {
171 message: message.into(),
172 source: source.into(),
173 }
174 }
175}
176
177pub fn user_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
178 CommandError::new(CommandErrorKind::User, err)
179}
180
181pub fn user_error_with_hint(
182 err: impl Into<Box<dyn error::Error + Send + Sync>>,
183 hint: impl Into<String>,
184) -> CommandError {
185 user_error(err).hinted(hint)
186}
187
188pub fn user_error_with_message(
189 message: impl Into<String>,
190 source: impl Into<Box<dyn error::Error + Send + Sync>>,
191) -> CommandError {
192 CommandError::with_message(CommandErrorKind::User, message, source)
193}
194
195pub fn config_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
196 CommandError::new(CommandErrorKind::Config, err)
197}
198
199pub fn config_error_with_message(
200 message: impl Into<String>,
201 source: impl Into<Box<dyn error::Error + Send + Sync>>,
202) -> CommandError {
203 CommandError::with_message(CommandErrorKind::Config, message, source)
204}
205
206pub fn cli_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
207 CommandError::new(CommandErrorKind::Cli, err)
208}
209
210pub fn cli_error_with_message(
211 message: impl Into<String>,
212 source: impl Into<Box<dyn error::Error + Send + Sync>>,
213) -> CommandError {
214 CommandError::with_message(CommandErrorKind::Cli, message, source)
215}
216
217pub fn internal_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
218 CommandError::new(CommandErrorKind::Internal, err)
219}
220
221pub fn internal_error_with_message(
222 message: impl Into<String>,
223 source: impl Into<Box<dyn error::Error + Send + Sync>>,
224) -> CommandError {
225 CommandError::with_message(CommandErrorKind::Internal, message, source)
226}
227
228fn format_similarity_hint<S: AsRef<str>>(candidates: &[S]) -> Option<String> {
229 match candidates {
230 [] => None,
231 names => {
232 let quoted_names = names.iter().map(|s| format!("`{}`", s.as_ref())).join(", ");
233 Some(format!("Did you mean {quoted_names}?"))
234 }
235 }
236}
237
238impl From<io::Error> for CommandError {
239 fn from(err: io::Error) -> Self {
240 let kind = match err.kind() {
241 io::ErrorKind::BrokenPipe => CommandErrorKind::BrokenPipe,
242 _ => CommandErrorKind::User,
243 };
244 CommandError::new(kind, err)
245 }
246}
247
248impl From<jj_lib::file_util::PathError> for CommandError {
249 fn from(err: jj_lib::file_util::PathError) -> Self {
250 user_error(err)
251 }
252}
253
254impl From<ConfigFileSaveError> for CommandError {
255 fn from(err: ConfigFileSaveError) -> Self {
256 user_error(err)
257 }
258}
259
260impl From<ConfigGetError> for CommandError {
261 fn from(err: ConfigGetError) -> Self {
262 let hint = config_get_error_hint(&err);
263 let mut cmd_err = config_error(err);
264 cmd_err.extend_hints(hint);
265 cmd_err
266 }
267}
268
269impl From<ConfigLoadError> for CommandError {
270 fn from(err: ConfigLoadError) -> Self {
271 let hint = match &err {
272 ConfigLoadError::Read(_) => None,
273 ConfigLoadError::Parse { source_path, .. } => source_path
274 .as_ref()
275 .map(|path| format!("Check the config file: {}", path.display())),
276 };
277 let mut cmd_err = config_error(err);
278 cmd_err.extend_hints(hint);
279 cmd_err
280 }
281}
282
283impl From<ConfigMigrateError> for CommandError {
284 fn from(err: ConfigMigrateError) -> Self {
285 let hint = err
286 .source_path
287 .as_ref()
288 .map(|path| format!("Check the config file: {}", path.display()));
289 let mut cmd_err = config_error(err);
290 cmd_err.extend_hints(hint);
291 cmd_err
292 }
293}
294
295impl From<RewriteRootCommit> for CommandError {
296 fn from(err: RewriteRootCommit) -> Self {
297 internal_error_with_message("Attempted to rewrite the root commit", err)
298 }
299}
300
301impl From<EditCommitError> for CommandError {
302 fn from(err: EditCommitError) -> Self {
303 internal_error_with_message("Failed to edit a commit", err)
304 }
305}
306
307impl From<CheckOutCommitError> for CommandError {
308 fn from(err: CheckOutCommitError) -> Self {
309 internal_error_with_message("Failed to check out a commit", err)
310 }
311}
312
313impl From<RenameWorkspaceError> for CommandError {
314 fn from(err: RenameWorkspaceError) -> Self {
315 user_error_with_message("Failed to rename a workspace", err)
316 }
317}
318
319impl From<BackendError> for CommandError {
320 fn from(err: BackendError) -> Self {
321 match &err {
322 BackendError::Unsupported(_) => user_error(err),
323 _ => internal_error_with_message("Unexpected error from backend", err),
324 }
325 }
326}
327
328impl From<OpHeadsStoreError> for CommandError {
329 fn from(err: OpHeadsStoreError) -> Self {
330 internal_error_with_message("Unexpected error from operation heads store", err)
331 }
332}
333
334impl From<WorkspaceInitError> for CommandError {
335 fn from(err: WorkspaceInitError) -> Self {
336 match err {
337 WorkspaceInitError::DestinationExists(_) => {
338 user_error("The target repo already exists")
339 }
340 WorkspaceInitError::NonUnicodePath => {
341 user_error("The target repo path contains non-unicode characters")
342 }
343 WorkspaceInitError::CheckOutCommit(err) => {
344 internal_error_with_message("Failed to check out the initial commit", err)
345 }
346 WorkspaceInitError::Path(err) => {
347 internal_error_with_message("Failed to access the repository", err)
348 }
349 WorkspaceInitError::OpHeadsStore(err) => {
350 user_error_with_message("Failed to record initial operation", err)
351 }
352 WorkspaceInitError::Backend(err) => {
353 user_error_with_message("Failed to access the repository", err)
354 }
355 WorkspaceInitError::WorkingCopyState(err) => {
356 internal_error_with_message("Failed to access the repository", err)
357 }
358 WorkspaceInitError::SignInit(err) => user_error(err),
359 WorkspaceInitError::TransactionCommit(err) => err.into(),
360 }
361 }
362}
363
364impl From<OpHeadResolutionError> for CommandError {
365 fn from(err: OpHeadResolutionError) -> Self {
366 match err {
367 OpHeadResolutionError::NoHeads => {
368 internal_error_with_message("Corrupt repository", err)
369 }
370 }
371 }
372}
373
374impl From<OpsetEvaluationError> for CommandError {
375 fn from(err: OpsetEvaluationError) -> Self {
376 match err {
377 OpsetEvaluationError::OpsetResolution(err) => {
378 let hint = opset_resolution_error_hint(&err);
379 let mut cmd_err = user_error(err);
380 cmd_err.extend_hints(hint);
381 cmd_err
382 }
383 OpsetEvaluationError::OpHeadResolution(err) => err.into(),
384 OpsetEvaluationError::OpHeadsStore(err) => err.into(),
385 OpsetEvaluationError::OpStore(err) => err.into(),
386 }
387 }
388}
389
390impl From<SnapshotError> for CommandError {
391 fn from(err: SnapshotError) -> Self {
392 internal_error_with_message("Failed to snapshot the working copy", err)
393 }
394}
395
396impl From<OpStoreError> for CommandError {
397 fn from(err: OpStoreError) -> Self {
398 internal_error_with_message("Failed to load an operation", err)
399 }
400}
401
402impl From<RepoLoaderError> for CommandError {
403 fn from(err: RepoLoaderError) -> Self {
404 internal_error_with_message("Failed to load the repo", err)
405 }
406}
407
408impl From<ResetError> for CommandError {
409 fn from(err: ResetError) -> Self {
410 internal_error_with_message("Failed to reset the working copy", err)
411 }
412}
413
414impl From<TransactionCommitError> for CommandError {
415 fn from(err: TransactionCommitError) -> Self {
416 internal_error(err)
417 }
418}
419
420impl From<WalkPredecessorsError> for CommandError {
421 fn from(err: WalkPredecessorsError) -> Self {
422 match err {
423 WalkPredecessorsError::Backend(err) => err.into(),
424 WalkPredecessorsError::OpStore(err) => err.into(),
425 WalkPredecessorsError::CycleDetected(_) => internal_error(err),
426 }
427 }
428}
429
430impl From<DiffEditError> for CommandError {
431 fn from(err: DiffEditError) -> Self {
432 user_error_with_message("Failed to edit diff", err)
433 }
434}
435
436impl From<DiffRenderError> for CommandError {
437 fn from(err: DiffRenderError) -> Self {
438 match err {
439 DiffRenderError::DiffGenerate(_) => user_error(err),
440 DiffRenderError::Backend(err) => err.into(),
441 DiffRenderError::AccessDenied { .. } => user_error(err),
442 DiffRenderError::InvalidRepoPath(_) => user_error(err),
443 DiffRenderError::Io(err) => err.into(),
444 }
445 }
446}
447
448impl From<ConflictResolveError> for CommandError {
449 fn from(err: ConflictResolveError) -> Self {
450 match err {
451 ConflictResolveError::Backend(err) => err.into(),
452 ConflictResolveError::Io(err) => err.into(),
453 _ => {
454 let hint = match &err {
455 ConflictResolveError::ConflictTooComplicated { .. } => {
456 Some("Edit the conflict markers manually to resolve this.".to_owned())
457 }
458 ConflictResolveError::ExecutableConflict { .. } => {
459 Some("Use `jj file chmod` to update the executable bit.".to_owned())
460 }
461 _ => None,
462 };
463 let mut cmd_err = user_error_with_message("Failed to resolve conflicts", err);
464 cmd_err.extend_hints(hint);
465 cmd_err
466 }
467 }
468 }
469}
470
471impl From<MergeToolPartialResolutionError> for CommandError {
472 fn from(err: MergeToolPartialResolutionError) -> Self {
473 user_error(err)
474 }
475}
476
477impl From<MergeToolConfigError> for CommandError {
478 fn from(err: MergeToolConfigError) -> Self {
479 match &err {
480 MergeToolConfigError::MergeArgsNotConfigured { tool_name } => {
481 let tool_name = tool_name.clone();
482 user_error_with_hint(
483 err,
484 format!(
485 "To use `{tool_name}` as a merge tool, the config \
486 `merge-tools.{tool_name}.merge-args` must be defined (see docs for \
487 details)"
488 ),
489 )
490 }
491 _ => user_error_with_message("Failed to load tool configuration", err),
492 }
493 }
494}
495
496impl From<TextEditError> for CommandError {
497 fn from(err: TextEditError) -> Self {
498 user_error(err)
499 }
500}
501
502impl From<TempTextEditError> for CommandError {
503 fn from(err: TempTextEditError) -> Self {
504 let hint = err.path.as_ref().map(|path| {
505 let name = err.name.as_deref().unwrap_or("file");
506 format!("Edited {name} is left in {path}", path = path.display())
507 });
508 let mut cmd_err = user_error(err);
509 cmd_err.extend_hints(hint);
510 cmd_err
511 }
512}
513
514impl From<TrailerParseError> for CommandError {
515 fn from(err: TrailerParseError) -> Self {
516 user_error(err)
517 }
518}
519
520#[cfg(feature = "git")]
521mod git {
522 use jj_lib::git::GitExportError;
523 use jj_lib::git::GitFetchError;
524 use jj_lib::git::GitImportError;
525 use jj_lib::git::GitPushError;
526 use jj_lib::git::GitRemoteManagementError;
527 use jj_lib::git::GitResetHeadError;
528 use jj_lib::git::UnexpectedGitBackendError;
529
530 use super::*;
531
532 impl From<GitImportError> for CommandError {
533 fn from(err: GitImportError) -> Self {
534 let hint = match &err {
535 GitImportError::MissingHeadTarget { .. }
536 | GitImportError::MissingRefAncestor { .. } => Some(
537 "\
538Is this Git repository a partial clone (cloned with the --filter argument)?
539jj currently does not support partial clones. To use jj with this repository, try re-cloning with \
540 the full repository contents."
541 .to_string(),
542 ),
543 GitImportError::Backend(_) => None,
544 GitImportError::Git(_) => None,
545 GitImportError::UnexpectedBackend(_) => None,
546 };
547 let mut cmd_err =
548 user_error_with_message("Failed to import refs from underlying Git repo", err);
549 cmd_err.extend_hints(hint);
550 cmd_err
551 }
552 }
553
554 impl From<GitExportError> for CommandError {
555 fn from(err: GitExportError) -> Self {
556 user_error_with_message("Failed to export refs to underlying Git repo", err)
557 }
558 }
559
560 impl From<GitFetchError> for CommandError {
561 fn from(err: GitFetchError) -> Self {
562 if let GitFetchError::InvalidBranchPattern(pattern) = &err {
563 if pattern.as_exact().is_some_and(|s| s.contains('*')) {
564 return user_error_with_hint(
565 "Branch names may not include `*`.",
566 "Prefix the pattern with `glob:` to expand `*` as a glob",
567 );
568 }
569 }
570 match err {
571 GitFetchError::NoSuchRemote(_) => user_error(err),
572 GitFetchError::RemoteName(_) => user_error_with_hint(
573 err,
574 "Run `jj git remote rename` to give a different name.",
575 ),
576 GitFetchError::InvalidBranchPattern(_) => user_error(err),
577 GitFetchError::Subprocess(_) => user_error(err),
578 }
579 }
580 }
581
582 impl From<GitPushError> for CommandError {
583 fn from(err: GitPushError) -> Self {
584 match err {
585 GitPushError::NoSuchRemote(_) => user_error(err),
586 GitPushError::RemoteName(_) => user_error_with_hint(
587 err,
588 "Run `jj git remote rename` to give a different name.",
589 ),
590 GitPushError::Subprocess(_) => user_error(err),
591 GitPushError::UnexpectedBackend(_) => user_error(err),
592 }
593 }
594 }
595
596 impl From<GitRemoteManagementError> for CommandError {
597 fn from(err: GitRemoteManagementError) -> Self {
598 user_error(err)
599 }
600 }
601
602 impl From<GitResetHeadError> for CommandError {
603 fn from(err: GitResetHeadError) -> Self {
604 user_error_with_message("Failed to reset Git HEAD state", err)
605 }
606 }
607
608 impl From<UnexpectedGitBackendError> for CommandError {
609 fn from(err: UnexpectedGitBackendError) -> Self {
610 user_error(err)
611 }
612 }
613}
614
615impl From<RevsetEvaluationError> for CommandError {
616 fn from(err: RevsetEvaluationError) -> Self {
617 user_error(err)
618 }
619}
620
621impl From<FilesetParseError> for CommandError {
622 fn from(err: FilesetParseError) -> Self {
623 let hint = fileset_parse_error_hint(&err);
624 let mut cmd_err =
625 user_error_with_message(format!("Failed to parse fileset: {}", err.kind()), err);
626 cmd_err.extend_hints(hint);
627 cmd_err
628 }
629}
630
631impl From<RecoverWorkspaceError> for CommandError {
632 fn from(err: RecoverWorkspaceError) -> Self {
633 match err {
634 RecoverWorkspaceError::Backend(err) => err.into(),
635 RecoverWorkspaceError::Reset(err) => err.into(),
636 RecoverWorkspaceError::RewriteRootCommit(err) => err.into(),
637 RecoverWorkspaceError::TransactionCommit(err) => err.into(),
638 err @ RecoverWorkspaceError::WorkspaceMissingWorkingCopy(_) => user_error(err),
639 }
640 }
641}
642
643impl From<RevsetParseError> for CommandError {
644 fn from(err: RevsetParseError) -> Self {
645 let hint = revset_parse_error_hint(&err);
646 let mut cmd_err =
647 user_error_with_message(format!("Failed to parse revset: {}", err.kind()), err);
648 cmd_err.extend_hints(hint);
649 cmd_err
650 }
651}
652
653impl From<RevsetResolutionError> for CommandError {
654 fn from(err: RevsetResolutionError) -> Self {
655 let hint = revset_resolution_error_hint(&err);
656 let mut cmd_err = user_error(err);
657 cmd_err.extend_hints(hint);
658 cmd_err
659 }
660}
661
662impl From<UserRevsetEvaluationError> for CommandError {
663 fn from(err: UserRevsetEvaluationError) -> Self {
664 match err {
665 UserRevsetEvaluationError::Resolution(err) => err.into(),
666 UserRevsetEvaluationError::Evaluation(err) => err.into(),
667 }
668 }
669}
670
671impl From<TemplateParseError> for CommandError {
672 fn from(err: TemplateParseError) -> Self {
673 let hint = template_parse_error_hint(&err);
674 let mut cmd_err =
675 user_error_with_message(format!("Failed to parse template: {}", err.kind()), err);
676 cmd_err.extend_hints(hint);
677 cmd_err
678 }
679}
680
681impl From<UiPathParseError> for CommandError {
682 fn from(err: UiPathParseError) -> Self {
683 user_error(err)
684 }
685}
686
687impl From<clap::Error> for CommandError {
688 fn from(err: clap::Error) -> Self {
689 let hint = find_source_parse_error_hint(&err);
690 let mut cmd_err = cli_error(err);
691 cmd_err.extend_hints(hint);
692 cmd_err
693 }
694}
695
696impl From<WorkingCopyStateError> for CommandError {
697 fn from(err: WorkingCopyStateError) -> Self {
698 internal_error_with_message("Failed to access working copy state", err)
699 }
700}
701
702impl From<GitIgnoreError> for CommandError {
703 fn from(err: GitIgnoreError) -> Self {
704 user_error_with_message("Failed to process .gitignore.", err)
705 }
706}
707
708impl From<ParseBulkEditMessageError> for CommandError {
709 fn from(err: ParseBulkEditMessageError) -> Self {
710 user_error(err)
711 }
712}
713
714impl From<AbsorbError> for CommandError {
715 fn from(err: AbsorbError) -> Self {
716 match err {
717 AbsorbError::Backend(err) => err.into(),
718 AbsorbError::RevsetEvaluation(err) => err.into(),
719 }
720 }
721}
722
723impl From<FixError> for CommandError {
724 fn from(err: FixError) -> Self {
725 match err {
726 FixError::Backend(err) => err.into(),
727 FixError::RevsetEvaluation(err) => err.into(),
728 FixError::IO(err) => err.into(),
729 FixError::FixContent(err) => internal_error_with_message(
730 "An error occurred while attempting to fix file content",
731 err,
732 ),
733 }
734 }
735}
736
737fn find_source_parse_error_hint(err: &dyn error::Error) -> Option<String> {
738 let source = err.source()?;
739 if let Some(source) = source.downcast_ref() {
740 bookmark_name_parse_error_hint(source)
741 } else if let Some(source) = source.downcast_ref() {
742 config_get_error_hint(source)
743 } else if let Some(source) = source.downcast_ref() {
744 file_pattern_parse_error_hint(source)
745 } else if let Some(source) = source.downcast_ref() {
746 fileset_parse_error_hint(source)
747 } else if let Some(source) = source.downcast_ref() {
748 revset_parse_error_hint(source)
749 } else if let Some(source) = source.downcast_ref() {
750 revset_resolution_error_hint(source)
751 } else if let Some(UserRevsetEvaluationError::Resolution(source)) = source.downcast_ref() {
752 revset_resolution_error_hint(source)
753 } else if let Some(source) = source.downcast_ref() {
754 string_pattern_parse_error_hint(source)
755 } else if let Some(source) = source.downcast_ref() {
756 template_parse_error_hint(source)
757 } else {
758 None
759 }
760}
761
762fn bookmark_name_parse_error_hint(err: &BookmarkNameParseError) -> Option<String> {
763 use revset::ExpressionKind;
764 match revset::parse_program(&err.input).map(|node| node.kind) {
765 Ok(ExpressionKind::RemoteSymbol(symbol)) => Some(format!(
766 "Looks like remote bookmark. Run `jj bookmark track {symbol}` to track it."
767 )),
768 _ => Some(
769 "See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for how \
770 to quote symbols."
771 .into(),
772 ),
773 }
774}
775
776fn config_get_error_hint(err: &ConfigGetError) -> Option<String> {
777 match &err {
778 ConfigGetError::NotFound { .. } => None,
779 ConfigGetError::Type { source_path, .. } => source_path
780 .as_ref()
781 .map(|path| format!("Check the config file: {}", path.display())),
782 }
783}
784
785fn file_pattern_parse_error_hint(err: &FilePatternParseError) -> Option<String> {
786 match err {
787 FilePatternParseError::InvalidKind(_) => Some(String::from(
788 "See https://jj-vcs.github.io/jj/latest/filesets/#file-patterns or `jj help -k \
789 filesets` for valid prefixes.",
790 )),
791 FilePatternParseError::UiPath(UiPathParseError::Fs(e)) => {
793 RepoPathBuf::from_relative_path(&e.input).ok().map(|path| {
794 format!(r#"Consider using root:{path:?} to specify repo-relative path"#)
795 })
796 }
797 FilePatternParseError::RelativePath(_) => None,
798 FilePatternParseError::GlobPattern(_) => None,
799 }
800}
801
802fn fileset_parse_error_hint(err: &FilesetParseError) -> Option<String> {
803 match err.kind() {
804 FilesetParseErrorKind::SyntaxError => Some(String::from(
805 "See https://jj-vcs.github.io/jj/latest/filesets/ or use `jj help -k filesets` for \
806 filesets syntax and how to match file paths.",
807 )),
808 FilesetParseErrorKind::NoSuchFunction {
809 name: _,
810 candidates,
811 } => format_similarity_hint(candidates),
812 FilesetParseErrorKind::InvalidArguments { .. } | FilesetParseErrorKind::Expression(_) => {
813 find_source_parse_error_hint(&err)
814 }
815 }
816}
817
818fn opset_resolution_error_hint(err: &OpsetResolutionError) -> Option<String> {
819 match err {
820 OpsetResolutionError::MultipleOperations {
821 expr: _,
822 candidates,
823 } => Some(format!(
824 "Try specifying one of the operations by ID: {}",
825 candidates.iter().map(short_operation_hash).join(", ")
826 )),
827 OpsetResolutionError::EmptyOperations(_)
828 | OpsetResolutionError::InvalidIdPrefix(_)
829 | OpsetResolutionError::NoSuchOperation(_)
830 | OpsetResolutionError::AmbiguousIdPrefix(_) => None,
831 }
832}
833
834fn revset_parse_error_hint(err: &RevsetParseError) -> Option<String> {
835 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
837 match bottom_err.kind() {
838 RevsetParseErrorKind::SyntaxError => Some(
839 "See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for \
840 revsets syntax and how to quote symbols."
841 .into(),
842 ),
843 RevsetParseErrorKind::NotPrefixOperator {
844 op: _,
845 similar_op,
846 description,
847 }
848 | RevsetParseErrorKind::NotPostfixOperator {
849 op: _,
850 similar_op,
851 description,
852 }
853 | RevsetParseErrorKind::NotInfixOperator {
854 op: _,
855 similar_op,
856 description,
857 } => Some(format!("Did you mean `{similar_op}` for {description}?")),
858 RevsetParseErrorKind::NoSuchFunction {
859 name: _,
860 candidates,
861 } => format_similarity_hint(candidates),
862 RevsetParseErrorKind::InvalidFunctionArguments { .. }
863 | RevsetParseErrorKind::Expression(_) => find_source_parse_error_hint(bottom_err),
864 _ => None,
865 }
866}
867
868fn revset_resolution_error_hint(err: &RevsetResolutionError) -> Option<String> {
869 match err {
870 RevsetResolutionError::NoSuchRevision {
871 name: _,
872 candidates,
873 } => format_similarity_hint(candidates),
874 RevsetResolutionError::EmptyString
875 | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. }
876 | RevsetResolutionError::AmbiguousCommitIdPrefix(_)
877 | RevsetResolutionError::AmbiguousChangeIdPrefix(_)
878 | RevsetResolutionError::Backend(_)
879 | RevsetResolutionError::Other(_) => None,
880 }
881}
882
883fn string_pattern_parse_error_hint(err: &StringPatternParseError) -> Option<String> {
884 match err {
885 StringPatternParseError::InvalidKind(_) => Some(
886 "Try prefixing with one of `exact:`, `glob:`, `regex:`, `substring:`, or one of these \
887 with `-i` suffix added (e.g. `glob-i:`) for case-insensitive matching"
888 .into(),
889 ),
890 StringPatternParseError::GlobPattern(_) | StringPatternParseError::Regex(_) => None,
891 }
892}
893
894fn template_parse_error_hint(err: &TemplateParseError) -> Option<String> {
895 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
897 match bottom_err.kind() {
898 TemplateParseErrorKind::NoSuchKeyword { candidates, .. }
899 | TemplateParseErrorKind::NoSuchFunction { candidates, .. }
900 | TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
901 format_similarity_hint(candidates)
902 }
903 TemplateParseErrorKind::InvalidArguments { .. } | TemplateParseErrorKind::Expression(_) => {
904 find_source_parse_error_hint(bottom_err)
905 }
906 _ => None,
907 }
908}
909
910const BROKEN_PIPE_EXIT_CODE: u8 = 3;
911
912pub(crate) fn handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> u8 {
913 try_handle_command_result(ui, result).unwrap_or(BROKEN_PIPE_EXIT_CODE)
914}
915
916fn try_handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> io::Result<u8> {
917 let Err(cmd_err) = &result else {
918 return Ok(0);
919 };
920 let err = &cmd_err.error;
921 let hints = &cmd_err.hints;
922 match cmd_err.kind {
923 CommandErrorKind::User => {
924 print_error(ui, "Error: ", err, hints)?;
925 Ok(1)
926 }
927 CommandErrorKind::Config => {
928 print_error(ui, "Config error: ", err, hints)?;
929 writeln!(
930 ui.stderr_formatter().labeled("hint"),
931 "For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k \
932 config`."
933 )?;
934 Ok(1)
935 }
936 CommandErrorKind::Cli => {
937 if let Some(err) = err.downcast_ref::<clap::Error>() {
938 handle_clap_error(ui, err, hints)
939 } else {
940 print_error(ui, "Error: ", err, hints)?;
941 Ok(2)
942 }
943 }
944 CommandErrorKind::BrokenPipe => {
945 Ok(BROKEN_PIPE_EXIT_CODE)
947 }
948 CommandErrorKind::Internal => {
949 print_error(ui, "Internal error: ", err, hints)?;
950 Ok(255)
951 }
952 }
953}
954
955fn print_error(
956 ui: &Ui,
957 heading: &str,
958 err: &dyn error::Error,
959 hints: &[ErrorHint],
960) -> io::Result<()> {
961 writeln!(ui.error_with_heading(heading), "{err}")?;
962 print_error_sources(ui, err.source())?;
963 print_error_hints(ui, hints)?;
964 Ok(())
965}
966
967pub fn print_error_sources(ui: &Ui, source: Option<&dyn error::Error>) -> io::Result<()> {
969 let Some(err) = source else {
970 return Ok(());
971 };
972 ui.stderr_formatter()
973 .with_label("error_source", |formatter| {
974 if err.source().is_none() {
975 write!(formatter.labeled("heading"), "Caused by: ")?;
976 writeln!(formatter, "{err}")?;
977 } else {
978 writeln!(formatter.labeled("heading"), "Caused by:")?;
979 for (i, err) in iter::successors(Some(err), |&err| err.source()).enumerate() {
980 write!(formatter.labeled("heading"), "{}: ", i + 1)?;
981 writeln!(formatter, "{err}")?;
982 }
983 }
984 Ok(())
985 })
986}
987
988fn print_error_hints(ui: &Ui, hints: &[ErrorHint]) -> io::Result<()> {
989 for hint in hints {
990 ui.stderr_formatter().with_label("hint", |formatter| {
991 write!(formatter.labeled("heading"), "Hint: ")?;
992 match hint {
993 ErrorHint::PlainText(message) => {
994 writeln!(formatter, "{message}")?;
995 }
996 ErrorHint::Formatted(recorded) => {
997 recorded.replay(formatter)?;
998 if !recorded.data().ends_with(b"\n") {
1001 writeln!(formatter)?;
1002 }
1003 }
1004 }
1005 io::Result::Ok(())
1006 })?;
1007 }
1008 Ok(())
1009}
1010
1011fn handle_clap_error(ui: &mut Ui, err: &clap::Error, hints: &[ErrorHint]) -> io::Result<u8> {
1012 let clap_str = if ui.color() {
1013 err.render().ansi().to_string()
1014 } else {
1015 err.render().to_string()
1016 };
1017
1018 match err.kind() {
1019 clap::error::ErrorKind::DisplayHelp
1020 | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(),
1021 _ => {}
1022 };
1023 match err.kind() {
1026 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
1027 write!(ui.stdout(), "{clap_str}")?;
1028 return Ok(0);
1029 }
1030 _ => {}
1031 }
1032 write!(ui.stderr(), "{clap_str}")?;
1033 print_error_sources(ui, err.source().and_then(|err| err.source()))?;
1035 print_error_hints(ui, hints)?;
1036 Ok(2)
1037}
1038
1039pub fn print_parse_diagnostics<T: error::Error>(
1041 ui: &Ui,
1042 context_message: &str,
1043 diagnostics: &Diagnostics<T>,
1044) -> io::Result<()> {
1045 for diag in diagnostics {
1046 writeln!(ui.warning_default(), "{context_message}")?;
1047 for err in iter::successors(Some(diag as &dyn error::Error), |&err| err.source()) {
1048 writeln!(ui.stderr(), "{err}")?;
1049 }
1050 }
1053 Ok(())
1054}