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