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