Skip to main content

grit_lib/refs/store/
files.rs

1//! Filesystem ref backend (loose refs + `packed-refs`) implementing [`super::RefStore`].
2
3use std::cell::RefCell;
4use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
5use std::fs;
6use std::io;
7use std::ops::ControlFlow;
8use std::path::{Path, PathBuf};
9use std::sync::Mutex;
10
11use crate::error::{Error, Result};
12use crate::git_dir::io_error::{
13    io_with_ref_store_path, map_ref_store_path_io, GitDirPathOperation,
14};
15use crate::lockfile::LockWait;
16use crate::objects::ObjectId;
17use crate::reflog::{self, ReflogEntry};
18use crate::refs::{
19    lock_path_for_ref, loose_ref_list_walk_root, packed_record_name, packed_record_oid,
20    prune_empty_ref_dirs, read_packed_refs_file_view, read_packed_refs_map_for_prefix_filtered,
21    read_ref_file, remove_empty_ref_directory, remove_packed_ref, remove_packed_ref_under_lock,
22    remove_packed_refs_under_lock, restore_packed_records_under_lock, PackedPeel,
23    PackedRecordFilter, Ref,
24};
25
26use super::apply::{
27    apply_case_only_ref_rename, apply_reflog_rename, apply_update, ref_change_target,
28    ref_lock_names_for_updates, simulate_batch_apply, RefBatchState,
29};
30use super::error::RefStoreError;
31use super::routing::{resolve_common_dir, route_ref_storage_in_common, RefStorageRoute};
32use super::transaction::expected_matches;
33use super::transaction::RefTransaction;
34use super::types::Expected;
35use super::validation::{
36    validate_storable_refname, verify_create_conflicts, verify_name_available_for_create,
37};
38use super::{PreparedRefTransaction, RawRef, RefEntry, RefStorageFormat, RefStore, RefUpdate};
39use crate::refs::RefnameUnavailable;
40
41type StoreResult<T> = std::result::Result<T, RefStoreError>;
42
43/// Explicit context for a files ref store (no environment reads for namespace).
44#[derive(Debug, Clone)]
45pub struct FilesRefStoreConfig {
46    /// Worktree or bare repository git directory.
47    pub git_dir: PathBuf,
48    /// Shared object/ref store (`commondir` target or same as `git_dir`).
49    pub common_dir: PathBuf,
50    /// Optional `GIT_NAMESPACE`-style prefix applied to storage paths (not read from env).
51    pub namespace_prefix: Option<String>,
52}
53
54/// Loose + packed ref storage on disk.
55#[derive(Debug)]
56pub struct FilesRefStore {
57    git_dir: PathBuf,
58    common_dir: PathBuf,
59    namespace_prefix: Option<String>,
60    packed_cache: Mutex<PackedCacheEntry>,
61    prepared: Mutex<Option<PreparedFilesBatch>>,
62}
63
64/// What a cached `packed-refs` view was read from. Writers replace the file by rename, so the
65/// file identity changes even when the new file has the same length and mtime.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67struct PackedRefsStamp {
68    len: u64,
69    modified: Option<std::time::SystemTime>,
70    /// Device, inode and status-change time (seconds, nanoseconds), where the platform has them.
71    identity: Option<(u64, u64, i64, i64)>,
72}
73
74#[derive(Debug)]
75struct PackedCacheEntry {
76    stamp: PackedRefsStamp,
77    loaded: bool,
78    file: Option<crate::refs::PackedRefsFileView>,
79    /// Full packed-refs map; only valid when [`Self::map_is_complete`].
80    map: Option<BTreeMap<String, ObjectId>>,
81    map_is_complete: bool,
82}
83
84/// Per-batch state shared by the steps of [`FilesRefStore::persist_state_diff`].
85#[derive(Debug, Clone, Copy)]
86struct CommitContext<'a> {
87    /// How long to wait for each loose ref lock, read from config once per batch.
88    ref_lock_wait: LockWait,
89    /// Whether the batch holds `packed-refs.lock`.
90    lock_held: bool,
91    /// Logical names whose packed records were already dropped in one up-front rewrite.
92    pruned: &'a HashSet<String>,
93    /// Values of the records that rewrite removed, so commit-time rechecks still see them.
94    removed: &'a HashMap<String, RawRef>,
95    /// Directories that deletions may have left empty (mapped to their storage directory),
96    /// pruned once the batch is done instead of after every ref.
97    emptied_dirs: &'a RefCell<BTreeMap<PathBuf, PathBuf>>,
98}
99
100/// What [`FilesRefStore::prune_deleted_packed_records`] took out of `packed-refs`.
101#[derive(Debug, Default)]
102struct PrunedPackedRecords {
103    /// Logical names of every ref the batch deletes.
104    names: HashSet<String>,
105    /// Removed values of deletions whose expectation the commit-time recheck verifies.
106    removed: HashMap<String, RawRef>,
107    /// Removed records verbatim, per storage directory.
108    records: Vec<(PathBuf, Vec<String>)>,
109}
110
111#[derive(Debug)]
112struct PreparedFilesBatch {
113    updates: Vec<RefUpdate>,
114    case_only_renames: Vec<super::RefCaseOnlyRename>,
115    ref_locks: HashMap<String, PathBuf>,
116    packed_lock: Option<PathBuf>,
117    ref_lock_wait: LockWait,
118    /// Ref/reflog state observed at prepare (must drive commit, not a reload).
119    before_commit: RefBatchState,
120    after_commit: RefBatchState,
121}
122
123impl FilesRefStore {
124    /// Open a files backend at `config.git_dir`.
125    #[must_use]
126    pub fn open(config: FilesRefStoreConfig) -> Self {
127        let path = super::paths::packed_refs_path(&config.common_dir);
128        let stamp = Self::packed_refs_stamp(&path);
129        Self {
130            git_dir: config.git_dir,
131            common_dir: config.common_dir,
132            namespace_prefix: config.namespace_prefix,
133            packed_cache: Mutex::new(PackedCacheEntry {
134                stamp,
135                loaded: false,
136                file: None,
137                map: None,
138                map_is_complete: false,
139            }),
140            prepared: Mutex::new(None),
141        }
142    }
143
144    /// Git directory this store reads `HEAD` and per-worktree refs from.
145    pub(crate) fn git_dir(&self) -> &Path {
146        &self.git_dir
147    }
148
149    /// Convenience constructor for a simple repository directory.
150    pub fn from_git_dir(git_dir: impl Into<PathBuf>) -> Result<Self> {
151        let git_dir: PathBuf = git_dir.into();
152        let git_dir = fs::canonicalize(&git_dir).unwrap_or(git_dir);
153        let common_dir = resolve_common_dir(&git_dir);
154        Ok(Self::open(FilesRefStoreConfig {
155            git_dir: git_dir.clone(),
156            common_dir,
157            namespace_prefix: crate::ref_namespace::ref_storage_prefix_default(),
158        }))
159    }
160
161    fn packed_refs_stamp(path: &Path) -> PackedRefsStamp {
162        match fs::metadata(path) {
163            Ok(meta) => PackedRefsStamp {
164                len: meta.len(),
165                modified: meta.modified().ok(),
166                identity: file_identity(&meta),
167            },
168            Err(_) => PackedRefsStamp {
169                len: 0,
170                modified: None,
171                identity: None,
172            },
173        }
174    }
175
176    /// Drop the cached `packed-refs` view so the next lookup reads the file again.
177    ///
178    /// Stamps can miss a replacement on filesystems with coarse timestamps or reused inodes,
179    /// so checks whose answer must reflect the file right now (the create-conflict recheck
180    /// under the ref lock) start from a fresh read.
181    fn forget_packed_cache(&self) {
182        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
183        guard.loaded = false;
184        guard.file = None;
185        guard.map = None;
186        guard.map_is_complete = false;
187    }
188
189    fn invalidate_packed_cache_after_write(&self) {
190        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
191        guard.map = None;
192        guard.map_is_complete = false;
193    }
194
195    fn refresh_packed_cache_if_stale(&self) -> Result<()> {
196        let path = super::paths::packed_refs_path(&self.common_dir);
197        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
198        let stamp = Self::packed_refs_stamp(&path);
199        if guard.loaded && guard.stamp == stamp {
200            return Ok(());
201        }
202        guard.file = read_packed_refs_file_view(&self.common_dir)?;
203        guard.map = None;
204        guard.map_is_complete = false;
205        guard.stamp = stamp;
206        guard.loaded = true;
207        Ok(())
208    }
209
210    fn ensure_full_packed_map(&self) -> Result<()> {
211        self.refresh_packed_cache_if_stale()?;
212        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
213        if guard.map.is_some() && guard.map_is_complete {
214            return Ok(());
215        }
216        let map = if let Some(ref file) = guard.file {
217            let path = super::paths::packed_refs_path(&self.common_dir);
218            file.map_for_prefix(&path, None)?
219                .into_iter()
220                .collect::<BTreeMap<_, _>>()
221        } else {
222            BTreeMap::new()
223        };
224        guard.map = Some(map);
225        guard.map_is_complete = true;
226        Ok(())
227    }
228
229    fn first_packed_descendant(&self, storage_name: &str) -> Result<Option<(String, ObjectId)>> {
230        self.refresh_packed_cache_if_stale()?;
231        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
232        let Some(ref file) = guard.file else {
233            return Ok(None);
234        };
235        let path = super::paths::packed_refs_path(&self.common_dir);
236        file.first_descendant(&path, storage_name)
237    }
238
239    /// Records of the common dir's `packed-refs` under `prefix` (storage names), read through
240    /// the cached file view so a listing and later lookups index the file once.
241    fn packed_map_for_prefix_filtered(
242        &self,
243        prefix: &str,
244        keep: Option<PackedRecordFilter<'_>>,
245    ) -> Result<HashMap<String, ObjectId>> {
246        self.refresh_packed_cache_if_stale()?;
247        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
248        let Some(ref file) = guard.file else {
249            return Ok(HashMap::new());
250        };
251        let path = super::paths::packed_refs_path(&self.common_dir);
252        let prefix = (!prefix.is_empty()).then_some(prefix);
253        file.map_for_prefix_filtered(&path, prefix, keep)
254    }
255
256    /// The common dir's `packed-refs` view this store reads through, refreshed when the file
257    /// changed (`None` without a `packed-refs`). The copy shares the file's bytes and index.
258    pub(crate) fn packed_refs_view(&self) -> Result<Option<crate::refs::PackedRefsFileView>> {
259        self.refresh_packed_cache_if_stale()?;
260        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
261        Ok(guard.file.clone())
262    }
263
264    fn lookup_packed_oid(&self, storage_name: &str) -> Result<Option<ObjectId>> {
265        self.refresh_packed_cache_if_stale()?;
266        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
267        let Some(ref file) = guard.file else {
268            return Ok(None);
269        };
270        let path = super::paths::packed_refs_path(&self.common_dir);
271        file.lookup_oid(&path, storage_name)
272    }
273
274    /// Add to `refs` every existing ref that can block creating a new ref of `updates`: its
275    /// ancestors (`refs/heads/a` for `refs/heads/a/b`) and the refs under `<name>/`.
276    ///
277    /// `refs` must already hold every update target that exists. With these neighbors,
278    /// [`verify_create_conflicts`] reaches the same verdict as against the whole store, so a
279    /// create costs a few lookups instead of reading every ref and reflog.
280    fn insert_create_conflict_neighbors(
281        &self,
282        refs: &mut BTreeMap<String, RawRef>,
283        updates: &[RefUpdate],
284    ) -> Result<()> {
285        let creates: Vec<&str> = updates
286            .iter()
287            .filter(|u| u.new_value.is_some() && !u.flags.log_only && !refs.contains_key(&u.name))
288            .map(|u| u.name.as_str())
289            .collect();
290        for name in creates {
291            for (slash, _) in name.match_indices('/') {
292                let ancestor = &name[..slash];
293                if refs.contains_key(ancestor) {
294                    continue;
295                }
296                if let Some(raw) = self.read_existing_ref_named(ancestor)? {
297                    refs.insert(ancestor.to_owned(), raw);
298                }
299            }
300            for (descendant, raw) in self.descendant_refs(name)? {
301                refs.entry(descendant).or_insert(raw);
302            }
303        }
304        Ok(())
305    }
306
307    /// Whether `name` can be created next to the refs on disk and the other names of its batch
308    /// (`extras`), under the rules of [`crate::refs::verify_refname_available_for_create`].
309    ///
310    /// Lookups go through this store's packed-refs cache, which revalidates against the file's
311    /// size and mtime, instead of re-reading `packed-refs` for every ancestor. Any file under
312    /// `<name>/` blocks the create, since the directory could not be replaced by the ref file.
313    /// Unreadable refs count as absent, as in the path-based check.
314    fn verify_create_available(
315        &self,
316        name: &str,
317        extras: &BTreeSet<String>,
318    ) -> std::result::Result<(), RefnameUnavailable> {
319        let mut blocking = BTreeSet::new();
320        for (slash, _) in name.match_indices('/') {
321            let ancestor = &name[..slash];
322            if let Ok(Some(_)) = self.read_existing_ref_named(ancestor) {
323                blocking.insert(ancestor.to_owned());
324            }
325        }
326        let storage = self.route(name).storage_name;
327        let mut storage_names = BTreeSet::new();
328        for base in self.store_bases() {
329            collect_loose_file_names(&base.join(&storage), &storage, &mut storage_names);
330        }
331        if storage.contains('/') {
332            if let Ok(Some((packed, _))) = self.first_packed_descendant(&storage) {
333                storage_names.insert(packed);
334            }
335        }
336        blocking.extend(storage_names.iter().map(|s| self.logical_name(s)));
337        verify_name_available_for_create(name, &blocking, extras)
338    }
339
340    fn store_bases(&self) -> impl Iterator<Item = &Path> {
341        let common = self.common_dir.as_path();
342        let git = self.git_dir.as_path();
343        std::iter::once(common).chain((git != common).then_some(git))
344    }
345
346    /// The value of `name` as a full listing would report it: a parseable loose file in either
347    /// store directory, else a visible packed record. Directories and unparseable files are
348    /// not refs.
349    fn read_existing_ref_named(&self, name: &str) -> Result<Option<RawRef>> {
350        let storage = self.route(name).storage_name;
351        for base in self.store_bases() {
352            let path = base.join(&storage);
353            if !path.is_file() {
354                continue;
355            }
356            if let Ok(r) = read_ref_file(&path) {
357                return Ok(Some(ref_to_raw(r)));
358            }
359        }
360        if !self.packed_storage_visible(&storage) {
361            return Ok(None);
362        }
363        Ok(self.lookup_packed_oid(&storage)?.map(RawRef::Direct))
364    }
365
366    /// Loose refs under `<name>/` in either store directory, plus the first packed one.
367    fn descendant_refs(&self, name: &str) -> Result<BTreeMap<String, RawRef>> {
368        let storage = self.route(name).storage_name;
369        let mut storage_map = BTreeMap::new();
370        for base in self.store_bases() {
371            let dir = base.join(&storage);
372            if dir.is_dir() {
373                collect_loose_raw_refs(&dir, &format!("{storage}/"), "", &mut storage_map)?;
374            }
375        }
376        // Refs live under `refs/`, so a one-level name has no packed descendants; skipping it
377        // also avoids scanning from a pseudoref like `ORIG_HEAD` to the end of the file.
378        if storage.contains('/') {
379            if let Some((packed, oid)) = self.first_packed_descendant(&storage)? {
380                if self.packed_storage_visible(&packed) {
381                    storage_map.entry(packed).or_insert(RawRef::Direct(oid));
382                }
383            }
384        }
385        Ok(storage_map
386            .into_iter()
387            .map(|(storage, value)| (self.logical_name(&storage), value))
388            .collect())
389    }
390
391    /// Load `start` and any further symbolic targets into `refs` (up to [`crate::refs::SYMREF_MAXDEPTH`]).
392    fn insert_symref_peel_chain(
393        &self,
394        refs: &mut BTreeMap<String, RawRef>,
395        start: &str,
396    ) -> Result<()> {
397        let mut name = start.to_owned();
398        for _ in 0..crate::refs::SYMREF_MAXDEPTH {
399            if refs.contains_key(&name) {
400                match refs.get(&name) {
401                    Some(RawRef::Symbolic(next)) => name = next.clone(),
402                    _ => break,
403                }
404                continue;
405            }
406            let Some(raw) = self.read_raw(&name)? else {
407                break;
408            };
409            let advance = match &raw {
410                RawRef::Symbolic(next) => Some(next.clone()),
411                RawRef::Direct(_) => None,
412            };
413            refs.insert(name.clone(), raw);
414            match advance {
415                Some(next) => name = next,
416                None => break,
417            }
418        }
419        Ok(())
420    }
421
422    fn load_batch_state_for_prepare(
423        &self,
424        updates: &[RefUpdate],
425        reflog_renames: &[super::RefReflogRename],
426        case_only_renames: &[super::RefCaseOnlyRename],
427    ) -> Result<RefBatchState> {
428        let mut refs = BTreeMap::new();
429        for update in updates {
430            if let Some(raw) = self.read_raw(&update.name)? {
431                refs.insert(update.name.clone(), raw);
432            }
433        }
434        for rename in case_only_renames {
435            if refs.contains_key(&rename.from) {
436                continue;
437            }
438            if let Some(raw) = self.read_raw(&rename.from)? {
439                refs.insert(rename.from.clone(), raw);
440            }
441        }
442        self.insert_head_for_batch_state(&mut refs)?;
443
444        for update in updates {
445            if matches!(refs.get(&update.name), Some(RawRef::Symbolic(_))) {
446                self.insert_symref_peel_chain(&mut refs, &update.name)?;
447            }
448        }
449        if matches!(refs.get("HEAD"), Some(RawRef::Symbolic(_))) {
450            self.insert_symref_peel_chain(&mut refs, "HEAD")?;
451        }
452
453        let mut reflogs = self.load_reflogs_for_updates(&refs, updates)?;
454        for rename in reflog_renames {
455            if reflogs.contains_key(&rename.from) {
456                continue;
457            }
458            let entries = read_reflog_at_path(&self.reflog_path(&rename.from))?;
459            if !entries.is_empty() {
460                reflogs.insert(rename.from.clone(), entries);
461            }
462        }
463        Ok(RefBatchState { refs, reflogs })
464    }
465
466    fn ensure_reflog_path_writable(&self, name: &str) -> StoreResult<()> {
467        let path = self.reflog_path(name);
468        if path.is_dir() {
469            return Err(RefStoreError::Corrupt(format!(
470                "refusing to write reflog at '{}': path is a directory",
471                path.display()
472            )));
473        }
474        Ok(())
475    }
476
477    fn read_raw_at_route(&self, route: &RefStorageRoute) -> Result<Option<RawRef>> {
478        let path = route.storage_dir.join(&route.storage_name);
479        match read_ref_file(&path) {
480            Ok(Ref::Direct(oid)) => return Ok(Some(RawRef::Direct(oid))),
481            Ok(Ref::Symbolic(target)) => return Ok(Some(RawRef::Symbolic(target))),
482            Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
483            Err(e) => return Err(e),
484        }
485        if let Some(oid) = self.lookup_packed_oid(&route.storage_name)? {
486            return Ok(Some(RawRef::Direct(oid)));
487        }
488        Ok(None)
489    }
490
491    fn packed_storage_visible(&self, storage: &str) -> bool {
492        match self.namespace_prefix.as_deref() {
493            None => true,
494            Some(prefix) => storage.starts_with(prefix),
495        }
496    }
497
498    /// Prefix for scanning `packed-refs` lines (storage paths, not logical names).
499    fn packed_refs_scan_prefix(&self, logical_prefix: &str) -> String {
500        match self.namespace_prefix.as_deref() {
501            Some(ns) if !logical_prefix.is_empty() => format!("{ns}{logical_prefix}"),
502            Some(ns) => ns.trim_end_matches('/').to_owned(),
503            None => logical_prefix.to_owned(),
504        }
505    }
506
507    /// Raw refs whose logical names start with `logical_prefix`, loose values overriding packed
508    /// ones. `keep`, given a packed record's storage name, id and peel, may leave it out.
509    fn load_raw_map_prefix(
510        &self,
511        logical_prefix: &str,
512        keep: Option<PackedRecordFilter<'_>>,
513    ) -> Result<BTreeMap<String, RawRef>> {
514        let logical_prefix = logical_prefix.trim_end_matches('/');
515        let mut map: BTreeMap<String, RawRef> = BTreeMap::new();
516        if logical_prefix.is_empty() {
517            self.ensure_full_packed_map()?;
518            let cache = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
519            if let Some(packed) = cache.map.as_ref() {
520                for (storage, oid) in packed.iter() {
521                    if !self.packed_storage_visible(storage)
522                        || keep.is_some_and(|keep| !keep(storage, oid, PackedPeel::Unknown))
523                    {
524                        continue;
525                    }
526                    let logical = self.logical_name(storage);
527                    map.insert(logical, RawRef::Direct(*oid));
528                }
529            }
530            drop(cache);
531        } else {
532            let common = self.common_dir.as_path();
533            let git = self.git_dir.as_path();
534            let bases: [&Path; 2] = if common == git {
535                [common, common]
536            } else {
537                [common, git]
538            };
539            for (i, base) in bases.iter().enumerate() {
540                if i == 1 && *base == bases[0] {
541                    continue;
542                }
543                let packed_prefix = self.packed_refs_scan_prefix(logical_prefix);
544                let packed = if i == 0 {
545                    self.packed_map_for_prefix_filtered(&packed_prefix, keep)?
546                } else {
547                    read_packed_refs_map_for_prefix_filtered(base, &packed_prefix, keep)?
548                };
549                for (storage, oid) in packed {
550                    if !self.packed_storage_visible(&storage) {
551                        continue;
552                    }
553                    let logical = self.logical_name(&storage);
554                    map.insert(logical, RawRef::Direct(oid));
555                }
556            }
557        }
558
559        for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
560            let storage_prefix = self.namespace_prefix.as_deref().unwrap_or("refs/");
561            if self.namespace_prefix.is_some() {
562                let root = base.join(storage_prefix.trim_end_matches('/'));
563                if root.is_dir() {
564                    let mut storage_map = BTreeMap::new();
565                    collect_loose_raw_refs(&root, storage_prefix, "", &mut storage_map)?;
566                    for (storage, value) in storage_map {
567                        let logical = self.logical_name(&storage);
568                        if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
569                            map.insert(logical, value);
570                        }
571                    }
572                }
573            } else {
574                let (walk_root, walk_prefix) = loose_ref_list_walk_root(base, logical_prefix);
575                if walk_root.is_dir() {
576                    let mut storage_map = BTreeMap::new();
577                    collect_loose_raw_refs(
578                        &walk_root,
579                        &walk_prefix,
580                        logical_prefix,
581                        &mut storage_map,
582                    )?;
583                    for (storage, value) in storage_map {
584                        let logical = self.logical_name(&storage);
585                        if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
586                            map.insert(logical, value);
587                        }
588                    }
589                }
590            }
591            if head_matches_prefix(logical_prefix) && base.join("HEAD").is_file() {
592                if let Ok(r) = read_ref_file(&base.join("HEAD")) {
593                    map.insert("HEAD".to_owned(), ref_to_raw(r));
594                }
595            }
596        }
597        Ok(map)
598    }
599
600    /// Reflogs a batch can append to: each update that carries a reflog, and the ref its
601    /// symbolic chain (as recorded in `refs`) ends at. Updates without a reflog touch none,
602    /// so plain deletes and CAS updates skip reading `logs/` entirely.
603    fn load_reflogs_for_updates(
604        &self,
605        refs: &BTreeMap<String, RawRef>,
606        updates: &[RefUpdate],
607    ) -> Result<BTreeMap<String, Vec<ReflogEntry>>> {
608        let mut names = BTreeSet::new();
609        for update in updates.iter().filter(|u| u.reflog.is_some()) {
610            let mut name = update.name.as_str();
611            names.insert(name.to_owned());
612            for _ in 0..crate::refs::SYMREF_MAXDEPTH {
613                let Some(RawRef::Symbolic(next)) = refs.get(name) else {
614                    break;
615                };
616                name = next;
617                names.insert(name.to_owned());
618            }
619        }
620        let mut out = BTreeMap::new();
621        for name in names {
622            let entries = read_reflog_at_path(&self.reflog_path(&name))?;
623            if !entries.is_empty() {
624                out.insert(name, entries);
625            }
626        }
627        Ok(out)
628    }
629
630    fn insert_head_for_batch_state(&self, map: &mut BTreeMap<String, RawRef>) -> Result<()> {
631        if map.contains_key("HEAD") {
632            return Ok(());
633        }
634        for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
635            let head_path = base.join("HEAD");
636            if head_path.is_file() {
637                let raw = read_ref_file(&head_path).map(ref_to_raw)?;
638                map.insert("HEAD".to_owned(), raw);
639                break;
640            }
641        }
642        Ok(())
643    }
644
645    fn loose_ref_file_exists(&self, refname: &str) -> Result<bool> {
646        let route = self.route(refname);
647        let path = route.storage_dir.join(&route.storage_name);
648        Ok(path.is_file())
649    }
650
651    fn route(&self, refname: &str) -> RefStorageRoute {
652        route_ref_storage_in_common(
653            &self.git_dir,
654            &self.common_dir,
655            refname,
656            self.namespace_prefix.as_deref(),
657        )
658    }
659
660    fn logical_name(&self, storage: &str) -> String {
661        crate::ref_namespace::logical_ref_name_with_prefix(
662            self.namespace_prefix.as_deref(),
663            storage,
664        )
665    }
666
667    fn reflog_path(&self, logical: &str) -> PathBuf {
668        let route = self.route(logical);
669        route.storage_dir.join("logs").join(&route.storage_name)
670    }
671
672    fn ref_lock_paths(&self, refname: &str) -> (RefStorageRoute, PathBuf, PathBuf) {
673        let route = self.route(refname);
674        let path = route.storage_dir.join(&route.storage_name);
675        let lock = lock_path_for_ref(&path);
676        (route, path, lock)
677    }
678
679    fn needs_packed_lock(&self, updates: &[RefUpdate]) -> bool {
680        updates.iter().any(|u| {
681            if u.new_value.is_some() || u.flags.log_only {
682                return false;
683            }
684            let storage = self.route(&u.name).storage_name;
685            self.lookup_packed_oid(&storage).ok().flatten().is_some()
686        })
687    }
688
689    fn acquire_ref_lock_mapped(
690        &self,
691        lock_path: &Path,
692        refname: &str,
693        wait: LockWait,
694    ) -> Result<()> {
695        use crate::lockfile::{LockError, LockFile};
696        LockFile::acquire(lock_path, wait)
697            .map(|guard| guard.detach())
698            .map_err(|err| match err {
699                LockError::Held { .. } => Error::RefStore(RefStoreError::LockHeld {
700                    name: refname.to_owned(),
701                }),
702                LockError::Io { source, path } => map_ref_store_path_io(
703                    &self.common_dir,
704                    &self.git_dir,
705                    path.as_path(),
706                    GitDirPathOperation::UpdateRef,
707                    source,
708                ),
709            })
710    }
711
712    fn ref_store_error_from_lib(err: Error) -> RefStoreError {
713        match err {
714            Error::GitDirPathIo {
715                path,
716                operation,
717                source,
718            } => RefStoreError::git_dir_path_io(path, operation, source),
719            Error::RefStore(store_err) => store_err,
720            other => RefStoreError::Corrupt(other.to_string()),
721        }
722    }
723
724    fn io_ref_store<T>(
725        &self,
726        path: &Path,
727        operation: GitDirPathOperation,
728        result: io::Result<T>,
729    ) -> Result<T> {
730        io_with_ref_store_path(&self.common_dir, &self.git_dir, path, operation, result)
731    }
732
733    fn release_locks(batch: &PreparedFilesBatch) {
734        for lock in batch.ref_locks.values() {
735            let _ = fs::remove_file(lock);
736        }
737        if let Some(p) = &batch.packed_lock {
738            let _ = fs::remove_file(p);
739        }
740    }
741
742    fn write_loose_value(
743        common_dir: &Path,
744        checkout_git_dir: &Path,
745        path: &Path,
746        lock: &Path,
747        value: &RawRef,
748    ) -> Result<()> {
749        remove_empty_ref_directory(path);
750        if let Some(parent) = path.parent() {
751            io_with_ref_store_path(
752                common_dir,
753                checkout_git_dir,
754                parent,
755                GitDirPathOperation::CreateDir,
756                fs::create_dir_all(parent),
757            )?;
758        }
759        let content = match value {
760            RawRef::Direct(oid) => format!("{oid}\n"),
761            RawRef::Symbolic(target) => format!("ref: {target}\n"),
762        };
763        io_with_ref_store_path(
764            common_dir,
765            checkout_git_dir,
766            lock,
767            GitDirPathOperation::UpdateRef,
768            fs::write(lock, content),
769        )?;
770        io_with_ref_store_path(
771            common_dir,
772            checkout_git_dir,
773            path,
774            GitDirPathOperation::UpdateRef,
775            fs::rename(lock, path),
776        )?;
777        Ok(())
778    }
779
780    fn persist_case_only_loose_ref(
781        &self,
782        rename: &super::RefCaseOnlyRename,
783        after: &RefBatchState,
784        ctx: CommitContext<'_>,
785    ) -> Result<()> {
786        let Some(value) = after.refs.get(&rename.to) else {
787            return Err(Error::Message(format!(
788                "case-only rename missing destination ref '{}'",
789                rename.to
790            )));
791        };
792        let (_, from_path, from_lock) = self.ref_lock_paths(&rename.from);
793        if let Some(parent) = from_lock.parent() {
794            fs::create_dir_all(parent).map_err(Error::Io)?;
795        }
796        self.acquire_ref_lock_mapped(&from_lock, &rename.from, ctx.ref_lock_wait)?;
797        if !from_path.is_file() {
798            self.remove_packed_record(&rename.from, ctx)?;
799            FilesRefStore::write_loose_value(
800                &self.common_dir,
801                &self.git_dir,
802                &from_path,
803                &from_lock,
804                value,
805            )?;
806        }
807        let (_, to_path, _) = self.ref_lock_paths(&rename.to);
808        if from_path != to_path {
809            if let Some(parent) = to_path.parent() {
810                fs::create_dir_all(parent).map_err(Error::Io)?;
811            }
812            fs::rename(&from_path, &to_path).map_err(Error::Io)?;
813        }
814        let _ = fs::remove_file(&from_lock);
815        Ok(())
816    }
817
818    /// Drop `refname`'s `packed-refs` record unless the up-front batch rewrite already did.
819    fn remove_packed_record(&self, refname: &str, ctx: CommitContext<'_>) -> Result<()> {
820        let route = self.route(refname);
821        if !ctx.lock_held {
822            return remove_packed_ref(&route.storage_dir, &route.storage_name);
823        }
824        if ctx.pruned.contains(refname) {
825            return Ok(());
826        }
827        remove_packed_ref_under_lock(&route.storage_dir, &route.storage_name)
828    }
829
830    /// Remove the `packed-refs` records of every ref `before` has and `after` drops, one
831    /// rewrite per storage directory, ahead of any loose-file change (Git's order, so a
832    /// deleted loose ref never uncovers a stale packed value).
833    ///
834    /// The returned [`PrunedPackedRecords`] covers every pruned name, the removed values the
835    /// commit-time recheck still needs, and the records themselves for
836    /// [`Self::restore_unprocessed_packed_records`].
837    fn prune_deleted_packed_records(
838        &self,
839        before: &RefBatchState,
840        after: &RefBatchState,
841        updates: &[RefUpdate],
842    ) -> Result<PrunedPackedRecords> {
843        let checked: HashSet<&str> = updates
844            .iter()
845            .filter(|u| u.new_value.is_none() && !u.flags.log_only && u.expected != Expected::Any)
846            .map(|u| u.name.as_str())
847            .collect();
848        let mut pruned = PrunedPackedRecords::default();
849        let mut by_dir: BTreeMap<PathBuf, (HashSet<String>, HashMap<String, String>)> =
850            BTreeMap::new();
851        for name in before.refs.keys() {
852            if after.refs.contains_key(name) {
853                continue;
854            }
855            let route = self.route(name);
856            let (storage_names, checked_names) = by_dir.entry(route.storage_dir).or_default();
857            if checked.contains(name.as_str()) {
858                checked_names.insert(route.storage_name.clone(), name.clone());
859            }
860            storage_names.insert(route.storage_name);
861            pruned.names.insert(name.clone());
862        }
863        for (dir, (storage_names, checked_names)) in by_dir {
864            let records = remove_packed_refs_under_lock(&dir, &storage_names)?;
865            for record in &records {
866                let Some(logical) = checked_names.get(packed_record_name(record)) else {
867                    continue;
868                };
869                if let Some(oid) = packed_record_oid(record) {
870                    pruned.removed.insert(logical.clone(), RawRef::Direct(oid));
871                }
872            }
873            if !records.is_empty() {
874                pruned.records.push((dir, records));
875            }
876        }
877        self.invalidate_packed_cache_after_write();
878        Ok(pruned)
879    }
880
881    /// After a commit failed part-way, write back the pruned `packed-refs` records of refs the
882    /// batch never got to (names not in `persisted`), so a failed batch leaves them in place.
883    fn restore_unprocessed_packed_records(
884        &self,
885        pruned: &PrunedPackedRecords,
886        persisted: &HashSet<String>,
887    ) -> Result<()> {
888        let mut unprocessed: HashMap<PathBuf, HashSet<String>> = HashMap::new();
889        for name in pruned.names.difference(persisted) {
890            let route = self.route(name);
891            unprocessed
892                .entry(route.storage_dir)
893                .or_default()
894                .insert(route.storage_name);
895        }
896        for (dir, records) in &pruned.records {
897            let Some(names) = unprocessed.get(dir) else {
898                continue;
899            };
900            let restore: Vec<&str> = records
901                .iter()
902                .map(String::as_str)
903                .filter(|record| names.contains(packed_record_name(record)))
904                .collect();
905            restore_packed_records_under_lock(dir, &restore)?;
906        }
907        self.invalidate_packed_cache_after_write();
908        Ok(())
909    }
910
911    fn delete_loose_ref(&self, refname: &str, lock: &Path, ctx: CommitContext<'_>) -> Result<()> {
912        let route = self.route(refname);
913        let path = route.storage_dir.join(&route.storage_name);
914        self.remove_packed_record(refname, ctx)?;
915        remove_empty_ref_directory(&path);
916        let _ = fs::remove_file(&path);
917        let _ = fs::remove_file(lock);
918        if let Some(parent) = path.parent() {
919            ctx.emptied_dirs
920                .borrow_mut()
921                .entry(parent.to_path_buf())
922                .or_insert(route.storage_dir);
923        }
924        Ok(())
925    }
926
927    fn persist_reflog(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
928        let path = self.reflog_path(name);
929        if entries.is_empty() {
930            if let Some(parent) = path.parent() {
931                self.io_ref_store(
932                    parent,
933                    GitDirPathOperation::CreateDir,
934                    fs::create_dir_all(parent),
935                )?;
936            }
937            self.io_ref_store(
938                &path,
939                GitDirPathOperation::AppendReflog,
940                fs::write(&path, ""),
941            )?;
942            return Ok(());
943        }
944        if let Some(parent) = path.parent() {
945            self.io_ref_store(
946                parent,
947                GitDirPathOperation::CreateDir,
948                fs::create_dir_all(parent),
949            )?;
950        }
951        let mut body = String::new();
952        for entry in entries {
953            let (old_hex, new_hex) =
954                crate::refs::reflog_oid_hex_pair(&entry.old_oid, &entry.new_oid);
955            if entry.message.is_empty() {
956                body.push_str(&format!("{old_hex} {new_hex} {}\n", entry.identity));
957            } else {
958                body.push_str(&format!(
959                    "{old_hex} {new_hex} {}\t{}\n",
960                    entry.identity, entry.message
961                ));
962            }
963        }
964        self.io_ref_store(
965            &path,
966            GitDirPathOperation::AppendReflog,
967            fs::write(&path, body),
968        )?;
969        Ok(())
970    }
971
972    fn peel_ref_to_oid(&self, start: &str) -> Result<Option<ObjectId>> {
973        let mut name = start.to_owned();
974        for _ in 0..crate::refs::SYMREF_MAXDEPTH {
975            match self.read_raw(&name)? {
976                Some(RawRef::Direct(oid)) => return Ok(Some(oid)),
977                Some(RawRef::Symbolic(next)) => name = next,
978                None => return Ok(None),
979            }
980        }
981        Err(Error::RefStore(RefStoreError::SymrefLoop))
982    }
983
984    fn recheck_update_at_commit(
985        &self,
986        update: &RefUpdate,
987        batch_extras: &BTreeSet<String>,
988        batch_start: &RefBatchState,
989        state_before: &RefBatchState,
990        ctx: CommitContext<'_>,
991    ) -> StoreResult<()> {
992        if update.flags.log_only {
993            return Ok(());
994        }
995        let batch_differs =
996            batch_start.refs.get(&update.name) != state_before.refs.get(&update.name);
997        if !super::apply::expected_matches_for_update(state_before, update) {
998            return Err(RefStoreError::ExpectedMismatch {
999                name: update.name.clone(),
1000                expected: update.expected.clone(),
1001                actual: state_before.refs.get(&update.name).cloned(),
1002            });
1003        }
1004        if !batch_differs && update.new_value.is_none() && update.expected == Expected::Any {
1005            return Ok(());
1006        }
1007        let actual = if batch_differs {
1008            state_before.refs.get(&update.name).cloned()
1009        } else {
1010            self.read_raw(&update.name)
1011                .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1012                .or_else(|| ctx.removed.get(&update.name).cloned())
1013        };
1014        let actual_ref = actual.as_ref();
1015        if !batch_differs {
1016            let disk_ok = match &update.expected {
1017                Expected::Oid(want)
1018                    if matches!(actual_ref, Some(RawRef::Symbolic(_)))
1019                        && matches!(update.new_value.as_ref(), Some(RawRef::Direct(_)))
1020                        && !update.flags.no_deref =>
1021                {
1022                    self.peel_ref_to_oid(&update.name)
1023                        .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1024                        .is_some_and(|got| got == *want)
1025                }
1026                _ => expected_matches(actual_ref, &update.expected),
1027            };
1028            if !disk_ok {
1029                return Err(RefStoreError::ExpectedMismatch {
1030                    name: update.name.clone(),
1031                    expected: update.expected.clone(),
1032                    actual,
1033                });
1034            }
1035        }
1036        if update.new_value.is_some() && actual_ref.is_none() && !batch_differs {
1037            self.verify_create_available(&update.name, batch_extras)?;
1038        }
1039        Ok(())
1040    }
1041
1042    /// Store `value` as `name` (`None` deletes it).
1043    fn persist_one_ref_change(
1044        &self,
1045        name: &str,
1046        value: Option<&RawRef>,
1047        lock: &Path,
1048        ctx: CommitContext<'_>,
1049    ) -> Result<()> {
1050        let (_, path, _) = self.ref_lock_paths(name);
1051        match value {
1052            None => self.delete_loose_ref(name, lock, ctx),
1053            Some(value) => FilesRefStore::write_loose_value(
1054                &self.common_dir,
1055                &self.git_dir,
1056                &path,
1057                lock,
1058                value,
1059            ),
1060        }
1061    }
1062
1063    fn persist_one_ref_name(
1064        &self,
1065        name: &str,
1066        value: Option<&RawRef>,
1067        ctx: CommitContext<'_>,
1068        persisted_refs: &mut HashSet<String>,
1069    ) -> Result<()> {
1070        let (_, _, lock_path) = self.ref_lock_paths(name);
1071        if let Some(parent) = lock_path.parent() {
1072            self.io_ref_store(
1073                parent,
1074                GitDirPathOperation::CreateDir,
1075                fs::create_dir_all(parent),
1076            )?;
1077        }
1078        self.acquire_ref_lock_mapped(&lock_path, name, ctx.ref_lock_wait)?;
1079        let write_result = self.persist_one_ref_change(name, value, &lock_path, ctx);
1080        let _ = fs::remove_file(&lock_path);
1081        write_result?;
1082        persisted_refs.insert(name.to_owned());
1083        Ok(())
1084    }
1085
1086    /// Lock every ref `update` writes (and `update.name` when its expectation must be
1087    /// verified), recheck the expectation against disk under those locks, then write the refs.
1088    ///
1089    /// `running` is the batch state before `update`; `touched` is the one ref the update
1090    /// stores [`RefUpdate::new_value`] into (see [`ref_change_target`]).
1091    ///
1092    /// Holding the write targets' locks across the recheck matters for symbolic refs too: an
1093    /// update to `HEAD` that dereferences to `refs/heads/main` must exclude writers of `main`,
1094    /// not only of `HEAD`. Each successful write consumes its lock; anything left is released.
1095    #[allow(clippy::too_many_arguments)]
1096    fn persist_update_locked(
1097        &self,
1098        update: &RefUpdate,
1099        batch_extras: &BTreeSet<String>,
1100        before: &RefBatchState,
1101        running: &RefBatchState,
1102        touched: Option<&str>,
1103        ctx: CommitContext<'_>,
1104        persisted_refs: &mut HashSet<String>,
1105        writes_ref: bool,
1106        carried: &mut Option<(String, PathBuf)>,
1107    ) -> Result<()> {
1108        let value_after = |name: &str| {
1109            if touched == Some(name) {
1110                update.new_value.as_ref()
1111            } else {
1112                running.refs.get(name)
1113            }
1114        };
1115        let mut targets: Vec<String> = touched
1116            .filter(|t| *t != update.name && running.refs.get(*t) != update.new_value.as_ref())
1117            .map(str::to_owned)
1118            .into_iter()
1119            .collect();
1120        if writes_ref {
1121            targets.push(update.name.clone());
1122        }
1123
1124        let mut held: BTreeMap<String, PathBuf> = BTreeMap::new();
1125        let release = |held: &BTreeMap<String, PathBuf>| {
1126            for lock in held.values() {
1127                let _ = fs::remove_file(lock);
1128            }
1129        };
1130        let verify_name = (!matches!(update.expected, Expected::Any)).then_some(&update.name);
1131        for name in targets.iter().chain(verify_name) {
1132            if held.contains_key(name) {
1133                continue;
1134            }
1135            if let Some((_, lock)) = carried.take_if(|(carried_name, _)| carried_name == name) {
1136                held.insert(name.clone(), lock);
1137                continue;
1138            }
1139            let (_, _, lock_path) = self.ref_lock_paths(name);
1140            let acquired = match lock_path.parent() {
1141                Some(parent) => self.io_ref_store(
1142                    parent,
1143                    GitDirPathOperation::CreateDir,
1144                    fs::create_dir_all(parent),
1145                ),
1146                None => Ok(()),
1147            }
1148            .and_then(|()| self.acquire_ref_lock_mapped(&lock_path, name, ctx.ref_lock_wait));
1149            if let Err(err) = acquired {
1150                release(&held);
1151                return Err(err);
1152            }
1153            held.insert(name.clone(), lock_path);
1154        }
1155
1156        if let Err(err) = self.recheck_update_at_commit(update, batch_extras, before, running, ctx)
1157        {
1158            release(&held);
1159            return Err(err.into());
1160        }
1161        #[cfg(test)]
1162        tests::run_after_recheck_hook();
1163
1164        for name in &targets {
1165            let Some(lock_path) = held.remove(name) else {
1166                continue;
1167            };
1168            if let Err(err) = self.persist_one_ref_change(name, value_after(name), &lock_path, ctx)
1169            {
1170                let _ = fs::remove_file(&lock_path);
1171                release(&held);
1172                return Err(err);
1173            }
1174            persisted_refs.insert(name.clone());
1175        }
1176        release(&held);
1177        Ok(())
1178    }
1179
1180    fn persist_state_diff(
1181        &self,
1182        batch: &PreparedFilesBatch,
1183        carried: &mut Option<(String, PathBuf)>,
1184        emptied_dirs: &RefCell<BTreeMap<PathBuf, PathBuf>>,
1185    ) -> Result<()> {
1186        let before = &batch.before_commit;
1187        let after = &batch.after_commit;
1188        let updates = batch.updates.as_slice();
1189        let packed_lock_held = batch.packed_lock.is_some();
1190        let batch_extras: BTreeSet<String> = updates.iter().map(|u| u.name.clone()).collect();
1191        if updates.iter().any(|u| {
1192            u.new_value.is_some() && !u.flags.log_only && !before.refs.contains_key(&u.name)
1193        }) {
1194            self.forget_packed_cache();
1195        }
1196
1197        let mut names: BTreeSet<String> = BTreeSet::new();
1198        names.extend(before.refs.keys().cloned());
1199        names.extend(after.refs.keys().cloned());
1200        names.extend(before.reflogs.keys().cloned());
1201        names.extend(after.reflogs.keys().cloned());
1202
1203        for name in names {
1204            if before.reflogs.get(&name) != after.reflogs.get(&name) {
1205                let entries = after.reflogs.get(&name).cloned().unwrap_or_default();
1206                self.persist_reflog(&name, &entries)?;
1207            }
1208        }
1209
1210        let pruned = if packed_lock_held {
1211            self.prune_deleted_packed_records(before, after, updates)?
1212        } else {
1213            PrunedPackedRecords::default()
1214        };
1215        let ctx = CommitContext {
1216            ref_lock_wait: batch.ref_lock_wait,
1217            lock_held: packed_lock_held,
1218            pruned: &pruned.names,
1219            removed: &pruned.removed,
1220            emptied_dirs,
1221        };
1222
1223        let mut persisted_refs = HashSet::new();
1224        let persisted =
1225            self.persist_batch_refs(batch, &batch_extras, ctx, &mut persisted_refs, carried);
1226        if persisted.is_err() {
1227            // Keep the error that stopped the batch; a failed restore cannot be reported
1228            // alongside it.
1229            let _ = self.restore_unprocessed_packed_records(&pruned, &persisted_refs);
1230        }
1231        persisted?;
1232        self.invalidate_packed_cache_after_write();
1233        Ok(())
1234    }
1235
1236    /// Write every ref change of `batch` (case-only renames, then each update under its locks,
1237    /// then anything the updates left out), recording each written name in `persisted_refs`.
1238    fn persist_batch_refs(
1239        &self,
1240        batch: &PreparedFilesBatch,
1241        batch_extras: &BTreeSet<String>,
1242        ctx: CommitContext<'_>,
1243        persisted_refs: &mut HashSet<String>,
1244        carried: &mut Option<(String, PathBuf)>,
1245    ) -> Result<()> {
1246        let before = &batch.before_commit;
1247        let after = &batch.after_commit;
1248        let updates = batch.updates.as_slice();
1249        for rename in &batch.case_only_renames {
1250            self.persist_case_only_loose_ref(rename, after, ctx)?;
1251            persisted_refs.insert(rename.from.clone());
1252            persisted_refs.insert(rename.to.clone());
1253        }
1254        let mut running = before.clone();
1255        for (idx, update) in updates.iter().enumerate() {
1256            if update.flags.log_only {
1257                apply_update(&mut running, update).map_err(Error::from)?;
1258                continue;
1259            }
1260            let touched = ref_change_target(&running, update).map_err(Error::from)?;
1261            let name = &update.name;
1262            let b = running.refs.get(name);
1263            let a = if touched.as_deref() == Some(name.as_str()) {
1264                update.new_value.as_ref()
1265            } else {
1266                b
1267            };
1268            let materialize_loose = update.new_value.is_some()
1269                && a.is_some()
1270                && b == a
1271                && !self
1272                    .loose_ref_file_exists(name)
1273                    .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1274            let writes_ref = b != a || materialize_loose;
1275
1276            self.persist_update_locked(
1277                update,
1278                batch_extras,
1279                before,
1280                &running,
1281                touched.as_deref(),
1282                ctx,
1283                persisted_refs,
1284                writes_ref,
1285                carried,
1286            )?;
1287
1288            apply_update(&mut running, update).map_err(Error::from)?;
1289            crate::refs::test_ref_store_batch_pause_after_ref_write(idx, &self.common_dir);
1290        }
1291
1292        for name in before
1293            .refs
1294            .keys()
1295            .chain(after.refs.keys())
1296            .collect::<BTreeSet<_>>()
1297        {
1298            if persisted_refs.contains(name) {
1299                continue;
1300            }
1301            let b = before.refs.get(name);
1302            let a = after.refs.get(name);
1303            if b == a {
1304                continue;
1305            }
1306            self.persist_one_ref_name(name, a, ctx, persisted_refs)?;
1307        }
1308        Ok(())
1309    }
1310}
1311
1312impl FilesRefStore {
1313    /// [`RefStore::for_each_ref`], skipping the `packed-refs` records `keep` rejects.
1314    ///
1315    /// `keep` sees each packed record's storage name, object id and [`PackedPeel`] before
1316    /// anything is allocated for it. A loose ref is listed whatever `keep` says about a packed
1317    /// record of the same name.
1318    pub(crate) fn for_each_ref_filtered(
1319        &self,
1320        prefix: &str,
1321        keep: Option<PackedRecordFilter<'_>>,
1322        f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1323    ) -> Result<()> {
1324        let prefix = prefix.trim_end_matches('/');
1325        let mut map = self.load_raw_map_prefix(prefix, keep)?;
1326        for (name, value) in map.split_off(prefix) {
1327            if !name.starts_with(prefix) {
1328                break;
1329            }
1330            let peeled = match &value {
1331                RawRef::Direct(_) => None,
1332                RawRef::Symbolic(target) => self.resolve(target).ok(),
1333            };
1334            let entry = RefEntry {
1335                name,
1336                value,
1337                peeled,
1338            };
1339            if f(&entry).is_break() {
1340                break;
1341            }
1342        }
1343        Ok(())
1344    }
1345}
1346
1347impl RefStore for FilesRefStore {
1348    fn format(&self) -> RefStorageFormat {
1349        RefStorageFormat::Files
1350    }
1351
1352    fn read_raw(&self, name: &str) -> Result<Option<RawRef>> {
1353        self.read_raw_at_route(&self.route(name))
1354    }
1355
1356    fn for_each_ref(
1357        &self,
1358        prefix: &str,
1359        f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1360    ) -> Result<()> {
1361        self.for_each_ref_filtered(prefix, None, f)
1362    }
1363
1364    fn prepare(
1365        &self,
1366        txn: RefTransaction,
1367    ) -> StoreResult<Box<dyn PreparedRefTransaction + Send + '_>> {
1368        txn.validate()?;
1369        let (updates, reflog_renames, case_only_renames) = txn.into_parts();
1370
1371        let mut prepared_guard = self.prepared.lock().unwrap_or_else(|e| e.into_inner());
1372        if prepared_guard.is_some() {
1373            return Err(RefStoreError::LockHeld {
1374                name: updates
1375                    .first()
1376                    .map(|u| u.name.clone())
1377                    .unwrap_or_else(|| "refs".to_owned()),
1378            });
1379        }
1380
1381        let mut preliminary = self
1382            .load_batch_state_for_prepare(&updates, &reflog_renames, &case_only_renames)
1383            .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1384        self.insert_create_conflict_neighbors(&mut preliminary.refs, &updates)
1385            .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1386        verify_create_conflicts(&preliminary.refs, &updates)?;
1387        for rename in &reflog_renames {
1388            self.ensure_reflog_path_writable(&rename.to)?;
1389        }
1390        for rename in &case_only_renames {
1391            self.ensure_reflog_path_writable(&rename.to)?;
1392        }
1393        let lock_names = ref_lock_names_for_updates(&preliminary, &updates);
1394        let ref_lock_wait = LockWait::files_ref_lock_for_git_dir(&self.common_dir);
1395
1396        let mut ref_locks: HashMap<String, PathBuf> = HashMap::new();
1397        for name in &lock_names {
1398            let (_, _, lock) = self.ref_lock_paths(name);
1399            if let Some(parent) = lock.parent() {
1400                if let Err(err) = self.io_ref_store(
1401                    parent,
1402                    GitDirPathOperation::CreateDir,
1403                    fs::create_dir_all(parent),
1404                ) {
1405                    for acquired in ref_locks.values() {
1406                        let _ = fs::remove_file(acquired);
1407                    }
1408                    return Err(Self::ref_store_error_from_lib(err));
1409                }
1410            }
1411            if let Err(err) = self.acquire_ref_lock_mapped(&lock, name, ref_lock_wait) {
1412                for acquired in ref_locks.values() {
1413                    let _ = fs::remove_file(acquired);
1414                }
1415                return Err(Self::ref_store_error_from_lib(err));
1416            }
1417            ref_locks.insert(name.clone(), lock);
1418        }
1419
1420        if let Err(err) =
1421            simulate_batch_apply(&preliminary, &updates, &reflog_renames, &case_only_renames)
1422        {
1423            for acquired in ref_locks.values() {
1424                let _ = fs::remove_file(acquired);
1425            }
1426            return Err(err);
1427        }
1428
1429        let extras: BTreeSet<String> = updates.iter().map(|u| u.name.clone()).collect();
1430        for update in &updates {
1431            if update.new_value.is_none() || update.flags.log_only {
1432                continue;
1433            }
1434            if preliminary.refs.contains_key(&update.name) {
1435                continue;
1436            }
1437            if let Err(reason) = self.verify_create_available(&update.name, &extras) {
1438                for acquired in ref_locks.values() {
1439                    let _ = fs::remove_file(acquired);
1440                }
1441                return Err(reason.into());
1442            }
1443        }
1444
1445        let before_commit = preliminary;
1446        let mut after_commit = before_commit.clone();
1447        for update in &updates {
1448            apply_update(&mut after_commit, update)?;
1449        }
1450        for rename in &case_only_renames {
1451            apply_case_only_ref_rename(&mut after_commit, rename)?;
1452        }
1453        for rename in &reflog_renames {
1454            apply_reflog_rename(&mut after_commit, rename)?;
1455        }
1456        let mut changed = changed_ref_names(&before_commit.refs, &after_commit.refs);
1457        for update in &updates {
1458            if update.flags.log_only || update.new_value.is_none() {
1459                continue;
1460            }
1461            if before_commit.refs.contains_key(&update.name)
1462                && !self
1463                    .loose_ref_file_exists(&update.name)
1464                    .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1465            {
1466                changed.insert(update.name.clone());
1467            }
1468        }
1469
1470        for name in &changed {
1471            if ref_locks.contains_key(name) {
1472                continue;
1473            }
1474            let (_, _, lock) = self.ref_lock_paths(name);
1475            if let Some(parent) = lock.parent() {
1476                if let Err(err) = self.io_ref_store(
1477                    parent,
1478                    GitDirPathOperation::CreateDir,
1479                    fs::create_dir_all(parent),
1480                ) {
1481                    for acquired in ref_locks.values() {
1482                        let _ = fs::remove_file(acquired);
1483                    }
1484                    return Err(Self::ref_store_error_from_lib(err));
1485                }
1486            }
1487            if let Err(err) = self.acquire_ref_lock_mapped(&lock, name, ref_lock_wait) {
1488                for acquired in ref_locks.values() {
1489                    let _ = fs::remove_file(acquired);
1490                }
1491                return Err(Self::ref_store_error_from_lib(err));
1492            }
1493            ref_locks.insert(name.clone(), lock);
1494        }
1495
1496        let mut packed_lock = None;
1497        if self.needs_packed_lock(&updates) {
1498            let packed_path = super::paths::packed_refs_path(&self.common_dir);
1499            let lock = lock_path_for_ref(&packed_path);
1500            use crate::lockfile::{LockError, LockFile};
1501            let wait = LockWait::packed_refs_for_git_dir(&self.common_dir);
1502            if let Err(err) = LockFile::acquire(&lock, wait).map(|guard| guard.detach()) {
1503                for acquired in ref_locks.values() {
1504                    let _ = fs::remove_file(acquired);
1505                }
1506                return Err(match err {
1507                    LockError::Held { .. } => RefStoreError::LockHeld {
1508                        name: super::paths::PACKED_REFS.to_owned(),
1509                    },
1510                    LockError::Io { source, .. } => RefStoreError::Corrupt(source.to_string()),
1511                });
1512            }
1513            packed_lock = Some(lock);
1514        }
1515
1516        *prepared_guard = Some(PreparedFilesBatch {
1517            updates,
1518            case_only_renames,
1519            ref_locks,
1520            packed_lock,
1521            ref_lock_wait,
1522            before_commit,
1523            after_commit,
1524        });
1525
1526        Ok(Box::new(FilesPrepared {
1527            store: self,
1528            committed: false,
1529        }))
1530    }
1531
1532    fn reflog_exists(&self, name: &str) -> Result<bool> {
1533        Ok(self.reflog_path(name).is_file())
1534    }
1535
1536    fn for_each_reflog_entry(
1537        &self,
1538        name: &str,
1539        reverse: bool,
1540        f: &mut dyn FnMut(&ReflogEntry) -> ControlFlow<()>,
1541    ) -> Result<()> {
1542        let entries = read_reflog_at_path(&self.reflog_path(name))?;
1543        if reverse {
1544            for entry in entries.iter().rev() {
1545                if f(entry).is_break() {
1546                    break;
1547                }
1548            }
1549        } else {
1550            for entry in &entries {
1551                if f(entry).is_break() {
1552                    break;
1553                }
1554            }
1555        }
1556        Ok(())
1557    }
1558
1559    fn create_reflog(&self, name: &str) -> Result<()> {
1560        validate_storable_refname(name).map_err(|e| Error::Message(e.to_string()))?;
1561        let path = self.reflog_path(name);
1562        if let Some(parent) = path.parent() {
1563            self.io_ref_store(
1564                parent,
1565                GitDirPathOperation::CreateDir,
1566                fs::create_dir_all(parent),
1567            )?;
1568        }
1569        self.io_ref_store(
1570            &path,
1571            GitDirPathOperation::AppendReflog,
1572            fs::OpenOptions::new().create(true).append(true).open(&path),
1573        )?;
1574        Ok(())
1575    }
1576
1577    fn delete_reflog(&self, name: &str) -> Result<()> {
1578        let route = self.route(name);
1579        let logs_root = route.storage_dir.join("logs");
1580        let path = logs_root.join(&route.storage_name);
1581        let _ = fs::remove_file(&path);
1582        // Empty parents left under `logs/` would block a later ref whose reflog needs that
1583        // path as a file (deleting `a/b`, then creating `a`).
1584        let mut parent = path.parent();
1585        while let Some(dir) = parent.filter(|d| *d != logs_root && d.starts_with(&logs_root)) {
1586            if fs::remove_dir(dir).is_err() {
1587                break;
1588            }
1589            parent = dir.parent();
1590        }
1591        Ok(())
1592    }
1593
1594    fn replace_reflog(&self, name: &str, entries: Vec<ReflogEntry>) -> Result<()> {
1595        self.persist_reflog(name, &entries)
1596    }
1597
1598    fn for_each_reflog_ref(&self, f: &mut dyn FnMut(&str) -> ControlFlow<()>) -> Result<()> {
1599        let mut names = BTreeSet::new();
1600        for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
1601            let logs = base.join("logs");
1602            if logs.is_dir() {
1603                collect_reflog_ref_names(&logs, "", self, &mut names)?;
1604            }
1605        }
1606        for name in names {
1607            if f(&name).is_break() {
1608                break;
1609            }
1610        }
1611        Ok(())
1612    }
1613
1614    fn as_files(&self) -> Option<&FilesRefStore> {
1615        Some(self)
1616    }
1617}
1618
1619struct FilesPrepared<'a> {
1620    store: &'a FilesRefStore,
1621    committed: bool,
1622}
1623
1624impl PreparedRefTransaction for FilesPrepared<'_> {
1625    fn commit(mut self: Box<Self>) -> StoreResult<()> {
1626        let mut guard = self
1627            .store
1628            .prepared
1629            .lock()
1630            .unwrap_or_else(|e| e.into_inner());
1631        let Some(mut batch) = guard.take() else {
1632            return Ok(());
1633        };
1634
1635        // The first update is written before any other ref, so it keeps the lock it took in
1636        // `prepare` instead of releasing and re-creating it. The rest are released here and
1637        // re-taken one update at a time, so other writers can touch later refs mid-batch.
1638        let mut ref_locks = std::mem::take(&mut batch.ref_locks);
1639        let mut carried = batch
1640            .updates
1641            .iter()
1642            .find(|u| !u.flags.log_only)
1643            .and_then(|u| ref_locks.remove_entry(&u.name));
1644        for lock in ref_locks.values() {
1645            let _ = fs::remove_file(lock);
1646        }
1647        // The packed cache revalidates against the file's size and mtime on every read, so
1648        // rechecks under the ref locks see concurrent rewrites without a forced re-parse.
1649        let emptied_dirs = RefCell::new(BTreeMap::new());
1650        let persisted = self
1651            .store
1652            .persist_state_diff(&batch, &mut carried, &emptied_dirs);
1653        if let Some((_, lock)) = carried {
1654            let _ = fs::remove_file(lock);
1655        }
1656        // Pruning runs after every lock of the batch is gone: a leftover lock file would make
1657        // its directory look non-empty (issue #1317). Deepest first, so a parent is only
1658        // checked once its emptied children are removed.
1659        let mut emptied_dirs: Vec<_> = emptied_dirs.into_inner().into_iter().collect();
1660        emptied_dirs.sort_by_key(|(dir, _)| std::cmp::Reverse(dir.components().count()));
1661        for (dir, storage_dir) in &emptied_dirs {
1662            prune_empty_ref_dirs(storage_dir, dir);
1663        }
1664        self.store.invalidate_packed_cache_after_write();
1665        if let Err(err) = persisted {
1666            if let Some(p) = &batch.packed_lock {
1667                let _ = fs::remove_file(p);
1668            }
1669            return Err(match err {
1670                Error::RefStore(store_err) => store_err,
1671                other => RefStoreError::Corrupt(other.to_string()),
1672            });
1673        }
1674        if let Some(p) = &batch.packed_lock {
1675            let _ = fs::remove_file(p);
1676        }
1677        self.committed = true;
1678        Ok(())
1679    }
1680
1681    fn abort(mut self: Box<Self>) -> StoreResult<()> {
1682        let mut guard = self
1683            .store
1684            .prepared
1685            .lock()
1686            .unwrap_or_else(|e| e.into_inner());
1687        if let Some(batch) = guard.take() {
1688            FilesRefStore::release_locks(&batch);
1689        }
1690        self.store.invalidate_packed_cache_after_write();
1691        self.committed = true;
1692        Ok(())
1693    }
1694}
1695
1696impl Drop for FilesPrepared<'_> {
1697    fn drop(&mut self) {
1698        if self.committed {
1699            return;
1700        }
1701        if let Ok(mut guard) = self.store.prepared.lock() {
1702            if let Some(batch) = guard.take() {
1703                FilesRefStore::release_locks(&batch);
1704            }
1705        }
1706        self.store.invalidate_packed_cache_after_write();
1707    }
1708}
1709
1710fn head_matches_prefix(logical_prefix: &str) -> bool {
1711    let prefix = logical_prefix.trim_end_matches('/');
1712    if prefix.is_empty() {
1713        // `git for-each-ref` with no pattern lists refs under `refs/`, not `HEAD`.
1714        return false;
1715    }
1716    "HEAD".starts_with(prefix) || prefix.starts_with("HEAD")
1717}
1718
1719fn ref_to_raw(r: Ref) -> RawRef {
1720    match r {
1721        Ref::Direct(oid) => RawRef::Direct(oid),
1722        Ref::Symbolic(s) => RawRef::Symbolic(s),
1723    }
1724}
1725
1726/// Read every loose ref file below `dir` into `out`, keyed by `prefix` plus its relative path.
1727///
1728/// Only names starting with `want` are read; entries that cannot lead to one are skipped
1729/// without a `stat`, so a partial-name prefix such as `refs/tags/v1` does not read every
1730/// file in `refs/tags/`. An empty `want` reads everything.
1731fn collect_loose_raw_refs(
1732    dir: &Path,
1733    prefix: &str,
1734    want: &str,
1735    out: &mut BTreeMap<String, RawRef>,
1736) -> Result<()> {
1737    let read = match fs::read_dir(dir) {
1738        Ok(r) => r,
1739        Err(e)
1740            if e.kind() == io::ErrorKind::NotFound || e.kind() == io::ErrorKind::NotADirectory =>
1741        {
1742            return Ok(());
1743        }
1744        Err(e) => return Err(Error::Io(e)),
1745    };
1746    for entry in read {
1747        let entry = entry?;
1748        let name_owned = entry.file_name().into_string().map_err(|_| {
1749            Error::Io(io::Error::new(
1750                io::ErrorKind::InvalidData,
1751                "non-utf8 ref path component",
1752            ))
1753        })?;
1754        let refname = format!("{prefix}{name_owned}");
1755        let wanted = refname.starts_with(want);
1756        let leads_to_wanted = || {
1757            want.strip_prefix(refname.as_str())
1758                .is_some_and(|rest| rest.starts_with('/'))
1759        };
1760        if !wanted && !leads_to_wanted() {
1761            continue;
1762        }
1763        let path = entry.path();
1764        let meta = match fs::metadata(&path) {
1765            Ok(m) => m,
1766            // Concurrent delete or an unreadable entry (e.g. dangling symlink): skip it.
1767            Err(_) => continue,
1768        };
1769        if meta.is_dir() {
1770            collect_loose_raw_refs(&path, &format!("{refname}/"), want, out)?;
1771        } else if meta.is_file() && wanted {
1772            if let Ok(r) = read_ref_file(&path) {
1773                out.insert(refname, ref_to_raw(r));
1774            }
1775        }
1776    }
1777    Ok(())
1778}
1779
1780/// Add `<prefix>/<relative path>` for every file below `dir` to `out`; a missing or unreadable
1781/// directory adds nothing.
1782fn collect_loose_file_names(dir: &Path, prefix: &str, out: &mut BTreeSet<String>) {
1783    let Ok(read) = fs::read_dir(dir) else {
1784        return;
1785    };
1786    for entry in read.flatten() {
1787        let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
1788        let path = entry.path();
1789        match fs::metadata(&path) {
1790            Ok(meta) if meta.is_dir() => collect_loose_file_names(&path, &name, out),
1791            Ok(meta) if meta.is_file() => {
1792                out.insert(name);
1793            }
1794            _ => {}
1795        }
1796    }
1797}
1798
1799#[cfg(unix)]
1800fn file_identity(meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
1801    use std::os::unix::fs::MetadataExt;
1802    Some((meta.dev(), meta.ino(), meta.ctime(), meta.ctime_nsec()))
1803}
1804
1805#[cfg(not(unix))]
1806fn file_identity(_meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
1807    None
1808}
1809
1810fn read_reflog_at_path(path: &Path) -> Result<Vec<ReflogEntry>> {
1811    let content = match fs::read_to_string(path) {
1812        Ok(c) => c,
1813        // A ref being created can collide with a file or directory in `logs/`; that is a
1814        // missing log here, and the ref conflict check reports the collision.
1815        Err(e)
1816            if matches!(
1817                e.kind(),
1818                io::ErrorKind::NotFound
1819                    | io::ErrorKind::NotADirectory
1820                    | io::ErrorKind::IsADirectory
1821            ) =>
1822        {
1823            return Ok(Vec::new());
1824        }
1825        Err(e) => return Err(Error::Io(e)),
1826    };
1827    Ok(content
1828        .lines()
1829        .filter_map(reflog::parse_reflog_line_for_store)
1830        .collect())
1831}
1832
1833fn changed_ref_names(
1834    before: &BTreeMap<String, RawRef>,
1835    after: &BTreeMap<String, RawRef>,
1836) -> BTreeSet<String> {
1837    let mut names: BTreeSet<String> = BTreeSet::new();
1838    names.extend(before.keys().cloned());
1839    names.extend(after.keys().cloned());
1840    names
1841        .into_iter()
1842        .filter(|name| before.get(name) != after.get(name))
1843        .collect()
1844}
1845
1846fn collect_reflog_ref_names(
1847    dir: &Path,
1848    prefix: &str,
1849    store: &FilesRefStore,
1850    out: &mut BTreeSet<String>,
1851) -> Result<()> {
1852    let read = match fs::read_dir(dir) {
1853        Ok(r) => r,
1854        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1855        Err(e) => return Err(Error::Io(e)),
1856    };
1857    for entry in read {
1858        let entry = entry?;
1859        let name = entry.file_name().to_string_lossy().into_owned();
1860        let refname = format!("{prefix}{name}");
1861        let path = entry.path();
1862        if path.is_dir() {
1863            collect_reflog_ref_names(&path, &format!("{refname}/"), store, out)?;
1864        } else if path.is_file() {
1865            out.insert(store.logical_name(&refname));
1866        }
1867    }
1868    Ok(())
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873    use std::cell::RefCell;
1874
1875    use super::*;
1876    use crate::refs::store::RefUpdateFlags;
1877
1878    // hygiene: test-only commit interleaving hook
1879    thread_local! {
1880        static AFTER_RECHECK: RefCell<Option<Box<dyn FnOnce()>>> = const { RefCell::new(None) };
1881    }
1882
1883    /// Runs (once) the hook installed by [`after_next_recheck`] on this thread.
1884    pub(super) fn run_after_recheck_hook() {
1885        if let Some(hook) = AFTER_RECHECK.with(|slot| slot.borrow_mut().take()) {
1886            hook();
1887        }
1888    }
1889
1890    /// Run `hook` inside the next commit on this thread, right after an update passed its
1891    /// on-disk compare-and-swap recheck and before the ref is written.
1892    fn after_next_recheck(hook: impl FnOnce() + 'static) {
1893        AFTER_RECHECK.with(|slot| *slot.borrow_mut() = Some(Box::new(hook)));
1894    }
1895
1896    fn oid(byte: u8) -> ObjectId {
1897        let mut bytes = [0u8; 20];
1898        bytes[19] = byte;
1899        ObjectId::from_bytes(&bytes).expect("oid")
1900    }
1901
1902    fn store(git_dir: &Path) -> FilesRefStore {
1903        FilesRefStore::open(FilesRefStoreConfig {
1904            git_dir: git_dir.to_path_buf(),
1905            common_dir: git_dir.to_path_buf(),
1906            namespace_prefix: None,
1907        })
1908    }
1909
1910    fn set(
1911        store: &FilesRefStore,
1912        name: &str,
1913        value: ObjectId,
1914        expected: Expected,
1915    ) -> StoreResult<()> {
1916        let txn = RefTransaction::new().update(RefUpdate {
1917            name: name.to_owned(),
1918            new_value: Some(RawRef::Direct(value)),
1919            expected,
1920            reflog: None,
1921            flags: RefUpdateFlags::default(),
1922        })?;
1923        store.prepare(txn)?.commit()
1924    }
1925
1926    fn git_dir() -> tempfile::TempDir {
1927        let dir = tempfile::tempdir().expect("tempdir");
1928        fs::create_dir_all(dir.path().join("refs/heads")).expect("refs");
1929        fs::write(dir.path().join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
1930        dir
1931    }
1932
1933    fn listed_names(store: &FilesRefStore, prefix: &str) -> Vec<String> {
1934        let mut names = Vec::new();
1935        store
1936            .for_each_ref(prefix, &mut |entry| {
1937                names.push(entry.name.clone());
1938                ControlFlow::Continue(())
1939            })
1940            .expect("list");
1941        names
1942    }
1943
1944    #[test]
1945    fn partial_name_prefix_lists_loose_refs_without_visiting_siblings() {
1946        let dir = git_dir();
1947        let tags = dir.path().join("refs/tags");
1948        for name in ["v1", "v10", "v1x/a", "v2/deep", "w"] {
1949            let path = tags.join(name);
1950            fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
1951            fs::write(path, format!("{}\n", oid(1))).expect("ref");
1952        }
1953        // A sibling that cannot be stat'ed fails any walk that looks at it.
1954        #[cfg(unix)]
1955        std::os::unix::fs::symlink(tags.join("missing-target"), tags.join("zzz"))
1956            .expect("dangling symlink");
1957        let store = store(dir.path());
1958
1959        assert_eq!(
1960            listed_names(&store, "refs/tags/v1"),
1961            ["refs/tags/v1", "refs/tags/v10", "refs/tags/v1x/a"]
1962        );
1963        assert_eq!(listed_names(&store, "refs/tags/v2/"), ["refs/tags/v2/deep"]);
1964        assert_eq!(listed_names(&store, "refs/tags/w"), ["refs/tags/w"]);
1965        assert!(listed_names(&store, "refs/tags/x").is_empty());
1966    }
1967
1968    /// Two writers that both expect `expected` must not both succeed: the one that commits
1969    /// first holds the ref lock from its recheck to its write.
1970    fn assert_racing_writer_is_locked_out(expected: Expected, seed: Option<ObjectId>) {
1971        let dir = git_dir();
1972        let name = "refs/notes/commits";
1973        let writer = store(dir.path());
1974        if let Some(seed) = seed {
1975            set(&writer, name, seed, Expected::Missing).expect("seed");
1976        }
1977
1978        let other = store(dir.path());
1979        let racing_expected = expected.clone();
1980        let racer = std::rc::Rc::new(RefCell::new(None));
1981        let racer_slot = racer.clone();
1982        after_next_recheck(move || {
1983            *racer_slot.borrow_mut() = Some(set(&other, name, oid(2), racing_expected));
1984        });
1985        set(&writer, name, oid(3), expected).expect("first committer wins");
1986
1987        let racer = racer.borrow_mut().take().expect("hook ran");
1988        assert!(
1989            matches!(racer, Err(RefStoreError::LockHeld { .. })),
1990            "racing writer must not commit while the ref is locked: {racer:?}"
1991        );
1992        assert_eq!(
1993            writer.read_raw(name).expect("read"),
1994            Some(RawRef::Direct(oid(3)))
1995        );
1996        assert!(
1997            !dir.path().join("refs/notes/commits.lock").exists(),
1998            "the lock is released after the write"
1999        );
2000    }
2001
2002    #[test]
2003    fn expected_oid_update_holds_the_lock_through_the_write() {
2004        assert_racing_writer_is_locked_out(Expected::Oid(oid(1)), Some(oid(1)));
2005    }
2006
2007    #[test]
2008    fn expected_missing_create_holds_the_lock_through_the_write() {
2009        assert_racing_writer_is_locked_out(Expected::Missing, None);
2010    }
2011
2012    #[test]
2013    fn symbolic_update_holds_the_dereferenced_target_lock_through_the_write() {
2014        let dir = git_dir();
2015        let writer = store(dir.path());
2016        set(&writer, "refs/heads/main", oid(1), Expected::Missing).expect("seed main");
2017
2018        let other = store(dir.path());
2019        let racer = std::rc::Rc::new(RefCell::new(None));
2020        let racer_slot = racer.clone();
2021        after_next_recheck(move || {
2022            *racer_slot.borrow_mut() = Some(set(
2023                &other,
2024                "refs/heads/main",
2025                oid(2),
2026                Expected::Oid(oid(1)),
2027            ));
2028        });
2029        set(&writer, "HEAD", oid(3), Expected::Oid(oid(1))).expect("update through HEAD");
2030
2031        let racer = racer.borrow_mut().take().expect("hook ran");
2032        assert!(
2033            matches!(racer, Err(RefStoreError::LockHeld { .. })),
2034            "a direct writer of the HEAD target must be locked out: {racer:?}"
2035        );
2036        assert_eq!(
2037            writer.read_raw("refs/heads/main").expect("read"),
2038            Some(RawRef::Direct(oid(3)))
2039        );
2040        assert_eq!(
2041            writer.read_raw("HEAD").expect("read"),
2042            Some(RawRef::Symbolic("refs/heads/main".to_owned()))
2043        );
2044        for lock in ["HEAD.lock", "refs/heads/main.lock"] {
2045            assert!(!dir.path().join(lock).exists(), "{lock} released");
2046        }
2047    }
2048
2049    #[test]
2050    fn create_availability_matches_the_path_based_check() {
2051        let dir = git_dir();
2052        let path = dir.path();
2053        let hex = oid(1).to_hex();
2054        fs::write(
2055            path.join("packed-refs"),
2056            format!(
2057                "# pack-refs with: peeled fully-peeled sorted \n\
2058                 {hex} refs/heads/p\n\
2059                 {hex} refs/heads/pd/x\n\
2060                 {hex} refs/heads/pd-sibling\n\
2061                 {hex} refs/tags/v1\n"
2062            ),
2063        )
2064        .expect("packed-refs");
2065        for loose in ["refs/heads/l", "refs/heads/ld/y/z"] {
2066            let file = path.join(loose);
2067            fs::create_dir_all(file.parent().expect("parent")).expect("dirs");
2068            fs::write(file, format!("{hex}\n")).expect("loose");
2069        }
2070        let refs = store(path);
2071        let extras: BTreeSet<String> = ["refs/heads/batch/one".to_owned()].into();
2072        for name in [
2073            "refs/heads/new",
2074            "refs/heads/p/child",
2075            "refs/heads/pd",
2076            "refs/heads/pd-sibling/x",
2077            "refs/heads/l/child",
2078            "refs/heads/ld",
2079            "refs/heads/ld/y",
2080            "refs/heads/batch",
2081            "refs/heads/batch/one/two",
2082            "refs/tags/v1/x",
2083            "refs/tags/v2",
2084        ] {
2085            assert_eq!(
2086                refs.verify_create_available(name, &extras),
2087                crate::refs::verify_refname_available_for_create(
2088                    path,
2089                    name,
2090                    &extras,
2091                    &HashSet::new()
2092                ),
2093                "{name}"
2094            );
2095        }
2096    }
2097
2098    #[test]
2099    fn mapped_packed_refs_are_reread_after_replacement() {
2100        let dir = git_dir();
2101        let path = dir.path();
2102        let records = |value: ObjectId| -> Vec<crate::refs::PackedRefRecord> {
2103            (0..500)
2104                .map(|i| crate::refs::PackedRefRecord {
2105                    refname: format!("refs/tags/t{i:04}"),
2106                    oid: value,
2107                    peeled: None,
2108                })
2109                .collect()
2110        };
2111        crate::refs::write_packed_refs_file(path, &records(oid(1))).expect("pack");
2112        assert!(fs::metadata(path.join("packed-refs")).expect("meta").len() > 8192);
2113        let refs = store(path);
2114        assert_eq!(
2115            refs.read_raw("refs/tags/t0499").expect("read"),
2116            Some(RawRef::Direct(oid(1)))
2117        );
2118        let mut replaced = records(oid(2));
2119        replaced.truncate(10);
2120        crate::refs::write_packed_refs_file(path, &replaced).expect("repack");
2121        assert_eq!(refs.read_raw("refs/tags/t0499").expect("read"), None);
2122        assert_eq!(
2123            refs.read_raw("refs/tags/t0009").expect("read"),
2124            Some(RawRef::Direct(oid(2)))
2125        );
2126    }
2127
2128    /// Replace `packed-refs` by renaming a same-length file over it, then put back the old
2129    /// mtime (as a coarse-mtime filesystem would show it).
2130    fn replace_packed_refs_keeping_len_and_mtime(path: &Path, body: &str) {
2131        let packed = path.join("packed-refs");
2132        let before = fs::metadata(&packed).expect("meta");
2133        assert_eq!(before.len(), body.len() as u64, "same length");
2134        let tmp = path.join("packed-refs.new");
2135        fs::write(&tmp, body).expect("write");
2136        fs::File::options()
2137            .write(true)
2138            .open(&tmp)
2139            .expect("open")
2140            .set_modified(before.modified().expect("mtime"))
2141            .expect("pin mtime");
2142        fs::rename(&tmp, &packed).expect("rename");
2143        let after = fs::metadata(&packed).expect("meta");
2144        assert_eq!(after.len(), before.len());
2145        assert_eq!(after.modified().ok(), before.modified().ok());
2146    }
2147
2148    #[test]
2149    fn create_rechecks_a_packed_refs_replaced_with_same_len_and_mtime() {
2150        assert_create_blocked_by_replaced_packed_descendant(false);
2151    }
2152
2153    #[test]
2154    fn create_rechecks_packed_refs_even_when_the_stamp_cannot_tell() {
2155        assert_create_blocked_by_replaced_packed_descendant(true);
2156    }
2157
2158    /// Prime a store with `refs/heads/blocked-child` packed, swap in an equal-length
2159    /// `packed-refs` holding `refs/heads/blocked/child`, then create `refs/heads/blocked`.
2160    fn assert_create_blocked_by_replaced_packed_descendant(stamp_collides: bool) {
2161        let dir = git_dir();
2162        let path = dir.path();
2163        let hex = oid(1).to_hex();
2164        let header = "# pack-refs with: peeled fully-peeled sorted \n";
2165        fs::write(
2166            path.join("packed-refs"),
2167            format!("{header}{hex} refs/heads/blocked-child\n"),
2168        )
2169        .expect("packed-refs");
2170        let writer = store(path);
2171        assert_eq!(
2172            writer.read_raw("refs/heads/blocked-child").expect("prime"),
2173            Some(RawRef::Direct(oid(1)))
2174        );
2175        replace_packed_refs_keeping_len_and_mtime(
2176            path,
2177            &format!("{header}{hex} refs/heads/blocked/child\n"),
2178        );
2179        if stamp_collides {
2180            // A replacement no stamp can see (coarse timestamps and a reused inode): only a
2181            // fresh read under the ref lock notices it.
2182            let mut guard = writer.packed_cache.lock().expect("cache");
2183            guard.stamp = FilesRefStore::packed_refs_stamp(&path.join("packed-refs"));
2184        }
2185
2186        let err = set(&writer, "refs/heads/blocked", oid(2), Expected::Missing)
2187            .expect_err("packed descendant blocks the create");
2188        assert!(
2189            matches!(
2190                err,
2191                RefStoreError::NameUnavailable {
2192                    reason: RefnameUnavailable::DescendantExists { ref blocking, .. }
2193                } if blocking == "refs/heads/blocked/child"
2194            ),
2195            "{err:?}"
2196        );
2197        assert!(!path.join("refs/heads/blocked").exists());
2198        assert!(!path.join("refs/heads/blocked.lock").exists());
2199    }
2200
2201    #[test]
2202    fn create_with_reflog_next_to_a_ref_file_in_logs_reports_the_conflict() {
2203        let dir = git_dir();
2204        let path = dir.path();
2205        let writer = store(path);
2206        set(&writer, "refs/heads/a", oid(1), Expected::Missing).expect("create");
2207        fs::create_dir_all(path.join("logs/refs/heads")).expect("logs");
2208        fs::write(path.join("logs/refs/heads/a"), "").expect("log");
2209        let txn = RefTransaction::new()
2210            .update(RefUpdate {
2211                name: "refs/heads/a/b".to_owned(),
2212                new_value: Some(RawRef::Direct(oid(2))),
2213                expected: Expected::Missing,
2214                reflog: Some(super::super::ReflogUpdate {
2215                    identity: "T <t@t>".into(),
2216                    message: "create".into(),
2217                    time: time::OffsetDateTime::UNIX_EPOCH,
2218                    old_oid: None,
2219                    new_oid: None,
2220                }),
2221                flags: RefUpdateFlags::default(),
2222            })
2223            .expect("txn");
2224        let err = writer.prepare(txn).map(|_| ()).expect_err("conflict");
2225        assert!(
2226            matches!(
2227                err,
2228                RefStoreError::NameUnavailable {
2229                    reason: RefnameUnavailable::AncestorExists { ref blocking, .. }
2230                } if blocking == "refs/heads/a"
2231            ),
2232            "{err:?}"
2233        );
2234        assert!(!path.join("refs/heads/a.lock").exists());
2235    }
2236
2237    #[test]
2238    fn failed_recheck_releases_the_lock() {
2239        let dir = git_dir();
2240        let name = "refs/heads/main";
2241        let writer = store(dir.path());
2242        set(&writer, name, oid(1), Expected::Missing).expect("create");
2243        let other = store(dir.path());
2244        set(&other, name, oid(2), Expected::Oid(oid(1))).expect("other moves it");
2245
2246        let err = set(&writer, name, oid(3), Expected::Oid(oid(1))).expect_err("stale");
2247        assert!(
2248            matches!(err, RefStoreError::ExpectedMismatch { .. }),
2249            "{err:?}"
2250        );
2251        assert!(!dir.path().join("refs/heads/main.lock").exists());
2252        set(&writer, name, oid(3), Expected::Oid(oid(2))).expect("retry against current value");
2253        assert_eq!(
2254            writer.read_raw(name).expect("read"),
2255            Some(RawRef::Direct(oid(3)))
2256        );
2257    }
2258
2259    fn delete(name: &str, expected: Expected) -> RefUpdate {
2260        RefUpdate {
2261            name: name.to_owned(),
2262            new_value: None,
2263            expected,
2264            reflog: None,
2265            flags: RefUpdateFlags::default(),
2266        }
2267    }
2268
2269    fn commit_all(store: &FilesRefStore, updates: Vec<RefUpdate>) -> StoreResult<()> {
2270        store
2271            .prepare(RefTransaction::from_updates(updates)?)?
2272            .commit()
2273    }
2274
2275    /// `packed-refs` with twenty `refs/remotes/o/x<i>`, a peeled tag and `main`, plus a loose
2276    /// `x5` that overrides its packed value.
2277    fn packed_remote_refs(path: &Path) {
2278        let mut body = String::from("# pack-refs with: peeled fully-peeled sorted \n");
2279        body.push_str(&format!("{} refs/heads/main\n", oid(4)));
2280        let mut remotes: Vec<String> = (0..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
2281        remotes.sort();
2282        for name in &remotes {
2283            body.push_str(&format!("{} {name}\n", oid(1)));
2284        }
2285        body.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
2286        fs::write(path.join("packed-refs"), body).expect("packed-refs");
2287        fs::create_dir_all(path.join("refs/remotes/o")).expect("dir");
2288        fs::write(path.join("refs/remotes/o/x5"), format!("{}\n", oid(9))).expect("loose");
2289    }
2290
2291    #[test]
2292    fn batch_delete_of_packed_refs_rewrites_packed_refs_once_and_keeps_the_rest() {
2293        let dir = git_dir();
2294        let path = dir.path();
2295        packed_remote_refs(path);
2296        let writer = store(path);
2297        let updates = (0..20)
2298            .map(|i| {
2299                let expected = match i {
2300                    0 => Expected::Oid(oid(1)),
2301                    5 => Expected::Oid(oid(9)),
2302                    7 => Expected::Exists,
2303                    _ => Expected::Any,
2304                };
2305                delete(&format!("refs/remotes/o/x{i}"), expected)
2306            })
2307            .collect();
2308        commit_all(&writer, updates).expect("batch delete");
2309
2310        for i in 0..20 {
2311            let name = format!("refs/remotes/o/x{i}");
2312            assert_eq!(writer.read_raw(&name).expect("read"), None, "{name}");
2313        }
2314        let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2315        assert_eq!(
2316            packed,
2317            format!(
2318                "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n{} refs/tags/t\n^{}\n",
2319                oid(4),
2320                oid(2),
2321                oid(3)
2322            )
2323        );
2324        assert!(
2325            !path.join("refs/remotes").exists(),
2326            "emptied dirs are pruned"
2327        );
2328        assert!(!path.join("packed-refs.lock").exists());
2329        assert!(path.join("refs/heads").is_dir());
2330    }
2331
2332    #[test]
2333    fn stale_expectation_on_packed_ref_rejects_the_whole_batch() {
2334        let dir = git_dir();
2335        let path = dir.path();
2336        packed_remote_refs(path);
2337        let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
2338        let writer = store(path);
2339        let err = commit_all(
2340            &writer,
2341            vec![
2342                delete("refs/remotes/o/x1", Expected::Any),
2343                delete("refs/remotes/o/x2", Expected::Oid(oid(7))),
2344            ],
2345        )
2346        .expect_err("stale");
2347        assert!(
2348            matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x2"),
2349            "{err:?}"
2350        );
2351        assert_eq!(
2352            fs::read_to_string(path.join("packed-refs")).expect("packed"),
2353            before
2354        );
2355        assert!(!path.join("packed-refs.lock").exists());
2356        assert!(!path.join("refs/remotes/o/x1.lock").exists());
2357    }
2358
2359    #[test]
2360    fn batch_delete_prunes_nested_empty_dirs_deepest_first() {
2361        let dir = git_dir();
2362        let path = dir.path();
2363        let writer = store(path);
2364        for name in ["refs/heads/x/y/z", "refs/heads/x/w", "refs/heads/keep/k"] {
2365            set(&writer, name, oid(1), Expected::Missing).expect("create");
2366        }
2367        commit_all(
2368            &writer,
2369            vec![
2370                delete("refs/heads/x/w", Expected::Oid(oid(1))),
2371                delete("refs/heads/x/y/z", Expected::Any),
2372            ],
2373        )
2374        .expect("delete");
2375        assert!(!path.join("refs/heads/x").exists());
2376        assert!(path.join("refs/heads/keep/k").is_file());
2377    }
2378
2379    #[test]
2380    fn deleting_through_a_symref_removes_the_packed_target() {
2381        let dir = git_dir();
2382        let path = dir.path();
2383        packed_remote_refs(path);
2384        fs::write(path.join("refs/remotes/o/HEAD"), "ref: refs/remotes/o/x3\n").expect("symref");
2385        let writer = store(path);
2386        commit_all(&writer, vec![delete("refs/remotes/o/HEAD", Expected::Any)])
2387            .expect("deref delete");
2388        assert_eq!(writer.read_raw("refs/remotes/o/x3").expect("read"), None);
2389        assert_eq!(
2390            writer.read_raw("refs/remotes/o/HEAD").expect("read"),
2391            Some(RawRef::Symbolic("refs/remotes/o/x3".to_owned()))
2392        );
2393        let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2394        assert!(!packed.contains("refs/remotes/o/x3\n"), "{packed}");
2395        assert!(packed.contains("refs/remotes/o/x4\n"), "{packed}");
2396    }
2397
2398    #[test]
2399    fn late_commit_time_mismatch_keeps_packed_refs_the_batch_never_reached() {
2400        let dir = git_dir();
2401        let path = dir.path();
2402        packed_remote_refs(path);
2403        let writer = store(path);
2404        let other = store(path);
2405        after_next_recheck(move || {
2406            set(&other, "refs/remotes/o/x9", oid(7), Expected::Any).expect("concurrent write");
2407        });
2408        let updates = (0..20)
2409            .map(|i| {
2410                let expected = if i == 9 {
2411                    Expected::Oid(oid(1))
2412                } else {
2413                    Expected::Any
2414                };
2415                delete(&format!("refs/remotes/o/x{i}"), expected)
2416            })
2417            .collect();
2418        let err = commit_all(&writer, updates).expect_err("x9 moved under the batch");
2419        assert!(
2420            matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x9"),
2421            "{err:?}"
2422        );
2423
2424        for i in 0..9 {
2425            let name = format!("refs/remotes/o/x{i}");
2426            assert_eq!(
2427                writer.read_raw(&name).expect("read"),
2428                None,
2429                "{name} was deleted"
2430            );
2431        }
2432        assert_eq!(
2433            writer.read_raw("refs/remotes/o/x9").expect("read"),
2434            Some(RawRef::Direct(oid(7))),
2435            "the concurrent value survives"
2436        );
2437        for i in 10..20 {
2438            let name = format!("refs/remotes/o/x{i}");
2439            assert_eq!(
2440                writer.read_raw(&name).expect("read"),
2441                Some(RawRef::Direct(oid(1))),
2442                "{name} was never processed"
2443            );
2444        }
2445        let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2446        let mut expected = format!(
2447            "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n",
2448            oid(4)
2449        );
2450        let mut kept: Vec<String> = (9..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
2451        kept.sort();
2452        for name in kept {
2453            expected.push_str(&format!("{} {name}\n", oid(1)));
2454        }
2455        expected.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
2456        assert_eq!(packed, expected);
2457        assert!(!path.join("packed-refs.lock").exists());
2458        assert!(!path.join("refs/remotes/o/x10.lock").exists());
2459    }
2460
2461    #[test]
2462    fn restore_merges_records_back_in_sorted_order_with_peel_lines() {
2463        let dir = git_dir();
2464        let path = dir.path();
2465        fs::write(
2466            path.join("packed-refs"),
2467            format!(
2468                "# pack-refs with: peeled fully-peeled sorted \n{} refs/heads/b\n{} refs/tags/z\n^{}\n",
2469                oid(1),
2470                oid(2),
2471                oid(3)
2472            ),
2473        )
2474        .expect("packed");
2475        let names: HashSet<String> = ["refs/heads/b", "refs/tags/z"]
2476            .into_iter()
2477            .map(str::to_owned)
2478            .collect();
2479        let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
2480        let removed = remove_packed_refs_under_lock(path, &names).expect("remove");
2481        assert_eq!(removed.len(), 2);
2482        assert_eq!(packed_record_name(&removed[1]), "refs/tags/z");
2483        assert_eq!(packed_record_oid(&removed[1]), Some(oid(2)));
2484        fs::write(
2485            path.join("packed-refs"),
2486            format!(
2487                "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/a\n{} refs/heads/c\n",
2488                oid(5),
2489                oid(6)
2490            ),
2491        )
2492        .expect("packed");
2493        let records: Vec<&str> = removed.iter().map(String::as_str).collect();
2494        restore_packed_records_under_lock(path, &records).expect("restore");
2495        assert_eq!(
2496            fs::read_to_string(path.join("packed-refs")).expect("packed"),
2497            format!(
2498                "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/a\n{} refs/heads/b\n{} refs/heads/c\n{} refs/tags/z\n^{}\n",
2499                oid(5),
2500                oid(1),
2501                oid(6),
2502                oid(2),
2503                oid(3)
2504            )
2505        );
2506        assert!(before.contains("refs/tags/z"));
2507    }
2508
2509    /// Refs deleted while listing must not fail the walk (matches Git upload-pack behavior).
2510    #[test]
2511    fn concurrent_loose_ref_delete_during_list_is_tolerated() {
2512        use std::sync::atomic::{AtomicBool, Ordering};
2513        use std::sync::Arc;
2514        use std::thread;
2515        use std::time::Duration;
2516
2517        let dir = git_dir();
2518        set(
2519            &store(dir.path()),
2520            "refs/heads/main",
2521            oid(1),
2522            Expected::Missing,
2523        )
2524        .expect("seed main");
2525
2526        let stop = Arc::new(AtomicBool::new(false));
2527        let writer_dir = dir.path().to_path_buf();
2528        let writer_stop = stop.clone();
2529        let writer = thread::spawn(move || {
2530            use crate::refs::{delete_ref, write_ref};
2531            let c = oid(2);
2532            while !writer_stop.load(Ordering::Relaxed) {
2533                for i in 0..7 {
2534                    let name = format!("refs/heads/d{i}/sub/x");
2535                    let _ = write_ref(&writer_dir, &name, &c);
2536                    let _ = delete_ref(&writer_dir, &name);
2537                }
2538            }
2539        });
2540
2541        let lister = store(dir.path());
2542        let mut errors = 0usize;
2543        for _ in 0..400 {
2544            if lister
2545                .for_each_ref("", &mut |_| ControlFlow::Continue(()))
2546                .is_err()
2547            {
2548                errors += 1;
2549            }
2550        }
2551        stop.store(true, Ordering::Relaxed);
2552        writer.join().expect("writer");
2553        thread::sleep(Duration::from_millis(10));
2554        assert_eq!(
2555            errors, 0,
2556            "listing must tolerate refs deleted between readdir and open"
2557        );
2558    }
2559}