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