1use core::fmt;
2
3#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
5pub enum TransactionPhase {
6 Open,
7 Validate,
8 Lock,
9 DryRun,
10 Prepare,
11 Stage,
12 Commit,
13 Rollback,
14 Recover,
15 Cleanup,
16}
17
18impl TransactionPhase {
19 #[must_use]
20 pub const fn as_str(self) -> &'static str {
21 match self {
22 Self::Open => "OPEN",
23 Self::Validate => "VALIDATE",
24 Self::Lock => "LOCK",
25 Self::DryRun => "DRY_RUN",
26 Self::Prepare => "PREPARE",
27 Self::Stage => "STAGE",
28 Self::Commit => "COMMIT",
29 Self::Rollback => "ROLLBACK",
30 Self::Recover => "RECOVER",
31 Self::Cleanup => "CLEANUP",
32 }
33 }
34}
35
36impl fmt::Display for TransactionPhase {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 formatter.write_str(self.as_str())
39 }
40}
41
42#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub enum WorktreeErrorCode {
45 InvalidRoot,
46 InvalidOptions,
47 RootBusy,
48 RecoveryRequired,
49 InvalidPlan,
50 OperationConflict,
51 PathExists,
52 PathMissing,
53 ReservedPath,
54 PathEscape,
55 SymlinkNotAllowed,
56 CrossFilesystem,
57 NotRegularFile,
58 HardlinkNotAllowed,
59 ReadOnlyFile,
60 SourceTooLarge,
61 TransactionTooLarge,
62 NonUtf8Source,
63 SourceHashMismatch,
64 ConcurrentModification,
65 EditRejected,
66 StageFailed,
67 DurabilityFailed,
68 CommitFailed,
69 RollbackFailed,
70 JournalCorrupt,
71 UndoNotFound,
72 UndoConflict,
73 UndoStoreFull,
74 UndoCorrupt,
75 UndoFailed,
76 WorkerPanicked,
77 Cancelled,
78 Io,
79}
80
81impl WorktreeErrorCode {
82 #[must_use]
83 pub const fn as_str(self) -> &'static str {
84 match self {
85 Self::InvalidRoot => "INVALID_ROOT",
86 Self::InvalidOptions => "INVALID_OPTIONS",
87 Self::RootBusy => "ROOT_BUSY",
88 Self::RecoveryRequired => "RECOVERY_REQUIRED",
89 Self::InvalidPlan => "INVALID_PLAN",
90 Self::OperationConflict => "OPERATION_CONFLICT",
91 Self::PathExists => "PATH_EXISTS",
92 Self::PathMissing => "PATH_MISSING",
93 Self::ReservedPath => "RESERVED_PATH",
94 Self::PathEscape => "PATH_ESCAPE",
95 Self::SymlinkNotAllowed => "SYMLINK_NOT_ALLOWED",
96 Self::CrossFilesystem => "CROSS_FILESYSTEM",
97 Self::NotRegularFile => "NOT_REGULAR_FILE",
98 Self::HardlinkNotAllowed => "HARDLINK_NOT_ALLOWED",
99 Self::ReadOnlyFile => "READ_ONLY_FILE",
100 Self::SourceTooLarge => "SOURCE_TOO_LARGE",
101 Self::TransactionTooLarge => "TRANSACTION_TOO_LARGE",
102 Self::NonUtf8Source => "NON_UTF8_SOURCE",
103 Self::SourceHashMismatch => "SOURCE_HASH_MISMATCH",
104 Self::ConcurrentModification => "CONCURRENT_MODIFICATION",
105 Self::EditRejected => "EDIT_REJECTED",
106 Self::StageFailed => "STAGE_FAILED",
107 Self::DurabilityFailed => "DURABILITY_FAILED",
108 Self::CommitFailed => "COMMIT_FAILED",
109 Self::RollbackFailed => "ROLLBACK_FAILED",
110 Self::JournalCorrupt => "JOURNAL_CORRUPT",
111 Self::UndoNotFound => "UNDO_NOT_FOUND",
112 Self::UndoConflict => "UNDO_CONFLICT",
113 Self::UndoStoreFull => "UNDO_STORE_FULL",
114 Self::UndoCorrupt => "UNDO_CORRUPT",
115 Self::UndoFailed => "UNDO_FAILED",
116 Self::WorkerPanicked => "WORKER_PANICKED",
117 Self::Cancelled => "CANCELLED",
118 Self::Io => "IO",
119 }
120 }
121}
122
123impl fmt::Display for WorktreeErrorCode {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 formatter.write_str(self.as_str())
126 }
127}
128
129#[derive(Debug)]
131pub struct WorktreeError {
132 code: WorktreeErrorCode,
133 phase: TransactionPhase,
134 message: String,
135 path: Option<String>,
136 file_index: Option<usize>,
137 transaction_id: Option<String>,
138 recovery_required: bool,
139 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
140}
141
142impl WorktreeError {
143 pub(crate) fn new(
144 code: WorktreeErrorCode,
145 phase: TransactionPhase,
146 message: impl Into<String>,
147 ) -> Self {
148 Self {
149 code,
150 phase,
151 message: message.into(),
152 path: None,
153 file_index: None,
154 transaction_id: None,
155 recovery_required: matches!(
156 code,
157 WorktreeErrorCode::RecoveryRequired | WorktreeErrorCode::RollbackFailed
158 ),
159 source: None,
160 }
161 }
162
163 pub(crate) fn with_source<E>(
164 code: WorktreeErrorCode,
165 phase: TransactionPhase,
166 message: impl Into<String>,
167 source: E,
168 ) -> Self
169 where
170 E: std::error::Error + Send + Sync + 'static,
171 {
172 let mut error = Self::new(code, phase, message);
173 error.source = Some(Box::new(source));
174 error
175 }
176
177 #[must_use]
178 pub(crate) fn at_path(mut self, path: impl Into<String>) -> Self {
179 self.path = Some(path.into());
180 self
181 }
182
183 #[must_use]
184 pub(crate) const fn at_file(mut self, file_index: usize) -> Self {
185 self.file_index = Some(file_index);
186 self
187 }
188
189 #[must_use]
190 pub(crate) fn in_transaction(mut self, transaction_id: impl Into<String>) -> Self {
191 self.transaction_id = Some(transaction_id.into());
192 self
193 }
194
195 #[must_use]
196 pub(crate) const fn requiring_recovery(mut self) -> Self {
197 self.recovery_required = true;
198 self
199 }
200
201 #[must_use]
202 pub const fn code(&self) -> WorktreeErrorCode {
203 self.code
204 }
205
206 #[must_use]
207 pub const fn phase(&self) -> TransactionPhase {
208 self.phase
209 }
210
211 #[must_use]
212 pub fn message(&self) -> &str {
213 &self.message
214 }
215
216 #[must_use]
217 pub fn path(&self) -> Option<&str> {
218 self.path.as_deref()
219 }
220
221 #[must_use]
222 pub const fn file_index(&self) -> Option<usize> {
223 self.file_index
224 }
225
226 #[must_use]
227 pub fn transaction_id(&self) -> Option<&str> {
228 self.transaction_id.as_deref()
229 }
230
231 #[must_use]
232 pub const fn recovery_required(&self) -> bool {
233 self.recovery_required
234 }
235}
236
237impl fmt::Display for WorktreeError {
238 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
239 write!(formatter, "{} during {}", self.code, self.phase)?;
240 if let Some(path) = &self.path {
241 write!(formatter, " at {path}")?;
242 }
243 if let Some(index) = self.file_index {
244 write!(formatter, " [file {index}]")?;
245 }
246 write!(formatter, ": {}", self.message)
247 }
248}
249
250impl std::error::Error for WorktreeError {
251 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
252 self.source
253 .as_deref()
254 .map(|source| source as &(dyn std::error::Error + 'static))
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use crate::error::{TransactionPhase, WorktreeError, WorktreeErrorCode};
261
262 #[test]
263 fn stable_fields_survive_context_builders() {
264 let error = WorktreeError::new(
265 WorktreeErrorCode::CommitFailed,
266 TransactionPhase::Commit,
267 "replace failed",
268 )
269 .at_path("src/lib.rs")
270 .at_file(2)
271 .in_transaction("abc")
272 .requiring_recovery();
273
274 assert_eq!(error.code().as_str(), "COMMIT_FAILED");
275 assert_eq!(error.path(), Some("src/lib.rs"));
276 assert_eq!(error.file_index(), Some(2));
277 assert_eq!(error.transaction_id(), Some("abc"));
278 assert!(error.recovery_required());
279 }
280}