Skip to main content

gix_ref/store/file/transaction/
prepare.rs

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