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