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}