just playing with tangled
at gvimdiff 36 kB view raw
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}