Skip to main content

gix_ref/store/file/transaction/
prepare.rs

1use crate::{
2    FullName, FullNameRef, Reference, Target, packed,
3    packed::transaction::buffer_into_transaction,
4    store_impl::{
5        file,
6        file::{
7            Transaction, loose,
8            transaction::{Edit, PackedRefs},
9        },
10    },
11    transaction::{Change, LogChange, PreviousValue, RefEdit, RefEditsExt, RefLog},
12};
13
14impl Transaction<'_, '_> {
15    /// Read the current value of a reference from loose storage, falling back to packed refs.
16    ///
17    /// Must be called while holding the lock for `name` so the subsequent CAS check
18    /// compares against a value that cannot be changed concurrently.
19    fn read_existing_ref(
20        store: &file::Store,
21        name: &FullNameRef,
22        packed: Option<&packed::Buffer>,
23    ) -> Result<Option<Reference>, Error> {
24        store
25            .ref_contents(name)
26            .map_err(Error::from)
27            .and_then(|maybe_loose| {
28                maybe_loose
29                    .map(|buf| {
30                        loose::Reference::try_from_path(name.to_owned(), &buf, store.object_hash)
31                            .map(Reference::from)
32                            .map_err(Error::from)
33                    })
34                    .transpose()
35            })
36            .or_else(|err| match err {
37                Error::ReferenceDecode(_) => Ok(None),
38                other => Err(other),
39            })
40            .and_then(|maybe_loose| match (maybe_loose, packed) {
41                (None, Some(packed)) => packed
42                    .try_find(name)
43                    .map(|opt| opt.map(Into::into))
44                    .map_err(Error::from),
45                (None, None) => Ok(None),
46                (maybe_loose, _) => Ok(maybe_loose),
47            })
48    }
49
50    /// Map a lock-acquisition failure to our error type, surfacing genuine I/O errors
51    /// (such as a path collision reported as `NotADirectory`) as [`Error::Io`] rather than
52    /// burying them in [`Error::LockAcquire`], which is reserved for actual contention.
53    // This happens for path collisions where `a` is a ref file, and `a/b` is the lock to be created.
54    fn lock_acquire_error(err: gix_lock::acquire::Error, full_name: &str) -> Error {
55        match err {
56            gix_lock::acquire::Error::Io(err) => Error::Io(err),
57            source => Error::LockAcquire {
58                source,
59                full_name: full_name.into(),
60            },
61        }
62    }
63
64    fn lock_ref_and_apply_change(
65        store: &file::Store,
66        lock_fail_mode: gix_lock::acquire::Fail,
67        packed: Option<&packed::Buffer>,
68        change: &mut Edit,
69        direct_to_packed_refs: bool,
70    ) -> Result<(), Error> {
71        use std::io::Write;
72        assert!(
73            change.lock.is_none(),
74            "locks can only be acquired once and it's all or nothing"
75        );
76
77        // Reject Windows reserved device names before acquiring the lock.
78        // The lock file itself (e.g. `CON.lock`) is also a device name,
79        // so acquiring it would fail or open the device instead of
80        // returning the configured validation error.
81        store.check_windows_device_name(change.update.name.as_ref())?;
82
83        let lock = match &mut change.update.change {
84            Change::Delete { expected, .. } => {
85                let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref());
86                let lock = gix_lock::Marker::acquire_to_hold_resource(
87                    base.join(relative_path.as_ref()),
88                    lock_fail_mode,
89                    Some(base.clone().into_owned()),
90                )
91                .map_err(|err| Self::lock_acquire_error(err, "borrowcheck won't allow change.name()"))?;
92
93                let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?;
94
95                match (&expected, &existing_ref) {
96                    (PreviousValue::MustNotExist, _) => {
97                        panic!("BUG: MustNotExist constraint makes no sense if references are to be deleted")
98                    }
99                    (PreviousValue::ExistingMustMatch(_) | PreviousValue::Any, None)
100                    | (PreviousValue::MustExist | PreviousValue::Any, Some(_)) => {}
101                    (PreviousValue::MustExist | PreviousValue::MustExistAndMatch(_), None) => {
102                        return Err(Error::DeleteReferenceMustExist {
103                            full_name: change.name(),
104                        });
105                    }
106                    (
107                        PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous),
108                        Some(existing),
109                    ) => {
110                        let actual = existing.target.clone();
111                        if *previous != actual {
112                            let expected = previous.clone();
113                            return Err(Error::ReferenceOutOfDate {
114                                full_name: change.name(),
115                                expected,
116                                actual,
117                            });
118                        }
119                    }
120                }
121
122                // Keep the previous value for the caller and ourselves. Maybe they want to keep a log of sorts.
123                if let Some(existing) = existing_ref {
124                    *expected = PreviousValue::MustExistAndMatch(existing.target);
125                }
126
127                Some(lock)
128            }
129            Change::Update { expected, new, .. } => {
130                let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref());
131                let obtain_lock = || {
132                    gix_lock::File::acquire_to_update_resource(
133                        base.join(relative_path.as_ref()),
134                        lock_fail_mode,
135                        Some(base.clone().into_owned()),
136                    )
137                    .map_err(|err| {
138                        Self::lock_acquire_error(
139                            err,
140                            "borrowcheck won't allow change.name() and this will be corrected by caller",
141                        )
142                    })
143                };
144                let mut lock = obtain_lock()?;
145
146                let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?;
147
148                match (&expected, &existing_ref) {
149                    (PreviousValue::Any, _)
150                    | (PreviousValue::MustExist, Some(_))
151                    | (PreviousValue::MustNotExist | PreviousValue::ExistingMustMatch(_), None) => {}
152                    (PreviousValue::MustExist, None) => {
153                        let expected = Target::Object(store.object_hash.null());
154                        let full_name = change.name();
155                        return Err(Error::MustExist { full_name, expected });
156                    }
157                    (PreviousValue::MustNotExist, Some(existing)) => {
158                        if existing.target != *new {
159                            let new = new.clone();
160                            return Err(Error::MustNotExist {
161                                full_name: change.name(),
162                                actual: existing.target.clone(),
163                                new,
164                            });
165                        }
166                    }
167                    (
168                        PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous),
169                        Some(existing),
170                    ) => {
171                        if *previous != existing.target {
172                            let actual = existing.target.clone();
173                            let expected = previous.to_owned();
174                            let full_name = change.name();
175                            return Err(Error::ReferenceOutOfDate {
176                                full_name,
177                                actual,
178                                expected,
179                            });
180                        }
181                    }
182
183                    (PreviousValue::MustExistAndMatch(previous), None) => {
184                        let expected = previous.to_owned();
185                        let full_name = change.name();
186                        return Err(Error::MustExist { full_name, expected });
187                    }
188                }
189
190                fn new_would_change_existing(new: &Target, existing: &Target) -> (bool, bool) {
191                    match (new, existing) {
192                        (Target::Object(new), Target::Object(old)) => (old != new, false),
193                        (Target::Symbolic(new), Target::Symbolic(old)) => (old != new, true),
194                        (Target::Object(_), _) => (true, false),
195                        (Target::Symbolic(_), _) => (true, true),
196                    }
197                }
198
199                let (is_effective, is_symbolic) = if let Some(existing) = existing_ref {
200                    let (effective, is_symbolic) = new_would_change_existing(new, &existing.target);
201                    *expected = PreviousValue::MustExistAndMatch(existing.target);
202                    (effective, is_symbolic)
203                } else {
204                    (true, matches!(new, Target::Symbolic(_)))
205                };
206
207                let keep_lock_for_loose_source_delete = direct_to_packed_refs && matches!(new, Target::Object(_));
208                if (is_effective && !direct_to_packed_refs) || is_symbolic {
209                    lock.with_mut(|file| match new {
210                        Target::Object(oid) => writeln!(file, "{oid}"),
211                        Target::Symbolic(name) => writeln!(file, "ref: {}", name.0),
212                    })?;
213                    Some(lock.close()?)
214                } else if keep_lock_for_loose_source_delete {
215                    Some(lock.close()?)
216                } else {
217                    None
218                }
219            }
220        };
221        change.lock = lock;
222        Ok(())
223    }
224}
225
226impl Transaction<'_, '_> {
227    /// Prepare for calling [`commit(…)`][Transaction::commit()] in a way that can be rolled back perfectly.
228    ///
229    /// If the operation succeeds, the transaction can be committed or dropped to cause a rollback automatically.
230    /// Rollbacks happen automatically on failure and they tend to be perfect.
231    /// This method is idempotent.
232    pub fn prepare(
233        self,
234        edits: impl IntoIterator<Item = RefEdit>,
235        ref_files_lock_fail_mode: gix_lock::acquire::Fail,
236        packed_refs_lock_fail_mode: gix_lock::acquire::Fail,
237    ) -> Result<Self, Error> {
238        self.prepare_inner(
239            &mut edits.into_iter(),
240            ref_files_lock_fail_mode,
241            packed_refs_lock_fail_mode,
242        )
243    }
244
245    fn prepare_inner(
246        mut self,
247        edits: &mut dyn Iterator<Item = RefEdit>,
248        ref_files_lock_fail_mode: gix_lock::acquire::Fail,
249        packed_refs_lock_fail_mode: gix_lock::acquire::Fail,
250    ) -> Result<Self, Error> {
251        assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times");
252        let store = self.store;
253        let mut updates: Vec<_> = edits
254            .map(|update| Edit {
255                update,
256                lock: None,
257                parent_index: None,
258                leaf_referent_previous_oid: None,
259            })
260            .collect();
261        updates
262            .pre_process(
263                &mut |name| {
264                    let symbolic_refs_are_never_packed = None;
265                    store
266                        .find_existing_inner(name, symbolic_refs_are_never_packed)
267                        .map(|r| r.target)
268                        .ok()
269                },
270                &mut |idx, update| Edit {
271                    update,
272                    lock: None,
273                    parent_index: Some(idx),
274                    leaf_referent_previous_oid: None,
275                },
276            )
277            .map_err(Error::PreprocessingFailed)?;
278
279        let mut maybe_updates_for_packed_refs = match self.packed_refs {
280            PackedRefs::DeletionsAndNonSymbolicUpdates(_)
281            | PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) => Some(0_usize),
282            PackedRefs::DeletionsOnly => None,
283        };
284        if maybe_updates_for_packed_refs.is_some()
285            || self.store.packed_refs_path().is_file()
286            || self.store.packed_refs_lock_path().is_file()
287        {
288            let mut edits_for_packed_transaction = Vec::<RefEdit>::new();
289            let mut needs_packed_refs_lookups = false;
290            for edit in &updates {
291                let log_mode = match edit.update.change {
292                    Change::Update {
293                        log: LogChange { mode, .. },
294                        ..
295                    } => mode,
296                    Change::Delete { log, .. } => log,
297                };
298                if log_mode == RefLog::Only {
299                    continue;
300                }
301                let name = match possibly_adjust_name_for_prefixes(edit.update.name.as_ref()) {
302                    Some(n) => n,
303                    None => continue,
304                };
305                if let Some(ref mut num_updates) = maybe_updates_for_packed_refs {
306                    if let Change::Update {
307                        new: Target::Object(_), ..
308                    } = edit.update.change
309                    {
310                        edits_for_packed_transaction.push(RefEdit {
311                            name,
312                            ..edit.update.clone()
313                        });
314                        *num_updates += 1;
315                        continue;
316                    }
317                }
318                match edit.update.change {
319                    Change::Update {
320                        expected: PreviousValue::ExistingMustMatch(_) | PreviousValue::MustExistAndMatch(_),
321                        ..
322                    } => needs_packed_refs_lookups = true,
323                    Change::Delete { .. } => {
324                        edits_for_packed_transaction.push(RefEdit {
325                            name,
326                            ..edit.update.clone()
327                        });
328                    }
329                    _ => {
330                        needs_packed_refs_lookups = true;
331                    }
332                }
333            }
334
335            if !edits_for_packed_transaction.is_empty() || needs_packed_refs_lookups {
336                // What follows means that we will only create a transaction if we have to access packed refs for looking
337                // up current ref values, or that we definitely have a transaction if we need to make updates. Otherwise
338                // we may have no transaction at all which isn't required if we had none and would only try making deletions.
339                let packed_transaction: Option<_> =
340                    if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() {
341                        // We have to create a packed-ref even if it doesn't exist
342                        self.store
343                            .packed_transaction(packed_refs_lock_fail_mode)
344                            .map_err(|err| match err {
345                                file::packed::transaction::Error::BufferOpen(err) => Error::from(err),
346                                file::packed::transaction::Error::TransactionLock(err) => {
347                                    Error::PackedTransactionAcquire(err)
348                                }
349                            })?
350                            .into()
351                    } else {
352                        // A packed transaction is optional - we only have deletions that can't be made if
353                        // no packed-ref file exists anyway
354                        self.store
355                            .assure_packed_refs_uptodate()?
356                            .map(|p| {
357                                buffer_into_transaction(
358                                    p,
359                                    packed_refs_lock_fail_mode,
360                                    self.store.precompose_unicode,
361                                    self.store.namespace.clone(),
362                                )
363                                .map_err(Error::PackedTransactionAcquire)
364                            })
365                            .transpose()?
366                    };
367                if let Some(transaction) = packed_transaction {
368                    self.packed_transaction = Some(match &mut self.packed_refs {
369                        PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(f)
370                        | PackedRefs::DeletionsAndNonSymbolicUpdates(f) => {
371                            transaction.prepare(&mut edits_for_packed_transaction.into_iter(), &**f)?
372                        }
373                        PackedRefs::DeletionsOnly => transaction
374                            .prepare(&mut edits_for_packed_transaction.into_iter(), &gix_object::find::Never)?,
375                    });
376                }
377            }
378        }
379
380        for cid in 0..updates.len() {
381            let change = &mut updates[cid];
382            if let Err(err) = Self::lock_ref_and_apply_change(
383                self.store,
384                ref_files_lock_fail_mode,
385                self.packed_transaction.as_ref().and_then(packed::Transaction::buffer),
386                change,
387                matches!(
388                    self.packed_refs,
389                    PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_)
390                ),
391            ) {
392                let err = match err {
393                    Error::LockAcquire {
394                        source,
395                        full_name: _bogus,
396                    } => Error::LockAcquire {
397                        source,
398                        full_name: {
399                            let mut cursor = change.parent_index;
400                            let mut ref_name = change.name();
401                            while let Some(parent_idx) = cursor {
402                                let parent = &updates[parent_idx];
403                                if parent.parent_index.is_none() {
404                                    ref_name = parent.name();
405                                } else {
406                                    cursor = parent.parent_index;
407                                }
408                            }
409                            ref_name
410                        },
411                    },
412                    other => other,
413                };
414                return Err(err);
415            }
416
417            // traverse parent chain from leaf/peeled ref and set the leaf previous oid accordingly
418            // to help with their reflog entries
419            if let (Some(crate::TargetRef::Object(oid)), Some(parent_idx)) =
420                (change.update.change.previous_value(), change.parent_index)
421            {
422                let oid = oid.to_owned();
423                let mut parent_idx_cursor = Some(parent_idx);
424                while let Some(parent) = parent_idx_cursor.take().map(|idx| &mut updates[idx]) {
425                    parent_idx_cursor = parent.parent_index;
426                    parent.leaf_referent_previous_oid = Some(oid);
427                }
428            }
429        }
430        self.updates = Some(updates);
431        Ok(self)
432    }
433
434    /// Rollback all intermediate state and return the `RefEdits` as we know them thus far.
435    ///
436    /// Note that they have been altered compared to what was initially provided as they have
437    /// been split and know about their current state on disk.
438    ///
439    /// # Note
440    ///
441    /// A rollback happens automatically as this instance is dropped as well.
442    pub fn rollback(self) -> Vec<RefEdit> {
443        self.updates
444            .map(|updates| updates.into_iter().map(|u| u.update).collect())
445            .unwrap_or_default()
446    }
447}
448
449fn possibly_adjust_name_for_prefixes(name: &FullNameRef) -> Option<FullName> {
450    match name.category_and_short_name() {
451        Some((c, sn)) => {
452            use crate::Category::*;
453            let sn = FullNameRef::new_unchecked(sn);
454            match c {
455                Bisect | Rewritten | WorktreePrivate | LinkedPseudoRef { .. } | PseudoRef | MainPseudoRef => None,
456                Tag | LocalBranch | RemoteBranch | Note => name.into(),
457                MainRef | LinkedRef { .. } => sn
458                    .category()
459                    .is_some_and(|cat| !cat.is_worktree_private())
460                    .then_some(sn),
461            }
462            .map(ToOwned::to_owned)
463        }
464        None => Some(name.to_owned()), // allow (uncategorized/very special) refs to be packed
465    }
466}
467
468mod error {
469    use gix_object::bstr::BString;
470
471    use crate::{
472        Target,
473        store_impl::{file, packed},
474    };
475
476    /// The error returned by various [`Transaction`][super::Transaction] methods.
477    #[derive(Debug, thiserror::Error)]
478    #[expect(missing_docs)]
479    pub enum Error {
480        #[error("The packed ref buffer could not be loaded")]
481        Packed(#[from] packed::buffer::open::Error),
482        #[error("The lock for the packed-ref file could not be obtained")]
483        PackedTransactionAcquire(#[source] gix_lock::acquire::Error),
484        #[error("The packed transaction could not be prepared")]
485        PackedTransactionPrepare(#[from] packed::transaction::prepare::Error),
486        #[error("The packed ref file could not be parsed")]
487        PackedFind(#[from] packed::find::Error),
488        #[error("Edit preprocessing failed with an error")]
489        PreprocessingFailed(#[source] std::io::Error),
490        #[error("A lock could not be obtained for reference {full_name:?}")]
491        LockAcquire {
492            source: gix_lock::acquire::Error,
493            full_name: BString,
494        },
495        #[error("An IO error occurred while applying an edit")]
496        Io(#[from] std::io::Error),
497        #[error("The reference {full_name:?} for deletion did not exist or could not be parsed")]
498        DeleteReferenceMustExist { full_name: BString },
499        #[error(
500            "Reference {full_name:?} was not supposed to exist when writing it with value {new:?}, but actual content was {actual:?}"
501        )]
502        MustNotExist {
503            full_name: BString,
504            actual: Target,
505            new: Target,
506        },
507        #[error("Reference {full_name:?} was supposed to exist with value {expected}, but didn't.")]
508        MustExist { full_name: BString, expected: Target },
509        #[error("The reference {full_name:?} should have content {expected}, actual content was {actual}")]
510        ReferenceOutOfDate {
511            full_name: BString,
512            expected: Target,
513            actual: Target,
514        },
515        #[error("Could not read reference")]
516        ReferenceDecode(#[from] file::loose::reference::decode::Error),
517    }
518}
519
520pub use error::Error;