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