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 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 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 let packed_transaction: Option<_> =
311 if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() {
312 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 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 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 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()), }
438}
439
440mod error {
441 use gix_object::bstr::BString;
442
443 use crate::{
444 Target,
445 store_impl::{file, packed},
446 };
447
448 #[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;