1use std::fmt;
30
31use crate::types::{BackendKind, CommitId, ObjectId};
32
33pub type Result<T> = std::result::Result<T, Error>;
37
38#[derive(Debug)]
45#[non_exhaustive]
46pub enum Error {
47 NotARepository { path: std::path::PathBuf },
49
50 EmptyRepository,
52
53 NotFound { kind: NotFoundKind, name: String },
55
56 InvalidCommitId { value: String },
58
59 InvalidObjectId { value: String },
61
62 InvalidRefName { value: String },
64
65 NotACommit { id: CommitId },
67
68 NotATree { id: ObjectId },
70
71 PathNotFound {
73 path: std::path::PathBuf,
74 commit: Option<CommitId>,
75 },
76
77 NonUtf8Path { path: std::path::PathBuf },
79
80 BareRepositoryUnsupported { operation: &'static str },
82
83 UnsupportedBackendFeature {
85 backend: Option<BackendKind>,
86 feature: &'static str,
87 },
88
89 UnsupportedObjectFormat { format: String },
91
92 HashCollision,
94
95 CorruptRepository { message: String },
97
98 Io(std::io::Error),
100
101 TaskJoin { message: String },
103
104 Backend {
109 message: String,
110 source: Option<Box<dyn std::error::Error + Send + Sync>>,
111 },
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120#[non_exhaustive]
121pub enum NotFoundKind {
122 Commit,
123 Ref,
124 Branch,
125 Tag,
126 Remote,
127 Path,
128 Worktree,
129 Submodule,
130}
131
132impl fmt::Display for NotFoundKind {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 NotFoundKind::Commit => write!(f, "commit"),
136 NotFoundKind::Ref => write!(f, "ref"),
137 NotFoundKind::Branch => write!(f, "branch"),
138 NotFoundKind::Tag => write!(f, "tag"),
139 NotFoundKind::Remote => write!(f, "remote"),
140 NotFoundKind::Path => write!(f, "path"),
141 NotFoundKind::Worktree => write!(f, "worktree"),
142 NotFoundKind::Submodule => write!(f, "submodule"),
143 }
144 }
145}
146
147impl fmt::Display for Error {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Error::NotARepository { path } =>
153 write!(f, "not a repository: {}", path.display()),
154 Error::EmptyRepository =>
155 write!(f, "repository has no commits"),
156 Error::NotFound { kind, name } =>
157 write!(f, "{kind} not found: {name}"),
158 Error::InvalidCommitId { value } =>
159 write!(f, "invalid commit id {value:?}: expected 40 (SHA-1) or 64 (SHA-256) hex chars"),
160 Error::InvalidObjectId { value } =>
161 write!(f, "invalid object id {value:?}: expected 40 (SHA-1) or 64 (SHA-256) hex chars"),
162 Error::InvalidRefName { value } =>
163 write!(f, "invalid ref name: {value:?}"),
164 Error::NotACommit { id } =>
165 write!(f, "object {} is not a commit", id.short()),
166 Error::NotATree { id } =>
167 write!(f, "object {} is not a tree", id.short()),
168 Error::PathNotFound { path, commit: None } =>
169 write!(f, "path not found: {}", path.display()),
170 Error::PathNotFound { path, commit: Some(c) } =>
171 write!(f, "path not found at commit {}: {}", c.short(), path.display()),
172 Error::NonUtf8Path { path } =>
173 write!(f, "path is not valid UTF-8: {}", path.display()),
174 Error::BareRepositoryUnsupported { operation } =>
175 write!(f, "operation not supported on bare repository: {operation}"),
176 Error::UnsupportedBackendFeature { backend: None, feature } =>
177 write!(f, "backend does not support {feature}"),
178 Error::UnsupportedBackendFeature { backend: Some(b), feature } =>
179 write!(f, "{b:?} backend does not support {feature}"),
180 Error::UnsupportedObjectFormat { format } =>
181 write!(f, "unsupported object format: {format}"),
182 Error::HashCollision =>
183 write!(f, "SHA-1 hash collision detected"),
184 Error::CorruptRepository { message } =>
185 write!(f, "corrupt repository: {message}"),
186 Error::Io(e) =>
187 write!(f, "I/O error: {e}"),
188 Error::TaskJoin { message } =>
189 write!(f, "async task failed to join: {message}"),
190 Error::Backend { message, .. } =>
191 write!(f, "backend error: {message}"),
192 }
193 }
194}
195
196impl std::error::Error for Error {
199 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
200 match self {
201 Error::Io(e) => Some(e),
202 Error::Backend { source: Some(s), .. } => Some(&**s),
203 _ => None,
204 }
205 }
206}
207
208impl From<std::io::Error> for Error {
211 fn from(e: std::io::Error) -> Self {
212 Error::Io(e)
213 }
214}
215
216pub fn anyhow_to_backend(err: anyhow::Error) -> Error {
222 Error::Backend {
223 message: err.to_string(),
224 source: None,
225 }
226}
227
228#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn display_not_a_repository() {
236 let e = Error::NotARepository { path: "/tmp/nope".into() };
237 assert!(e.to_string().contains("not a repository"));
238 assert!(e.to_string().contains("nope"));
239 }
240
241 #[test]
242 fn display_not_found_commit() {
243 let e = Error::NotFound {
244 kind: NotFoundKind::Commit,
245 name: "abc123".into(),
246 };
247 assert!(e.to_string().contains("commit"));
248 assert!(e.to_string().contains("abc123"));
249 }
250
251 #[test]
252 fn display_unsupported_jj_tag() {
253 let e = Error::UnsupportedBackendFeature {
254 backend: Some(BackendKind::Jj),
255 feature: "create_annotated_tag",
256 };
257 let s = e.to_string();
258 assert!(s.contains("Jj") || s.contains("jj"));
259 assert!(s.contains("create_annotated_tag"));
260 }
261
262 #[test]
263 fn display_io_error() {
264 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
265 let e = Error::Io(io);
266 assert!(e.to_string().contains("I/O"));
267 }
268
269 #[test]
270 fn source_for_io_error() {
271 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
272 let e = Error::Io(io);
273 assert!(std::error::Error::source(&e).is_some());
274 }
275
276 #[test]
277 fn source_for_backend_without_source() {
278 let e = Error::Backend { message: "oops".into(), source: None };
279 assert!(std::error::Error::source(&e).is_none());
280 }
281
282 #[test]
283 fn from_io_error() {
284 let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
285 let e: Error = io.into();
286 assert!(matches!(e, Error::Io(_)));
287 }
288
289 #[test]
290 fn not_found_kind_display() {
291 assert_eq!(NotFoundKind::Commit.to_string(), "commit");
292 assert_eq!(NotFoundKind::Branch.to_string(), "branch");
293 assert_eq!(NotFoundKind::Tag.to_string(), "tag");
294 assert_eq!(NotFoundKind::Remote.to_string(), "remote");
295 assert_eq!(NotFoundKind::Worktree.to_string(), "worktree");
296 assert_eq!(NotFoundKind::Submodule.to_string(), "submodule");
297 }
298
299 #[test]
300 fn hash_collision_display() {
301 let e = Error::HashCollision;
302 assert!(e.to_string().contains("collision"));
303 }
304}