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