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