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