Skip to main content

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    /// The same ref name appears more than once in one transaction.
56    #[error("multiple updates for ref '{name}' not allowed")]
57    DuplicateUpdate {
58        /// Duplicate ref name.
59        name: String,
60    },
61
62    /// Symbolic reference resolution exceeded [`crate::refs::SYMREF_MAXDEPTH`].
63    #[error("symbolic ref loop or chain too deep")]
64    SymrefLoop,
65
66    /// The ref (or the target at the end of its symbolic chain) does not exist.
67    #[error("ref not found: {name}")]
68    NotFound {
69        /// Name of the missing ref: the requested name, or the dangling symbolic target.
70        name: String,
71    },
72
73    /// Stored data for a ref or reflog is malformed.
74    #[error("corrupt ref store: {0}")]
75    Corrupt(String),
76
77    /// Filesystem I/O on a git-directory path during ref store work.
78    #[error("{}", crate::git_dir::io_error::format_git_dir_io_message(.path, *operation, &Self::git_dir_io_source(*.os_error)))]
79    GitDirPathIo {
80        /// Display path such as `.git/refs/heads/main` or `.git/worktrees/wt/HEAD.lock`.
81        path: String,
82        /// What the operation was trying to do when I/O failed.
83        operation: GitDirPathOperation,
84        /// Raw OS error code from the failing I/O call.
85        os_error: i32,
86    },
87
88    /// Ref name is not safe to store (traversal, illegal component, etc.).
89    ///
90    /// Every backend checks update names and both sides of reflog and case-only renames
91    /// in [`super::RefStore::prepare`], before taking locks.
92    #[error("refusing to update ref with bad name '{name}'")]
93    InvalidRefName {
94        /// The rejected ref name.
95        name: String,
96    },
97
98    /// A symbolic ref would point at a target Git refuses (outside `refs/` for `HEAD`, not
99    /// safe to store, or the same name as the ref being updated).
100    #[error("refusing to point '{name}' at symbolic target '{target}'")]
101    InvalidSymrefTarget {
102        /// The ref being updated.
103        name: String,
104        /// The symbolic target that was rejected.
105        target: String,
106    },
107
108    /// An update would store the all-zero (null) object id as a direct ref value.
109    ///
110    /// The null id names no object, so Git treats such a ref as broken. Delete a ref with
111    /// `new_value: None` instead. Reflog-only updates
112    /// ([`super::RefUpdateFlags::log_only`]) may still carry the null id.
113    #[error("refusing to point ref '{name}' at the null object id")]
114    NullOid {
115        /// The ref the update targeted.
116        name: String,
117    },
118
119    /// An update would point a ref at an object the repository does not store.
120    ///
121    /// Raised by the stores a [`crate::repo::Repository`] hands out (see
122    /// [`super::RefStore::prepare`]); the bare backends do not look at objects. Set
123    /// [`super::RefUpdateFlags::skip_oid_verification`] to write such a ref on purpose.
124    #[error("refusing to point ref '{name}' at {oid}: no such object in the repository")]
125    MissingObject {
126        /// The ref the update targeted.
127        name: String,
128        /// The object id the update would store.
129        oid: ObjectId,
130    },
131
132    /// An update would point a branch (`refs/heads/…`) or `HEAD` at something other than a
133    /// commit.
134    ///
135    /// Checked alongside [`Self::MissingObject`]. Tags, remote-tracking and other refs may
136    /// name any object.
137    #[error("refusing to point '{name}' at {kind} {oid}: branches and HEAD must name a commit")]
138    NotACommit {
139        /// The branch or `HEAD` the update targeted.
140        name: String,
141        /// The object id the update would store.
142        oid: ObjectId,
143        /// What `oid` actually is.
144        kind: ObjectKind,
145    },
146
147    /// Looking up the object an update would store failed (unreadable pack, I/O, a failing
148    /// custom object store), so the update could not be checked.
149    #[error("cannot check object {oid} for ref '{name}': {source}")]
150    ObjectLookup {
151        /// The ref the update targeted.
152        name: String,
153        /// The object id the update would store.
154        oid: ObjectId,
155        /// The object database error, downcastable to [`crate::error::Error`].
156        source: BackendError,
157    },
158
159    /// Reflog identity or message would forge or corrupt a log line.
160    #[error(transparent)]
161    InvalidReflog(#[from] crate::ident::SignatureError),
162
163    /// A custom backend's own storage failed (database, network, …).
164    ///
165    /// Built-in backends never return this; their I/O failures stay typed elsewhere.
166    #[error(transparent)]
167    Backend(BackendError),
168
169    /// A `reference-transaction` hook refused or could not run on the `prepared` phase.
170    #[error(transparent)]
171    Hook(ReferenceTransactionHookErr),
172}
173
174/// [`RefStoreError::Hook`] payload ( [`crate::hooks::HookError`] is not `Clone`).
175#[derive(Clone, Debug)]
176pub struct ReferenceTransactionHookErr(Arc<crate::hooks::HookError>);
177
178impl PartialEq for ReferenceTransactionHookErr {
179    fn eq(&self, other: &Self) -> bool {
180        self.0.to_string() == other.0.to_string()
181    }
182}
183
184impl Eq for ReferenceTransactionHookErr {}
185
186impl From<crate::hooks::HookError> for ReferenceTransactionHookErr {
187    fn from(value: crate::hooks::HookError) -> Self {
188        Self(Arc::new(value))
189    }
190}
191
192impl ReferenceTransactionHookErr {
193    pub(crate) fn into_hook_error(self) -> crate::hooks::HookError {
194        Arc::try_unwrap(self.0).unwrap_or_else(|arc| match arc.as_ref() {
195            crate::hooks::HookError::Failed {
196                hook_name,
197                status,
198                output,
199            } => crate::hooks::HookError::Failed {
200                hook_name: hook_name.clone(),
201                status: *status,
202                output: output.clone(),
203            },
204            crate::hooks::HookError::Spawn {
205                hook_name,
206                path,
207                source,
208                output,
209            } => crate::hooks::HookError::Spawn {
210                hook_name: hook_name.clone(),
211                path: path.clone(),
212                source: std::io::Error::new(source.kind(), source.to_string()),
213                output: output.clone(),
214            },
215            crate::hooks::HookError::Internal(msg) => {
216                crate::hooks::HookError::Internal(msg.clone())
217            }
218        })
219    }
220}
221
222impl std::fmt::Display for ReferenceTransactionHookErr {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        self.0.fmt(f)
225    }
226}
227
228impl std::error::Error for ReferenceTransactionHookErr {
229    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
230        Some(self.0.as_ref())
231    }
232}
233
234impl RefStoreError {
235    pub(crate) fn git_dir_path_io(
236        path: String,
237        operation: GitDirPathOperation,
238        source: std::io::Error,
239    ) -> Self {
240        Self::GitDirPathIo {
241            path,
242            operation,
243            os_error: source.raw_os_error().unwrap_or(0),
244        }
245    }
246
247    pub(crate) fn git_dir_io_source(os_error: i32) -> std::io::Error {
248        if os_error == 0 {
249            std::io::Error::from(std::io::ErrorKind::Other)
250        } else {
251            std::io::Error::from_raw_os_error(os_error)
252        }
253    }
254
255    pub(crate) fn not_found(name: &str) -> Self {
256        Self::NotFound {
257            name: name.to_owned(),
258        }
259    }
260
261    /// Wrap a custom backend's storage error as [`Self::Backend`].
262    pub fn backend(err: impl std::error::Error + Send + Sync + 'static) -> Self {
263        Self::Backend(BackendError::new(err))
264    }
265}
266
267/// A custom ref backend's storage error, carried by [`RefStoreError::Backend`].
268///
269/// Cheap to clone (the error is shared). Two values are equal only when they share the same
270/// underlying error. Use [`Self::downcast_ref`] to recover the backend's concrete type.
271#[derive(Clone)]
272pub struct BackendError(Arc<dyn std::error::Error + Send + Sync + 'static>);
273
274impl BackendError {
275    /// Wrap `err`.
276    pub fn new(err: impl std::error::Error + Send + Sync + 'static) -> Self {
277        Self(Arc::new(err))
278    }
279
280    /// Borrow the wrapped error.
281    #[must_use]
282    pub fn get_ref(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
283        self.0.as_ref()
284    }
285
286    /// The wrapped error as `E`, when it has that type.
287    #[must_use]
288    pub fn downcast_ref<E: std::error::Error + 'static>(&self) -> Option<&E> {
289        self.0.downcast_ref::<E>()
290    }
291}
292
293impl fmt::Debug for BackendError {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        fmt::Debug::fmt(&self.0, f)
296    }
297}
298
299impl fmt::Display for BackendError {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        fmt::Display::fmt(&self.0, f)
302    }
303}
304
305impl std::error::Error for BackendError {
306    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
307        self.0.source()
308    }
309}
310
311impl PartialEq for BackendError {
312    fn eq(&self, other: &Self) -> bool {
313        Arc::ptr_eq(&self.0, &other.0)
314    }
315}
316
317impl Eq for BackendError {}
318
319impl From<BackendError> for RefStoreError {
320    fn from(err: BackendError) -> Self {
321        Self::Backend(err)
322    }
323}
324
325impl From<RefnameUnavailable> for RefStoreError {
326    fn from(reason: RefnameUnavailable) -> Self {
327        Self::NameUnavailable { reason }
328    }
329}