jj_cli/
command_error.rs

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