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