1use std::error;
16use std::error::Error as _;
17use std::io;
18use std::io::Write as _;
19use std::iter;
20use std::sync::Arc;
21
22use itertools::Itertools as _;
23use jj_lib::absorb::AbsorbError;
24use jj_lib::backend::BackendError;
25use jj_lib::backend::CommitId;
26use jj_lib::bisect::BisectionError;
27use jj_lib::config::ConfigFileSaveError;
28use jj_lib::config::ConfigGetError;
29use jj_lib::config::ConfigLoadError;
30use jj_lib::config::ConfigMigrateError;
31use jj_lib::dsl_util::Diagnostics;
32use jj_lib::evolution::WalkPredecessorsError;
33use jj_lib::fileset::FilePatternParseError;
34use jj_lib::fileset::FilesetParseError;
35use jj_lib::fileset::FilesetParseErrorKind;
36use jj_lib::fix::FixError;
37use jj_lib::gitignore::GitIgnoreError;
38use jj_lib::op_heads_store::OpHeadResolutionError;
39use jj_lib::op_heads_store::OpHeadsStoreError;
40use jj_lib::op_store::OpStoreError;
41use jj_lib::op_walk::OpsetEvaluationError;
42use jj_lib::op_walk::OpsetResolutionError;
43use jj_lib::repo::CheckOutCommitError;
44use jj_lib::repo::EditCommitError;
45use jj_lib::repo::RepoLoaderError;
46use jj_lib::repo::RewriteRootCommit;
47use jj_lib::repo_path::RepoPathBuf;
48use jj_lib::repo_path::UiPathParseError;
49use jj_lib::revset;
50use jj_lib::revset::RevsetEvaluationError;
51use jj_lib::revset::RevsetParseError;
52use jj_lib::revset::RevsetParseErrorKind;
53use jj_lib::revset::RevsetResolutionError;
54use jj_lib::str_util::StringPatternParseError;
55use jj_lib::trailer::TrailerParseError;
56use jj_lib::transaction::TransactionCommitError;
57use jj_lib::view::RenameWorkspaceError;
58use jj_lib::working_copy::RecoverWorkspaceError;
59use jj_lib::working_copy::ResetError;
60use jj_lib::working_copy::SnapshotError;
61use jj_lib::working_copy::WorkingCopyStateError;
62use jj_lib::workspace::WorkspaceInitError;
63use thiserror::Error;
64
65use crate::cli_util::short_operation_hash;
66use crate::description_util::ParseBulkEditMessageError;
67use crate::description_util::TempTextEditError;
68use crate::description_util::TextEditError;
69use crate::diff_util::DiffRenderError;
70use crate::formatter::FormatRecorder;
71use crate::formatter::Formatter;
72use crate::formatter::FormatterExt as _;
73use crate::merge_tools::ConflictResolveError;
74use crate::merge_tools::DiffEditError;
75use crate::merge_tools::MergeToolConfigError;
76use crate::merge_tools::MergeToolPartialResolutionError;
77use crate::revset_util::BookmarkNameParseError;
78use crate::revset_util::UserRevsetEvaluationError;
79use crate::template_parser::TemplateParseError;
80use crate::template_parser::TemplateParseErrorKind;
81use crate::ui::Ui;
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum CommandErrorKind {
85 User,
86 Config,
87 Cli,
89 BrokenPipe,
90 Internal,
91}
92
93#[derive(Clone, Debug)]
94pub struct CommandError {
95 pub kind: CommandErrorKind,
96 pub error: Arc<dyn error::Error + Send + Sync>,
97 pub hints: Vec<ErrorHint>,
98}
99
100impl CommandError {
101 pub fn new(
102 kind: CommandErrorKind,
103 err: impl Into<Box<dyn error::Error + Send + Sync>>,
104 ) -> Self {
105 Self {
106 kind,
107 error: Arc::from(err.into()),
108 hints: vec![],
109 }
110 }
111
112 pub fn with_message(
113 kind: CommandErrorKind,
114 message: impl Into<String>,
115 source: impl Into<Box<dyn error::Error + Send + Sync>>,
116 ) -> Self {
117 Self::new(kind, ErrorWithMessage::new(message, source))
118 }
119
120 pub fn hinted(mut self, hint: impl Into<String>) -> Self {
122 self.add_hint(hint);
123 self
124 }
125
126 pub fn add_hint(&mut self, hint: impl Into<String>) {
128 self.hints.push(ErrorHint::PlainText(hint.into()));
129 }
130
131 pub fn add_formatted_hint(&mut self, hint: FormatRecorder) {
133 self.hints.push(ErrorHint::Formatted(hint));
134 }
135
136 pub fn add_formatted_hint_with(
138 &mut self,
139 write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>,
140 ) {
141 let mut formatter = FormatRecorder::new();
142 write(&mut formatter).expect("write() to FormatRecorder should never fail");
143 self.add_formatted_hint(formatter);
144 }
145
146 pub fn extend_hints(&mut self, hints: impl IntoIterator<Item = String>) {
148 self.hints
149 .extend(hints.into_iter().map(ErrorHint::PlainText));
150 }
151}
152
153#[derive(Clone, Debug)]
154pub enum ErrorHint {
155 PlainText(String),
156 Formatted(FormatRecorder),
157}
158
159#[derive(Debug, Error)]
161#[error("{message}")]
162struct ErrorWithMessage {
163 message: String,
164 source: Box<dyn error::Error + Send + Sync>,
165}
166
167impl ErrorWithMessage {
168 fn new(
169 message: impl Into<String>,
170 source: impl Into<Box<dyn error::Error + Send + Sync>>,
171 ) -> Self {
172 Self {
173 message: message.into(),
174 source: source.into(),
175 }
176 }
177}
178
179pub fn user_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
180 CommandError::new(CommandErrorKind::User, err)
181}
182
183pub fn user_error_with_hint(
184 err: impl Into<Box<dyn error::Error + Send + Sync>>,
185 hint: impl Into<String>,
186) -> CommandError {
187 user_error(err).hinted(hint)
188}
189
190pub fn user_error_with_message(
191 message: impl Into<String>,
192 source: impl Into<Box<dyn error::Error + Send + Sync>>,
193) -> CommandError {
194 CommandError::with_message(CommandErrorKind::User, message, source)
195}
196
197pub fn config_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
198 CommandError::new(CommandErrorKind::Config, err)
199}
200
201pub fn config_error_with_message(
202 message: impl Into<String>,
203 source: impl Into<Box<dyn error::Error + Send + Sync>>,
204) -> CommandError {
205 CommandError::with_message(CommandErrorKind::Config, message, source)
206}
207
208pub fn cli_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
209 CommandError::new(CommandErrorKind::Cli, err)
210}
211
212pub fn cli_error_with_message(
213 message: impl Into<String>,
214 source: impl Into<Box<dyn error::Error + Send + Sync>>,
215) -> CommandError {
216 CommandError::with_message(CommandErrorKind::Cli, message, source)
217}
218
219pub fn internal_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
220 CommandError::new(CommandErrorKind::Internal, err)
221}
222
223pub fn internal_error_with_message(
224 message: impl Into<String>,
225 source: impl Into<Box<dyn error::Error + Send + Sync>>,
226) -> CommandError {
227 CommandError::with_message(CommandErrorKind::Internal, message, source)
228}
229
230fn format_similarity_hint<S: AsRef<str>>(candidates: &[S]) -> Option<String> {
231 match candidates {
232 [] => None,
233 names => {
234 let quoted_names = names.iter().map(|s| format!("`{}`", s.as_ref())).join(", ");
235 Some(format!("Did you mean {quoted_names}?"))
236 }
237 }
238}
239
240impl From<io::Error> for CommandError {
241 fn from(err: io::Error) -> Self {
242 let kind = match err.kind() {
243 io::ErrorKind::BrokenPipe => CommandErrorKind::BrokenPipe,
244 _ => CommandErrorKind::User,
245 };
246 Self::new(kind, err)
247 }
248}
249
250impl From<jj_lib::file_util::PathError> for CommandError {
251 fn from(err: jj_lib::file_util::PathError) -> Self {
252 user_error(err)
253 }
254}
255
256impl From<ConfigFileSaveError> for CommandError {
257 fn from(err: ConfigFileSaveError) -> Self {
258 user_error(err)
259 }
260}
261
262impl From<ConfigGetError> for CommandError {
263 fn from(err: ConfigGetError) -> Self {
264 let hint = config_get_error_hint(&err);
265 let mut cmd_err = config_error(err);
266 cmd_err.extend_hints(hint);
267 cmd_err
268 }
269}
270
271impl From<ConfigLoadError> for CommandError {
272 fn from(err: ConfigLoadError) -> Self {
273 let hint = match &err {
274 ConfigLoadError::Read(_) => None,
275 ConfigLoadError::Parse { source_path, .. } => source_path
276 .as_ref()
277 .map(|path| format!("Check the config file: {}", path.display())),
278 };
279 let mut cmd_err = config_error(err);
280 cmd_err.extend_hints(hint);
281 cmd_err
282 }
283}
284
285impl From<ConfigMigrateError> for CommandError {
286 fn from(err: ConfigMigrateError) -> Self {
287 let hint = err
288 .source_path
289 .as_ref()
290 .map(|path| format!("Check the config file: {}", path.display()));
291 let mut cmd_err = config_error(err);
292 cmd_err.extend_hints(hint);
293 cmd_err
294 }
295}
296
297impl From<RewriteRootCommit> for CommandError {
298 fn from(err: RewriteRootCommit) -> Self {
299 internal_error_with_message("Attempted to rewrite the root commit", err)
300 }
301}
302
303impl From<EditCommitError> for CommandError {
304 fn from(err: EditCommitError) -> Self {
305 internal_error_with_message("Failed to edit a commit", err)
306 }
307}
308
309impl From<CheckOutCommitError> for CommandError {
310 fn from(err: CheckOutCommitError) -> Self {
311 internal_error_with_message("Failed to check out a commit", err)
312 }
313}
314
315impl From<RenameWorkspaceError> for CommandError {
316 fn from(err: RenameWorkspaceError) -> Self {
317 user_error_with_message("Failed to rename a workspace", err)
318 }
319}
320
321impl From<BackendError> for CommandError {
322 fn from(err: BackendError) -> Self {
323 match &err {
324 BackendError::Unsupported(_) => user_error(err),
325 _ => internal_error_with_message("Unexpected error from backend", err),
326 }
327 }
328}
329
330impl From<OpHeadsStoreError> for CommandError {
331 fn from(err: OpHeadsStoreError) -> Self {
332 internal_error_with_message("Unexpected error from operation heads store", err)
333 }
334}
335
336impl From<WorkspaceInitError> for CommandError {
337 fn from(err: WorkspaceInitError) -> Self {
338 match err {
339 WorkspaceInitError::DestinationExists(_) => {
340 user_error("The target repo already exists")
341 }
342 WorkspaceInitError::EncodeRepoPath(_) => user_error(err),
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::GitDefaultRefspecError;
523 use jj_lib::git::GitExportError;
524 use jj_lib::git::GitFetchError;
525 use jj_lib::git::GitImportError;
526 use jj_lib::git::GitPushError;
527 use jj_lib::git::GitRemoteManagementError;
528 use jj_lib::git::GitResetHeadError;
529 use jj_lib::git::UnexpectedGitBackendError;
530
531 use super::*;
532
533 impl From<GitImportError> for CommandError {
534 fn from(err: GitImportError) -> Self {
535 let hint = match &err {
536 GitImportError::MissingHeadTarget { .. }
537 | GitImportError::MissingRefAncestor { .. } => Some(
538 "\
539Is this Git repository a partial clone (cloned with the --filter argument)?
540jj currently does not support partial clones. To use jj with this repository, try re-cloning with \
541 the full repository contents."
542 .to_string(),
543 ),
544 GitImportError::Backend(_) => None,
545 GitImportError::Git(_) => None,
546 GitImportError::UnexpectedBackend(_) => None,
547 };
548 let mut cmd_err =
549 user_error_with_message("Failed to import refs from underlying Git repo", err);
550 cmd_err.extend_hints(hint);
551 cmd_err
552 }
553 }
554
555 impl From<GitExportError> for CommandError {
556 fn from(err: GitExportError) -> Self {
557 user_error_with_message("Failed to export refs to underlying Git repo", err)
558 }
559 }
560
561 impl From<GitFetchError> for CommandError {
562 fn from(err: GitFetchError) -> Self {
563 if let GitFetchError::InvalidBranchPattern(pattern) = &err
564 && pattern.as_exact().is_some_and(|s| s.contains('*'))
565 {
566 return user_error_with_hint(
567 "Branch names may not include `*`.",
568 "Prefix the pattern with `glob:` to expand `*` as a glob",
569 );
570 }
571 match err {
572 GitFetchError::NoSuchRemote(_) => user_error(err),
573 GitFetchError::RemoteName(_) => user_error_with_hint(
574 err,
575 "Run `jj git remote rename` to give a different name.",
576 ),
577 GitFetchError::InvalidBranchPattern(_) => user_error(err),
578 GitFetchError::Subprocess(_) => user_error(err),
579 }
580 }
581 }
582
583 impl From<GitDefaultRefspecError> for CommandError {
584 fn from(err: GitDefaultRefspecError) -> Self {
585 match err {
586 GitDefaultRefspecError::NoSuchRemote(_) => user_error(err),
587 GitDefaultRefspecError::InvalidRemoteConfiguration(_, _) => user_error(err),
588 }
589 }
590 }
591
592 impl From<GitPushError> for CommandError {
593 fn from(err: GitPushError) -> Self {
594 match err {
595 GitPushError::NoSuchRemote(_) => user_error(err),
596 GitPushError::RemoteName(_) => user_error_with_hint(
597 err,
598 "Run `jj git remote rename` to give a different name.",
599 ),
600 GitPushError::Subprocess(_) => user_error(err),
601 GitPushError::UnexpectedBackend(_) => user_error(err),
602 }
603 }
604 }
605
606 impl From<GitRemoteManagementError> for CommandError {
607 fn from(err: GitRemoteManagementError) -> Self {
608 user_error(err)
609 }
610 }
611
612 impl From<GitResetHeadError> for CommandError {
613 fn from(err: GitResetHeadError) -> Self {
614 user_error_with_message("Failed to reset Git HEAD state", err)
615 }
616 }
617
618 impl From<UnexpectedGitBackendError> for CommandError {
619 fn from(err: UnexpectedGitBackendError) -> Self {
620 user_error(err)
621 }
622 }
623}
624
625impl From<RevsetEvaluationError> for CommandError {
626 fn from(err: RevsetEvaluationError) -> Self {
627 user_error(err)
628 }
629}
630
631impl From<FilesetParseError> for CommandError {
632 fn from(err: FilesetParseError) -> Self {
633 let hint = fileset_parse_error_hint(&err);
634 let mut cmd_err =
635 user_error_with_message(format!("Failed to parse fileset: {}", err.kind()), err);
636 cmd_err.extend_hints(hint);
637 cmd_err
638 }
639}
640
641impl From<RecoverWorkspaceError> for CommandError {
642 fn from(err: RecoverWorkspaceError) -> Self {
643 match err {
644 RecoverWorkspaceError::Backend(err) => err.into(),
645 RecoverWorkspaceError::Reset(err) => err.into(),
646 RecoverWorkspaceError::RewriteRootCommit(err) => err.into(),
647 RecoverWorkspaceError::TransactionCommit(err) => err.into(),
648 err @ RecoverWorkspaceError::WorkspaceMissingWorkingCopy(_) => user_error(err),
649 }
650 }
651}
652
653impl From<RevsetParseError> for CommandError {
654 fn from(err: RevsetParseError) -> Self {
655 let hint = revset_parse_error_hint(&err);
656 let mut cmd_err =
657 user_error_with_message(format!("Failed to parse revset: {}", err.kind()), err);
658 cmd_err.extend_hints(hint);
659 cmd_err
660 }
661}
662
663impl From<RevsetResolutionError> for CommandError {
664 fn from(err: RevsetResolutionError) -> Self {
665 let hints = revset_resolution_error_hints(&err);
666 let mut cmd_err = user_error(err);
667 cmd_err.extend_hints(hints);
668 cmd_err
669 }
670}
671
672impl From<UserRevsetEvaluationError> for CommandError {
673 fn from(err: UserRevsetEvaluationError) -> Self {
674 match err {
675 UserRevsetEvaluationError::Resolution(err) => err.into(),
676 UserRevsetEvaluationError::Evaluation(err) => err.into(),
677 }
678 }
679}
680
681impl From<TemplateParseError> for CommandError {
682 fn from(err: TemplateParseError) -> Self {
683 let hint = template_parse_error_hint(&err);
684 let mut cmd_err =
685 user_error_with_message(format!("Failed to parse template: {}", err.kind()), err);
686 cmd_err.extend_hints(hint);
687 cmd_err
688 }
689}
690
691impl From<UiPathParseError> for CommandError {
692 fn from(err: UiPathParseError) -> Self {
693 user_error(err)
694 }
695}
696
697impl From<clap::Error> for CommandError {
698 fn from(err: clap::Error) -> Self {
699 let hint = find_source_parse_error_hint(&err);
700 let mut cmd_err = cli_error(err);
701 cmd_err.extend_hints(hint);
702 cmd_err
703 }
704}
705
706impl From<WorkingCopyStateError> for CommandError {
707 fn from(err: WorkingCopyStateError) -> Self {
708 internal_error_with_message("Failed to access working copy state", err)
709 }
710}
711
712impl From<GitIgnoreError> for CommandError {
713 fn from(err: GitIgnoreError) -> Self {
714 user_error_with_message("Failed to process .gitignore.", err)
715 }
716}
717
718impl From<ParseBulkEditMessageError> for CommandError {
719 fn from(err: ParseBulkEditMessageError) -> Self {
720 user_error(err)
721 }
722}
723
724impl From<AbsorbError> for CommandError {
725 fn from(err: AbsorbError) -> Self {
726 match err {
727 AbsorbError::Backend(err) => err.into(),
728 AbsorbError::RevsetEvaluation(err) => err.into(),
729 }
730 }
731}
732
733impl From<FixError> for CommandError {
734 fn from(err: FixError) -> Self {
735 match err {
736 FixError::Backend(err) => err.into(),
737 FixError::RevsetEvaluation(err) => err.into(),
738 FixError::IO(err) => err.into(),
739 FixError::FixContent(err) => internal_error_with_message(
740 "An error occurred while attempting to fix file content",
741 err,
742 ),
743 }
744 }
745}
746
747impl From<BisectionError> for CommandError {
748 fn from(err: BisectionError) -> Self {
749 match err {
750 BisectionError::RevsetEvaluationError(_) => user_error(err),
751 }
752 }
753}
754
755fn find_source_parse_error_hint(err: &dyn error::Error) -> Option<String> {
756 let source = err.source()?;
757 if let Some(source) = source.downcast_ref() {
758 bookmark_name_parse_error_hint(source)
759 } else if let Some(source) = source.downcast_ref() {
760 config_get_error_hint(source)
761 } else if let Some(source) = source.downcast_ref() {
762 file_pattern_parse_error_hint(source)
763 } else if let Some(source) = source.downcast_ref() {
764 fileset_parse_error_hint(source)
765 } else if let Some(source) = source.downcast_ref() {
766 revset_parse_error_hint(source)
767 } else if let Some(source) = source.downcast_ref() {
768 revset_resolution_error_hints(source).into_iter().next()
770 } else if let Some(UserRevsetEvaluationError::Resolution(source)) = source.downcast_ref() {
771 revset_resolution_error_hints(source).into_iter().next()
773 } else if let Some(source) = source.downcast_ref() {
774 string_pattern_parse_error_hint(source)
775 } else if let Some(source) = source.downcast_ref() {
776 template_parse_error_hint(source)
777 } else {
778 None
779 }
780}
781
782fn bookmark_name_parse_error_hint(err: &BookmarkNameParseError) -> Option<String> {
783 use revset::ExpressionKind;
784 match revset::parse_program(&err.input).map(|node| node.kind) {
785 Ok(ExpressionKind::RemoteSymbol(symbol)) => Some(format!(
786 "Looks like remote bookmark. Run `jj bookmark track {symbol}` to track it."
787 )),
788 _ => Some(
789 "See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for how \
790 to quote symbols."
791 .into(),
792 ),
793 }
794}
795
796fn config_get_error_hint(err: &ConfigGetError) -> Option<String> {
797 match &err {
798 ConfigGetError::NotFound { .. } => None,
799 ConfigGetError::Type { source_path, .. } => source_path
800 .as_ref()
801 .map(|path| format!("Check the config file: {}", path.display())),
802 }
803}
804
805fn file_pattern_parse_error_hint(err: &FilePatternParseError) -> Option<String> {
806 match err {
807 FilePatternParseError::InvalidKind(_) => Some(String::from(
808 "See https://jj-vcs.github.io/jj/latest/filesets/#file-patterns or `jj help -k \
809 filesets` for valid prefixes.",
810 )),
811 FilePatternParseError::UiPath(UiPathParseError::Fs(e)) => {
813 RepoPathBuf::from_relative_path(&e.input).ok().map(|path| {
814 format!(r#"Consider using root:{path:?} to specify repo-relative path"#)
815 })
816 }
817 FilePatternParseError::RelativePath(_) => None,
818 FilePatternParseError::GlobPattern(_) => None,
819 }
820}
821
822fn fileset_parse_error_hint(err: &FilesetParseError) -> Option<String> {
823 match err.kind() {
824 FilesetParseErrorKind::SyntaxError => Some(String::from(
825 "See https://jj-vcs.github.io/jj/latest/filesets/ or use `jj help -k filesets` for \
826 filesets syntax and how to match file paths.",
827 )),
828 FilesetParseErrorKind::NoSuchFunction {
829 name: _,
830 candidates,
831 } => format_similarity_hint(candidates),
832 FilesetParseErrorKind::InvalidArguments { .. } | FilesetParseErrorKind::Expression(_) => {
833 find_source_parse_error_hint(&err)
834 }
835 }
836}
837
838fn opset_resolution_error_hint(err: &OpsetResolutionError) -> Option<String> {
839 match err {
840 OpsetResolutionError::MultipleOperations {
841 expr: _,
842 candidates,
843 } => Some(format!(
844 "Try specifying one of the operations by ID: {}",
845 candidates.iter().map(short_operation_hash).join(", ")
846 )),
847 OpsetResolutionError::EmptyOperations(_)
848 | OpsetResolutionError::InvalidIdPrefix(_)
849 | OpsetResolutionError::NoSuchOperation(_)
850 | OpsetResolutionError::AmbiguousIdPrefix(_) => None,
851 }
852}
853
854fn revset_parse_error_hint(err: &RevsetParseError) -> Option<String> {
855 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
857 match bottom_err.kind() {
858 RevsetParseErrorKind::SyntaxError => Some(
859 "See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for \
860 revsets syntax and how to quote symbols."
861 .into(),
862 ),
863 RevsetParseErrorKind::NotPrefixOperator {
864 op: _,
865 similar_op,
866 description,
867 }
868 | RevsetParseErrorKind::NotPostfixOperator {
869 op: _,
870 similar_op,
871 description,
872 }
873 | RevsetParseErrorKind::NotInfixOperator {
874 op: _,
875 similar_op,
876 description,
877 } => Some(format!("Did you mean `{similar_op}` for {description}?")),
878 RevsetParseErrorKind::NoSuchFunction {
879 name: _,
880 candidates,
881 } => format_similarity_hint(candidates),
882 RevsetParseErrorKind::InvalidFunctionArguments { .. }
883 | RevsetParseErrorKind::Expression(_) => find_source_parse_error_hint(bottom_err),
884 _ => None,
885 }
886}
887
888fn revset_resolution_error_hints(err: &RevsetResolutionError) -> Vec<String> {
889 let multiple_targets_hint = |targets: &[CommitId]| {
890 format!(
891 "Use commit ID to select single revision from: {}",
892 targets.iter().map(|id| format!("{id:.12}")).join(", ")
893 )
894 };
895 match err {
896 RevsetResolutionError::NoSuchRevision {
897 name: _,
898 candidates,
899 } => format_similarity_hint(candidates).into_iter().collect(),
900 RevsetResolutionError::DivergentChangeId { symbol, targets } => vec![
901 multiple_targets_hint(targets),
902 format!("Use `change_id({symbol})` to select all revisions"),
903 "To abandon unneeded revisions, run `jj abandon <commit_id>`".to_owned(),
904 ],
905 RevsetResolutionError::ConflictedRef {
906 kind: "bookmark",
907 symbol,
908 targets,
909 } => vec![
910 multiple_targets_hint(targets),
911 format!("Use `bookmarks(exact:{symbol})` to select all revisions"),
912 format!(
913 "To set which revision the bookmark points to, run `jj bookmark set {symbol} -r \
914 <REVISION>`"
915 ),
916 ],
917 RevsetResolutionError::ConflictedRef {
918 kind: _,
919 symbol: _,
920 targets,
921 } => vec![multiple_targets_hint(targets)],
922 RevsetResolutionError::EmptyString
923 | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. }
924 | RevsetResolutionError::AmbiguousCommitIdPrefix(_)
925 | RevsetResolutionError::AmbiguousChangeIdPrefix(_)
926 | RevsetResolutionError::Backend(_)
927 | RevsetResolutionError::Other(_) => vec![],
928 }
929}
930
931fn string_pattern_parse_error_hint(err: &StringPatternParseError) -> Option<String> {
932 match err {
933 StringPatternParseError::InvalidKind(_) => Some(
934 "Try prefixing with one of `exact:`, `glob:`, `regex:`, `substring:`, or one of these \
935 with `-i` suffix added (e.g. `glob-i:`) for case-insensitive matching"
936 .into(),
937 ),
938 StringPatternParseError::GlobPattern(_) | StringPatternParseError::Regex(_) => None,
939 }
940}
941
942fn template_parse_error_hint(err: &TemplateParseError) -> Option<String> {
943 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
945 match bottom_err.kind() {
946 TemplateParseErrorKind::NoSuchKeyword { candidates, .. }
947 | TemplateParseErrorKind::NoSuchFunction { candidates, .. }
948 | TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
949 format_similarity_hint(candidates)
950 }
951 TemplateParseErrorKind::InvalidArguments { .. } | TemplateParseErrorKind::Expression(_) => {
952 find_source_parse_error_hint(bottom_err)
953 }
954 _ => None,
955 }
956}
957
958const BROKEN_PIPE_EXIT_CODE: u8 = 3;
959
960pub(crate) fn handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> u8 {
961 try_handle_command_result(ui, result).unwrap_or(BROKEN_PIPE_EXIT_CODE)
962}
963
964fn try_handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> io::Result<u8> {
965 let Err(cmd_err) = &result else {
966 return Ok(0);
967 };
968 let err = &cmd_err.error;
969 let hints = &cmd_err.hints;
970 match cmd_err.kind {
971 CommandErrorKind::User => {
972 print_error(ui, "Error: ", err, hints)?;
973 Ok(1)
974 }
975 CommandErrorKind::Config => {
976 print_error(ui, "Config error: ", err, hints)?;
977 writeln!(
978 ui.stderr_formatter().labeled("hint"),
979 "For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k \
980 config`."
981 )?;
982 Ok(1)
983 }
984 CommandErrorKind::Cli => {
985 if let Some(err) = err.downcast_ref::<clap::Error>() {
986 handle_clap_error(ui, err, hints)
987 } else {
988 print_error(ui, "Error: ", err, hints)?;
989 Ok(2)
990 }
991 }
992 CommandErrorKind::BrokenPipe => {
993 Ok(BROKEN_PIPE_EXIT_CODE)
995 }
996 CommandErrorKind::Internal => {
997 print_error(ui, "Internal error: ", err, hints)?;
998 Ok(255)
999 }
1000 }
1001}
1002
1003fn print_error(
1004 ui: &Ui,
1005 heading: &str,
1006 err: &dyn error::Error,
1007 hints: &[ErrorHint],
1008) -> io::Result<()> {
1009 writeln!(ui.error_with_heading(heading), "{err}")?;
1010 print_error_sources(ui, err.source())?;
1011 print_error_hints(ui, hints)?;
1012 Ok(())
1013}
1014
1015pub fn print_error_sources(ui: &Ui, source: Option<&dyn error::Error>) -> io::Result<()> {
1017 let Some(err) = source else {
1018 return Ok(());
1019 };
1020 let mut formatter = ui.stderr_formatter().into_labeled("error_source");
1021 if err.source().is_none() {
1022 write!(formatter.labeled("heading"), "Caused by: ")?;
1023 writeln!(formatter, "{err}")?;
1024 } else {
1025 writeln!(formatter.labeled("heading"), "Caused by:")?;
1026 for (i, err) in iter::successors(Some(err), |&err| err.source()).enumerate() {
1027 write!(formatter.labeled("heading"), "{}: ", i + 1)?;
1028 writeln!(formatter, "{err}")?;
1029 }
1030 }
1031 Ok(())
1032}
1033
1034fn print_error_hints(ui: &Ui, hints: &[ErrorHint]) -> io::Result<()> {
1035 let mut formatter = ui.stderr_formatter().into_labeled("hint");
1036 for hint in hints {
1037 write!(formatter.labeled("heading"), "Hint: ")?;
1038 match hint {
1039 ErrorHint::PlainText(message) => {
1040 writeln!(formatter, "{message}")?;
1041 }
1042 ErrorHint::Formatted(recorded) => {
1043 recorded.replay(formatter.as_mut())?;
1044 if !recorded.data().ends_with(b"\n") {
1047 writeln!(formatter)?;
1048 }
1049 }
1050 }
1051 }
1052 Ok(())
1053}
1054
1055fn handle_clap_error(ui: &mut Ui, err: &clap::Error, hints: &[ErrorHint]) -> io::Result<u8> {
1056 let clap_str = if ui.color() {
1057 err.render().ansi().to_string()
1058 } else {
1059 err.render().to_string()
1060 };
1061
1062 match err.kind() {
1063 clap::error::ErrorKind::DisplayHelp
1064 | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(),
1065 _ => {}
1066 };
1067 match err.kind() {
1070 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
1071 write!(ui.stdout(), "{clap_str}")?;
1072 return Ok(0);
1073 }
1074 _ => {}
1075 }
1076 write!(ui.stderr(), "{clap_str}")?;
1077 print_error_sources(ui, err.source().and_then(|err| err.source()))?;
1079 print_error_hints(ui, hints)?;
1080 Ok(2)
1081}
1082
1083pub fn print_parse_diagnostics<T: error::Error>(
1085 ui: &Ui,
1086 context_message: &str,
1087 diagnostics: &Diagnostics<T>,
1088) -> io::Result<()> {
1089 for diag in diagnostics {
1090 writeln!(ui.warning_default(), "{context_message}")?;
1091 for err in iter::successors(Some(diag as &dyn error::Error), |&err| err.source()) {
1092 writeln!(ui.stderr(), "{err}")?;
1093 }
1094 }
1097 Ok(())
1098}