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