diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 8c4c9328f..d84c302d1 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -71,6 +71,7 @@ use jj_lib::workspace::{ }; use jj_lib::{dag_walk, file_util, git, op_walk, revset}; use once_cell::unsync::OnceCell; +use thiserror::Error; use toml_edit; use tracing::instrument; use tracing_chrome::ChromeLayerBuilder; @@ -99,7 +100,27 @@ pub enum CommandError { /// Invalid command line detected by clap ClapCliError(Arc), BrokenPipe, - InternalError(String), + InternalError(Arc), +} + +/// Wraps error with user-visible message. +#[derive(Debug, Error)] +#[error("{message}: {source}")] +struct ErrorWithMessage { + message: String, + source: Box, +} + +impl ErrorWithMessage { + fn new( + message: impl Into, + source: impl Into>, + ) -> Self { + ErrorWithMessage { + message: message.into(), + source: source.into(), + } + } } pub fn user_error(message: impl Into) -> CommandError { @@ -115,6 +136,17 @@ pub fn user_error_with_hint(message: impl Into, hint: impl Into) } } +pub fn internal_error(err: impl Into>) -> CommandError { + CommandError::InternalError(Arc::from(err.into())) +} + +pub fn internal_error_with_message( + message: impl Into, + source: impl Into>, +) -> CommandError { + CommandError::InternalError(Arc::new(ErrorWithMessage::new(message, source))) +} + fn format_similarity_hint>(candidates: &[S]) -> Option { match candidates { [] => None, @@ -153,19 +185,19 @@ impl From for CommandError { impl From for CommandError { fn from(err: RewriteRootCommit) -> Self { - CommandError::InternalError(format!("Attempted to rewrite the root commit: {err}")) + internal_error_with_message("Attempted to rewrite the root commit", err) } } impl From for CommandError { fn from(err: EditCommitError) -> Self { - CommandError::InternalError(format!("Failed to edit a commit: {err}")) + internal_error_with_message("Failed to edit a commit", err) } } impl From for CommandError { fn from(err: CheckOutCommitError) -> Self { - CommandError::InternalError(format!("Failed to check out a commit: {err}")) + internal_error_with_message("Failed to check out a commit", err) } } @@ -184,11 +216,11 @@ impl From for CommandError { WorkspaceInitError::NonUnicodePath => { user_error("The target repo path contains non-unicode characters") } - WorkspaceInitError::CheckOutCommit(err) => CommandError::InternalError(format!( - "Failed to check out the initial commit: {err}" - )), + WorkspaceInitError::CheckOutCommit(err) => { + internal_error_with_message("Failed to check out the initial commit", err) + } WorkspaceInitError::Path(err) => { - CommandError::InternalError(format!("Failed to access the repository: {err}")) + internal_error_with_message("Failed to access the repository", err) } WorkspaceInitError::PathNotFound(path) => { user_error(format!("{} doesn't exist", path.display())) @@ -197,12 +229,12 @@ impl From for CommandError { user_error(format!("Failed to access the repository: {err}")) } WorkspaceInitError::WorkingCopyState(err) => { - CommandError::InternalError(format!("Failed to access the repository: {err}")) + internal_error_with_message("Failed to access the repository", err) } WorkspaceInitError::SignInit(err @ SignInitError::UnknownBackend(_)) => { user_error(format!("{err}")) } - WorkspaceInitError::SignInit(err) => CommandError::InternalError(format!("{err}")), + WorkspaceInitError::SignInit(err) => internal_error(err), } } } @@ -210,9 +242,9 @@ impl From for CommandError { impl From for CommandError { fn from(err: OpHeadResolutionError) -> Self { match err { - OpHeadResolutionError::NoHeads => CommandError::InternalError( - "Corrupt repository: there are no operations".to_string(), - ), + OpHeadResolutionError::NoHeads => { + internal_error_with_message("Corrupt repository", err) + } } } } @@ -235,34 +267,32 @@ impl From for CommandError { r#"Increase the value of the `snapshot.max-new-file-size` config option if you want this file to be snapshotted. Otherwise add it to your `.gitignore` file."#, ), - err => { - CommandError::InternalError(format!("Failed to snapshot the working copy: {err}")) - } + err => internal_error_with_message("Failed to snapshot the working copy", err), } } } impl From for CommandError { fn from(err: TreeMergeError) -> Self { - CommandError::InternalError(format!("Merge failed: {err}")) + internal_error_with_message("Merge failed", err) } } impl From for CommandError { fn from(err: OpStoreError) -> Self { - CommandError::InternalError(format!("Failed to load an operation: {err}")) + internal_error_with_message("Failed to load an operation", err) } } impl From for CommandError { fn from(err: RepoLoaderError) -> Self { - CommandError::InternalError(format!("Failed to load the repo: {err}")) + internal_error_with_message("Failed to load the repo", err) } } impl From for CommandError { - fn from(_: ResetError) -> Self { - CommandError::InternalError("Failed to reset the working copy".to_string()) + fn from(err: ResetError) -> Self { + internal_error_with_message("Failed to reset the working copy", err) } } @@ -318,9 +348,7 @@ repository contents." impl From for CommandError { fn from(err: GitExportError) -> Self { - CommandError::InternalError(format!( - "Failed to export refs to underlying Git repo: {err}" - )) + internal_error_with_message("Failed to export refs to underlying Git repo", err) } } @@ -406,13 +434,13 @@ impl From for CommandError { impl From for CommandError { fn from(err: GitConfigParseError) -> Self { - CommandError::InternalError(format!("Failed to parse Git config: {err} ")) + internal_error_with_message("Failed to parse Git config", err) } } impl From for CommandError { fn from(err: WorkingCopyStateError) -> Self { - CommandError::InternalError(format!("Failed to access working copy state: {err}")) + internal_error_with_message("Failed to access working copy state", err) } } @@ -497,9 +525,7 @@ impl TracingSubscription { .with_default_directive(tracing::metadata::LevelFilter::DEBUG.into()) .from_env_lossy() }) - .map_err(|err| { - CommandError::InternalError(format!("failed to enable verbose logging: {err}")) - })?; + .map_err(|err| internal_error_with_message("failed to enable verbose logging", err))?; tracing::info!("verbose logging enabled"); Ok(()) } @@ -702,7 +728,7 @@ impl CommandHelper { let op_id = workspace.working_copy().operation_id(); let op_data = op_store .read_operation(op_id) - .map_err(|e| CommandError::InternalError(format!("Failed to read operation: {e}")))?; + .map_err(|e| internal_error_with_message("Failed to read operation", e))?; let operation = Operation::new(op_store.clone(), op_id.clone(), op_data); let repo = workspace.repo_loader().load_at(&operation)?; self.for_loaded_repo(ui, workspace, repo) @@ -1405,7 +1431,7 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin )); } WorkingCopyFreshness::SiblingOperation => { - return Err(CommandError::InternalError(format!( + return Err(internal_error(format!( "The repo was loaded at operation {}, which seems to be a sibling of the \ working copy's operation {}", short_operation_hash(repo.op_id()), @@ -1837,19 +1863,18 @@ jj init --git-repo=.", WorkspaceLoadError::Path(e) => user_error(format!("{}: {}", e, e.error)), WorkspaceLoadError::NonUnicodePath => user_error(err.to_string()), WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => { - CommandError::InternalError(format!( - "This version of the jj binary doesn't support this type of repo: {err}" - )) + internal_error_with_message( + "This version of the jj binary doesn't support this type of repo", + err, + ) } WorkspaceLoadError::StoreLoadError( err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)), - ) => CommandError::InternalError(format!( - "The repository appears broken or inaccessible: {err}" - )), + ) => internal_error_with_message("The repository appears broken or inaccessible", err), WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing( err @ SignInitError::UnknownBackend(_), )) => user_error(format!("{err}")), - WorkspaceLoadError::StoreLoadError(err) => CommandError::InternalError(format!("{err}")), + WorkspaceLoadError::StoreLoadError(err) => internal_error(err), } } @@ -2104,11 +2129,10 @@ pub fn update_working_copy( let stats = workspace .check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit) .map_err(|err| { - CommandError::InternalError(format!( - "Failed to check out commit {}: {}", - new_commit.id().hex(), - err - )) + internal_error_with_message( + format!("Failed to check out commit {}", new_commit.id().hex()), + err, + ) })?; Some(stats) } else { @@ -2821,8 +2845,8 @@ pub fn handle_command_result( // A broken pipe is not an error, but a signal to exit gracefully. Ok(ExitCode::from(BROKEN_PIPE_EXIT_CODE)) } - Err(CommandError::InternalError(message)) => { - writeln!(ui.error(), "Internal error: {message}")?; + Err(CommandError::InternalError(err)) => { + writeln!(ui.error(), "Internal error: {err}")?; Ok(ExitCode::from(255)) } } diff --git a/cli/src/commands/debug.rs b/cli/src/commands/debug.rs index 4dbc57892..d2b670ff2 100644 --- a/cli/src/commands/debug.rs +++ b/cli/src/commands/debug.rs @@ -23,7 +23,7 @@ use jj_lib::object_id::ObjectId; use jj_lib::working_copy::WorkingCopy; use jj_lib::{op_walk, revset}; -use crate::cli_util::{user_error, CommandError, CommandHelper, RevisionArg}; +use crate::cli_util::{internal_error, user_error, CommandError, CommandHelper, RevisionArg}; use crate::template_parser; use crate::ui::Ui; @@ -205,7 +205,7 @@ fn cmd_debug_index( let index_store = repo_loader.index_store(); let index = index_store .get_index_at_op(&op, repo_loader.store()) - .map_err(|err| CommandError::InternalError(err.to_string()))?; + .map_err(internal_error)?; if let Some(default_index) = index.as_any().downcast_ref::() { let stats = default_index.as_composite().stats(); writeln!(ui.stdout(), "Number of commits: {}", stats.num_commits)?; @@ -244,12 +244,10 @@ fn cmd_debug_reindex( let op = op_walk::resolve_op_for_load(repo_loader, &command.global_args().at_operation)?; let index_store = repo_loader.index_store(); if let Some(default_index_store) = index_store.as_any().downcast_ref::() { - default_index_store - .reinit() - .map_err(|err| CommandError::InternalError(err.to_string()))?; + default_index_store.reinit().map_err(internal_error)?; let default_index = default_index_store .build_index_at_operation(&op, repo_loader.store()) - .map_err(|err| CommandError::InternalError(err.to_string()))?; + .map_err(internal_error)?; writeln!( ui.stderr(), "Finished indexing {:?} commits.", diff --git a/cli/src/commands/sparse.rs b/cli/src/commands/sparse.rs index be90c72ad..bf313789f 100644 --- a/cli/src/commands/sparse.rs +++ b/cli/src/commands/sparse.rs @@ -24,7 +24,9 @@ use jj_lib::repo_path::RepoPathBuf; use jj_lib::settings::UserSettings; use tracing::instrument; -use crate::cli_util::{edit_temp_file, print_checkout_stats, CommandError, CommandHelper}; +use crate::cli_util::{ + edit_temp_file, internal_error_with_message, print_checkout_stats, CommandError, CommandHelper, +}; use crate::ui::Ui; /// Manage which paths from the working-copy commit are present in the working @@ -147,9 +149,7 @@ fn cmd_sparse_set( let stats = locked_ws .locked_wc() .set_sparse_patterns(new_patterns) - .map_err(|err| { - CommandError::InternalError(format!("Failed to update working copy paths: {err}")) - })?; + .map_err(|err| internal_error_with_message("Failed to update working copy paths", err))?; let operation_id = locked_ws.locked_wc().old_operation_id().clone(); locked_ws.finish(operation_id)?; print_checkout_stats(ui, stats, &wc_commit)?; diff --git a/cli/src/commands/workspace.rs b/cli/src/commands/workspace.rs index 334de480d..bdb61e640 100644 --- a/cli/src/commands/workspace.rs +++ b/cli/src/commands/workspace.rs @@ -27,8 +27,8 @@ use jj_lib::workspace::Workspace; use tracing::instrument; use crate::cli_util::{ - self, check_stale_working_copy, print_checkout_stats, user_error, CommandError, CommandHelper, - RevisionArg, WorkspaceCommandHelper, + self, check_stale_working_copy, internal_error_with_message, print_checkout_stats, user_error, + CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper, }; use crate::ui::Ui; @@ -315,11 +315,13 @@ fn cmd_workspace_update_stale( .locked_wc() .check_out(&desired_wc_commit) .map_err(|err| { - CommandError::InternalError(format!( - "Failed to check out commit {}: {}", - desired_wc_commit.id().hex(), - err - )) + internal_error_with_message( + format!( + "Failed to check out commit {}", + desired_wc_commit.id().hex() + ), + err, + ) })?; locked_ws.finish(repo.op_id().clone())?; write!(ui.stderr(), "Working copy now at: ")?;