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 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 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 let packed_transaction: Option<_> =
302 if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() {
303 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 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 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 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()), }
426}
427
428mod error {
429 use git_object::bstr::BString;
430
431 use crate::{
432 store_impl::{file, packed},
433 Target,
434 };
435
436 #[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;