Skip to main content

vcs_watch/
error.rs

1//! The crate's error type: filesystem-watcher setup failures plus the underlying
2//! `vcs-core` re-query errors.
3
4/// An error from setting up or running a [`RepoWatcher`](crate::RepoWatcher).
5#[derive(Debug)]
6#[non_exhaustive]
7pub enum Error {
8    /// The `notify` filesystem watcher failed to start or register a path.
9    Notify(notify::Error),
10    /// A `vcs-core` query (detection / `snapshot` / `local_branches`) failed —
11    /// chiefly while *building* the watcher (capturing the baseline state). A
12    /// re-query failure *during* watching is skipped and retried, not surfaced
13    /// here (see [`RepoWatcher`](crate::RepoWatcher)).
14    Vcs(vcs_core::Error),
15    /// A filesystem operation failed (e.g. resolving a worktree gitlink).
16    Io(std::io::Error),
17}
18
19impl Error {
20    /// Whether this wraps a **transient** failure worth retrying — an
21    /// interrupted / would-block / resource-busy io/spawn failure from the
22    /// underlying `vcs-core` query (delegates to
23    /// [`vcs_core::Error::is_transient`]), **or** a baseline-snapshot **timeout**
24    /// (`Io` `TimedOut`, raised when the startup snapshot exceeds
25    /// `requery_timeout`) — a wedged repo may un-wedge, and the loop already treats
26    /// a re-query timeout as a transient skip, so `build()` agrees. Other `Io` and
27    /// `Notify` errors are `false`. Mirrors the classifier family on the other facades.
28    pub fn is_transient(&self) -> bool {
29        match self {
30            Error::Vcs(e) => e.is_transient(),
31            Error::Io(e) => e.kind() == std::io::ErrorKind::TimedOut,
32            _ => false,
33        }
34    }
35
36    /// Whether the underlying VCS binary (`git`/`jj`) **wasn't found** — a setup
37    /// problem (not installed / not on `PATH`), surfaced while building the
38    /// watcher's baseline. Delegates to [`vcs_core::Error::is_not_found`].
39    pub fn is_not_found(&self) -> bool {
40        matches!(self, Error::Vcs(e) if e.is_not_found())
41    }
42
43    /// The structured underlying [`processkit::Error`], if this error came from a
44    /// VCS subprocess — flattening the two-level
45    /// `Vcs(`[`vcs_core::Error::Vcs`]`(_))` nesting so a caller (or a language
46    /// binding) can read its structured fields (`program`, plus `code`/`stdout`/
47    /// `stderr` on an `Exit`) without hand-walking it. `None` for a `Notify`/`Io`
48    /// failure or a non-subprocess `vcs-core` error (e.g. "not a repository").
49    pub fn processkit_error(&self) -> Option<&processkit::Error> {
50        match self {
51            Error::Vcs(vcs_core::Error::Vcs(e)) => Some(e),
52            _ => None,
53        }
54    }
55}
56
57impl std::fmt::Display for Error {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Error::Notify(e) => write!(f, "filesystem watch failed: {e}"),
61            Error::Vcs(e) => write!(f, "{e}"),
62            Error::Io(e) => write!(f, "{e}"),
63        }
64    }
65}
66
67impl std::error::Error for Error {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Error::Notify(e) => Some(e),
71            Error::Vcs(e) => Some(e),
72            Error::Io(e) => Some(e),
73        }
74    }
75}
76
77impl From<notify::Error> for Error {
78    fn from(e: notify::Error) -> Self {
79        Error::Notify(e)
80    }
81}
82
83impl From<vcs_core::Error> for Error {
84    fn from(e: vcs_core::Error) -> Self {
85        Error::Vcs(e)
86    }
87}
88
89impl From<std::io::Error> for Error {
90    fn from(e: std::io::Error) -> Self {
91        Error::Io(e)
92    }
93}
94
95/// `Result` specialised to the watcher [`Error`].
96pub type Result<T> = std::result::Result<T, Error>;
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    /// The classifiers delegate through the `Vcs(vcs_core::Error)` layer and the
103    /// accessor flattens the two-level nesting; non-VCS errors are inert.
104    #[test]
105    fn classifiers_and_accessor_reach_through_the_vcs_layer() {
106        // A transient io/spawn hiccup from the underlying vcs-core query.
107        let transient = Error::Vcs(vcs_core::Error::Vcs(processkit::Error::Spawn {
108            program: "git".into(),
109            source: std::io::Error::from(std::io::ErrorKind::Interrupted),
110        }));
111        assert!(transient.is_transient(), "interrupted spawn is transient");
112        assert!(!transient.is_not_found());
113        assert!(
114            transient.processkit_error().is_some(),
115            "reaches the inner error"
116        );
117
118        // The VCS binary wasn't found (setup problem), not transient.
119        let missing = Error::Vcs(vcs_core::Error::Vcs(processkit::Error::NotFound {
120            program: "jj".into(),
121            searched: None,
122        }));
123        assert!(missing.is_not_found(), "missing binary is not-found");
124        assert!(!missing.is_transient());
125        assert!(missing.processkit_error().is_some());
126
127        // A generic filesystem (`Io`) failure is neither, and carries no process error.
128        let io = Error::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
129        assert!(!io.is_transient() && !io.is_not_found());
130        assert!(
131            io.processkit_error().is_none(),
132            "no subprocess behind an Io error"
133        );
134
135        // ...but a baseline-snapshot timeout (`Io` `TimedOut`, R4) IS transient — a
136        // wedged repo may un-wedge, so `build()` is worth retrying.
137        let baseline_timeout = Error::Io(std::io::Error::from(std::io::ErrorKind::TimedOut));
138        assert!(
139            baseline_timeout.is_transient(),
140            "a baseline TimedOut is transient (retryable)"
141        );
142    }
143}