git_spawn/error.rs
1//! Error types for git-spawn.
2//!
3//! All commands return [`Result<T, Error>`]. The [`enum@Error`] type is
4//! non-exhaustive in spirit: callers should match the variants they care
5//! about and fall through to a generic arm.
6//!
7//! ```no_run
8//! use git_spawn::{Error, GitCommand, Repository};
9//!
10//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
11//! let repo = Repository::open("/path/to/repo")?;
12//! match repo.log().max_count(10).execute().await {
13//! Ok(out) => println!("{}", out.stdout),
14//! Err(Error::GitNotFound) => eprintln!("install git first"),
15//! Err(Error::CommandFailed { stderr, exit_code, .. }) => {
16//! eprintln!("git failed (exit {exit_code}):\n{stderr}")
17//! }
18//! Err(Error::Timeout { timeout_seconds }) => {
19//! eprintln!("git didn't respond within {timeout_seconds}s")
20//! }
21//! Err(e) => eprintln!("unexpected: {e}"),
22//! }
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! ## When each variant occurs
28//!
29//! - [`Error::GitNotFound`] — the OS reported *file not found* while spawning.
30//! Install git or check `PATH`.
31//! - [`Error::CommandFailed`] — git exited non-zero. Read `stderr` for the
32//! user-facing message; keep `stdout` for anything git wrote to the
33//! fast-path.
34//! - [`Error::Timeout`] — the process exceeded the duration passed to
35//! [`with_timeout`](crate::GitCommand::with_timeout).
36//! - [`Error::Io`] — OS-level failure unrelated to exit status (e.g. cwd
37//! doesn't exist, pipe error while reading output).
38//! - [`Error::InvalidConfig`] — a builder was missing a required field
39//! (for example [`MvCommand`](crate::MvCommand) with no source).
40//! - [`Error::NotARepository`] — [`Repository::open`](crate::Repository::open)
41//! was called on a path that has no `.git`.
42//! - [`Error::ParseError`] — a parser in [`crate::parse`] could not decode
43//! the captured output.
44//! - [`Error::UnsupportedVersion`] — reserved for future version gating; not
45//! currently emitted.
46//! - [`Error::Custom`] — catch-all for cases the library cannot classify.
47
48use thiserror::Error;
49
50/// Result type for git-spawn operations.
51pub type Result<T> = std::result::Result<T, Error>;
52
53/// Main error type for all git-spawn operations.
54#[derive(Error, Debug)]
55pub enum Error {
56 /// Git binary not found in PATH.
57 #[error("git binary not found in PATH")]
58 GitNotFound,
59
60 /// Git version is below the minimum supported.
61 #[error("git version {found} is not supported (minimum: {minimum})")]
62 UnsupportedVersion {
63 /// Version reported by `git --version`.
64 found: String,
65 /// Minimum required version.
66 minimum: String,
67 },
68
69 /// A git command exited with a non-zero status.
70 #[error("git command failed: {command}")]
71 CommandFailed {
72 /// The full command line that was executed.
73 command: String,
74 /// Exit code returned by the command.
75 exit_code: i32,
76 /// Captured stdout.
77 stdout: String,
78 /// Captured stderr.
79 stderr: String,
80 },
81
82 /// Failed to parse git output into a typed value.
83 #[error("failed to parse git output: {message}")]
84 ParseError {
85 /// Description of the parse failure.
86 message: String,
87 },
88
89 /// Invalid configuration supplied to a builder.
90 #[error("invalid configuration: {message}")]
91 InvalidConfig {
92 /// Description of the misconfiguration.
93 message: String,
94 },
95
96 /// Operation targeted a path that is not a git repository.
97 #[error("not a git repository: {path}")]
98 NotARepository {
99 /// Path that was expected to be a repo.
100 path: String,
101 },
102
103 /// IO error while spawning or reading from a git process.
104 #[error("io error: {message}")]
105 Io {
106 /// Human-readable message.
107 message: String,
108 /// Underlying IO error.
109 #[source]
110 source: std::io::Error,
111 },
112
113 /// Command exceeded its configured timeout.
114 #[error("operation timed out after {timeout_seconds} seconds")]
115 Timeout {
116 /// Configured timeout in seconds.
117 timeout_seconds: u64,
118 },
119
120 /// Generic error with a custom message.
121 #[error("{message}")]
122 Custom {
123 /// Custom error message.
124 message: String,
125 },
126}
127
128impl Error {
129 /// Create a [`Error::CommandFailed`].
130 pub fn command_failed(
131 command: impl Into<String>,
132 exit_code: i32,
133 stdout: impl Into<String>,
134 stderr: impl Into<String>,
135 ) -> Self {
136 Self::CommandFailed {
137 command: command.into(),
138 exit_code,
139 stdout: stdout.into(),
140 stderr: stderr.into(),
141 }
142 }
143
144 /// Create a [`Error::ParseError`].
145 pub fn parse_error(message: impl Into<String>) -> Self {
146 Self::ParseError {
147 message: message.into(),
148 }
149 }
150
151 /// Create a [`Error::InvalidConfig`].
152 pub fn invalid_config(message: impl Into<String>) -> Self {
153 Self::InvalidConfig {
154 message: message.into(),
155 }
156 }
157
158 /// Create a [`Error::NotARepository`].
159 pub fn not_a_repository(path: impl Into<String>) -> Self {
160 Self::NotARepository { path: path.into() }
161 }
162
163 /// Create a [`Error::Timeout`].
164 #[must_use]
165 pub fn timeout(timeout_seconds: u64) -> Self {
166 Self::Timeout { timeout_seconds }
167 }
168
169 /// Create a [`Error::Custom`].
170 pub fn custom(message: impl Into<String>) -> Self {
171 Self::Custom {
172 message: message.into(),
173 }
174 }
175
176 /// A coarse category useful for logging and metrics.
177 #[must_use]
178 pub fn category(&self) -> &'static str {
179 match self {
180 Self::GitNotFound | Self::UnsupportedVersion { .. } => "prerequisites",
181 Self::CommandFailed { .. } | Self::Timeout { .. } => "command",
182 Self::ParseError { .. } => "parsing",
183 Self::InvalidConfig { .. } => "config",
184 Self::NotARepository { .. } => "repository",
185 Self::Io { .. } => "io",
186 Self::Custom { .. } => "custom",
187 }
188 }
189}
190
191impl From<std::io::Error> for Error {
192 fn from(err: std::io::Error) -> Self {
193 Self::Io {
194 message: err.to_string(),
195 source: err,
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn categories() {
206 assert_eq!(Error::GitNotFound.category(), "prerequisites");
207 assert_eq!(
208 Error::command_failed("git status", 1, "", "").category(),
209 "command"
210 );
211 assert_eq!(Error::parse_error("x").category(), "parsing");
212 assert_eq!(Error::not_a_repository("/tmp").category(), "repository");
213 }
214
215 #[test]
216 fn from_io_error() {
217 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "nope");
218 let err: Error = io.into();
219 assert!(matches!(err, Error::Io { .. }));
220 }
221}