Skip to main content

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}