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}