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 read_existing_ref(
20 store: &file::Store,
21 name: &FullNameRef,
22 packed: Option<&packed::Buffer>,
23 ) -> Result<Option<Reference>, Error> {
24 store
25 .ref_contents(name)
26 .map_err(Error::from)
27 .and_then(|maybe_loose| {
28 maybe_loose
29 .map(|buf| {
30 loose::Reference::try_from_path(name.to_owned(), &buf, store.object_hash)
31 .map(Reference::from)
32 .map_err(Error::from)
33 })
34 .transpose()
35 })
36 .or_else(|err| match err {
37 Error::ReferenceDecode(_) => Ok(None),
38 other => Err(other),
39 })
40 .and_then(|maybe_loose| match (maybe_loose, packed) {
41 (None, Some(packed)) => packed
42 .try_find(name)
43 .map(|opt| opt.map(Into::into))
44 .map_err(Error::from),
45 (None, None) => Ok(None),
46 (maybe_loose, _) => Ok(maybe_loose),
47 })
48 }
49
50 fn lock_acquire_error(err: gix_lock::acquire::Error, full_name: &str) -> Error {
55 match err {
56 gix_lock::acquire::Error::Io(err) => Error::Io(err),
57 source => Error::LockAcquire {
58 source,
59 full_name: full_name.into(),
60 },
61 }
62 }
63
64 fn lock_ref_and_apply_change(
65 store: &file::Store,
66 lock_fail_mode: gix_lock::acquire::Fail,
67 packed: Option<&packed::Buffer>,
68 change: &mut Edit,
69 direct_to_packed_refs: bool,
70 ) -> Result<(), Error> {
71 use std::io::Write;
72 assert!(
73 change.lock.is_none(),
74 "locks can only be acquired once and it's all or nothing"
75 );
76
77 store.check_windows_device_name(change.update.name.as_ref())?;
82
83 let lock = match &mut change.update.change {
84 Change::Delete { expected, .. } => {
85 let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref());
86 let lock = gix_lock::Marker::acquire_to_hold_resource(
87 base.join(relative_path.as_ref()),
88 lock_fail_mode,
89 Some(base.clone().into_owned()),
90 )
91 .map_err(|err| Self::lock_acquire_error(err, "borrowcheck won't allow change.name()"))?;
92
93 let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?;
94
95 match (&expected, &existing_ref) {
96 (PreviousValue::MustNotExist, _) => {
97 panic!("BUG: MustNotExist constraint makes no sense if references are to be deleted")
98 }
99 (PreviousValue::ExistingMustMatch(_) | PreviousValue::Any, None)
100 | (PreviousValue::MustExist | PreviousValue::Any, Some(_)) => {}
101 (PreviousValue::MustExist | PreviousValue::MustExistAndMatch(_), None) => {
102 return Err(Error::DeleteReferenceMustExist {
103 full_name: change.name(),
104 });
105 }
106 (
107 PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous),
108 Some(existing),
109 ) => {
110 let actual = existing.target.clone();
111 if *previous != actual {
112 let expected = previous.clone();
113 return Err(Error::ReferenceOutOfDate {
114 full_name: change.name(),
115 expected,
116 actual,
117 });
118 }
119 }
120 }
121
122 if let Some(existing) = existing_ref {
124 *expected = PreviousValue::MustExistAndMatch(existing.target);
125 }
126
127 Some(lock)
128 }
129 Change::Update { expected, new, .. } => {
130 let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref());
131 let obtain_lock = || {
132 gix_lock::File::acquire_to_update_resource(
133 base.join(relative_path.as_ref()),
134 lock_fail_mode,
135 Some(base.clone().into_owned()),
136 )
137 .map_err(|err| {
138 Self::lock_acquire_error(
139 err,
140 "borrowcheck won't allow change.name() and this will be corrected by caller",
141 )
142 })
143 };
144 let mut lock = obtain_lock()?;
145
146 let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?;
147
148 match (&expected, &existing_ref) {
149 (PreviousValue::Any, _)
150 | (PreviousValue::MustExist, Some(_))
151 | (PreviousValue::MustNotExist | PreviousValue::ExistingMustMatch(_), None) => {}
152 (PreviousValue::MustExist, None) => {
153 let expected = Target::Object(store.object_hash.null());
154 let full_name = change.name();
155 return Err(Error::MustExist { full_name, expected });
156 }
157 (PreviousValue::MustNotExist, Some(existing)) => {
158 if existing.target != *new {
159 let new = new.clone();
160 return Err(Error::MustNotExist {
161 full_name: change.name(),
162 actual: existing.target.clone(),
163 new,
164 });
165 }
166 }
167 (
168 PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous),
169 Some(existing),
170 ) => {
171 if *previous != existing.target {
172 let actual = existing.target.clone();
173 let expected = previous.to_owned();
174 let full_name = change.name();
175 return Err(Error::ReferenceOutOfDate {
176 full_name,
177 actual,
178 expected,
179 });
180 }
181 }
182
183 (PreviousValue::MustExistAndMatch(previous), None) => {
184 let expected = previous.to_owned();
185 let full_name = change.name();
186 return Err(Error::MustExist { full_name, expected });
187 }
188 }
189
190 fn new_would_change_existing(new: &Target, existing: &Target) -> (bool, bool) {
191 match (new, existing) {
192 (Target::Object(new), Target::Object(old)) => (old != new, false),
193 (Target::Symbolic(new), Target::Symbolic(old)) => (old != new, true),
194 (Target::Object(_), _) => (true, false),
195 (Target::Symbolic(_), _) => (true, true),
196 }
197 }
198
199 let (is_effective, is_symbolic) = if let Some(existing) = existing_ref {
200 let (effective, is_symbolic) = new_would_change_existing(new, &existing.target);
201 *expected = PreviousValue::MustExistAndMatch(existing.target);
202 (effective, is_symbolic)
203 } else {
204 (true, matches!(new, Target::Symbolic(_)))
205 };
206
207 let keep_lock_for_loose_source_delete = direct_to_packed_refs && matches!(new, Target::Object(_));
208 if (is_effective && !direct_to_packed_refs) || is_symbolic {
209 lock.with_mut(|file| match new {
210 Target::Object(oid) => writeln!(file, "{oid}"),
211 Target::Symbolic(name) => writeln!(file, "ref: {}", name.0),
212 })?;
213 Some(lock.close()?)
214 } else if keep_lock_for_loose_source_delete {
215 Some(lock.close()?)
216 } else {
217 None
218 }
219 }
220 };
221 change.lock = lock;
222 Ok(())
223 }
224}
225
226impl Transaction<'_, '_> {
227 pub fn prepare(
233 self,
234 edits: impl IntoIterator<Item = RefEdit>,
235 ref_files_lock_fail_mode: gix_lock::acquire::Fail,
236 packed_refs_lock_fail_mode: gix_lock::acquire::Fail,
237 ) -> Result<Self, Error> {
238 self.prepare_inner(
239 &mut edits.into_iter(),
240 ref_files_lock_fail_mode,
241 packed_refs_lock_fail_mode,
242 )
243 }
244
245 fn prepare_inner(
246 mut self,
247 edits: &mut dyn Iterator<Item = RefEdit>,
248 ref_files_lock_fail_mode: gix_lock::acquire::Fail,
249 packed_refs_lock_fail_mode: gix_lock::acquire::Fail,
250 ) -> Result<Self, Error> {
251 assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times");
252 let store = self.store;
253 let mut updates: Vec<_> = edits
254 .map(|update| Edit {
255 update,
256 lock: None,
257 parent_index: None,
258 leaf_referent_previous_oid: None,
259 })
260 .collect();
261 updates
262 .pre_process(
263 &mut |name| {
264 let symbolic_refs_are_never_packed = None;
265 store
266 .find_existing_inner(name, symbolic_refs_are_never_packed)
267 .map(|r| r.target)
268 .ok()
269 },
270 &mut |idx, update| Edit {
271 update,
272 lock: None,
273 parent_index: Some(idx),
274 leaf_referent_previous_oid: None,
275 },
276 )
277 .map_err(Error::PreprocessingFailed)?;
278
279 let mut maybe_updates_for_packed_refs = match self.packed_refs {
280 PackedRefs::DeletionsAndNonSymbolicUpdates(_)
281 | PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) => Some(0_usize),
282 PackedRefs::DeletionsOnly => None,
283 };
284 if maybe_updates_for_packed_refs.is_some()
285 || self.store.packed_refs_path().is_file()
286 || self.store.packed_refs_lock_path().is_file()
287 {
288 let mut edits_for_packed_transaction = Vec::<RefEdit>::new();
289 let mut needs_packed_refs_lookups = false;
290 for edit in &updates {
291 let log_mode = match edit.update.change {
292 Change::Update {
293 log: LogChange { mode, .. },
294 ..
295 } => mode,
296 Change::Delete { log, .. } => log,
297 };
298 if log_mode == RefLog::Only {
299 continue;
300 }
301 let name = match possibly_adjust_name_for_prefixes(edit.update.name.as_ref()) {
302 Some(n) => n,
303 None => continue,
304 };
305 if let Some(ref mut num_updates) = maybe_updates_for_packed_refs {
306 if let Change::Update {
307 new: Target::Object(_), ..
308 } = edit.update.change
309 {
310 edits_for_packed_transaction.push(RefEdit {
311 name,
312 ..edit.update.clone()
313 });
314 *num_updates += 1;
315 continue;
316 }
317 }
318 match edit.update.change {
319 Change::Update {
320 expected: PreviousValue::ExistingMustMatch(_) | PreviousValue::MustExistAndMatch(_),
321 ..
322 } => needs_packed_refs_lookups = true,
323 Change::Delete { .. } => {
324 edits_for_packed_transaction.push(RefEdit {
325 name,
326 ..edit.update.clone()
327 });
328 }
329 _ => {
330 needs_packed_refs_lookups = true;
331 }
332 }
333 }
334
335 if !edits_for_packed_transaction.is_empty() || needs_packed_refs_lookups {
336 let packed_transaction: Option<_> =
340 if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() {
341 self.store
343 .packed_transaction(packed_refs_lock_fail_mode)
344 .map_err(|err| match err {
345 file::packed::transaction::Error::BufferOpen(err) => Error::from(err),
346 file::packed::transaction::Error::TransactionLock(err) => {
347 Error::PackedTransactionAcquire(err)
348 }
349 })?
350 .into()
351 } else {
352 self.store
355 .assure_packed_refs_uptodate()?
356 .map(|p| {
357 buffer_into_transaction(
358 p,
359 packed_refs_lock_fail_mode,
360 self.store.precompose_unicode,
361 self.store.namespace.clone(),
362 )
363 .map_err(Error::PackedTransactionAcquire)
364 })
365 .transpose()?
366 };
367 if let Some(transaction) = packed_transaction {
368 self.packed_transaction = Some(match &mut self.packed_refs {
369 PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(f)
370 | PackedRefs::DeletionsAndNonSymbolicUpdates(f) => {
371 transaction.prepare(&mut edits_for_packed_transaction.into_iter(), &**f)?
372 }
373 PackedRefs::DeletionsOnly => transaction
374 .prepare(&mut edits_for_packed_transaction.into_iter(), &gix_object::find::Never)?,
375 });
376 }
377 }
378 }
379
380 for cid in 0..updates.len() {
381 let change = &mut updates[cid];
382 if let Err(err) = Self::lock_ref_and_apply_change(
383 self.store,
384 ref_files_lock_fail_mode,
385 self.packed_transaction.as_ref().and_then(packed::Transaction::buffer),
386 change,
387 matches!(
388 self.packed_refs,
389 PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_)
390 ),
391 ) {
392 let err = match err {
393 Error::LockAcquire {
394 source,
395 full_name: _bogus,
396 } => Error::LockAcquire {
397 source,
398 full_name: {
399 let mut cursor = change.parent_index;
400 let mut ref_name = change.name();
401 while let Some(parent_idx) = cursor {
402 let parent = &updates[parent_idx];
403 if parent.parent_index.is_none() {
404 ref_name = parent.name();
405 } else {
406 cursor = parent.parent_index;
407 }
408 }
409 ref_name
410 },
411 },
412 other => other,
413 };
414 return Err(err);
415 }
416
417 if let (Some(crate::TargetRef::Object(oid)), Some(parent_idx)) =
420 (change.update.change.previous_value(), change.parent_index)
421 {
422 let oid = oid.to_owned();
423 let mut parent_idx_cursor = Some(parent_idx);
424 while let Some(parent) = parent_idx_cursor.take().map(|idx| &mut updates[idx]) {
425 parent_idx_cursor = parent.parent_index;
426 parent.leaf_referent_previous_oid = Some(oid);
427 }
428 }
429 }
430 self.updates = Some(updates);
431 Ok(self)
432 }
433
434 pub fn rollback(self) -> Vec<RefEdit> {
443 self.updates
444 .map(|updates| updates.into_iter().map(|u| u.update).collect())
445 .unwrap_or_default()
446 }
447}
448
449fn possibly_adjust_name_for_prefixes(name: &FullNameRef) -> Option<FullName> {
450 match name.category_and_short_name() {
451 Some((c, sn)) => {
452 use crate::Category::*;
453 let sn = FullNameRef::new_unchecked(sn);
454 match c {
455 Bisect | Rewritten | WorktreePrivate | LinkedPseudoRef { .. } | PseudoRef | MainPseudoRef => None,
456 Tag | LocalBranch | RemoteBranch | Note => name.into(),
457 MainRef | LinkedRef { .. } => sn
458 .category()
459 .is_some_and(|cat| !cat.is_worktree_private())
460 .then_some(sn),
461 }
462 .map(ToOwned::to_owned)
463 }
464 None => Some(name.to_owned()), }
466}
467
468mod error {
469 use gix_object::bstr::BString;
470
471 use crate::{
472 Target,
473 store_impl::{file, packed},
474 };
475
476 #[derive(Debug, thiserror::Error)]
478 #[expect(missing_docs)]
479 pub enum Error {
480 #[error("The packed ref buffer could not be loaded")]
481 Packed(#[from] packed::buffer::open::Error),
482 #[error("The lock for the packed-ref file could not be obtained")]
483 PackedTransactionAcquire(#[source] gix_lock::acquire::Error),
484 #[error("The packed transaction could not be prepared")]
485 PackedTransactionPrepare(#[from] packed::transaction::prepare::Error),
486 #[error("The packed ref file could not be parsed")]
487 PackedFind(#[from] packed::find::Error),
488 #[error("Edit preprocessing failed with an error")]
489 PreprocessingFailed(#[source] std::io::Error),
490 #[error("A lock could not be obtained for reference {full_name:?}")]
491 LockAcquire {
492 source: gix_lock::acquire::Error,
493 full_name: BString,
494 },
495 #[error("An IO error occurred while applying an edit")]
496 Io(#[from] std::io::Error),
497 #[error("The reference {full_name:?} for deletion did not exist or could not be parsed")]
498 DeleteReferenceMustExist { full_name: BString },
499 #[error(
500 "Reference {full_name:?} was not supposed to exist when writing it with value {new:?}, but actual content was {actual:?}"
501 )]
502 MustNotExist {
503 full_name: BString,
504 actual: Target,
505 new: Target,
506 },
507 #[error("Reference {full_name:?} was supposed to exist with value {expected}, but didn't.")]
508 MustExist { full_name: BString, expected: Target },
509 #[error("The reference {full_name:?} should have content {expected}, actual content was {actual}")]
510 ReferenceOutOfDate {
511 full_name: BString,
512 expected: Target,
513 actual: Target,
514 },
515 #[error("Could not read reference")]
516 ReferenceDecode(#[from] file::loose::reference::decode::Error),
517 }
518}
519
520pub use error::Error;