1use std::{error::Error, fmt, path::Path};
5
6use crate::object::{ChangeId, ContentHash, TreeError};
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}
18
19impl RecoveryDetails {
20 pub fn safety_refusal(
21 kind: &'static str,
22 error: impl Into<String>,
23 hint: impl Into<String>,
24 unsafe_condition: impl Into<String>,
25 would_change: impl Into<String>,
26 already_preserved: impl Into<String>,
27 ) -> Self {
28 Self {
29 kind,
30 error: error.into(),
31 hint: hint.into(),
32 unsafe_condition: unsafe_condition.into(),
33 would_change: would_change.into(),
34 preserved: already_preserved.into(),
35 }
36 }
37
38 pub fn invalid_usage(
39 kind: &'static str,
40 error: impl Into<String>,
41 hint: impl Into<String>,
42 ) -> Self {
43 Self::safety_refusal(
44 kind,
45 error,
46 hint,
47 "the command arguments do not describe a valid operation",
48 "running with ambiguous or invalid arguments could target the wrong repository state or metadata",
49 "no repository objects, refs, metadata, or worktree files were changed",
50 )
51 }
52
53 pub fn feature_unavailable(command: &str, feature: &str) -> Self {
54 Self::safety_refusal(
55 "feature_unavailable",
56 format!("{command} requires building heddle with --features {feature}"),
57 format!(
58 "Use a binary built with the `{feature}` feature, or rerun without the feature-specific flag."
59 ),
60 format!("this heddle binary was built without the `{feature}` feature"),
61 format!("{command} cannot run because the requested analysis engine is unavailable"),
62 "repository state, refs, and worktree files were left unchanged",
63 )
64 }
65
66 pub fn serialization_error(detail: impl fmt::Display) -> Self {
67 Self::safety_refusal(
68 "state_corrupted",
69 "Repository state is corrupted or unreadable",
70 "Inspect repository integrity before attempting repair.",
71 format!("a stored repository object failed to decode: {detail}"),
72 "continuing would read or write through repository state Heddle cannot decode",
73 "the command stopped before mutating repository state; intact objects were left unchanged",
74 )
75 }
76
77 pub fn repository_integrity_error(error: impl Into<String>) -> Self {
78 Self::safety_refusal(
79 "repository_integrity_error",
80 error,
81 "Inspect repository integrity, then restore or repair the reported object/ref.",
82 "repository object or ref integrity did not pass validation",
83 "continuing could compound corruption or hide the missing object",
84 "the command stopped before applying the requested mutation",
85 )
86 }
87
88 pub fn repository_not_found(path: &Path) -> Self {
89 Self::safety_refusal(
90 "repository_not_found",
91 format!("repository not found at {}", path.display()),
92 "Initialize the requested repository before running repository commands.",
93 format!("no Heddle repository was found at '{}'", path.display()),
94 "the command cannot inspect or change repository state until initialization",
95 "no repository objects, refs, metadata, or worktree files were changed",
96 )
97 }
98
99 pub fn state_not_found(state_id: impl fmt::Display) -> Self {
100 Self::safety_refusal(
101 "state_not_found",
102 format!("State not found: {state_id}"),
103 "List recent states with `heddle log`, then choose an existing state id.",
104 "the requested state id does not exist in this repository",
105 "continuing with a guessed state could target the wrong history point",
106 "repository state, refs, metadata, and worktree files were left unchanged",
107 )
108 }
109}
110
111impl fmt::Display for RecoveryDetails {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(
114 f,
115 "{}. Unsafe: {}. Would change: {}. Preserved: {}.",
116 self.error, self.unsafe_condition, self.would_change, self.preserved
117 )?;
118 Ok(())
119 }
120}
121
122impl Error for RecoveryDetails {}
123
124#[derive(Debug, thiserror::Error)]
126pub enum HeddleError {
127 #[error("{0}")]
128 Recovery(Box<RecoveryDetails>),
129 #[error("object not found: {0}")]
130 NotFound(String),
131 #[error("No merge in progress")]
132 NoMergeInProgress,
133 #[error("state not found: {0}")]
134 StateNotFound(ChangeId),
135 #[error("invalid object: {0}")]
136 InvalidObject(String),
137 #[error("repository not found at {0}")]
138 RepositoryNotFound(std::path::PathBuf),
139 #[error("repository already exists at {0}")]
140 RepositoryExists(std::path::PathBuf),
141 #[error(
142 "repository config at {path} uses repository format {found} but this binary supports {supported}; upgrade heddle or run `heddle migrate`"
143 )]
144 RepositoryFormatTooNew {
145 path: std::path::PathBuf,
146 found: u32,
147 supported: u32,
148 },
149 #[error("io error: {0}")]
150 Io(#[from] std::io::Error),
151 #[error("serialization error: {0}")]
152 Serialization(String),
153 #[error("configuration error: {0}")]
154 Config(String),
155 #[error("configuration parse error at {path}: {source}")]
156 ConfigParse {
157 path: std::path::PathBuf,
158 #[source]
163 source: toml::de::Error,
164 },
165 #[error(
166 "invalid {key}: '{value}' — valid values are {} (in {path})",
167 valid_values.join(" or ")
168 )]
169 ConfigInvalidValue {
170 path: std::path::PathBuf,
171 key: String,
172 value: String,
173 valid_values: Vec<String>,
174 },
175 #[error("conflict: {0}")]
176 Conflict(String),
177 #[error("compression error: {0}")]
178 Compression(String),
179 #[error("invalid ref name: {0}")]
180 InvalidRefName(String),
181 #[error("file too large: {0} bytes")]
182 InvalidFileSize(u64),
183 #[error("symlink target escapes repository: {0}")]
184 InvalidSymlinkTarget(std::path::PathBuf),
185 #[error("object corruption: expected {expected}, found {found}")]
186 Corruption {
187 expected: ContentHash,
188 found: ContentHash,
189 },
190 #[error(
191 "missing {object_type} object: {id} (run `heddle fsck --full` to inspect store integrity)"
192 )]
193 MissingObject { object_type: String, id: String },
194 #[error("invalid tree entry: {0}")]
195 InvalidTreeEntry(#[from] TreeError),
196}
197
198impl HeddleError {
199 pub fn recovery(details: RecoveryDetails) -> Self {
200 HeddleError::Recovery(Box::new(details))
201 }
202}
203
204impl From<rmp_serde::encode::Error> for HeddleError {
205 fn from(e: rmp_serde::encode::Error) -> Self {
206 HeddleError::Serialization(e.to_string())
207 }
208}
209
210impl From<rmp_serde::decode::Error> for HeddleError {
211 fn from(e: rmp_serde::decode::Error) -> Self {
212 HeddleError::Serialization(e.to_string())
213 }
214}
215
216impl From<toml::de::Error> for HeddleError {
217 fn from(e: toml::de::Error) -> Self {
218 HeddleError::Config(e.to_string())
219 }
220}
221
222impl From<toml::ser::Error> for HeddleError {
223 fn from(e: toml::ser::Error) -> Self {
224 HeddleError::Config(e.to_string())
225 }
226}
227
228impl From<serde_json::Error> for HeddleError {
229 fn from(e: serde_json::Error) -> Self {
230 HeddleError::Serialization(e.to_string())
231 }
232}
233
234pub type Result<T> = std::result::Result<T, HeddleError>;
236
237#[cfg(test)]
238mod tests {
239 use super::{HeddleError, RecoveryDetails};
240
241 #[test]
242 fn safety_refusal_formats_domain_details() {
243 let details = RecoveryDetails::safety_refusal(
244 "example",
245 "error",
246 "hint",
247 "unsafe",
248 "would change",
249 "preserved",
250 );
251
252 assert_eq!(
253 details.to_string(),
254 "error. Unsafe: unsafe. Would change: would change. Preserved: preserved."
255 );
256 }
257
258 #[test]
259 fn recovery_error_displays_structured_error_copy() {
260 let err = HeddleError::recovery(RecoveryDetails::serialization_error("bad marker"));
261
262 assert!(err.to_string().contains("Repository state is corrupted"));
263 assert!(!err.to_string().contains("heddle fsck --full"));
264 }
265}