Skip to main content

jj_cli/
command_error.rs

1// Copyright 2022-2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Invalid command line. The inner error type may be `clap::Error`.
91    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    /// Returns error with the given plain-text `hint` attached.
124    pub fn hinted(mut self, hint: impl Into<String>) -> Self {
125        self.add_hint(hint);
126        self
127    }
128
129    /// Appends plain-text `hint` to the error.
130    pub fn add_hint(&mut self, hint: impl Into<String>) {
131        self.hints.push(ErrorHint::PlainText(hint.into()));
132    }
133
134    /// Appends formatted `hint` to the error.
135    pub fn add_formatted_hint(&mut self, hint: FormatRecorder) {
136        self.hints.push(ErrorHint::Formatted(hint));
137    }
138
139    /// Constructs formatted hint and appends it to the error.
140    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    /// Appends 0 or more plain-text `hints` to the error.
150    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/// Wraps error with user-visible message.
163#[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        // TODO: propagate all hints?
774        revset_resolution_error_hints(source).into_iter().next()
775    } else if let Some(UserRevsetEvaluationError::Resolution(source)) = source.downcast_ref() {
776        // TODO: propagate all hints?
777        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 {name} --remote={remote}` to \
797             track it.",
798            name = symbol.name.as_symbol(),
799            remote = symbol.remote.as_symbol()
800        )),
801        _ => Some(REVSET_SYMBOL_HINT.to_owned()),
802    }
803}
804
805fn config_get_error_hint(err: &ConfigGetError) -> Option<String> {
806    match &err {
807        ConfigGetError::NotFound { .. } => None,
808        ConfigGetError::Type { source_path, .. } => source_path
809            .as_ref()
810            .map(|path| format!("Check the config file: {}", path.display())),
811    }
812}
813
814fn file_pattern_parse_error_hint(err: &FilePatternParseError) -> Option<String> {
815    match err {
816        FilePatternParseError::InvalidKind(_) => Some(String::from(
817            "See https://docs.jj-vcs.dev/latest/filesets/#file-patterns or `jj help -k filesets` \
818             for valid prefixes.",
819        )),
820        // Suggest root:"<path>" if input can be parsed as repo-relative path
821        FilePatternParseError::UiPath(UiPathParseError::Fs(e)) => {
822            RepoPathBuf::from_relative_path(&e.input).ok().map(|path| {
823                format!(r#"Consider using root:{path:?} to specify repo-relative path"#)
824            })
825        }
826        FilePatternParseError::RelativePath(_) => None,
827        FilePatternParseError::GlobPattern(_) => None,
828    }
829}
830
831fn fileset_parse_error_hint(err: &FilesetParseError) -> Option<String> {
832    match err.kind() {
833        FilesetParseErrorKind::SyntaxError => Some(String::from(
834            "See https://docs.jj-vcs.dev/latest/filesets/ or use `jj help -k filesets` for \
835             filesets syntax and how to match file paths.",
836        )),
837        FilesetParseErrorKind::NoSuchFunction {
838            name: _,
839            candidates,
840        } => format_similarity_hint(candidates),
841        FilesetParseErrorKind::InvalidArguments { .. } => find_source_parse_error_hint(&err),
842        FilesetParseErrorKind::RedefinedFunctionParameter => None,
843        FilesetParseErrorKind::Expression(_) => find_source_parse_error_hint(&err),
844        FilesetParseErrorKind::InAliasExpansion(_)
845        | FilesetParseErrorKind::InParameterExpansion(_)
846        | FilesetParseErrorKind::RecursiveAlias(_) => None,
847    }
848}
849
850fn opset_resolution_error_hint(err: &OpsetResolutionError) -> Option<String> {
851    match err {
852        OpsetResolutionError::MultipleOperations {
853            expr: _,
854            candidates,
855        } => Some(format!(
856            "Try specifying one of the operations by ID: {}",
857            candidates.iter().map(short_operation_hash).join(", ")
858        )),
859        OpsetResolutionError::EmptyOperations(_)
860        | OpsetResolutionError::InvalidIdPrefix(_)
861        | OpsetResolutionError::NoSuchOperation(_)
862        | OpsetResolutionError::AmbiguousIdPrefix(_) => None,
863    }
864}
865
866pub(crate) fn revset_parse_error_hint(err: &RevsetParseError) -> Option<String> {
867    // Only for the bottom error, which is usually the root cause
868    let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
869    match bottom_err.kind() {
870        RevsetParseErrorKind::SyntaxError => Some(
871            "See https://docs.jj-vcs.dev/latest/revsets/ or use `jj help -k revsets` for revsets \
872             syntax and how to quote symbols."
873                .into(),
874        ),
875        RevsetParseErrorKind::NotPrefixOperator {
876            op: _,
877            similar_op,
878            description,
879        }
880        | RevsetParseErrorKind::NotPostfixOperator {
881            op: _,
882            similar_op,
883            description,
884        }
885        | RevsetParseErrorKind::NotInfixOperator {
886            op: _,
887            similar_op,
888            description,
889        } => Some(format!("Did you mean `{similar_op}` for {description}?")),
890        RevsetParseErrorKind::NoSuchFunction {
891            name: _,
892            candidates,
893        } => format_similarity_hint(candidates),
894        RevsetParseErrorKind::InvalidFunctionArguments { .. }
895        | RevsetParseErrorKind::Expression(_) => find_source_parse_error_hint(bottom_err),
896        _ => None,
897    }
898}
899
900fn revset_resolution_error_hints(err: &RevsetResolutionError) -> Vec<String> {
901    let multiple_targets_hint = |targets: &[CommitId]| {
902        format!(
903            "Use commit ID to select single revision from: {}",
904            targets.iter().map(|id| format!("{id:.12}")).join(", ")
905        )
906    };
907    match err {
908        RevsetResolutionError::NoSuchRevision {
909            name: _,
910            candidates,
911        } => format_similarity_hint(candidates).into_iter().collect(),
912        RevsetResolutionError::DivergentChangeId {
913            symbol,
914            visible_targets,
915        } => vec![
916            format!(
917                "Use change offset to select single revision: {}",
918                visible_targets
919                    .iter()
920                    .map(|(offset, _)| format!("{symbol}/{offset}"))
921                    .join(", ")
922            ),
923            format!("Use `change_id({symbol})` to select all revisions"),
924            "To abandon unneeded revisions, run `jj abandon <commit_id>`".to_owned(),
925        ],
926        RevsetResolutionError::ConflictedRef {
927            kind: "bookmark",
928            symbol,
929            targets,
930        } => vec![
931            multiple_targets_hint(targets),
932            format!("Use `bookmarks({symbol})` to select all revisions"),
933            format!(
934                "To set which revision the bookmark points to, run `jj bookmark set {symbol} -r \
935                 <REVISION>`"
936            ),
937        ],
938        RevsetResolutionError::ConflictedRef {
939            kind: _,
940            symbol: _,
941            targets,
942        } => vec![multiple_targets_hint(targets)],
943        RevsetResolutionError::EmptyString
944        | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. }
945        | RevsetResolutionError::AmbiguousCommitIdPrefix(_)
946        | RevsetResolutionError::AmbiguousChangeIdPrefix(_)
947        | RevsetResolutionError::Backend(_)
948        | RevsetResolutionError::Other(_) => vec![],
949    }
950}
951
952fn string_pattern_parse_error_hint(err: &StringPatternParseError) -> Option<String> {
953    match err {
954        StringPatternParseError::InvalidKind(_) => Some(
955            "Try prefixing with one of `exact:`, `glob:`, `regex:`, `substring:`, or one of these \
956             with `-i` suffix added (e.g. `glob-i:`) for case-insensitive matching"
957                .into(),
958        ),
959        StringPatternParseError::GlobPattern(_) | StringPatternParseError::Regex(_) => None,
960    }
961}
962
963fn tag_name_parse_error_hint(_: &TagNameParseError) -> Option<String> {
964    Some(REVSET_SYMBOL_HINT.to_owned())
965}
966
967fn template_parse_error_hint(err: &TemplateParseError) -> Option<String> {
968    // Only for the bottom error, which is usually the root cause
969    let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
970    match bottom_err.kind() {
971        TemplateParseErrorKind::NoSuchKeyword { candidates, .. }
972        | TemplateParseErrorKind::NoSuchFunction { candidates, .. }
973        | TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
974            format_similarity_hint(candidates)
975        }
976        TemplateParseErrorKind::InvalidArguments { .. } | TemplateParseErrorKind::Expression(_) => {
977            find_source_parse_error_hint(bottom_err)
978        }
979        _ => None,
980    }
981}
982
983const BROKEN_PIPE_EXIT_CODE: u8 = 3;
984
985pub(crate) fn handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> u8 {
986    try_handle_command_result(ui, result).unwrap_or(BROKEN_PIPE_EXIT_CODE)
987}
988
989fn try_handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> io::Result<u8> {
990    let Err(cmd_err) = &result else {
991        return Ok(0);
992    };
993    let err = &cmd_err.error;
994    let hints = &cmd_err.hints;
995    match cmd_err.kind {
996        CommandErrorKind::User => {
997            print_error(ui, "Error: ", err, hints)?;
998            Ok(1)
999        }
1000        CommandErrorKind::Config => {
1001            print_error(ui, "Config error: ", err, hints)?;
1002            writeln!(
1003                ui.stderr_formatter().labeled("hint"),
1004                "For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`."
1005            )?;
1006            Ok(1)
1007        }
1008        CommandErrorKind::Cli => {
1009            if let Some(err) = err.downcast_ref::<clap::Error>() {
1010                handle_clap_error(ui, err, hints)
1011            } else {
1012                print_error(ui, "Error: ", err, hints)?;
1013                Ok(2)
1014            }
1015        }
1016        CommandErrorKind::BrokenPipe => {
1017            // A broken pipe is not an error, but a signal to exit gracefully.
1018            Ok(BROKEN_PIPE_EXIT_CODE)
1019        }
1020        CommandErrorKind::Internal => {
1021            print_error(ui, "Internal error: ", err, hints)?;
1022            Ok(255)
1023        }
1024    }
1025}
1026
1027fn print_error(
1028    ui: &Ui,
1029    heading: &str,
1030    err: &dyn error::Error,
1031    hints: &[ErrorHint],
1032) -> io::Result<()> {
1033    writeln!(ui.error_with_heading(heading), "{err}")?;
1034    print_error_sources(ui, err.source())?;
1035    print_error_hints(ui, hints)?;
1036    Ok(())
1037}
1038
1039/// Prints error sources one by one from the given `source` inclusive.
1040pub fn print_error_sources(ui: &Ui, source: Option<&dyn error::Error>) -> io::Result<()> {
1041    let Some(err) = source else {
1042        return Ok(());
1043    };
1044    let mut formatter = ui.stderr_formatter().into_labeled("error_source");
1045    if err.source().is_none() {
1046        write!(formatter.labeled("heading"), "Caused by: ")?;
1047        writeln!(formatter, "{err}")?;
1048    } else {
1049        writeln!(formatter.labeled("heading"), "Caused by:")?;
1050        for (i, err) in iter::successors(Some(err), |&err| err.source()).enumerate() {
1051            write!(formatter.labeled("heading"), "{}: ", i + 1)?;
1052            writeln!(formatter, "{err}")?;
1053        }
1054    }
1055    Ok(())
1056}
1057
1058fn print_error_hints(ui: &Ui, hints: &[ErrorHint]) -> io::Result<()> {
1059    let mut formatter = ui.stderr_formatter().into_labeled("hint");
1060    for hint in hints {
1061        write!(formatter.labeled("heading"), "Hint: ")?;
1062        match hint {
1063            ErrorHint::PlainText(message) => {
1064                writeln!(formatter, "{message}")?;
1065            }
1066            ErrorHint::Formatted(recorded) => {
1067                recorded.replay(formatter.as_mut())?;
1068                // Formatted hint is usually multi-line text, and it's
1069                // convenient if trailing "\n" doesn't have to be omitted.
1070                if !recorded.data().ends_with(b"\n") {
1071                    writeln!(formatter)?;
1072                }
1073            }
1074        }
1075    }
1076    Ok(())
1077}
1078
1079fn handle_clap_error(ui: &mut Ui, err: &clap::Error, hints: &[ErrorHint]) -> io::Result<u8> {
1080    let clap_str = if ui.color() {
1081        err.render().ansi().to_string()
1082    } else {
1083        err.render().to_string()
1084    };
1085
1086    match err.kind() {
1087        clap::error::ErrorKind::DisplayHelp
1088        | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(),
1089        _ => {}
1090    }
1091    // Definitions for exit codes and streams come from
1092    // https://github.com/clap-rs/clap/blob/master/src/error/mod.rs
1093    match err.kind() {
1094        clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
1095            write!(ui.stdout(), "{clap_str}")?;
1096            return Ok(0);
1097        }
1098        _ => {}
1099    }
1100    write!(ui.stderr(), "{clap_str}")?;
1101    // Skip the first source error, which should be printed inline.
1102    print_error_sources(ui, err.source().and_then(|err| err.source()))?;
1103    print_error_hints(ui, hints)?;
1104    Ok(2)
1105}
1106
1107/// Prints diagnostic messages emitted during parsing.
1108pub fn print_parse_diagnostics<T: error::Error>(
1109    ui: &Ui,
1110    context_message: &str,
1111    diagnostics: &Diagnostics<T>,
1112) -> io::Result<()> {
1113    for diag in diagnostics {
1114        writeln!(ui.warning_default(), "{context_message}")?;
1115        for err in iter::successors(Some(diag as &dyn error::Error), |&err| err.source()) {
1116            writeln!(ui.stderr(), "{err}")?;
1117        }
1118        // If we add support for multiple error diagnostics, we might have to do
1119        // find_source_parse_error_hint() and print it here.
1120    }
1121    Ok(())
1122}