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 #[error("no search index found — run `repograph index` first")]
89 IndexMissing,
90
91 #[error("search index error: {0}")]
96 Index(String),
97}
98
99impl From<rusqlite::Error> for RepographError {
100 fn from(e: rusqlite::Error) -> Self {
101 Self::Index(e.to_string())
102 }
103}
104
105impl RepographError {
106 #[must_use]
109 pub fn exit_code(&self) -> u8 {
110 match self {
111 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 4,
112 Self::PermissionDenied { .. } => 4,
113 Self::GitOpen { .. } | Self::NotFound { .. } | Self::IndexMissing => 3,
114 Self::Conflict { .. } => 5,
115 Self::InvalidName { .. } | Self::NeedsInit { .. } | Self::UsageError(_) => 2,
116 Self::Io(_)
117 | Self::ConfigParse(_)
118 | Self::ConfigWrite(_)
119 | Self::DoctorErrorsFound { .. }
120 | Self::UpdateFailed(_)
121 | Self::Index(_) => 1,
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 #![allow(clippy::unwrap_used)]
129 use super::*;
130
131 #[test]
132 fn io_permission_denied_maps_to_4() {
133 let err = RepographError::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
134 assert_eq!(err.exit_code(), 4);
135 }
136
137 #[test]
138 fn other_io_maps_to_1() {
139 let err = RepographError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
140 assert_eq!(err.exit_code(), 1);
141 }
142
143 #[test]
144 fn explicit_permission_denied_maps_to_4() {
145 let err = RepographError::PermissionDenied {
146 path: PathBuf::from("/nope"),
147 };
148 assert_eq!(err.exit_code(), 4);
149 }
150
151 #[test]
152 fn not_found_maps_to_3() {
153 let err = RepographError::NotFound {
154 kind: "repo",
155 name: "foo".into(),
156 };
157 assert_eq!(err.exit_code(), 3);
158 }
159
160 #[test]
161 fn git_open_maps_to_3() {
162 let err = RepographError::GitOpen {
163 path: PathBuf::from("/tmp/x"),
164 source: git2::Error::from_str("synthetic"),
165 };
166 assert_eq!(err.exit_code(), 3);
167 }
168
169 #[test]
170 fn conflict_maps_to_5() {
171 let err = RepographError::Conflict {
172 kind: "name",
173 name: "foo".into(),
174 };
175 assert_eq!(err.exit_code(), 5);
176 }
177
178 #[test]
179 fn usage_error_maps_to_2() {
180 let err = RepographError::UsageError("nope".into());
181 assert_eq!(err.exit_code(), 2);
182 }
183
184 #[test]
185 fn invalid_name_maps_to_2() {
186 let err = RepographError::InvalidName {
187 kind: "workspace",
188 name: "Bad Name".into(),
189 reason: "must be lowercase",
190 };
191 assert_eq!(err.exit_code(), 2);
192 }
193
194 #[test]
195 fn needs_init_maps_to_2() {
196 let err = RepographError::NeedsInit("agents not configured; run `repograph init`".into());
197 assert_eq!(err.exit_code(), 2);
198 assert!(err.to_string().contains("repograph init"));
199 }
200
201 #[test]
202 fn update_failed_maps_to_1() {
203 let err = RepographError::UpdateFailed("network unreachable".into());
204 assert_eq!(err.exit_code(), 1);
205 }
206
207 #[test]
208 fn index_missing_maps_to_3_and_names_index_command() {
209 let err = RepographError::IndexMissing;
210 assert_eq!(err.exit_code(), 3);
211 assert!(err.to_string().contains("repograph index"));
212 }
213
214 #[test]
215 fn index_error_maps_to_1() {
216 let err = RepographError::Index("disk image is malformed".into());
217 assert_eq!(err.exit_code(), 1);
218 }
219
220 #[test]
221 fn config_parse_maps_to_1() {
222 let err: RepographError = toml::from_str::<toml::Value>("[unterminated")
223 .unwrap_err()
224 .into();
225 assert_eq!(err.exit_code(), 1);
226 }
227}