use std::path::PathBuf;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Notify(WatchError),
Vcs(vcs_core::Error),
Io(std::io::Error),
}
impl Error {
pub fn is_transient(&self) -> bool {
match self {
Error::Vcs(e) => e.is_transient(),
Error::Io(e) => e.kind() == std::io::ErrorKind::TimedOut,
_ => false,
}
}
pub fn is_not_found(&self) -> bool {
matches!(self, Error::Vcs(e) if e.is_not_found())
}
pub fn watch_error(&self) -> Option<&WatchError> {
match self {
Error::Notify(e) => Some(e),
_ => None,
}
}
pub fn processkit_error(&self) -> Option<&processkit::Error> {
match self {
Error::Vcs(vcs_core::Error::Vcs(e)) => Some(e),
_ => None,
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Notify(e) => write!(f, "filesystem watch failed: {e}"),
Error::Vcs(e) => write!(f, "{e}"),
Error::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Notify(e) => Some(e),
Error::Vcs(e) => Some(e),
Error::Io(e) => Some(e),
}
}
}
impl From<notify::Error> for Error {
fn from(e: notify::Error) -> Self {
Error::Notify(WatchError(e))
}
}
impl From<vcs_core::Error> for Error {
fn from(e: vcs_core::Error) -> Self {
Error::Vcs(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
#[derive(Debug)]
pub struct WatchError(notify::Error);
impl WatchError {
pub fn is_path_not_found(&self) -> bool {
matches!(self.0.kind, notify::ErrorKind::PathNotFound)
}
pub fn is_watch_limit(&self) -> bool {
matches!(self.0.kind, notify::ErrorKind::MaxFilesWatch)
}
pub fn io_error(&self) -> Option<&std::io::Error> {
match &self.0.kind {
notify::ErrorKind::Io(e) => Some(e),
_ => None,
}
}
pub fn paths(&self) -> &[PathBuf] {
&self.0.paths
}
}
impl std::fmt::Display for WatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for WatchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.0.kind {
notify::ErrorKind::Io(e) => Some(e),
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifiers_and_accessor_reach_through_the_vcs_layer() {
let transient = Error::Vcs(vcs_core::Error::Vcs(processkit::Error::spawn(
"git",
std::io::Error::from(std::io::ErrorKind::Interrupted),
)));
assert!(transient.is_transient(), "interrupted spawn is transient");
assert!(!transient.is_not_found());
assert!(
transient.processkit_error().is_some(),
"reaches the inner error"
);
assert!(
transient.watch_error().is_none(),
"a vcs-core error is not a watch error"
);
let missing = Error::Vcs(vcs_core::Error::Vcs(processkit::Error::not_found(
"jj", None,
)));
assert!(missing.is_not_found(), "missing binary is not-found");
assert!(!missing.is_transient());
assert!(missing.processkit_error().is_some());
let io = Error::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
assert!(!io.is_transient() && !io.is_not_found());
assert!(
io.processkit_error().is_none(),
"no subprocess behind an Io error"
);
let baseline_timeout = Error::Io(std::io::Error::from(std::io::ErrorKind::TimedOut));
assert!(
baseline_timeout.is_transient(),
"a baseline TimedOut is transient (retryable)"
);
}
#[test]
fn watch_error_classifies_backend_kinds() {
let e: Error = notify::Error::path_not_found()
.add_path(PathBuf::from("/repo/.git"))
.into();
let w = e
.watch_error()
.expect("a Notify error exposes its WatchError");
assert!(w.is_path_not_found());
assert!(!w.is_watch_limit());
assert!(w.io_error().is_none());
assert_eq!(w.paths(), [PathBuf::from("/repo/.git")]);
assert!(std::error::Error::source(w).is_none());
assert!(!e.is_transient() && !e.is_not_found());
assert!(e.processkit_error().is_none());
let limit: Error = notify::Error::new(notify::ErrorKind::MaxFilesWatch).into();
let w = limit.watch_error().expect("WatchError");
assert!(w.is_watch_limit() && !w.is_path_not_found());
let io: Error =
notify::Error::io(std::io::Error::from(std::io::ErrorKind::PermissionDenied)).into();
let w = io.watch_error().expect("WatchError");
assert_eq!(
w.io_error().map(|e| e.kind()),
Some(std::io::ErrorKind::PermissionDenied)
);
let src = std::error::Error::source(w).expect("io cause is source-chained");
assert!(src.downcast_ref::<std::io::Error>().is_some());
}
#[test]
fn top_level_source_chain_reaches_io_through_watch_error() {
let e: Error = notify::Error::io(std::io::Error::from(std::io::ErrorKind::NotFound)).into();
let first = std::error::Error::source(&e).expect("WatchError is the first source");
assert!(
first.downcast_ref::<WatchError>().is_some(),
"the opaque wrapper is the immediate source"
);
let second = first.source().expect("io::Error is the next link");
assert!(second.downcast_ref::<std::io::Error>().is_some());
}
}