Skip to main content

grit_lib/refs/store/
reftable.rs

1//! Reftable-backed [`super::RefStore`] over [`crate::reftable::ReftableStack`].
2
3use 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
37/// On-disk ref storage using the reftable backend (`extensions.refStorage = reftable`).
38///
39/// # Atomicity
40///
41/// Refs live in one reftable stack per location: the common stack in the repository's git
42/// directory, and in a linked worktree a second stack under `worktrees/<id>/` for `HEAD` and
43/// other per-worktree refs. A transaction touching one stack commits as a single new table,
44/// so readers see all of it or none of it.
45///
46/// A transaction touching both stacks (for example updating `HEAD` in a linked worktree,
47/// which writes the branch in the common stack and logs `HEAD` in the worktree stack) takes
48/// the `tables.list` lock of every stack in [`RefStore::prepare`], so no other writer can
49/// interleave, and all checks pass before anything is written. Commit then writes one table
50/// per stack, common stack first. These writes are not atomic together: if a later write
51/// fails (an I/O error), [`PreparedRefTransaction::commit`] returns the error, the earlier
52/// stacks keep their new table, and every remaining lock is released. For a `HEAD` update
53/// this means the branch already points at the new commit with its reflog line, `HEAD` still
54/// resolves to it through the symbolic ref, and only `HEAD`'s own reflog line is missing.
55pub 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    /// Open the reftable store for an existing repository git directory.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`Error::Io`] when the reftable stack cannot be read.
89    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    /// Create a new ephemeral reftable repository (tests).
112    ///
113    /// # Errors
114    ///
115    /// Returns an error when initialization fails.
116    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        // Match [`MemoryRefStore`]: conformance seeds start from an empty namespace.
133        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    /// Write low-level reftable transaction updates (used by legacy `reftable_*` helpers).
155    ///
156    /// # Errors
157    ///
158    /// Propagates lock, CAS, and I/O failures from the underlying stack.
159    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    /// Rewrite updates through a symbolic ref whose target lives in another stack (a linked
199    /// worktree's `HEAD` pointing at a branch in the common stack) as two updates: the write
200    /// of the target, checked and logged in its own stack, and a log-only line for the
201    /// symbolic ref in its stack.
202    ///
203    /// The two halves commit to different stacks; see the atomicity notes on
204    /// [`ReftableRefStore`] for what a failed second write leaves behind.
205    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            // Only per-worktree refs (HEAD, refs/worktree/*, …) can point across stacks.
214            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    /// End of the symbolic chain `update` writes through, when it lies outside `update_stack`.
267    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            // A shared-stack snapshot must use this worktree's HEAD, not the main one's.
308            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        // `tz_offset` holds the `HHMM` digits as a decimal number (Git's on-disk form).
324        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
348/// HEAD is authoritative in this worktree's stack; its loose file is only a discovery stub.
349fn 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}