Skip to main content

vcs_core/
error.rs

1//! The facade's error type: a thin wrapper that adds repo-detection failures on
2//! top of the underlying [`processkit::Error`] the per-tool clients return.
3//!
4//! The [`Error::Vcs`] variant carries a [`processkit::Error`] verbatim — re-exported
5//! at the crate root (`vcs_core::processkit`) so you can match it without a direct
6//! `processkit` dependency. Prefer the `is_*` classifiers ([`is_merge_conflict`](Error::is_merge_conflict)
7//! / [`is_nothing_to_commit`](Error::is_nothing_to_commit) /
8//! [`is_transient_fetch_error`](Error::is_transient_fetch_error) /
9//! [`is_transient`](Error::is_transient) / [`is_not_found`](Error::is_not_found))
10//! to branch on intent rather than matching the wrapped error's internals.
11
12use std::path::PathBuf;
13
14/// An error from a [`Repo`](crate::Repo) operation.
15#[derive(Debug)]
16#[non_exhaustive]
17pub enum Error {
18    /// [`Repo::open`](crate::Repo::open) found no `.git`/`.jj` from the start dir
19    /// up to the filesystem root.
20    NotARepository(PathBuf),
21    /// A worktree/workspace lookup by path matched no attached worktree.
22    WorktreeNotFound(PathBuf),
23    /// A filesystem operation failed (e.g. removing a workspace directory).
24    Io(std::io::Error),
25    /// An underlying `vcs-git` / `vcs-jj` (i.e. `processkit`) error.
26    Vcs(processkit::Error),
27}
28
29impl Error {
30    /// Whether this wraps a merge/rebase **conflict** from the backend — so a
31    /// caller can branch on "conflict, resolve it" vs. a hard failure without
32    /// matching on [`processkit::Error`] internals. (Recognises git's conflict
33    /// markers; jj surfaces conflicts as state, not errors — see
34    /// [`Repo::in_progress_state`](crate::Repo::in_progress_state).)
35    ///
36    /// Named to match the wrapper classifiers
37    /// ([`vcs_cli_support::is_merge_conflict`]) — one name per concept across the
38    /// workspace.
39    pub fn is_merge_conflict(&self) -> bool {
40        matches!(self, Error::Vcs(e) if vcs_cli_support::is_merge_conflict(e))
41    }
42
43    /// Whether this is a benign "nothing to commit" — an empty commit attempt the
44    /// caller likely wants to treat as a no-op.
45    pub fn is_nothing_to_commit(&self) -> bool {
46        matches!(self, Error::Vcs(e) if vcs_cli_support::is_nothing_to_commit(e))
47    }
48
49    /// Whether this is a **transient** fetch/network failure worth retrying
50    /// (DNS, connection reset, timeout). The underlying clients already retry
51    /// their own fetches; this is for retrying higher-level flows.
52    pub fn is_transient_fetch_error(&self) -> bool {
53        matches!(self, Error::Vcs(e) if vcs_cli_support::is_transient_fetch_error(e))
54    }
55
56    /// Whether the underlying error is a **transient io/spawn** failure
57    /// (interrupted / would-block / resource-busy) — delegates to
58    /// [`processkit::Error::is_transient`]. Narrower than
59    /// [`is_transient_fetch_error`](Error::is_transient_fetch_error) (which also
60    /// treats a timeout and the network markers as retryable); use this to retry
61    /// *any* operation past a momentary io hiccup. The facade's own
62    /// [`Io`](Error::Io)/[`NotARepository`](Error::NotARepository)/
63    /// [`WorktreeNotFound`](Error::WorktreeNotFound) variants are never transient.
64    pub fn is_transient(&self) -> bool {
65        matches!(self, Error::Vcs(e) if e.is_transient())
66    }
67
68    /// Whether the underlying CLI binary (`git`/`jj`) **wasn't found** — a setup
69    /// problem (the tool isn't installed or isn't on `PATH`), not a repository or
70    /// usage error. Delegates to [`processkit::Error::is_not_found`]; lets a caller
71    /// surface a "please install git/jj" hint instead of a raw spawn failure.
72    pub fn is_not_found(&self) -> bool {
73        matches!(self, Error::Vcs(e) if e.is_not_found())
74    }
75}
76
77impl std::fmt::Display for Error {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Error::NotARepository(p) => {
81                write!(
82                    f,
83                    "no git or jj repository found at or above {}",
84                    p.display()
85                )
86            }
87            Error::WorktreeNotFound(p) => {
88                write!(f, "no worktree found at {}", p.display())
89            }
90            Error::Io(e) => write!(f, "{e}"),
91            Error::Vcs(e) => write!(f, "{e}"),
92        }
93    }
94}
95
96impl std::error::Error for Error {
97    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98        match self {
99            Error::Io(e) => Some(e),
100            Error::Vcs(e) => Some(e),
101            _ => None,
102        }
103    }
104}
105
106impl From<std::io::Error> for Error {
107    fn from(e: std::io::Error) -> Self {
108        Error::Io(e)
109    }
110}
111
112impl From<processkit::Error> for Error {
113    fn from(e: processkit::Error) -> Self {
114        Error::Vcs(e)
115    }
116}
117
118/// `Result` specialised to the facade [`Error`].
119pub type Result<T> = std::result::Result<T, Error>;
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn is_transient_delegates_to_processkit_and_excludes_facade_variants() {
127        // An interrupted spawn is a transient io failure.
128        let interrupted = Error::Vcs(processkit::Error::Spawn {
129            program: "git".into(),
130            source: std::io::Error::from(std::io::ErrorKind::Interrupted),
131        });
132        assert!(interrupted.is_transient());
133        // A missing binary is NOT transient (retrying won't install it).
134        let missing = Error::Vcs(processkit::Error::Spawn {
135            program: "git".into(),
136            source: std::io::Error::from(std::io::ErrorKind::NotFound),
137        });
138        assert!(!missing.is_transient());
139        // The facade's own io/detection variants are never transient.
140        assert!(!Error::Io(std::io::Error::from(std::io::ErrorKind::Interrupted)).is_transient());
141        assert!(!Error::NotARepository("/x".into()).is_transient());
142    }
143
144    #[test]
145    fn is_not_found_only_for_a_missing_binary() {
146        let not_found = Error::Vcs(processkit::Error::NotFound {
147            program: "jj".into(),
148            searched: None,
149        });
150        assert!(not_found.is_not_found());
151        // An ordinary non-zero exit is not a "binary not found".
152        let exit = Error::Vcs(processkit::Error::Exit {
153            program: "git".into(),
154            code: 1,
155            stdout: String::new(),
156            stderr: "fatal: not a git repository".into(),
157        });
158        assert!(!exit.is_not_found());
159        assert!(!Error::NotARepository("/x".into()).is_not_found());
160    }
161}