grit_lib/refs/store/error.rs
1//! Errors for [`super::RefStore`] operations.
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::git_dir::GitDirPathOperation;
7use crate::objects::{ObjectId, ObjectKind};
8use crate::refs::RefnameUnavailable;
9
10use super::{Expected, RawRef};
11
12/// Failure modes for reference store reads and transactions.
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14#[non_exhaustive]
15pub enum RefStoreError {
16 /// Reflog contents changed between a snapshot and a locked [`super::RefStore::rewrite_reflog`]
17 /// re-read. Callers that compare against an earlier snapshot should retry.
18 #[error("reflog '{name}' changed during rewrite")]
19 ReflogRewriteStale {
20 /// Ref whose log no longer matched the expected prefix.
21 name: String,
22 },
23
24 /// Another writer holds a lock this transaction needs.
25 ///
26 /// For per-ref backends (files) that is the lock of `name` itself; for a backend with one
27 /// writer slot for the whole store ([`super::MemoryRefStore`], a database with a single
28 /// write transaction) it means that slot is busy, whichever refs the holder updates.
29 /// Transient: [`crate::refs::prepare_with_lock_wait`] retries it.
30 #[error("cannot lock ref '{name}': lock held")]
31 LockHeld {
32 /// A ref of the failed transaction: the locked ref, or the transaction's first ref
33 /// when the whole store is locked. Always one of the failed transaction's own refs.
34 name: String,
35 },
36
37 /// Compare-and-swap or existence check failed for an update.
38 #[error("ref '{name}' did not match expected state")]
39 ExpectedMismatch {
40 /// Ref that failed the check.
41 name: String,
42 /// What the caller required.
43 expected: Expected,
44 /// What was observed (`None` when the ref is absent).
45 actual: Option<RawRef>,
46 },
47
48 /// A ref name cannot be created because of a directory/file conflict.
49 #[error("cannot lock ref '{}': {}", reason.refname(), reason.lock_message_suffix())]
50 NameUnavailable {
51 /// Structured conflict reason (Git `refs_verify_refname_available` style).
52 reason: RefnameUnavailable,
53 },
54
55 /// A create was requested for a ref name that already exists under ASCII case folding
56 /// when `core.ignorecase` is enabled (or the git directory is on a case-insensitive
57 /// volume).
58 #[error("reference '{name}' already exists")]
59 CreateExists {
60 /// Ref the caller tried to create.
61 name: String,
62 },
63
64 /// Two creates in one transaction differ only by ASCII case while `core.ignorecase`
65 /// is enabled. The files ref backend cannot store both as loose refs; packed-refs
66 /// during an initial clone can hold both spellings.
67 #[error("cannot store refs '{name}' and '{other}' together when core.ignorecase is enabled")]
68 CaseConflict {
69 /// One ref in the conflicting pair.
70 name: String,
71 /// The other ref, equal under ASCII case folding.
72 other: String,
73 },
74
75 /// The same ref name appears more than once in one transaction.
76 #[error("multiple updates for ref '{name}' not allowed")]
77 DuplicateUpdate {
78 /// Duplicate ref name.
79 name: String,
80 },
81
82 /// Symbolic reference resolution exceeded [`crate::refs::SYMREF_MAXDEPTH`].
83 #[error("symbolic ref loop or chain too deep")]
84 SymrefLoop,
85
86 /// The ref (or the target at the end of its symbolic chain) does not exist.
87 #[error("ref not found: {name}")]
88 NotFound {
89 /// Name of the missing ref: the requested name, or the dangling symbolic target.
90 name: String,
91 },
92
93 /// Stored data for a ref or reflog is malformed.
94 #[error("corrupt ref store: {0}")]
95 Corrupt(String),
96
97 /// Filesystem I/O on a git-directory path during ref store work.
98 #[error("{}", crate::git_dir::io_error::format_git_dir_io_message(.path, *operation, &Self::git_dir_io_source(*.os_error)))]
99 GitDirPathIo {
100 /// Display path such as `.git/refs/heads/main` or `.git/worktrees/wt/HEAD.lock`.
101 path: String,
102 /// What the operation was trying to do when I/O failed.
103 operation: GitDirPathOperation,
104 /// Raw OS error code from the failing I/O call.
105 os_error: i32,
106 },
107
108 /// Ref name is not safe to store (traversal, illegal component, etc.).
109 ///
110 /// Every backend checks update names and both sides of reflog and case-only renames
111 /// in [`super::RefStore::prepare`], before taking locks.
112 #[error("refusing to update ref with bad name '{name}'")]
113 InvalidRefName {
114 /// The rejected ref name.
115 name: String,
116 },
117
118 /// A symbolic ref would point at a target Git refuses (outside `refs/` for `HEAD`, not
119 /// safe to store, or the same name as the ref being updated).
120 #[error("refusing to point '{name}' at symbolic target '{target}'")]
121 InvalidSymrefTarget {
122 /// The ref being updated.
123 name: String,
124 /// The symbolic target that was rejected.
125 target: String,
126 },
127
128 /// An update would store the all-zero (null) object id as a direct ref value.
129 ///
130 /// The null id names no object, so Git treats such a ref as broken. Delete a ref with
131 /// `new_value: None` instead. Reflog-only updates
132 /// ([`super::RefUpdateFlags::log_only`]) may still carry the null id.
133 #[error("refusing to point ref '{name}' at the null object id")]
134 NullOid {
135 /// The ref the update targeted.
136 name: String,
137 },
138
139 /// An update would point a ref at an object the repository does not store.
140 ///
141 /// Raised by the stores a [`crate::repo::Repository`] hands out (see
142 /// [`super::RefStore::prepare`]); the bare backends do not look at objects. Set
143 /// [`super::RefUpdateFlags::skip_oid_verification`] to write such a ref on purpose.
144 #[error("refusing to point ref '{name}' at {oid}: no such object in the repository")]
145 MissingObject {
146 /// The ref the update targeted.
147 name: String,
148 /// The object id the update would store.
149 oid: ObjectId,
150 },
151
152 /// An update would point a branch (`refs/heads/…`) or `HEAD` at something other than a
153 /// commit.
154 ///
155 /// Checked alongside [`Self::MissingObject`]. Tags, remote-tracking and other refs may
156 /// name any object.
157 #[error("refusing to point '{name}' at {kind} {oid}: branches and HEAD must name a commit")]
158 NotACommit {
159 /// The branch or `HEAD` the update targeted.
160 name: String,
161 /// The object id the update would store.
162 oid: ObjectId,
163 /// What `oid` actually is.
164 kind: ObjectKind,
165 },
166
167 /// Looking up the object an update would store failed (unreadable pack, I/O, a failing
168 /// custom object store), so the update could not be checked.
169 #[error("cannot check object {oid} for ref '{name}': {source}")]
170 ObjectLookup {
171 /// The ref the update targeted.
172 name: String,
173 /// The object id the update would store.
174 oid: ObjectId,
175 /// The object database error, downcastable to [`crate::error::Error`].
176 source: BackendError,
177 },
178
179 /// Reflog identity or message would forge or corrupt a log line.
180 #[error(transparent)]
181 InvalidReflog(#[from] crate::ident::SignatureError),
182
183 /// A custom backend's own storage failed (database, network, …).
184 ///
185 /// Built-in backends never return this; their I/O failures stay typed elsewhere.
186 #[error(transparent)]
187 Backend(BackendError),
188
189 /// A `reference-transaction` hook refused or could not run on the `prepared` phase.
190 #[error(transparent)]
191 Hook(ReferenceTransactionHookErr),
192}
193
194/// [`RefStoreError::Hook`] payload ( [`crate::hooks::HookError`] is not `Clone`).
195#[derive(Clone, Debug)]
196pub struct ReferenceTransactionHookErr(Arc<crate::hooks::HookError>);
197
198impl PartialEq for ReferenceTransactionHookErr {
199 fn eq(&self, other: &Self) -> bool {
200 self.0.to_string() == other.0.to_string()
201 }
202}
203
204impl Eq for ReferenceTransactionHookErr {}
205
206impl From<crate::hooks::HookError> for ReferenceTransactionHookErr {
207 fn from(value: crate::hooks::HookError) -> Self {
208 Self(Arc::new(value))
209 }
210}
211
212impl ReferenceTransactionHookErr {
213 pub(crate) fn into_hook_error(self) -> crate::hooks::HookError {
214 Arc::try_unwrap(self.0).unwrap_or_else(|arc| match arc.as_ref() {
215 crate::hooks::HookError::Failed {
216 hook_name,
217 status,
218 output,
219 } => crate::hooks::HookError::Failed {
220 hook_name: hook_name.clone(),
221 status: *status,
222 output: output.clone(),
223 },
224 crate::hooks::HookError::Spawn {
225 hook_name,
226 path,
227 source,
228 output,
229 } => crate::hooks::HookError::Spawn {
230 hook_name: hook_name.clone(),
231 path: path.clone(),
232 source: std::io::Error::new(source.kind(), source.to_string()),
233 output: output.clone(),
234 },
235 crate::hooks::HookError::Internal(msg) => {
236 crate::hooks::HookError::Internal(msg.clone())
237 }
238 crate::hooks::HookError::Cancelled => crate::hooks::HookError::Cancelled,
239 })
240 }
241}
242
243impl std::fmt::Display for ReferenceTransactionHookErr {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 self.0.fmt(f)
246 }
247}
248
249impl std::error::Error for ReferenceTransactionHookErr {
250 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
251 Some(self.0.as_ref())
252 }
253}
254
255impl RefStoreError {
256 pub(crate) fn git_dir_path_io(
257 path: String,
258 operation: GitDirPathOperation,
259 source: std::io::Error,
260 ) -> Self {
261 Self::GitDirPathIo {
262 path,
263 operation,
264 os_error: source.raw_os_error().unwrap_or(0),
265 }
266 }
267
268 pub(crate) fn git_dir_io_source(os_error: i32) -> std::io::Error {
269 if os_error == 0 {
270 std::io::Error::from(std::io::ErrorKind::Other)
271 } else {
272 std::io::Error::from_raw_os_error(os_error)
273 }
274 }
275
276 pub(crate) fn not_found(name: &str) -> Self {
277 Self::NotFound {
278 name: name.to_owned(),
279 }
280 }
281
282 /// Wrap a custom backend's storage error as [`Self::Backend`].
283 pub fn backend(err: impl std::error::Error + Send + Sync + 'static) -> Self {
284 Self::Backend(BackendError::new(err))
285 }
286}
287
288/// A custom ref backend's storage error, carried by [`RefStoreError::Backend`].
289///
290/// Cheap to clone (the error is shared). Two values are equal only when they share the same
291/// underlying error. Use [`Self::downcast_ref`] to recover the backend's concrete type.
292#[derive(Clone)]
293pub struct BackendError(Arc<dyn std::error::Error + Send + Sync + 'static>);
294
295impl BackendError {
296 /// Wrap `err`.
297 pub fn new(err: impl std::error::Error + Send + Sync + 'static) -> Self {
298 Self(Arc::new(err))
299 }
300
301 /// Borrow the wrapped error.
302 #[must_use]
303 pub fn get_ref(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
304 self.0.as_ref()
305 }
306
307 /// The wrapped error as `E`, when it has that type.
308 #[must_use]
309 pub fn downcast_ref<E: std::error::Error + 'static>(&self) -> Option<&E> {
310 self.0.downcast_ref::<E>()
311 }
312}
313
314impl fmt::Debug for BackendError {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 fmt::Debug::fmt(&self.0, f)
317 }
318}
319
320impl fmt::Display for BackendError {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 fmt::Display::fmt(&self.0, f)
323 }
324}
325
326impl std::error::Error for BackendError {
327 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
328 self.0.source()
329 }
330}
331
332impl PartialEq for BackendError {
333 fn eq(&self, other: &Self) -> bool {
334 Arc::ptr_eq(&self.0, &other.0)
335 }
336}
337
338impl Eq for BackendError {}
339
340impl From<BackendError> for RefStoreError {
341 fn from(err: BackendError) -> Self {
342 Self::Backend(err)
343 }
344}
345
346impl From<RefnameUnavailable> for RefStoreError {
347 fn from(reason: RefnameUnavailable) -> Self {
348 Self::NameUnavailable { reason }
349 }
350}