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