Skip to main content

git_atomic/core/
error.rs

1use std::path::PathBuf;
2use std::process::ExitCode;
3
4/// Top-level error type for git-atomic.
5#[derive(Debug, thiserror::Error)]
6pub enum Error {
7    #[error("{0}")]
8    General(String),
9
10    #[error(transparent)]
11    Config(#[from] ConfigError),
12
13    #[error(transparent)]
14    Git(#[from] GitError),
15
16    #[error("unmatched files: {}", .paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", "))]
17    UnmatchedFiles { paths: Vec<PathBuf> },
18
19    #[error("branch {branch} has diverged from {base}")]
20    DivergedBranch { branch: String, base: String },
21}
22
23/// Configuration errors (exit code 2).
24#[derive(Debug, thiserror::Error)]
25pub enum ConfigError {
26    #[error("config file not found: {path}")]
27    NotFound { path: PathBuf },
28
29    #[error("invalid config: {reason}")]
30    Invalid { reason: String },
31
32    #[error("invalid glob pattern in component {component:?}: {pattern:?}: {reason}")]
33    InvalidGlob {
34        component: String,
35        pattern: String,
36        reason: String,
37    },
38
39    #[error("failed to read config: {0}")]
40    Io(#[from] std::io::Error),
41}
42
43/// Git operation errors (exit code 3).
44#[derive(Debug, thiserror::Error)]
45pub enum GitError {
46    #[error("not a git repository: {path}")]
47    NotARepo { path: PathBuf },
48
49    #[error("failed to resolve reference {reference:?}: {reason}")]
50    ResolveRef { reference: String, reason: String },
51
52    #[error("git operation failed: {0}")]
53    Operation(String),
54
55    #[error("tree entry not found: {path}")]
56    TreeEntryNotFound { path: String },
57
58    #[error("ref update failed for {branch}: {reason}")]
59    RefUpdate { branch: String, reason: String },
60
61    #[error(transparent)]
62    Gix(Box<gix::open::Error>),
63}
64
65impl Error {
66    /// Map error to process exit code per ADR-004.
67    pub fn exit_code(&self) -> ExitCode {
68        match self {
69            Error::General(_) => ExitCode::from(1),
70            Error::Config(_) => ExitCode::from(2),
71            Error::Git(_) => ExitCode::from(3),
72            Error::UnmatchedFiles { .. } => ExitCode::from(4),
73            Error::DivergedBranch { .. } => ExitCode::from(5),
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn exit_codes_match_spec() {
84        assert_eq!(Error::General("x".into()).exit_code(), ExitCode::from(1));
85        assert_eq!(
86            Error::Config(ConfigError::Invalid {
87                reason: "bad".into()
88            })
89            .exit_code(),
90            ExitCode::from(2)
91        );
92        assert_eq!(
93            Error::Git(GitError::Operation("fail".into())).exit_code(),
94            ExitCode::from(3)
95        );
96        assert_eq!(
97            Error::UnmatchedFiles { paths: vec![] }.exit_code(),
98            ExitCode::from(4)
99        );
100        assert_eq!(
101            Error::DivergedBranch {
102                branch: "a".into(),
103                base: "b".into()
104            }
105            .exit_code(),
106            ExitCode::from(5)
107        );
108    }
109}