1use std::borrow::ToOwned;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6use std::fs;
7use std::ops::ControlFlow;
8use std::path::{Path, PathBuf};
9use std::sync::Mutex;
10
11use crate::environment::Environment;
12use crate::error::{Error, Result};
13use crate::objects::ObjectId;
14use crate::reflog::ReflogEntry;
15use crate::reftable::{
16 read_write_options, read_write_options_with_environment, reftable_storage_location, LogRecord,
17 RefValue, ReftableError, ReftableStack, ReftableTransactionUpdate, TablesListLock,
18 WriteOptions,
19};
20use crate::repo::init_repository;
21
22use super::apply::{apply_reflog_rename, RefBatchState};
23use super::backend::{check_transaction_updates, partition_updates_allowing_failures};
24use super::error::RefStoreError;
25use super::semantics::{
26 apply_update_to_map, deref_reflog_target, format_reflog_identity, reflog_new_oid_before,
27 reflog_old_oid_before, reftable_write_target,
28};
29use super::transaction::RefTransaction;
30use super::types::RefReflogRename;
31use super::{
32 Expected, PreparedRefTransaction, RawRef, RefEntry, RefStorageFormat, RefStore, RefUpdate,
33};
34
35type StoreResult<T> = std::result::Result<T, RefStoreError>;
36
37pub struct ReftableRefStore {
56 git_dir: PathBuf,
57 environment: Environment,
58 _keepalive: Option<tempfile::TempDir>,
59 prepared: Mutex<Option<PreparedReftableBatch>>,
60}
61
62struct PreparedReftableBatch {
63 stacks: Vec<LockedStackBatch>,
64 reflog_renames: Vec<RefReflogRename>,
65}
66
67struct LockedStackBatch {
68 _store_git_dir: PathBuf,
69 stack: ReftableStack,
70 lock: TablesListLock,
71 updates: Vec<ReftableTransactionUpdate>,
72 opts: WriteOptions,
73}
74
75impl fmt::Debug for ReftableRefStore {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.debug_struct("ReftableRefStore")
78 .field("git_dir", &self.git_dir)
79 .finish_non_exhaustive()
80 }
81}
82
83impl ReftableRefStore {
84 pub fn open(git_dir: PathBuf) -> Result<Self> {
90 Self::open_with_environment(git_dir, Environment::empty())
91 }
92
93 pub(crate) fn open_with_environment(
94 git_dir: PathBuf,
95 environment: Environment,
96 ) -> Result<Self> {
97 let store_git_dir = stack_root(&git_dir);
98 let _ = ReftableStack::open(&store_git_dir)?;
99 Ok(Self {
100 git_dir,
101 environment,
102 _keepalive: None,
103 prepared: Mutex::new(None),
104 })
105 }
106
107 fn write_options_for(&self, store_git_dir: &Path) -> WriteOptions {
108 read_write_options_with_environment(store_git_dir, &self.environment)
109 }
110
111 pub fn open_ephemeral() -> Result<Self> {
117 let root = tempfile::tempdir().map_err(Error::Io)?;
118 init_repository(
119 root.path(),
120 false,
121 "main",
122 None,
123 crate::ref_storage::RefStorageFormat::Reftable,
124 )?;
125 let git_dir = root.path().join(".git");
126 let store = Self {
127 git_dir: git_dir.clone(),
128 environment: Environment::empty(),
129 _keepalive: Some(root),
130 prepared: Mutex::new(None),
131 };
132 let txn = RefTransaction::new()
134 .update(RefUpdate {
135 name: "HEAD".to_owned(),
136 new_value: None,
137 expected: Expected::Any,
138 reflog: None,
139 flags: super::RefUpdateFlags {
140 no_deref: true,
141 log_only: false,
142 ..Default::default()
143 },
144 })
145 .map_err(|e| Error::Message(e.to_string()))?;
146 store
147 .prepare(txn)
148 .map_err(|e| Error::Message(e.to_string()))?
149 .commit()
150 .map_err(|e| Error::Message(e.to_string()))?;
151 Ok(store)
152 }
153
154 pub fn write_reftable_updates(
160 &self,
161 updates: Vec<ReftableTransactionUpdate>,
162 ) -> StoreResult<()> {
163 if updates.is_empty() {
164 return Ok(());
165 }
166 let mut grouped: BTreeMap<PathBuf, Vec<ReftableTransactionUpdate>> = BTreeMap::new();
167 for mut update in updates {
168 let (store_git_dir, storage_refname) =
169 reftable_storage_location(&self.git_dir, &update.refname);
170 update.refname = storage_refname.clone();
171 if let Some(log) = update.log.as_mut() {
172 log.refname = storage_refname;
173 }
174 grouped.entry(store_git_dir).or_default().push(update);
175 }
176 for (store_git_dir, group) in grouped {
177 let mut stack = ReftableStack::open(&store_git_dir)
178 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
179 let opts = self.write_options_for(&store_git_dir);
180 stack
181 .write_transaction(group, &opts)
182 .map_err(map_reftable_err)?;
183 }
184 Ok(())
185 }
186
187 fn read_raw_at(&self, refname: &str) -> Result<Option<RawRef>> {
188 if refname == "HEAD" {
189 return read_head_raw(&self.git_dir);
190 }
191 let (store_git_dir, storage_refname) = reftable_storage_location(&self.git_dir, refname);
192 let stack = ReftableStack::open(&store_git_dir)?;
193 Ok(stack
194 .lookup_ref(&storage_refname)?
195 .and_then(|rec| record_to_raw(&rec)))
196 }
197
198 fn split_cross_stack_symref_updates(
206 &self,
207 updates: Vec<RefUpdate>,
208 ) -> StoreResult<Vec<RefUpdate>> {
209 let common = stack_root(&self.git_dir);
210 let mut out = Vec::with_capacity(updates.len());
211 for update in updates {
212 let (update_stack, _) = reftable_storage_location(&self.git_dir, &update.name);
213 let target = if update_stack == common {
215 None
216 } else {
217 self.cross_stack_deref_target(&update, &update_stack)?
218 };
219 let Some(target) = target else {
220 out.push(update);
221 continue;
222 };
223 let old_oid = match self.read_raw_at(&target) {
224 Ok(Some(RawRef::Direct(oid))) => oid,
225 Ok(_) => ObjectId::zero(),
226 Err(e) => return Err(RefStoreError::Corrupt(e.to_string())),
227 };
228 let new_oid = match &update.new_value {
229 Some(RawRef::Direct(oid)) => *oid,
230 _ => ObjectId::zero(),
231 };
232 let (symref_expected, target_expected) = match &update.expected {
233 Expected::Symref(_) => (update.expected.clone(), Expected::Any),
234 other => (Expected::Any, other.clone()),
235 };
236 out.push(RefUpdate {
237 name: update.name.clone(),
238 new_value: update.new_value.clone(),
239 expected: symref_expected,
240 reflog: update.reflog.clone().map(|log| super::ReflogUpdate {
241 old_oid: log.old_oid.or(Some(old_oid)),
242 new_oid: log.new_oid.or(Some(new_oid)),
243 ..log
244 }),
245 flags: super::RefUpdateFlags {
246 log_only: true,
247 no_deref: true,
248 ..Default::default()
249 },
250 });
251 out.push(RefUpdate {
252 name: target,
253 new_value: update.new_value,
254 expected: target_expected,
255 reflog: update.reflog,
256 flags: super::RefUpdateFlags {
257 log_only: update.flags.log_only,
258 no_deref: true,
259 ..Default::default()
260 },
261 });
262 }
263 Ok(out)
264 }
265
266 fn cross_stack_deref_target(
268 &self,
269 update: &RefUpdate,
270 update_stack: &Path,
271 ) -> StoreResult<Option<String>> {
272 if update.flags.no_deref || matches!(update.new_value, Some(RawRef::Symbolic(_))) {
273 return Ok(None);
274 }
275 let read = |name: &str| {
276 self.read_raw_at(name)
277 .map_err(|e| RefStoreError::Corrupt(e.to_string()))
278 };
279 let Some(RawRef::Symbolic(mut target)) = read(&update.name)? else {
280 return Ok(None);
281 };
282 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
283 match read(&target)? {
284 Some(RawRef::Symbolic(next)) => target = next,
285 _ => {
286 let (target_stack, _) = reftable_storage_location(&self.git_dir, &target);
287 return Ok((target_stack != update_stack).then_some(target));
288 }
289 }
290 }
291 Err(RefStoreError::SymrefLoop)
292 }
293
294 fn merged_refs_from_locked_stack(
295 git_dir: &Path,
296 stack_git_dir: &Path,
297 stack: &ReftableStack,
298 ) -> Result<BTreeMap<String, RawRef>> {
299 let mut map = BTreeMap::new();
300 for rec in stack.read_refs()? {
301 if let Some(raw) = record_to_raw(&rec) {
302 map.insert(rec.name, raw);
303 }
304 }
305 let (head_git_dir, _) = reftable_storage_location(git_dir, "HEAD");
306 if head_git_dir != stack_git_dir {
307 map.remove("HEAD");
309 if let Some(head) = read_head_raw(git_dir)? {
310 map.insert("HEAD".to_owned(), head);
311 }
312 }
313 Ok(map)
314 }
315}
316
317fn read_reflog_entries_from_stack(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
318 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
319 let stack = ReftableStack::open(&store_git_dir)?;
320 let logs = stack.read_logs_for_ref(&storage_refname)?;
321 let mut entries = Vec::new();
322 for log in logs {
323 let tz_sign = if log.tz_offset >= 0 { '+' } else { '-' };
325 let tz_abs = log.tz_offset.unsigned_abs();
326 let tz_hours = tz_abs / 100;
327 let tz_mins = tz_abs % 100;
328 let identity = format!(
329 "{} <{}> {} {}{:02}{:02}",
330 log.name, log.email, log.time_seconds, tz_sign, tz_hours, tz_mins
331 );
332 let message = log
333 .message
334 .strip_suffix('\n')
335 .map(ToOwned::to_owned)
336 .unwrap_or(log.message);
337 entries.push(ReflogEntry {
338 old_oid: log.old_id,
339 new_oid: log.new_id,
340 identity,
341 message,
342 });
343 }
344 entries.reverse();
345 Ok(entries)
346}
347
348fn read_head_raw(git_dir: &Path) -> Result<Option<RawRef>> {
350 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, "HEAD");
351 let stack = ReftableStack::open(&store_git_dir)?;
352 Ok(stack
353 .lookup_ref(&storage_refname)?
354 .and_then(|rec| record_to_raw(&rec)))
355}
356
357fn empty_reflog_markers_path(git_dir: &Path) -> PathBuf {
358 git_dir.join("reftable").join("empty-reflogs")
359}
360
361fn read_empty_reflog_markers(git_dir: &Path) -> BTreeSet<String> {
362 fs::read_to_string(empty_reflog_markers_path(git_dir))
363 .map(|content| {
364 content
365 .lines()
366 .filter(|line| !line.trim().is_empty())
367 .map(ToOwned::to_owned)
368 .collect()
369 })
370 .unwrap_or_default()
371}
372
373fn write_empty_reflog_markers(git_dir: &Path, markers: &BTreeSet<String>) -> Result<()> {
374 let path = empty_reflog_markers_path(git_dir);
375 let content = markers.iter().cloned().collect::<Vec<_>>().join("\n");
376 fs::write(
377 path,
378 if content.is_empty() {
379 content
380 } else {
381 content + "\n"
382 },
383 )
384 .map_err(Error::Io)
385}
386
387fn stack_root(git_dir: &Path) -> PathBuf {
388 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
389}
390
391fn record_to_raw(rec: &crate::reftable::RefRecord) -> Option<RawRef> {
392 match rec.value {
393 RefValue::Deletion => None,
394 RefValue::Val1(oid) | RefValue::Val2(oid, _) => Some(RawRef::Direct(oid)),
395 RefValue::Symref(ref target) => Some(RawRef::Symbolic(target.clone())),
396 }
397}
398
399fn map_reftable_err(err: Error) -> RefStoreError {
400 match err {
401 Error::Reftable(ReftableError::Locked { .. }) => RefStoreError::LockHeld {
402 name: "refs".to_owned(),
403 },
404 Error::Reftable(ReftableError::ExpectedMismatch {
405 refname,
406 expected,
407 actual,
408 }) => RefStoreError::ExpectedMismatch {
409 name: refname,
410 expected: Expected::Oid(expected),
411 actual: actual.map(RawRef::Direct),
412 },
413 other => RefStoreError::Corrupt(other.to_string()),
414 }
415}
416
417fn log_record_from_update(
418 git_dir: &Path,
419 storage_refname: &str,
420 old_oid: ObjectId,
421 new_oid: ObjectId,
422 log: &super::ReflogUpdate,
423) -> Result<LogRecord> {
424 let (store_git_dir, _) = reftable_storage_location(git_dir, storage_refname);
425 let opts = read_write_options(&store_git_dir);
426 let identity = format_reflog_identity(&log.identity, log.time);
427 let (name, email, time_secs, tz) = crate::reftable::parse_identity_string(&identity);
428 Ok(LogRecord {
429 refname: storage_refname.to_owned(),
430 update_index: 0,
431 old_id: crate::reftable::widen_oid_to(old_oid, opts.hash_size),
432 new_id: crate::reftable::widen_oid_to(new_oid, opts.hash_size),
433 name,
434 email,
435 time_seconds: time_secs,
436 tz_offset: tz,
437 message: log.message.clone(),
438 })
439}
440
441fn build_reftable_updates_for_stack(
442 git_dir: &Path,
443 refs: &BTreeMap<String, RawRef>,
444 updates: &[RefUpdate],
445) -> StoreResult<Vec<ReftableTransactionUpdate>> {
446 let mut trial = refs.clone();
447 let mut ref_values: BTreeMap<String, Option<RefValue>> = BTreeMap::new();
448 let mut expected_old_for_ref: BTreeMap<String, Option<ObjectId>> = BTreeMap::new();
449 let mut log_updates: Vec<ReftableTransactionUpdate> = Vec::new();
450
451 for update in updates {
452 let target = reftable_write_target(&trial, git_dir, update)?;
453 let value = target.value.clone();
454 let old_oid = reflog_old_oid_before(&trial, refs, update);
455 let new_oid = reflog_new_oid_before(&trial, refs, update);
456 let (_, log_refname) = reftable_storage_location(git_dir, &update.name);
457 if let Some(log) = update.reflog.as_ref() {
458 let mut log_refnames = vec![log_refname];
459 if let Some(target_name) = deref_reflog_target(&trial, update) {
460 let (target_store, target_storage) =
461 reftable_storage_location(git_dir, &target_name);
462 let (update_store, _) = reftable_storage_location(git_dir, &update.name);
463 if target_store == update_store {
464 log_refnames.push(target_storage);
465 }
466 }
467 for refname in log_refnames {
468 let record = log_record_from_update(git_dir, &refname, old_oid, new_oid, log)
469 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
470 log_updates.push(ReftableTransactionUpdate {
471 refname,
472 value: None,
473 log: Some(record),
474 expected_old: None,
475 });
476 }
477 }
478 if value.is_some() {
479 let expected_old = match &update.expected {
480 Expected::Oid(oid) => Some(*oid),
481 _ => None,
482 };
483 ref_values.insert(target.storage_refname.clone(), value);
484 expected_old_for_ref.insert(target.storage_refname.clone(), expected_old);
485 }
486 apply_update_to_map(&mut trial, refs, update)?;
487 }
488
489 let mut out = Vec::new();
490 for (refname, value) in ref_values {
491 out.push(ReftableTransactionUpdate {
492 refname: refname.clone(),
493 value,
494 log: None,
495 expected_old: expected_old_for_ref.remove(&refname).flatten(),
496 });
497 }
498 out.extend(log_updates);
499 Ok(out)
500}
501
502impl ReftableRefStore {
503 fn for_each_refs_in_stack_dir(
504 &self,
505 stack_git_dir: &Path,
506 prefix: &str,
507 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
508 ) -> Result<()> {
509 let prefix = if prefix.is_empty() { "refs/" } else { prefix };
510 let (head_stack, _) = reftable_storage_location(&self.git_dir, "HEAD");
511 if head_stack == stack_git_dir && "HEAD".starts_with(prefix) {
512 if let Some(value) = read_head_raw(&self.git_dir)? {
513 let peeled = match &value {
514 RawRef::Direct(_) => None,
515 RawRef::Symbolic(_) => self.resolve("HEAD").ok(),
516 };
517 let entry = RefEntry {
518 name: "HEAD".to_owned(),
519 value,
520 peeled,
521 };
522 if f(&entry).is_break() {
523 return Ok(());
524 }
525 }
526 }
527 let stack = ReftableStack::open(stack_git_dir)?;
528 stack.for_each_merged_ref_under_prefix(prefix, |rec| {
529 if rec.name == "HEAD" {
530 return ControlFlow::Continue(());
531 }
532 let Some(value) = record_to_raw(rec) else {
533 return ControlFlow::Continue(());
534 };
535 if !rec.name.starts_with(prefix)
536 && !(prefix.ends_with('/') && rec.name == prefix.trim_end_matches('/'))
537 {
538 return ControlFlow::Continue(());
539 }
540 let peeled = match &value {
541 RawRef::Direct(_) => None,
542 RawRef::Symbolic(_) => self.resolve(&rec.name).ok(),
543 };
544 let entry = RefEntry {
545 name: rec.name.clone(),
546 value,
547 peeled,
548 };
549 f(&entry)
550 })
551 }
552}
553
554impl RefStore for ReftableRefStore {
555 fn format(&self) -> RefStorageFormat {
556 RefStorageFormat::Reftable
557 }
558
559 fn read_raw(&self, name: &str) -> Result<Option<RawRef>> {
560 self.read_raw_at(name)
561 }
562
563 fn for_each_ref(
564 &self,
565 prefix: &str,
566 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
567 ) -> Result<()> {
568 self.for_each_refs_in_stack_dir(&stack_root(&self.git_dir), prefix, f)
569 }
570
571 fn for_each_physical_stack_ref(
572 &self,
573 prefix: &str,
574 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
575 ) -> Result<()> {
576 self.for_each_refs_in_stack_dir(&self.git_dir, prefix, f)
577 }
578
579 fn prepare(
580 &self,
581 txn: RefTransaction,
582 ) -> StoreResult<Box<dyn PreparedRefTransaction + Send + '_>> {
583 txn.validate()?;
584 let mut guard_state = self.prepared.lock().unwrap_or_else(|e| e.into_inner());
585 if guard_state.is_some() {
586 return Err(RefStoreError::LockHeld {
587 name: txn.lock_name(),
588 });
589 }
590 let allow_failures = txn.allows_failures();
591 let (updates, reflog_renames, _case_only_renames) = txn.into_parts();
592
593 let updates = self.split_cross_stack_symref_updates(updates)?;
594 let mut grouped: BTreeMap<PathBuf, Vec<RefUpdate>> = BTreeMap::new();
595 for update in updates {
596 let (store_git_dir, _) = reftable_storage_location(&self.git_dir, &update.name);
597 grouped.entry(store_git_dir).or_default().push(update);
598 }
599
600 let mut stacks = Vec::new();
601 let mut rejections = Vec::new();
602 for (store_git_dir, group) in grouped {
603 let mut stack = ReftableStack::open(&store_git_dir)
604 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
605 let lock = stack
606 .acquire_tables_list_lock_for_store()
607 .map_err(map_reftable_err)?;
608 stack.reload_table_names();
609 let refs = Self::merged_refs_from_locked_stack(&self.git_dir, &store_git_dir, &stack)
610 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
611 let snapshot = RefBatchState {
612 refs,
613 reflogs: BTreeMap::new(),
614 };
615 let group = if allow_failures {
616 let (accepted, group_rejections) =
617 partition_updates_allowing_failures(&snapshot, group)?;
618 rejections.extend(group_rejections);
619 accepted
620 } else {
621 check_transaction_updates(&snapshot, &group)?;
622 group
623 };
624 let refs = snapshot.refs;
625 let rt_updates = build_reftable_updates_for_stack(&self.git_dir, &refs, &group)?;
626 let opts = self.write_options_for(&store_git_dir);
627 stacks.push(LockedStackBatch {
628 _store_git_dir: store_git_dir,
629 stack,
630 lock,
631 updates: rt_updates,
632 opts,
633 });
634 }
635
636 *guard_state = Some(PreparedReftableBatch {
637 stacks,
638 reflog_renames,
639 });
640
641 Ok(Box::new(ReftablePrepared {
642 store: self,
643 committed: false,
644 rejections,
645 }))
646 }
647
648 fn reflog_exists(&self, name: &str) -> Result<bool> {
649 let (store_git_dir, storage_refname) = reftable_storage_location(&self.git_dir, name);
650 if read_empty_reflog_markers(&store_git_dir).contains(&storage_refname) {
651 return Ok(true);
652 }
653 let stack = ReftableStack::open(&store_git_dir)?;
654 Ok(!stack.read_logs_for_ref(&storage_refname)?.is_empty())
655 }
656
657 fn for_each_reflog_entry(
658 &self,
659 name: &str,
660 reverse: bool,
661 f: &mut dyn FnMut(&ReflogEntry) -> ControlFlow<()>,
662 ) -> Result<()> {
663 let entries = read_reflog_entries_from_stack(&self.git_dir, name)?;
664 if reverse {
665 for entry in entries.iter().rev() {
666 if f(entry).is_break() {
667 break;
668 }
669 }
670 } else {
671 for entry in &entries {
672 if f(entry).is_break() {
673 break;
674 }
675 }
676 }
677 Ok(())
678 }
679
680 fn create_reflog(&self, name: &str) -> Result<()> {
681 let (store_git_dir, storage_refname) = reftable_storage_location(&self.git_dir, name);
682 let mut markers = read_empty_reflog_markers(&store_git_dir);
683 markers.insert(storage_refname);
684 write_empty_reflog_markers(&store_git_dir, &markers)
685 }
686
687 fn delete_reflog(&self, name: &str) -> Result<()> {
688 let (store_git_dir, storage_refname) = reftable_storage_location(&self.git_dir, name);
689 let mut markers = read_empty_reflog_markers(&store_git_dir);
690 markers.remove(&storage_refname);
691 write_empty_reflog_markers(&store_git_dir, &markers)?;
692 let mut stack = ReftableStack::open(&store_git_dir)?;
693 stack.replace_logs_for_ref(&storage_refname, &[])
694 }
695
696 fn replace_reflog(&self, name: &str, entries: Vec<ReflogEntry>) -> Result<()> {
697 self.rewrite_reflog(name, &mut |_| Ok(entries.clone()))
698 }
699
700 fn rewrite_reflog(
701 &self,
702 name: &str,
703 transform: &mut dyn FnMut(Vec<ReflogEntry>) -> Result<Vec<ReflogEntry>>,
704 ) -> Result<()> {
705 let (store_git_dir, storage_refname) = reftable_storage_location(&self.git_dir, name);
706 let mut stack = ReftableStack::open(&store_git_dir)?;
707 let mut empty = false;
708 stack.rewrite_logs_for_ref(&storage_refname, &mut |current| {
709 let rewritten = transform(current)?;
710 empty = rewritten.is_empty();
711 Ok(rewritten)
712 })?;
713 let mut markers = read_empty_reflog_markers(&store_git_dir);
714 if empty {
715 markers.insert(storage_refname);
716 } else {
717 markers.remove(&storage_refname);
718 }
719 write_empty_reflog_markers(&store_git_dir, &markers)
720 }
721
722 fn for_each_reflog_ref(&self, f: &mut dyn FnMut(&str) -> ControlFlow<()>) -> Result<()> {
723 let store_git_dir = stack_root(&self.git_dir);
724 let stack = ReftableStack::open(&store_git_dir)?;
725 let mut refs = read_empty_reflog_markers(&store_git_dir);
726 for log in stack.read_all_logs()? {
727 refs.insert(log.refname);
728 }
729 for name in refs {
730 if f(&name).is_break() {
731 break;
732 }
733 }
734 Ok(())
735 }
736}
737
738fn commit_reflog_rename_on_stack(
739 git_dir: &Path,
740 stack: &mut ReftableStack,
741 rename: &RefReflogRename,
742) -> StoreResult<()> {
743 let entries = read_reflog_entries_from_stack(git_dir, &rename.from)
744 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
745 let mut batch = RefBatchState {
746 refs: BTreeMap::new(),
747 reflogs: BTreeMap::from([(rename.from.clone(), entries)]),
748 };
749 apply_reflog_rename(&mut batch, rename)?;
750 let new_entries = batch.reflogs.get(&rename.to).cloned().unwrap_or_default();
751 let (_, from_storage) = reftable_storage_location(git_dir, &rename.from);
752 let (_, to_storage) = reftable_storage_location(git_dir, &rename.to);
753 stack
754 .replace_logs_for_ref(&to_storage, &new_entries)
755 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
756 stack
757 .replace_logs_for_ref(&from_storage, &[])
758 .map_err(|e| RefStoreError::Corrupt(e.to_string()))
759}
760
761struct ReftablePrepared<'a> {
762 store: &'a ReftableRefStore,
763 committed: bool,
764 rejections: Vec<(String, RefStoreError)>,
765}
766
767impl PreparedRefTransaction for ReftablePrepared<'_> {
768 fn rejected_updates(&self) -> &[(String, RefStoreError)] {
769 &self.rejections
770 }
771
772 fn commit(mut self: Box<Self>) -> StoreResult<()> {
773 let mut guard_state = self
774 .store
775 .prepared
776 .lock()
777 .unwrap_or_else(|e| e.into_inner());
778 let Some(batch) = guard_state.take() else {
779 return Ok(());
780 };
781 for mut stack_batch in batch.stacks {
782 if !stack_batch.updates.is_empty() {
783 stack_batch
784 .stack
785 .write_transaction_under_lock(
786 &mut stack_batch.lock,
787 stack_batch.updates,
788 &stack_batch.opts,
789 )
790 .map_err(map_reftable_err)?;
791 stack_batch
792 .stack
793 .maybe_auto_compact(&stack_batch.opts)
794 .map_err(map_reftable_err)?;
795 }
796 for rename in &batch.reflog_renames {
797 let (store_git_dir, _) =
798 reftable_storage_location(&self.store.git_dir, &rename.from);
799 if store_git_dir != stack_batch._store_git_dir {
800 continue;
801 }
802 commit_reflog_rename_on_stack(&self.store.git_dir, &mut stack_batch.stack, rename)
803 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
804 }
805 }
806 self.committed = true;
807 Ok(())
808 }
809
810 fn abort(mut self: Box<Self>) -> StoreResult<()> {
811 let mut guard_state = self
812 .store
813 .prepared
814 .lock()
815 .unwrap_or_else(|e| e.into_inner());
816 guard_state.take();
817 self.committed = true;
818 Ok(())
819 }
820}
821
822impl Drop for ReftablePrepared<'_> {
823 fn drop(&mut self) {
824 if self.committed {
825 return;
826 }
827 if let Ok(mut guard_state) = self.store.prepared.lock() {
828 guard_state.take();
829 }
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::super::RefUpdateFlags;
836 use super::*;
837
838 #[test]
839 fn map_reftable_err_keeps_lock_and_cas_failures_typed() {
840 let locked = map_reftable_err(
841 ReftableError::Locked {
842 lock_path: PathBuf::from("reftable/tables.list.lock"),
843 }
844 .into(),
845 );
846 assert!(matches!(locked, RefStoreError::LockHeld { name } if name == "refs"));
847
848 let expected = ObjectId::from_bytes(&[0x31; 20]).expect("oid");
849 let actual = ObjectId::from_bytes(&[0x32; 20]).expect("oid");
850 let mismatch = map_reftable_err(
851 ReftableError::ExpectedMismatch {
852 refname: "refs/heads/main".to_owned(),
853 expected,
854 actual: Some(actual),
855 }
856 .into(),
857 );
858 match mismatch {
859 RefStoreError::ExpectedMismatch {
860 name,
861 expected: Expected::Oid(want),
862 actual: Some(RawRef::Direct(found)),
863 } => {
864 assert_eq!(name, "refs/heads/main");
865 assert_eq!(want, expected);
866 assert_eq!(found, actual);
867 }
868 other => panic!("unexpected {other:?}"),
869 }
870
871 let corrupt = map_reftable_err(ReftableError::BadMagic.into());
872 assert!(matches!(corrupt, RefStoreError::Corrupt(msg) if msg.contains("bad magic")));
873 }
874 fn cas_update_oid(n: u32) -> ObjectId {
875 let mut b = [0u8; 20];
876 b[..4].copy_from_slice(&(n + 1).to_be_bytes());
877 ObjectId::from_bytes(&b).expect("oid")
878 }
879
880 #[test]
881 fn prepared_commit_runs_geometric_auto_compaction() {
882 let store = ReftableRefStore::open_ephemeral().expect("store");
883 const UPDATES: u32 = 100;
884 for i in 0..UPDATES {
885 let txn = RefTransaction::new()
886 .update(RefUpdate {
887 name: "refs/heads/hot".into(),
888 new_value: Some(RawRef::Direct(cas_update_oid(i))),
889 expected: if i == 0 {
890 Expected::Missing
891 } else {
892 Expected::Oid(cas_update_oid(i - 1))
893 },
894 reflog: None,
895 flags: RefUpdateFlags::default(),
896 })
897 .expect("txn");
898 store
899 .prepare(txn)
900 .expect("prepare")
901 .commit()
902 .expect("commit");
903 }
904 let stack = ReftableStack::open(&store.git_dir).expect("stack");
905 assert!(
906 stack.table_names().len() <= 8,
907 "expected bounded reftable stack, got {} tables",
908 stack.table_names().len()
909 );
910 assert_eq!(
911 store.read_raw("refs/heads/hot").expect("read"),
912 Some(RawRef::Direct(cas_update_oid(UPDATES - 1)))
913 );
914 }
915
916 #[test]
917 fn head_values_use_stack_transactions_and_ignore_loose_files() {
918 let store = ReftableRefStore::open_ephemeral().expect("store");
919 let update = |value, expected| {
920 RefTransaction::new()
921 .update(RefUpdate {
922 name: "HEAD".into(),
923 new_value: value,
924 expected,
925 reflog: None,
926 flags: super::super::RefUpdateFlags {
927 no_deref: true,
928 log_only: false,
929 ..Default::default()
930 },
931 })
932 .expect("transaction")
933 };
934 let symbolic = RawRef::Symbolic("refs/heads/topic".into());
935 store
936 .prepare(update(Some(symbolic.clone()), Expected::Missing))
937 .expect("prepare")
938 .commit()
939 .expect("commit");
940 let stub = fs::read(store.git_dir.join("HEAD")).expect("stub");
941 assert_eq!(stub, b"ref: refs/heads/.invalid\n");
942 assert_eq!(store.read_raw("HEAD").expect("read"), Some(symbolic));
943 let names_with_prefix = |prefix: &str| {
944 let mut names = Vec::new();
945 store
946 .for_each_ref(prefix, &mut |entry| {
947 names.push(entry.name.clone());
948 ControlFlow::Continue(())
949 })
950 .expect("list");
951 names
952 };
953 assert!(names_with_prefix("").is_empty());
954 assert_eq!(names_with_prefix("HEAD"), ["HEAD"]);
955 let oid = ObjectId::from_bytes(&[0x31; 20]).expect("oid");
956 let detached = RawRef::Direct(oid);
957 let txn = update(
958 Some(detached.clone()),
959 Expected::Symref("refs/heads/topic".into()),
960 );
961 store
962 .prepare(txn.clone())
963 .expect("prepare")
964 .abort()
965 .expect("abort");
966 assert!(matches!(
967 store.read_raw("HEAD").expect("read"),
968 Some(RawRef::Symbolic(_))
969 ));
970 store
971 .prepare(txn)
972 .expect("prepare")
973 .commit()
974 .expect("commit");
975 fs::write(store.git_dir.join("HEAD"), "ref: refs/heads/stale\n").expect("stale HEAD");
976 assert_eq!(
977 store.read_raw("HEAD").expect("read"),
978 Some(detached.clone())
979 );
980 assert_eq!(
981 crate::reftable::reftable_resolve_ref(&store.git_dir, "HEAD").expect("resolve"),
982 oid
983 );
984 assert!(store
985 .prepare(update(None, Expected::Oid(ObjectId::zero())))
986 .is_err());
987 assert_eq!(store.read_raw("HEAD").expect("read"), Some(detached));
988 store
989 .prepare(update(None, Expected::Oid(oid)))
990 .expect("prepare")
991 .commit()
992 .expect("commit");
993 assert_eq!(store.read_raw("HEAD").expect("deleted"), None);
994 ReftableStack::open(&store.git_dir)
995 .expect("stack")
996 .compact()
997 .expect("compact");
998 assert_eq!(
999 store.read_raw("HEAD").expect("deleted after compaction"),
1000 None
1001 );
1002 }
1003}