gix_ref/store/file/transaction/
prepare.rs

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