447 lines · 14.8 KB · Rust
RawDownload
// Copyright 2022-2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::error;use std::error::Error as _;use std::io;use std::io::Write as _;use std::iter; use jj_lib::dsl_util::Diagnostics;use jj_lib::formatter::FormatRecorder;use jj_lib::formatter::Formatter;use jj_lib::formatter::FormatterExt as _;use jj_lib::revset;use jj_lib::revset_util::UserRevsetEvaluationError;use jj_lib::user_error::ErrorHint;use jj_lib::user_error::UserError;pub use jj_lib::user_error::UserErrorKind as CommandErrorKind;use jj_lib::user_error::format_similarity_hint; use crate::description_util::ParseBulkEditMessageError;use crate::description_util::TempTextEditError;use crate::description_util::TextEditError;use crate::diff_util::DiffRenderError;use crate::merge_tools::ConflictResolveError;use crate::merge_tools::DiffEditError;use crate::merge_tools::MergeToolConfigError;use crate::merge_tools::MergeToolPartialResolutionError;use crate::revset_util::BookmarkNameParseError;use crate::revset_util::TagNameParseError;use crate::template_parser::TemplateParseError;use crate::template_parser::TemplateParseErrorKind;use crate::ui::Ui; #[derive(Clone, Debug)]pub struct CommandError(pub UserError); impl CommandError {    /// Returns error with the given plain-text `hint` attached.    pub fn hinted(mut self, hint: impl Into<String>) -> Self {        self.0.add_hint(hint);        self    }     /// Appends plain-text `hint` to the error.    pub fn add_hint(&mut self, hint: impl Into<String>) {        self.0.add_hint(hint);    }     /// Appends formatted `hint` to the error.    pub fn add_formatted_hint(&mut self, hint: FormatRecorder) {        self.0.add_formatted_hint(hint);    }     /// Constructs formatted hint and appends it to the error.    pub fn add_formatted_hint_with(        &mut self,        write: impl FnOnce(&mut dyn Formatter) -> io::Result<()>,    ) {        self.0.add_formatted_hint_with(write);    }     /// Appends 0 or more plain-text `hints` to the error.    pub fn extend_hints(&mut self, hints: impl IntoIterator<Item = String>) {        self.0.extend_hints(hints);    }} impl<T> From<T> for CommandErrorwhere    UserError: From<T>,{    fn from(value: T) -> Self {        Self(value.into())    }} pub fn user_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {    CommandError(UserError::new(CommandErrorKind::User, err))} pub fn user_error_with_message(    message: impl Into<String>,    source: impl Into<Box<dyn error::Error + Send + Sync>>,) -> CommandError {    CommandError(UserError::with_message(        CommandErrorKind::User,        message,        source,    ))} pub fn config_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {    CommandError(UserError::new(CommandErrorKind::Config, err))} pub fn config_error_with_message(    message: impl Into<String>,    source: impl Into<Box<dyn error::Error + Send + Sync>>,) -> CommandError {    CommandError(UserError::with_message(        CommandErrorKind::Config,        message,        source,    ))} pub fn internal_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {    CommandError(UserError::new(CommandErrorKind::Internal, err))} pub fn internal_error_with_message(    message: impl Into<String>,    source: impl Into<Box<dyn error::Error + Send + Sync>>,) -> CommandError {    CommandError(UserError::with_message(        CommandErrorKind::Internal,        message,        source,    ))} pub fn cli_error(err: impl Into<Box<dyn error::Error + Send + Sync>>) -> CommandError {    CommandError(UserError::new(CommandErrorKind::Cli, err))} pub fn cli_error_with_message(    message: impl Into<String>,    source: impl Into<Box<dyn error::Error + Send + Sync>>,) -> CommandError {    CommandError(UserError::with_message(        CommandErrorKind::Cli,        message,        source,    ))} pub fn clap_error(err: clap::Error) -> CommandError {    let hint = find_source_parse_error_hint(&err);    let mut cmd_err = cli_error(err);    cmd_err.extend_hints(hint);    cmd_err} impl From<DiffEditError> for CommandError {    fn from(err: DiffEditError) -> Self {        user_error_with_message("Failed to edit diff", err)    }} impl From<DiffRenderError> for CommandError {    fn from(err: DiffRenderError) -> Self {        match err {            DiffRenderError::DiffGenerate(_) => user_error(err),            DiffRenderError::Backend(err) => err.into(),            DiffRenderError::AccessDenied { .. } => user_error(err),            DiffRenderError::InvalidRepoPath(_) => user_error(err),            DiffRenderError::Io(err) => err.into(),        }    }} impl From<ConflictResolveError> for CommandError {    fn from(err: ConflictResolveError) -> Self {        match err {            ConflictResolveError::Backend(err) => err.into(),            ConflictResolveError::Io(err) => err.into(),            _ => {                let hint = match &err {                    ConflictResolveError::ConflictTooComplicated { .. } => {                        Some("Edit the conflict markers manually to resolve this.".to_owned())                    }                    ConflictResolveError::ExecutableConflict { .. } => {                        Some("Use `jj file chmod` to update the executable bit.".to_owned())                    }                    _ => None,                };                let mut cmd_err = user_error_with_message("Failed to resolve conflicts", err);                cmd_err.extend_hints(hint);                cmd_err            }        }    }} impl From<MergeToolPartialResolutionError> for CommandError {    fn from(err: MergeToolPartialResolutionError) -> Self {        user_error(err)    }} impl From<MergeToolConfigError> for CommandError {    fn from(err: MergeToolConfigError) -> Self {        match &err {            MergeToolConfigError::MergeArgsNotConfigured { tool_name } => {                let tool_name = tool_name.clone();                user_error(err).hinted(format!(                    "To use `{tool_name}` as a merge tool, the config \                     `merge-tools.{tool_name}.merge-args` must be defined (see docs for details)"                ))            }            _ => user_error_with_message("Failed to load tool configuration", err),        }    }} impl From<TextEditError> for CommandError {    fn from(err: TextEditError) -> Self {        user_error(err)    }} impl From<TempTextEditError> for CommandError {    fn from(err: TempTextEditError) -> Self {        let hint = err.path.as_ref().map(|path| {            let name = err.name.as_deref().unwrap_or("file");            format!("Edited {name} is left in {path}", path = path.display())        });        let mut cmd_err = user_error(err);        cmd_err.extend_hints(hint);        cmd_err    }} impl From<TemplateParseError> for CommandError {    fn from(err: TemplateParseError) -> Self {        let hint = template_parse_error_hint(&err);        let mut cmd_err =            user_error_with_message(format!("Failed to parse template: {}", err.kind()), err);        cmd_err.extend_hints(hint);        cmd_err    }} impl From<ParseBulkEditMessageError> for CommandError {    fn from(err: ParseBulkEditMessageError) -> Self {        user_error(err)    }} /// handles cli-level error, falling back to lib-level types if the source is/// more generalfn find_source_parse_error_hint(err: &dyn error::Error) -> Option<String> {    let source = err.source()?;    if let Some(source) = source.downcast_ref() {        bookmark_name_parse_error_hint(source)    } else if let Some(UserRevsetEvaluationError::Resolution(source)) = source.downcast_ref() {        // TODO: propagate all hints?        jj_lib::user_error::revset_resolution_error_hints(source)            .into_iter()            .next()    } else if let Some(source) = source.downcast_ref() {        tag_name_parse_error_hint(source)    } else if let Some(source) = source.downcast_ref() {        template_parse_error_hint(source)    } else {        jj_lib::user_error::find_source_parse_error_hint(err)    }} const REVSET_SYMBOL_HINT: &str = "See https://docs.jj-vcs.dev/latest/revsets/ or use `jj help -k \                                  revsets` for how to quote symbols."; fn bookmark_name_parse_error_hint(err: &BookmarkNameParseError) -> Option<String> {    use revset::ExpressionKind;    match revset::parse_program(&err.input).map(|node| node.kind) {        Ok(ExpressionKind::RemoteSymbol(symbol)) => Some(format!(            "Looks like remote bookmark. Run `jj bookmark track {name} --remote={remote}` to \             track it.",            name = symbol.name.as_symbol(),            remote = symbol.remote.as_symbol()        )),        _ => Some(REVSET_SYMBOL_HINT.to_owned()),    }} fn tag_name_parse_error_hint(_: &TagNameParseError) -> Option<String> {    Some(REVSET_SYMBOL_HINT.to_owned())} fn template_parse_error_hint(err: &TemplateParseError) -> Option<String> {    // Only for the bottom error, which is usually the root cause    let bottom_err = iter::successors(Some(err), |e| e.origin()).last().unwrap();    match bottom_err.kind() {        TemplateParseErrorKind::NoSuchKeyword { candidates, .. }        | TemplateParseErrorKind::NoSuchFunction { candidates, .. }        | TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {            format_similarity_hint(candidates)        }        TemplateParseErrorKind::InvalidArguments { .. } | TemplateParseErrorKind::Expression(_) => {            find_source_parse_error_hint(bottom_err)        }        _ => None,    }} const BROKEN_PIPE_EXIT_CODE: u8 = 3; pub(crate) fn handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> u8 {    try_handle_command_result(ui, result).unwrap_or(BROKEN_PIPE_EXIT_CODE)} fn try_handle_command_result(ui: &mut Ui, result: Result<(), CommandError>) -> io::Result<u8> {    let Err(cmd_err) = &result else {        return Ok(0);    };    let err = &cmd_err.0.error;    let hints = &cmd_err.0.hints;    match cmd_err.0.kind {        CommandErrorKind::User => {            print_error(ui, "Error: ", err, hints)?;            Ok(1)        }        CommandErrorKind::Config => {            print_error(ui, "Config error: ", err, hints)?;            writeln!(                ui.stderr_formatter().labeled("hint"),                "For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`."            )?;            Ok(1)        }        CommandErrorKind::Cli => {            if let Some(err) = err.downcast_ref::<clap::Error>() {                handle_clap_error(ui, err, hints)            } else {                print_error(ui, "Error: ", err, hints)?;                Ok(2)            }        }        CommandErrorKind::BrokenPipe => {            // A broken pipe is not an error, but a signal to exit gracefully.            Ok(BROKEN_PIPE_EXIT_CODE)        }        CommandErrorKind::Internal => {            print_error(ui, "Internal error: ", err, hints)?;            Ok(255)        }    }} fn print_error(    ui: &Ui,    heading: &str,    err: &dyn error::Error,    hints: &[ErrorHint],) -> io::Result<()> {    writeln!(ui.error_with_heading(heading), "{err}")?;    print_error_sources(ui, err.source())?;    print_error_hints(ui, hints)?;    Ok(())} /// Prints error sources one by one from the given `source` inclusive.pub fn print_error_sources(ui: &Ui, source: Option<&dyn error::Error>) -> io::Result<()> {    let Some(err) = source else {        return Ok(());    };    let mut formatter = ui.stderr_formatter().into_labeled("error_source");    if err.source().is_none() {        write!(formatter.labeled("heading"), "Caused by: ")?;        writeln!(formatter, "{err}")?;    } else {        writeln!(formatter.labeled("heading"), "Caused by:")?;        for (i, err) in iter::successors(Some(err), |&err| err.source()).enumerate() {            write!(formatter.labeled("heading"), "{}: ", i + 1)?;            writeln!(formatter, "{err}")?;        }    }    Ok(())} fn print_error_hints(ui: &Ui, hints: &[ErrorHint]) -> io::Result<()> {    let mut formatter = ui.stderr_formatter().into_labeled("hint");    for hint in hints {        write!(formatter.labeled("heading"), "Hint: ")?;        match hint {            ErrorHint::PlainText(message) => {                writeln!(formatter, "{message}")?;            }            ErrorHint::Formatted(recorded) => {                recorded.replay(formatter.as_mut())?;                // Formatted hint is usually multi-line text, and it's                // convenient if trailing "\n" doesn't have to be omitted.                if !recorded.data().ends_with(b"\n") {                    writeln!(formatter)?;                }            }        }    }    Ok(())} fn handle_clap_error(ui: &mut Ui, err: &clap::Error, hints: &[ErrorHint]) -> io::Result<u8> {    let clap_str = if ui.color() {        err.render().ansi().to_string()    } else {        err.render().to_string()    };     match err.kind() {        clap::error::ErrorKind::DisplayHelp        | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(),        _ => {}    }    // Definitions for exit codes and streams come from    // https://github.com/clap-rs/clap/blob/master/src/error/mod.rs    match err.kind() {        clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {            write!(ui.stdout(), "{clap_str}")?;            return Ok(0);        }        _ => {}    }    write!(ui.stderr(), "{clap_str}")?;    // Skip the first source error, which should be printed inline.    print_error_sources(ui, err.source().and_then(|err| err.source()))?;    print_error_hints(ui, hints)?;    Ok(2)} /// Prints diagnostic messages emitted during parsing.pub fn print_parse_diagnostics<T: error::Error>(    ui: &Ui,    context_message: &str,    diagnostics: &Diagnostics<T>,) -> io::Result<()> {    for diag in diagnostics {        writeln!(ui.warning_default(), "{context_message}")?;        for err in iter::successors(Some(diag as &dyn error::Error), |&err| err.source()) {            writeln!(ui.stderr(), "{err}")?;        }        // If we add support for multiple error diagnostics, we might have to do        // find_source_parse_error_hint() and print it here.    }    Ok(())}