1use std::{error::Error, fmt, io, path::Path};
5
6use crate::object::{ContentHash, StateId, TreeError, TreeStreamError};
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct RecoveryDetails {
11 pub kind: &'static str,
12 pub error: String,
13 pub hint: String,
14 pub unsafe_condition: String,
15 pub would_change: String,
16 pub preserved: String,
17 pub recovery_commands: Option<Vec<String>>,
22}
23
24impl RecoveryDetails {
25 pub fn safety_refusal(
26 kind: &'static str,
27 error: impl Into<String>,
28 hint: impl Into<String>,
29 unsafe_condition: impl Into<String>,
30 would_change: impl Into<String>,
31 already_preserved: impl Into<String>,
32 ) -> Self {
33 Self {
34 kind,
35 error: error.into(),
36 hint: hint.into(),
37 unsafe_condition: unsafe_condition.into(),
38 would_change: would_change.into(),
39 preserved: already_preserved.into(),
40 recovery_commands: None,
41 }
42 }
43
44 #[must_use]
48 pub fn with_recovery_commands(mut self, commands: Vec<String>) -> Self {
49 self.recovery_commands = Some(commands);
50 self
51 }
52
53 pub fn invalid_usage(
54 kind: &'static str,
55 error: impl Into<String>,
56 hint: impl Into<String>,
57 ) -> Self {
58 Self::safety_refusal(
59 kind,
60 error,
61 hint,
62 "the command arguments do not describe a valid operation",
63 "running with ambiguous or invalid arguments could target the wrong repository state or metadata",
64 "no repository objects, refs, metadata, or worktree files were changed",
65 )
66 }
67
68 pub fn feature_unavailable(command: &str, feature: &str) -> Self {
69 Self::safety_refusal(
70 "feature_unavailable",
71 format!("{command} requires building heddle with --features {feature}"),
72 format!(
73 "Use a binary built with the `{feature}` feature, or rerun without the feature-specific flag."
74 ),
75 format!("this heddle binary was built without the `{feature}` feature"),
76 format!("{command} cannot run because the requested analysis engine is unavailable"),
77 "repository state, refs, and worktree files were left unchanged",
78 )
79 }
80
81 pub fn serialization_error(detail: impl fmt::Display) -> Self {
82 Self::safety_refusal(
83 "state_corrupted",
84 "Repository state is corrupted or unreadable",
85 "Inspect repository integrity before attempting repair.",
86 format!("a stored repository object failed to decode: {detail}"),
87 "continuing would read or write through repository state Heddle cannot decode",
88 "the command stopped before mutating repository state; intact objects were left unchanged",
89 )
90 }
91
92 pub fn repository_integrity_error(error: impl Into<String>) -> Self {
93 Self::safety_refusal(
94 "repository_integrity_error",
95 error,
96 "Inspect repository integrity, then restore or repair the reported object/ref.",
97 "repository object or ref integrity did not pass validation",
98 "continuing could compound corruption or hide the missing object",
99 "the command stopped before applying the requested mutation",
100 )
101 }
102
103 pub fn repository_not_found(path: &Path) -> Self {
104 Self::safety_refusal(
105 "repository_not_found",
106 format!("repository not found at {}", path.display()),
107 "Initialize the requested repository before running repository commands.",
108 format!("no Heddle repository was found at '{}'", path.display()),
109 "the command cannot inspect or change repository state until initialization",
110 "no repository objects, refs, metadata, or worktree files were changed",
111 )
112 }
113
114 pub fn state_not_found(state_id: impl fmt::Display) -> Self {
115 Self::safety_refusal(
116 "state_not_found",
117 format!("State not found: {state_id}"),
118 "List recent states with `heddle log`, then choose an existing state id.",
119 "the requested state id does not exist in this repository",
120 "continuing with a guessed state could target the wrong history point",
121 "repository state, refs, metadata, and worktree files were left unchanged",
122 )
123 }
124}
125
126impl fmt::Display for RecoveryDetails {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(
129 f,
130 "{}. Unsafe: {}. Would change: {}. Preserved: {}.",
131 self.error, self.unsafe_condition, self.would_change, self.preserved
132 )?;
133 Ok(())
134 }
135}
136
137impl Error for RecoveryDetails {}
138
139#[derive(Debug, thiserror::Error)]
145pub enum LockError {
146 #[error("failed to acquire lock: {0}")]
147 Acquire(#[source] io::Error),
148 #[error("lock file not accessible: {0}")]
149 Io(#[source] io::Error),
150}
151
152#[derive(Debug, thiserror::Error)]
154pub enum HeddleError {
155 #[error("{0}")]
156 Recovery(Box<RecoveryDetails>),
157 #[error("object not found: {0}")]
158 NotFound(String),
159 #[error("No merge in progress")]
160 NoMergeInProgress,
161 #[error("no worktree changes to capture")]
162 NoChanges,
163 #[error("state not found: {0}")]
164 StateNotFound(StateId),
165 #[error("invalid object: {0}")]
166 InvalidObject(String),
167 #[error("repository not found at {0}")]
168 RepositoryNotFound(std::path::PathBuf),
169 #[error("repository already exists at {0}")]
170 RepositoryExists(std::path::PathBuf),
171 #[error("repository clone at {0} is incomplete and must be repaired from its origin")]
172 IncompleteClone(std::path::PathBuf),
173 #[error(
174 "repository config at {path} uses repository format {found} but this binary supports {supported}; upgrade Heddle before opening it"
175 )]
176 RepositoryFormatTooNew {
177 path: std::path::PathBuf,
178 found: u32,
179 supported: u32,
180 },
181 #[error(
182 "repository at {path} predates format v{required} (found v{found}); recreate it or re-adopt its Git history with this Heddle version"
183 )]
184 RepositoryFormatTooOld {
185 path: std::path::PathBuf,
186 found: u32,
187 required: u32,
188 },
189 #[error(
190 "{storage} uses format {found}, but this binary supports {supported}; upgrade Heddle before opening it"
191 )]
192 StorageFormatTooNew {
193 storage: String,
194 found: u32,
195 supported: u32,
196 },
197 #[error(
198 "{storage} predates required format {required} (found {found}); recreate the repository or re-adopt its Git history with this Heddle version"
199 )]
200 StorageFormatTooOld {
201 storage: String,
202 found: u32,
203 required: u32,
204 },
205 #[error("io error: {0}")]
206 Io(#[from] std::io::Error),
207 #[error("repository lock unavailable: {0}")]
208 Lock(#[from] LockError),
209 #[error("serialization error: {0}")]
210 Serialization(String),
211 #[error("configuration error: {0}")]
212 Config(String),
213 #[error("native Thread '{thread}' owner signing key is unavailable: {reason}")]
217 NativeSourceSignerUnavailable { thread: String, reason: String },
218 #[error("configuration parse error at {path}: {source}")]
219 ConfigParse {
220 path: std::path::PathBuf,
221 #[source]
226 source: toml::de::Error,
227 },
228 #[error(
229 "invalid {key}: '{value}' — valid values are {} (in {path})",
230 valid_values.join(" or ")
231 )]
232 ConfigInvalidValue {
233 path: std::path::PathBuf,
234 key: String,
235 value: String,
236 valid_values: Vec<String>,
237 },
238 #[error("conflict: {0}")]
239 Conflict(String),
240 #[error("compression error: {0}")]
241 Compression(String),
242 #[error("invalid ref name: {0}")]
243 InvalidRefName(String),
244 #[error("file too large: {0} bytes")]
245 InvalidFileSize(u64),
246 #[error(
247 "symlink target escapes repository: {} -> {}",
248 path.display(),
249 target.display()
250 )]
251 InvalidSymlinkTarget {
252 path: std::path::PathBuf,
253 target: std::path::PathBuf,
254 },
255 #[error("object corruption: expected {expected}, found {found}")]
256 Corruption {
257 expected: ContentHash,
258 found: ContentHash,
259 },
260 #[error(
261 "missing {object_type} object: {id} (run `heddle maintenance fsck --full` to inspect store integrity)"
262 )]
263 MissingObject { object_type: String, id: String },
264 #[error("invalid tree entry: {0}")]
265 InvalidTreeEntry(#[from] TreeError),
266 #[error("tree stream error: {0}")]
267 TreeStream(TreeStreamError),
268 #[error("redacted tree: {0}")]
276 RedactedTree(String),
277}
278
279impl From<TreeStreamError> for HeddleError {
280 fn from(error: TreeStreamError) -> Self {
281 match error {
282 TreeStreamError::Invalid(error) => Self::InvalidTreeEntry(error),
283 other => Self::TreeStream(other),
284 }
285 }
286}
287
288impl HeddleError {
289 pub fn recovery(details: RecoveryDetails) -> Self {
290 HeddleError::Recovery(Box::new(details))
291 }
292}
293
294impl From<rmp_serde::encode::Error> for HeddleError {
295 fn from(e: rmp_serde::encode::Error) -> Self {
296 HeddleError::Serialization(e.to_string())
297 }
298}
299
300impl From<rmp_serde::decode::Error> for HeddleError {
301 fn from(e: rmp_serde::decode::Error) -> Self {
302 HeddleError::Serialization(e.to_string())
303 }
304}
305
306impl From<crate::object::SemanticIndexError> for HeddleError {
307 fn from(e: crate::object::SemanticIndexError) -> Self {
308 HeddleError::InvalidObject(e.to_string())
309 }
310}
311
312impl From<toml::de::Error> for HeddleError {
313 fn from(e: toml::de::Error) -> Self {
314 HeddleError::Config(e.to_string())
315 }
316}
317
318impl From<toml::ser::Error> for HeddleError {
319 fn from(e: toml::ser::Error) -> Self {
320 HeddleError::Config(e.to_string())
321 }
322}
323
324impl From<serde_json::Error> for HeddleError {
325 fn from(e: serde_json::Error) -> Self {
326 HeddleError::Serialization(e.to_string())
327 }
328}
329
330impl From<heddle_format::compression::CompressionError> for HeddleError {
331 fn from(e: heddle_format::compression::CompressionError) -> Self {
332 HeddleError::Compression(e.to_string())
333 }
334}
335
336pub type Result<T> = std::result::Result<T, HeddleError>;
338
339#[cfg(test)]
340mod tests {
341 use super::{HeddleError, RecoveryDetails};
342
343 #[test]
344 fn safety_refusal_formats_domain_details() {
345 let details = RecoveryDetails::safety_refusal(
346 "example",
347 "error",
348 "hint",
349 "unsafe",
350 "would change",
351 "preserved",
352 );
353
354 assert_eq!(
355 details.to_string(),
356 "error. Unsafe: unsafe. Would change: would change. Preserved: preserved."
357 );
358 }
359
360 #[test]
361 fn recovery_error_displays_structured_error_copy() {
362 let err = HeddleError::recovery(RecoveryDetails::serialization_error("bad marker"));
363
364 assert!(err.to_string().contains("Repository state is corrupted"));
365 assert!(!err.to_string().contains("heddle maintenance fsck --full"));
366 }
367}