Skip to main content

grit_lib/refs/store/
files.rs

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