just playing with tangled
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::process::ExitCode;
21use std::str;
22use std::sync::Arc;
23
24use itertools::Itertools as _;
25use jj_lib::absorb::AbsorbError;
26use jj_lib::backend::BackendError;
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::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::view::RenameWorkspaceError;
55use jj_lib::working_copy::RecoverWorkspaceError;
56use jj_lib::working_copy::ResetError;
57use jj_lib::working_copy::SnapshotError;
58use jj_lib::working_copy::WorkingCopyStateError;
59use jj_lib::workspace::WorkspaceInitError;
60use thiserror::Error;
61
62use crate::cli_util::short_operation_hash;
63use crate::description_util::ParseBulkEditMessageError;
64use crate::description_util::TempTextEditError;
65use crate::description_util::TextEditError;
66use crate::diff_util::DiffRenderError;
67use crate::formatter::FormatRecorder;
68use crate::formatter::Formatter;
69use crate::merge_tools::ConflictResolveError;
70use crate::merge_tools::DiffEditError;
71use crate::merge_tools::MergeToolConfigError;
72use crate::merge_tools::MergeToolPartialResolutionError;
73use crate::revset_util::BookmarkNameParseError;
74use crate::revset_util::UserRevsetEvaluationError;
75use crate::template_parser::TemplateParseError;
76use crate::template_parser::TemplateParseErrorKind;
77use crate::ui::Ui;
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum CommandErrorKind {
81 User,
82 Config,
83 /// Invalid command line. The inner error type may be `clap::Error`.
84 Cli,
85 BrokenPipe,
86 Internal,
87}
88
89#[derive(Clone, Debug)]
90pub struct CommandError {
91 pub kind: CommandErrorKind,
92 pub error: Arc<dyn error::Error + Send + Sync>,
93 pub hints: Vec<ErrorHint>,
94}
95
96impl CommandError {
97 pub fn new(
98 kind: CommandErrorKind,
99 err: impl Into<Box<dyn error::Error + Send + Sync>>,
100 ) -> Self {
101 CommandError {
102 kind,
103 error: Arc::from(err.into()),
104 hints: vec![],
105 }
106 }
107
108 pub fn with_message(
109 kind: CommandErrorKind,
110 message: impl Into<String>,
111 source: impl Into<Box<dyn error::Error + Send + Sync>>,
112 ) -> Self {
113 Self::new(kind, ErrorWithMessage::new(message, source))
114 }
115
116 /// Returns error with the given plain-text `hint` attached.
117 pub fn hinted(mut self, hint: impl Into<String>) -> Self {
118 self.add_hint(hint);
119 self
120 }
121
122 /// Appends plain-text `hint` to the error.
123 pub fn add_hint(&mut self, hint: impl Into<String>) {
124 self.hints.push(ErrorHint::PlainText(hint.into()));
125 }
126
127 /// Appends formatted `hint` to the error.
128 pub fn add_formatted_hint(&mut self, hint: FormatRecorder) {
129 self.hints.push(ErrorHint::Formatted(hint));
130 }
131
132 /// Constructs formatted hint and appends it to the error.
133 pub fn add_formatted_hint_with(
134 &mut self,
135 write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>,
136 ) {
137 let mut formatter = FormatRecorder::new();
138 write(&mut formatter).expect("write() to FormatRecorder should never fail");
139 self.add_formatted_hint(formatter);
140 }
141
142 /// Appends 0 or more plain-text `hints` to the error.
143 pub fn extend_hints(&mut self, hints: impl IntoIterator<Item = String>) {
144 self.hints
145 .extend(hints.into_iter().map(ErrorHint::PlainText));
146 }
147}
148
149#[derive(Clone, Debug)]
150pub enum ErrorHint {
151 PlainText(String),
152 Formatted(FormatRecorder),
153}
154
155/// Wraps error with user-visible message.
156#[derive(Debug, Error)]
157#[error("{message}")]
158struct ErrorWithMessage {
159 message: String,
160 source: Box<dyn error::Error + Send + Sync>,
161}
162
163impl ErrorWithMessage {
164 fn new(
165 message: impl Into<String>,
166 source: impl Into<Box<dyn error::Error + Send + Sync>>,
167 ) -> Self {
168 ErrorWithMessage {
169 message: message.into(),
170 source: source.into(),
171 }
172 }
173}
174
175pub fn user_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
176 CommandError::new(CommandErrorKind::User, err)
177}
178
179pub fn user_error_with_hint(
180 err: impl Into<Box<dyn error::Error + Send + Sync>>,
181 hint: impl Into<String>,
182) -> CommandError {
183 user_error(err).hinted(hint)
184}
185
186pub fn user_error_with_message(
187 message: impl Into<String>,
188 source: impl Into<Box<dyn error::Error + Send + Sync>>,
189) -> CommandError {
190 CommandError::with_message(CommandErrorKind::User, message, source)
191}
192
193pub fn config_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
194 CommandError::new(CommandErrorKind::Config, err)
195}
196
197pub fn config_error_with_message(
198 message: impl Into<String>,
199 source: impl Into<Box<dyn error::Error + Send + Sync>>,
200) -> CommandError {
201 CommandError::with_message(CommandErrorKind::Config, message, source)
202}
203
204pub fn cli_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
205 CommandError::new(CommandErrorKind::Cli, err)
206}
207
208pub fn cli_error_with_message(
209 message: impl Into<String>,
210 source: impl Into<Box<dyn error::Error + Send + Sync>>,
211) -> CommandError {
212 CommandError::with_message(CommandErrorKind::Cli, message, source)
213}
214
215pub fn internal_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {
216 CommandError::new(CommandErrorKind::Internal, err)
217}
218
219pub fn internal_error_with_message(
220 message: impl Into<String>,
221 source: impl Into<Box<dyn error::Error + Send + Sync>>,
222) -> CommandError {
223 CommandError::with_message(CommandErrorKind::Internal, message, source)
224}
225
226fn format_similarity_hint<S: AsRef<str>>(candidates: &[S]) -> Option<String> {
227 match candidates {
228 [] => None,
229 names => {
230 let quoted_names = names.iter().map(|s| format!("`{}`", s.as_ref())).join(", ");
231 Some(format!("Did you mean {quoted_names}?"))
232 }
233 }
234}
235
236impl From<io::Error> for CommandError {
237 fn from(err: io::Error) -> Self {
238 let kind = match err.kind() {
239 io::ErrorKind::BrokenPipe => CommandErrorKind::BrokenPipe,
240 _ => CommandErrorKind::User,
241 };
242 CommandError::new(kind, err)
243 }
244}
245
246impl From<jj_lib::file_util::PathError> for CommandError {
247 fn from(err: jj_lib::file_util::PathError) -> Self {
248 user_error(err)
249 }
250}
251
252impl From<ConfigFileSaveError> for CommandError {
253 fn from(err: ConfigFileSaveError) -> Self {
254 user_error(err)
255 }
256}
257
258impl From<ConfigGetError> for CommandError {
259 fn from(err: ConfigGetError) -> Self {
260 let hint = config_get_error_hint(&err);
261 let mut cmd_err = config_error(err);
262 cmd_err.extend_hints(hint);
263 cmd_err
264 }
265}
266
267impl From<ConfigLoadError> for CommandError {
268 fn from(err: ConfigLoadError) -> Self {
269 let hint = match &err {
270 ConfigLoadError::Read(_) => None,
271 ConfigLoadError::Parse { source_path, .. } => source_path
272 .as_ref()
273 .map(|path| format!("Check the config file: {}", path.display())),
274 };
275 let mut cmd_err = config_error(err);
276 cmd_err.extend_hints(hint);
277 cmd_err
278 }
279}
280
281impl From<ConfigMigrateError> for CommandError {
282 fn from(err: ConfigMigrateError) -> Self {
283 let hint = err
284 .source_path
285 .as_ref()
286 .map(|path| format!("Check the config file: {}", path.display()));
287 let mut cmd_err = config_error(err);
288 cmd_err.extend_hints(hint);
289 cmd_err
290 }
291}
292
293impl From<RewriteRootCommit> for CommandError {
294 fn from(err: RewriteRootCommit) -> Self {
295 internal_error_with_message("Attempted to rewrite the root commit", err)
296 }
297}
298
299impl From<EditCommitError> for CommandError {
300 fn from(err: EditCommitError) -> Self {
301 internal_error_with_message("Failed to edit a commit", err)
302 }
303}
304
305impl From<CheckOutCommitError> for CommandError {
306 fn from(err: CheckOutCommitError) -> Self {
307 internal_error_with_message("Failed to check out a commit", err)
308 }
309}
310
311impl From<RenameWorkspaceError> for CommandError {
312 fn from(err: RenameWorkspaceError) -> Self {
313 user_error_with_message("Failed to rename a workspace", err)
314 }
315}
316
317impl From<BackendError> for CommandError {
318 fn from(err: BackendError) -> Self {
319 match &err {
320 BackendError::Unsupported(_) => user_error(err),
321 _ => internal_error_with_message("Unexpected error from backend", err),
322 }
323 }
324}
325
326impl From<OpHeadsStoreError> for CommandError {
327 fn from(err: OpHeadsStoreError) -> Self {
328 internal_error_with_message("Unexpected error from operation heads store", err)
329 }
330}
331
332impl From<WorkspaceInitError> for CommandError {
333 fn from(err: WorkspaceInitError) -> Self {
334 match err {
335 WorkspaceInitError::DestinationExists(_) => {
336 user_error("The target repo already exists")
337 }
338 WorkspaceInitError::NonUnicodePath => {
339 user_error("The target repo path contains non-unicode characters")
340 }
341 WorkspaceInitError::CheckOutCommit(err) => {
342 internal_error_with_message("Failed to check out the initial commit", err)
343 }
344 WorkspaceInitError::Path(err) => {
345 internal_error_with_message("Failed to access the repository", err)
346 }
347 WorkspaceInitError::OpHeadsStore(err) => {
348 user_error_with_message("Failed to record initial operation", err)
349 }
350 WorkspaceInitError::Backend(err) => {
351 user_error_with_message("Failed to access the repository", err)
352 }
353 WorkspaceInitError::WorkingCopyState(err) => {
354 internal_error_with_message("Failed to access the repository", err)
355 }
356 WorkspaceInitError::SignInit(err) => user_error(err),
357 }
358 }
359}
360
361impl From<OpHeadResolutionError> for CommandError {
362 fn from(err: OpHeadResolutionError) -> Self {
363 match err {
364 OpHeadResolutionError::NoHeads => {
365 internal_error_with_message("Corrupt repository", err)
366 }
367 }
368 }
369}
370
371impl From<OpsetEvaluationError> for CommandError {
372 fn from(err: OpsetEvaluationError) -> Self {
373 match err {
374 OpsetEvaluationError::OpsetResolution(err) => {
375 let hint = opset_resolution_error_hint(&err);
376 let mut cmd_err = user_error(err);
377 cmd_err.extend_hints(hint);
378 cmd_err
379 }
380 OpsetEvaluationError::OpHeadResolution(err) => err.into(),
381 OpsetEvaluationError::OpHeadsStore(err) => err.into(),
382 OpsetEvaluationError::OpStore(err) => err.into(),
383 }
384 }
385}
386
387impl From<SnapshotError> for CommandError {
388 fn from(err: SnapshotError) -> Self {
389 internal_error_with_message("Failed to snapshot the working copy", err)
390 }
391}
392
393impl From<OpStoreError> for CommandError {
394 fn from(err: OpStoreError) -> Self {
395 internal_error_with_message("Failed to load an operation", err)
396 }
397}
398
399impl From<RepoLoaderError> for CommandError {
400 fn from(err: RepoLoaderError) -> Self {
401 internal_error_with_message("Failed to load the repo", err)
402 }
403}
404
405impl From<ResetError> for CommandError {
406 fn from(err: ResetError) -> Self {
407 internal_error_with_message("Failed to reset the working copy", err)
408 }
409}
410
411impl From<DiffEditError> for CommandError {
412 fn from(err: DiffEditError) -> Self {
413 user_error_with_message("Failed to edit diff", err)
414 }
415}
416
417impl From<DiffRenderError> for CommandError {
418 fn from(err: DiffRenderError) -> Self {
419 match err {
420 DiffRenderError::DiffGenerate(_) => user_error(err),
421 DiffRenderError::Backend(err) => err.into(),
422 DiffRenderError::AccessDenied { .. } => user_error(err),
423 DiffRenderError::InvalidRepoPath(_) => user_error(err),
424 DiffRenderError::Io(err) => err.into(),
425 }
426 }
427}
428
429impl From<ConflictResolveError> for CommandError {
430 fn from(err: ConflictResolveError) -> Self {
431 match err {
432 ConflictResolveError::Backend(err) => err.into(),
433 ConflictResolveError::Io(err) => err.into(),
434 _ => user_error_with_message("Failed to resolve conflicts", err),
435 }
436 }
437}
438
439impl From<MergeToolPartialResolutionError> for CommandError {
440 fn from(err: MergeToolPartialResolutionError) -> Self {
441 user_error(err)
442 }
443}
444
445impl From<MergeToolConfigError> for CommandError {
446 fn from(err: MergeToolConfigError) -> Self {
447 match &err {
448 MergeToolConfigError::MergeArgsNotConfigured { tool_name } => {
449 let tool_name = tool_name.clone();
450 user_error_with_hint(
451 err,
452 format!(
453 "To use `{tool_name}` as a merge tool, the config \
454 `merge-tools.{tool_name}.merge-args` must be defined (see docs for \
455 details)"
456 ),
457 )
458 }
459 _ => user_error_with_message("Failed to load tool configuration", err),
460 }
461 }
462}
463
464impl From<TextEditError> for CommandError {
465 fn from(err: TextEditError) -> Self {
466 user_error(err)
467 }
468}
469
470impl From<TempTextEditError> for CommandError {
471 fn from(err: TempTextEditError) -> Self {
472 let hint = err.path.as_ref().map(|path| {
473 let name = err.name.as_deref().unwrap_or("file");
474 format!("Edited {name} is left in {path}", path = path.display())
475 });
476 let mut cmd_err = user_error(err);
477 cmd_err.extend_hints(hint);
478 cmd_err
479 }
480}
481
482#[cfg(feature = "git")]
483mod git {
484 use jj_lib::git::GitExportError;
485 use jj_lib::git::GitFetchError;
486 use jj_lib::git::GitFetchPrepareError;
487 use jj_lib::git::GitImportError;
488 use jj_lib::git::GitPushError;
489 use jj_lib::git::GitRemoteManagementError;
490 use jj_lib::git::GitResetHeadError;
491 use jj_lib::git::UnexpectedGitBackendError;
492
493 use super::*;
494
495 impl From<GitImportError> for CommandError {
496 fn from(err: GitImportError) -> Self {
497 let hint = match &err {
498 GitImportError::MissingHeadTarget { .. }
499 | GitImportError::MissingRefAncestor { .. } => Some(
500 "\
501Is this Git repository a partial clone (cloned with the --filter argument)?
502jj currently does not support partial clones. To use jj with this repository, try re-cloning with \
503 the full repository contents."
504 .to_string(),
505 ),
506 GitImportError::Backend(_) => None,
507 GitImportError::Git(_) => None,
508 GitImportError::UnexpectedBackend(_) => None,
509 };
510 let mut cmd_err =
511 user_error_with_message("Failed to import refs from underlying Git repo", err);
512 cmd_err.extend_hints(hint);
513 cmd_err
514 }
515 }
516
517 impl From<GitExportError> for CommandError {
518 fn from(err: GitExportError) -> Self {
519 user_error_with_message("Failed to export refs to underlying Git repo", err)
520 }
521 }
522
523 impl From<GitFetchError> for CommandError {
524 fn from(err: GitFetchError) -> Self {
525 if let GitFetchError::InvalidBranchPattern(pattern) = &err {
526 if pattern.as_exact().is_some_and(|s| s.contains('*')) {
527 return user_error_with_hint(
528 "Branch names may not include `*`.",
529 "Prefix the pattern with `glob:` to expand `*` as a glob",
530 );
531 }
532 }
533 match err {
534 GitFetchError::NoSuchRemote(_) => user_error(err),
535 GitFetchError::RemoteName(_) => user_error_with_hint(
536 err,
537 "Run `jj git remote rename` to give a different name.",
538 ),
539 GitFetchError::InvalidBranchPattern(_) => user_error(err),
540 #[cfg(feature = "git2")]
541 GitFetchError::Git2(err) => map_git2_error(err),
542 GitFetchError::Subprocess(_) => user_error(err),
543 }
544 }
545 }
546
547 impl From<GitFetchPrepareError> for CommandError {
548 fn from(err: GitFetchPrepareError) -> Self {
549 match err {
550 #[cfg(feature = "git2")]
551 GitFetchPrepareError::Git2(err) => map_git2_error(err),
552 GitFetchPrepareError::UnexpectedBackend(_) => user_error(err),
553 }
554 }
555 }
556
557 impl From<GitPushError> for CommandError {
558 fn from(err: GitPushError) -> Self {
559 match err {
560 GitPushError::NoSuchRemote(_) => user_error(err),
561 GitPushError::RemoteName(_) => user_error_with_hint(
562 err,
563 "Run `jj git remote rename` to give a different name.",
564 ),
565 #[cfg(feature = "git2")]
566 GitPushError::Git2(err) => map_git2_error(err),
567 GitPushError::Subprocess(_) => user_error(err),
568 GitPushError::UnexpectedBackend(_) => user_error(err),
569 }
570 }
571 }
572
573 impl From<GitRemoteManagementError> for CommandError {
574 fn from(err: GitRemoteManagementError) -> Self {
575 user_error(err)
576 }
577 }
578
579 impl From<GitResetHeadError> for CommandError {
580 fn from(err: GitResetHeadError) -> Self {
581 user_error_with_message("Failed to reset Git HEAD state", err)
582 }
583 }
584
585 impl From<UnexpectedGitBackendError> for CommandError {
586 fn from(err: UnexpectedGitBackendError) -> Self {
587 user_error(err)
588 }
589 }
590
591 #[cfg(feature = "git2")]
592 fn map_git2_error(err: git2::Error) -> CommandError {
593 if err.class() == git2::ErrorClass::Ssh {
594 let hint = if err.code() == git2::ErrorCode::Certificate
595 && std::env::var_os("HOME").is_none()
596 {
597 "The HOME environment variable is not set, and might be required for Git to \
598 successfully load certificates. Try setting it to the path of a directory that \
599 contains a `.ssh` directory."
600 } else {
601 "Jujutsu uses libssh2, which doesn't respect ~/.ssh/config. Does `ssh -F \
602 /dev/null` to the host work?"
603 };
604
605 user_error_with_hint(err, hint)
606 } else {
607 user_error(err)
608 }
609 }
610}
611
612impl From<RevsetEvaluationError> for CommandError {
613 fn from(err: RevsetEvaluationError) -> Self {
614 user_error(err)
615 }
616}
617
618impl From<FilesetParseError> for CommandError {
619 fn from(err: FilesetParseError) -> Self {
620 let hint = fileset_parse_error_hint(&err);
621 let mut cmd_err =
622 user_error_with_message(format!("Failed to parse fileset: {}", err.kind()), err);
623 cmd_err.extend_hints(hint);
624 cmd_err
625 }
626}
627
628impl From<RecoverWorkspaceError> for CommandError {
629 fn from(err: RecoverWorkspaceError) -> Self {
630 match err {
631 RecoverWorkspaceError::Backend(err) => err.into(),
632 RecoverWorkspaceError::OpHeadsStore(err) => err.into(),
633 RecoverWorkspaceError::Reset(err) => err.into(),
634 RecoverWorkspaceError::RewriteRootCommit(err) => err.into(),
635 err @ RecoverWorkspaceError::WorkspaceMissingWorkingCopy(_) => user_error(err),
636 }
637 }
638}
639
640impl From<RevsetParseError> for CommandError {
641 fn from(err: RevsetParseError) -> Self {
642 let hint = revset_parse_error_hint(&err);
643 let mut cmd_err =
644 user_error_with_message(format!("Failed to parse revset: {}", err.kind()), err);
645 cmd_err.extend_hints(hint);
646 cmd_err
647 }
648}
649
650impl From<RevsetResolutionError> for CommandError {
651 fn from(err: RevsetResolutionError) -> Self {
652 let hint = revset_resolution_error_hint(&err);
653 let mut cmd_err = user_error(err);
654 cmd_err.extend_hints(hint);
655 cmd_err
656 }
657}
658
659impl From<UserRevsetEvaluationError> for CommandError {
660 fn from(err: UserRevsetEvaluationError) -> Self {
661 match err {
662 UserRevsetEvaluationError::Resolution(err) => err.into(),
663 UserRevsetEvaluationError::Evaluation(err) => err.into(),
664 }
665 }
666}
667
668impl From<TemplateParseError> for CommandError {
669 fn from(err: TemplateParseError) -> Self {
670 let hint = template_parse_error_hint(&err);
671 let mut cmd_err =
672 user_error_with_message(format!("Failed to parse template: {}", err.kind()), err);
673 cmd_err.extend_hints(hint);
674 cmd_err
675 }
676}
677
678impl From<UiPathParseError> for CommandError {
679 fn from(err: UiPathParseError) -> Self {
680 user_error(err)
681 }
682}
683
684impl From<clap::Error> for CommandError {
685 fn from(err: clap::Error) -> Self {
686 let hint = find_source_parse_error_hint(&err);
687 let mut cmd_err = cli_error(err);
688 cmd_err.extend_hints(hint);
689 cmd_err
690 }
691}
692
693impl From<WorkingCopyStateError> for CommandError {
694 fn from(err: WorkingCopyStateError) -> Self {
695 internal_error_with_message("Failed to access working copy state", err)
696 }
697}
698
699impl From<GitIgnoreError> for CommandError {
700 fn from(err: GitIgnoreError) -> Self {
701 user_error_with_message("Failed to process .gitignore.", err)
702 }
703}
704
705impl From<ParseBulkEditMessageError> for CommandError {
706 fn from(err: ParseBulkEditMessageError) -> Self {
707 user_error(err)
708 }
709}
710
711impl From<AbsorbError> for CommandError {
712 fn from(err: AbsorbError) -> Self {
713 match err {
714 AbsorbError::Backend(err) => err.into(),
715 AbsorbError::RevsetEvaluation(err) => err.into(),
716 }
717 }
718}
719
720impl From<FixError> for CommandError {
721 fn from(err: FixError) -> Self {
722 match err {
723 FixError::Backend(err) => err.into(),
724 FixError::RevsetEvaluation(err) => err.into(),
725 FixError::IO(err) => err.into(),
726 FixError::FixContent(err) => internal_error_with_message(
727 "An error occurred while attempting to fix file content",
728 err,
729 ),
730 }
731 }
732}
733
734fn find_source_parse_error_hint(err: &dyn error::Error) -> Option<String> {
735 let source = err.source()?;
736 if let Some(source) = source.downcast_ref() {
737 bookmark_name_parse_error_hint(source)
738 } else if let Some(source) = source.downcast_ref() {
739 config_get_error_hint(source)
740 } else if let Some(source) = source.downcast_ref() {
741 file_pattern_parse_error_hint(source)
742 } else if let Some(source) = source.downcast_ref() {
743 fileset_parse_error_hint(source)
744 } else if let Some(source) = source.downcast_ref() {
745 revset_parse_error_hint(source)
746 } else if let Some(source) = source.downcast_ref() {
747 revset_resolution_error_hint(source)
748 } else if let Some(UserRevsetEvaluationError::Resolution(source)) = source.downcast_ref() {
749 revset_resolution_error_hint(source)
750 } else if let Some(source) = source.downcast_ref() {
751 string_pattern_parse_error_hint(source)
752 } else if let Some(source) = source.downcast_ref() {
753 template_parse_error_hint(source)
754 } else {
755 None
756 }
757}
758
759fn bookmark_name_parse_error_hint(err: &BookmarkNameParseError) -> Option<String> {
760 use revset::ExpressionKind;
761 match revset::parse_program(&err.input).map(|node| node.kind) {
762 Ok(ExpressionKind::RemoteSymbol(symbol)) => Some(format!(
763 "Looks like remote bookmark. Run `jj bookmark track {symbol}` to track it."
764 )),
765 _ => Some(
766 "See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for how \
767 to quote symbols."
768 .into(),
769 ),
770 }
771}
772
773fn config_get_error_hint(err: &ConfigGetError) -> Option<String> {
774 match &err {
775 ConfigGetError::NotFound { .. } => None,
776 ConfigGetError::Type { source_path, .. } => source_path
777 .as_ref()
778 .map(|path| format!("Check the config file: {}", path.display())),
779 }
780}
781
782fn file_pattern_parse_error_hint(err: &FilePatternParseError) -> Option<String> {
783 match err {
784 FilePatternParseError::InvalidKind(_) => Some(String::from(
785 "See https://jj-vcs.github.io/jj/latest/filesets/#file-patterns or `jj help -k \
786 filesets` for valid prefixes.",
787 )),
788 // Suggest root:"<path>" if input can be parsed as repo-relative path
789 FilePatternParseError::UiPath(UiPathParseError::Fs(e)) => {
790 RepoPathBuf::from_relative_path(&e.input).ok().map(|path| {
791 format!(r#"Consider using root:{path:?} to specify repo-relative path"#)
792 })
793 }
794 FilePatternParseError::RelativePath(_) => None,
795 FilePatternParseError::GlobPattern(_) => None,
796 }
797}
798
799fn fileset_parse_error_hint(err: &FilesetParseError) -> Option<String> {
800 match err.kind() {
801 FilesetParseErrorKind::SyntaxError => Some(String::from(
802 "See https://jj-vcs.github.io/jj/latest/filesets/ or use `jj help -k filesets` for \
803 filesets syntax and how to match file paths.",
804 )),
805 FilesetParseErrorKind::NoSuchFunction {
806 name: _,
807 candidates,
808 } => format_similarity_hint(candidates),
809 FilesetParseErrorKind::InvalidArguments { .. } | FilesetParseErrorKind::Expression(_) => {
810 find_source_parse_error_hint(&err)
811 }
812 }
813}
814
815fn opset_resolution_error_hint(err: &OpsetResolutionError) -> Option<String> {
816 match err {
817 OpsetResolutionError::MultipleOperations {
818 expr: _,
819 candidates,
820 } => Some(format!(
821 "Try specifying one of the operations by ID: {}",
822 candidates.iter().map(short_operation_hash).join(", ")
823 )),
824 OpsetResolutionError::EmptyOperations(_)
825 | OpsetResolutionError::InvalidIdPrefix(_)
826 | OpsetResolutionError::NoSuchOperation(_)
827 | OpsetResolutionError::AmbiguousIdPrefix(_) => None,
828 }
829}
830
831fn revset_parse_error_hint(err: &RevsetParseError) -> Option<String> {
832 // Only for the bottom error, which is usually the root cause
833 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
834 match bottom_err.kind() {
835 RevsetParseErrorKind::SyntaxError => Some(
836 "See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for \
837 revsets syntax and how to quote symbols."
838 .into(),
839 ),
840 RevsetParseErrorKind::NotPrefixOperator {
841 op: _,
842 similar_op,
843 description,
844 }
845 | RevsetParseErrorKind::NotPostfixOperator {
846 op: _,
847 similar_op,
848 description,
849 }
850 | RevsetParseErrorKind::NotInfixOperator {
851 op: _,
852 similar_op,
853 description,
854 } => Some(format!("Did you mean `{similar_op}` for {description}?")),
855 RevsetParseErrorKind::NoSuchFunction {
856 name: _,
857 candidates,
858 } => format_similarity_hint(candidates),
859 RevsetParseErrorKind::InvalidFunctionArguments { .. }
860 | RevsetParseErrorKind::Expression(_) => find_source_parse_error_hint(bottom_err),
861 _ => None,
862 }
863}
864
865fn revset_resolution_error_hint(err: &RevsetResolutionError) -> Option<String> {
866 match err {
867 RevsetResolutionError::NoSuchRevision {
868 name: _,
869 candidates,
870 } => format_similarity_hint(candidates),
871 RevsetResolutionError::EmptyString
872 | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. }
873 | RevsetResolutionError::AmbiguousCommitIdPrefix(_)
874 | RevsetResolutionError::AmbiguousChangeIdPrefix(_)
875 | RevsetResolutionError::StoreError(_)
876 | RevsetResolutionError::Other(_) => None,
877 }
878}
879
880fn string_pattern_parse_error_hint(err: &StringPatternParseError) -> Option<String> {
881 match err {
882 StringPatternParseError::InvalidKind(_) => Some(
883 "Try prefixing with one of `exact:`, `glob:`, `regex:`, `substring:`, or one of these \
884 with `-i` suffix added (e.g. `glob-i:`) for case-insensitive matching"
885 .into(),
886 ),
887 StringPatternParseError::GlobPattern(_) | StringPatternParseError::Regex(_) => None,
888 }
889}
890
891fn template_parse_error_hint(err: &TemplateParseError) -> Option<String> {
892 // Only for the bottom error, which is usually the root cause
893 let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();
894 match bottom_err.kind() {
895 TemplateParseErrorKind::NoSuchKeyword { candidates, .. }
896 | TemplateParseErrorKind::NoSuchFunction { candidates, .. }
897 | TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
898 format_similarity_hint(candidates)
899 }
900 TemplateParseErrorKind::InvalidArguments { .. } | TemplateParseErrorKind::Expression(_) => {
901 find_source_parse_error_hint(bottom_err)
902 }
903 _ => None,
904 }
905}
906
907const BROKEN_PIPE_EXIT_CODE: u8 = 3;
908
909pub(crate) fn handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> ExitCode {
910 try_handle_command_result(ui, result).unwrap_or_else(|_| ExitCode::from(BROKEN_PIPE_EXIT_CODE))
911}
912
913fn try_handle_command_result(
914 ui: &mut Ui,
915 result: Result<(), CommandError>,
916) -> io::Result<ExitCode> {
917 let Err(cmd_err) = &result else {
918 return Ok(ExitCode::SUCCESS);
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(ExitCode::from(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(ExitCode::from(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(ExitCode::from(2))
942 }
943 }
944 CommandErrorKind::BrokenPipe => {
945 // A broken pipe is not an error, but a signal to exit gracefully.
946 Ok(ExitCode::from(BROKEN_PIPE_EXIT_CODE))
947 }
948 CommandErrorKind::Internal => {
949 print_error(ui, "Internal error: ", err, hints)?;
950 Ok(ExitCode::from(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<ExitCode> {
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(ExitCode::SUCCESS);
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(ExitCode::from(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}