use std::fmt; /// Common error cases that can happen in many different APIs #[derive(Debug)] pub enum HgError { IoError { error: std::io::Error, context: IoErrorContext, }, /// A file under `.hg/` normally only written by Mercurial is not in the /// expected format. This indicates a bug in Mercurial, filesystem /// corruption, or hardware failure. /// /// The given string is a short explanation for users, not intended to be /// machine-readable. CorruptedRepository(String), /// The respository or requested operation involves a feature not /// supported by the Rust implementation. Falling back to the Python /// implementation may or may not work. /// /// The given string is a short explanation for users, not intended to be /// machine-readable. UnsupportedFeature(String), /// Operation cannot proceed for some other reason. /// /// The given string is a short explanation for users, not intended to be /// machine-readable. Abort(String), } /// Details about where an I/O error happened #[derive(Debug, derive_more::From)] pub enum IoErrorContext { /// A filesystem operation for the given file #[from] File(std::path::PathBuf), /// `std::env::current_dir` CurrentDir, /// `std::env::current_exe` CurrentExe, } impl HgError { pub fn corrupted(explanation: impl Into) -> Self { // TODO: capture a backtrace here and keep it in the error value // to aid debugging? // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html HgError::CorruptedRepository(explanation.into()) } pub fn unsupported(explanation: impl Into) -> Self { HgError::UnsupportedFeature(explanation.into()) } pub fn abort(explanation: impl Into) -> Self { HgError::Abort(explanation.into()) } } // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly? impl fmt::Display for HgError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { HgError::IoError { error, context } => { write!(f, "{}: {}", error, context) } HgError::CorruptedRepository(explanation) => { write!(f, "corrupted repository: {}", explanation) } HgError::UnsupportedFeature(explanation) => { write!(f, "unsupported feature: {}", explanation) } HgError::Abort(explanation) => explanation.fmt(f), } } } // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly? impl fmt::Display for IoErrorContext { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { IoErrorContext::File(path) => path.display().fmt(f), IoErrorContext::CurrentDir => f.write_str("current directory"), IoErrorContext::CurrentExe => f.write_str("current executable"), } } } pub trait IoResultExt { /// Annotate a possible I/O error as related to a file at the given path. /// /// This allows printing something like “File not found: example.txt” /// instead of just “File not found”. /// /// Converts a `Result` with `std::io::Error` into one with `HgError`. fn for_file(self, path: &std::path::Path) -> Result; } impl IoResultExt for std::io::Result { fn for_file(self, path: &std::path::Path) -> Result { self.map_err(|error| HgError::IoError { error, context: IoErrorContext::File(path.to_owned()), }) } } pub trait HgResultExt { /// Handle missing files separately from other I/O error cases. /// /// Wraps the `Ok` type in an `Option`: /// /// * `Ok(x)` becomes `Ok(Some(x))` /// * An I/O "not found" error becomes `Ok(None)` /// * Other errors are unchanged fn io_not_found_as_none(self) -> Result, HgError>; } impl HgResultExt for Result { fn io_not_found_as_none(self) -> Result, HgError> { match self { Ok(x) => Ok(Some(x)), Err(HgError::IoError { error, .. }) if error.kind() == std::io::ErrorKind::NotFound => { Ok(None) } Err(other_error) => Err(other_error), } } }