diff --git a/rust/hg-core/src/errors.rs b/rust/hg-core/src/errors.rs new file mode 100644 --- /dev/null +++ b/rust/hg-core/src/errors.rs @@ -0,0 +1,111 @@ +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 + /// + /// 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), +} + +/// Details about where an I/O error happened +#[derive(Debug, derive_more::From)] +pub enum IoErrorContext { + /// A filesystem operation returned `std::io::Error` + #[from] + File(std::path::PathBuf), + /// `std::env::current_dir` returned `std::io::Error` + CurrentDir, +} + +impl HgError { + pub fn corrupted(explanation: impl Into) -> Self { + HgError::CorruptedRepository(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) + } + } + } +} + +// 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"), + } + } +} + +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), + } + } +} diff --git a/rust/hg-core/src/lib.rs b/rust/hg-core/src/lib.rs --- a/rust/hg-core/src/lib.rs +++ b/rust/hg-core/src/lib.rs @@ -5,6 +5,7 @@ // GNU General Public License version 2 or any later version. mod ancestors; pub mod dagops; +pub mod errors; pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors}; mod dirstate; pub mod discovery;