1use std::path::PathBuf;
7
8#[derive(Debug, thiserror::Error)]
11pub enum RepographError {
12 #[error("i/o error: {0}")]
14 Io(#[from] std::io::Error),
15
16 #[error("invalid TOML in config file: {0}")]
18 ConfigParse(#[from] toml::de::Error),
19
20 #[error("failed to serialize config: {0}")]
22 ConfigWrite(#[from] toml::ser::Error),
23
24 #[error("not a git repository: {path}: {source}")]
26 GitOpen {
27 path: PathBuf,
28 #[source]
29 source: git2::Error,
30 },
31
32 #[error("{kind} '{name}' not found")]
34 NotFound { kind: &'static str, name: String },
35
36 #[error("{kind} '{name}' already registered")]
38 Conflict { kind: &'static str, name: String },
39
40 #[error("permission denied: {path}")]
42 PermissionDenied { path: PathBuf },
43
44 #[error("{0}")]
47 UsageError(String),
48
49 #[error("invalid {kind} name '{name}': {reason}")]
53 InvalidName {
54 kind: &'static str,
55 name: String,
56 reason: &'static str,
57 },
58
59 #[error("{0}")]
65 NeedsInit(String),
66
67 #[error("doctor found {count} error finding(s) — see report above")]
74 DoctorErrorsFound { count: u32 },
75
76 #[error("update failed: {0}")]
82 UpdateFailed(String),
83}
84
85impl RepographError {
86 #[must_use]
89 pub fn exit_code(&self) -> u8 {
90 match self {
91 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 4,
92 Self::PermissionDenied { .. } => 4,
93 Self::GitOpen { .. } | Self::NotFound { .. } => 3,
94 Self::Conflict { .. } => 5,
95 Self::InvalidName { .. } | Self::NeedsInit { .. } | Self::UsageError(_) => 2,
96 Self::Io(_)
97 | Self::ConfigParse(_)
98 | Self::ConfigWrite(_)
99 | Self::DoctorErrorsFound { .. }
100 | Self::UpdateFailed(_) => 1,
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 #![allow(clippy::unwrap_used)]
108 use super::*;
109
110 #[test]
111 fn io_permission_denied_maps_to_4() {
112 let err = RepographError::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
113 assert_eq!(err.exit_code(), 4);
114 }
115
116 #[test]
117 fn other_io_maps_to_1() {
118 let err = RepographError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
119 assert_eq!(err.exit_code(), 1);
120 }
121
122 #[test]
123 fn explicit_permission_denied_maps_to_4() {
124 let err = RepographError::PermissionDenied {
125 path: PathBuf::from("/nope"),
126 };
127 assert_eq!(err.exit_code(), 4);
128 }
129
130 #[test]
131 fn not_found_maps_to_3() {
132 let err = RepographError::NotFound {
133 kind: "repo",
134 name: "foo".into(),
135 };
136 assert_eq!(err.exit_code(), 3);
137 }
138
139 #[test]
140 fn git_open_maps_to_3() {
141 let err = RepographError::GitOpen {
142 path: PathBuf::from("/tmp/x"),
143 source: git2::Error::from_str("synthetic"),
144 };
145 assert_eq!(err.exit_code(), 3);
146 }
147
148 #[test]
149 fn conflict_maps_to_5() {
150 let err = RepographError::Conflict {
151 kind: "name",
152 name: "foo".into(),
153 };
154 assert_eq!(err.exit_code(), 5);
155 }
156
157 #[test]
158 fn usage_error_maps_to_2() {
159 let err = RepographError::UsageError("nope".into());
160 assert_eq!(err.exit_code(), 2);
161 }
162
163 #[test]
164 fn invalid_name_maps_to_2() {
165 let err = RepographError::InvalidName {
166 kind: "workspace",
167 name: "Bad Name".into(),
168 reason: "must be lowercase",
169 };
170 assert_eq!(err.exit_code(), 2);
171 }
172
173 #[test]
174 fn needs_init_maps_to_2() {
175 let err = RepographError::NeedsInit("agents not configured; run `repograph init`".into());
176 assert_eq!(err.exit_code(), 2);
177 assert!(err.to_string().contains("repograph init"));
178 }
179
180 #[test]
181 fn update_failed_maps_to_1() {
182 let err = RepographError::UpdateFailed("network unreachable".into());
183 assert_eq!(err.exit_code(), 1);
184 }
185
186 #[test]
187 fn config_parse_maps_to_1() {
188 let err: RepographError = toml::from_str::<toml::Value>("[unterminated")
189 .unwrap_err()
190 .into();
191 assert_eq!(err.exit_code(), 1);
192 }
193}