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