Skip to main content

grit_lib/refs/store/
files.rs

1//! Filesystem ref backend (loose refs + `packed-refs`) implementing [`super::RefStore`].
2//!
3//! On Windows, [`FilesRefStore`] rejects ref updates whose on-disk path would use a reserved
4//! device name (`CON`, `AUX`, `COM1`, …) or another component Win32 cannot represent without
5//! the `\\?\` prefix, because system Git cannot read such loose refs or reflogs. Deletes of
6//! refs that were already written remain allowed so grit can remove broken entries. Reftable
7//! and other backends are unchanged.
8
9use crate::git_path::CanonicalizeExt;
10use std::borrow::Cow;
11use std::cell::RefCell;
12use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
13use std::fs;
14use std::io;
15use std::ops::ControlFlow;
16use std::path::{Path, PathBuf};
17use std::sync::Mutex;
18
19use crate::error::{Error, Result};
20use crate::git_dir::io_error::{
21    io_with_ref_store_path, map_ref_store_path_io, GitDirPathOperation,
22};
23use crate::lockfile::LockWait;
24use crate::objects::ObjectId;
25use crate::reflog::{self, ReflogEntry};
26use crate::refs::{
27    lock_path_for_ref, loose_ref_list_walk_root, packed_record_name, packed_record_oid,
28    prune_empty_ref_dirs, read_packed_refs_file_view, read_packed_refs_map_for_prefix_filtered,
29    read_ref_file, remove_empty_ref_directory, remove_packed_ref, remove_packed_ref_under_lock,
30    remove_packed_refs_under_lock, restore_packed_records_under_lock, PackedPeel,
31    PackedRecordFilter, Ref,
32};
33
34use super::apply::{
35    apply_case_only_ref_rename, apply_reflog_rename, apply_update, ref_change_target,
36    ref_lock_names_for_updates, simulate_batch_apply, RefBatchState,
37};
38use super::backend::{is_per_ref_rejectable, partition_updates_allowing_failures};
39use super::error::RefStoreError;
40use super::routing::{resolve_common_dir, route_ref_storage_in_common, RefStorageRoute};
41use super::transaction::expected_matches;
42use super::transaction::RefTransaction;
43use super::types::Expected;
44use super::validation::{
45    validate_storable_refname, verify_create_conflicts, verify_name_available_for_create,
46};
47use super::{PreparedRefTransaction, RawRef, RefEntry, RefStorageFormat, RefStore, RefUpdate};
48use crate::refs::RefnameUnavailable;
49
50type StoreResult<T> = std::result::Result<T, RefStoreError>;
51
52/// Explicit context for a files ref store (no environment reads for namespace).
53#[derive(Debug, Clone)]
54pub struct FilesRefStoreConfig {
55    /// Worktree or bare repository git directory.
56    pub git_dir: PathBuf,
57    /// Shared object/ref store (`commondir` target or same as `git_dir`).
58    pub common_dir: PathBuf,
59    /// Optional `GIT_NAMESPACE`-style prefix applied to storage paths (not read from env).
60    pub namespace_prefix: Option<String>,
61}
62
63/// Loose + packed ref storage on disk.
64///
65/// On Windows, updates whose storage path includes a reserved device name or another
66/// non-round-tripping path component are rejected with [`RefStoreError::InvalidRefName`]
67/// so repositories stay readable by system Git.
68#[derive(Debug)]
69pub struct FilesRefStore {
70    git_dir: PathBuf,
71    common_dir: PathBuf,
72    namespace_prefix: Option<String>,
73    packed_cache: Mutex<PackedCacheEntry>,
74}
75
76/// What a cached `packed-refs` view was read from. Writers replace the file by rename, so the
77/// file identity changes even when the new file has the same length and mtime.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79struct PackedRefsStamp {
80    len: u64,
81    modified: Option<std::time::SystemTime>,
82    /// Device, inode and status-change time (seconds, nanoseconds), where the platform has them.
83    identity: Option<(u64, u64, i64, i64)>,
84}
85
86#[derive(Debug)]
87struct PackedCacheEntry {
88    stamp: PackedRefsStamp,
89    loaded: bool,
90    file: Option<crate::refs::PackedRefsFileView>,
91    /// Full packed-refs map; only valid when [`Self::map_is_complete`].
92    map: Option<BTreeMap<String, ObjectId>>,
93    map_is_complete: bool,
94}
95
96/// Per-batch state shared by the steps of [`FilesRefStore::persist_state_diff`].
97#[derive(Debug, Clone, Copy)]
98struct CommitContext<'a> {
99    /// How long to wait for each loose ref lock, read from config once per batch.
100    ref_lock_wait: LockWait,
101    /// Whether the batch holds `packed-refs.lock`.
102    lock_held: bool,
103    /// Logical names whose packed records were already dropped in one up-front rewrite.
104    pruned: &'a HashSet<String>,
105    /// Values of the records that rewrite removed, so commit-time rechecks still see them.
106    removed: &'a HashMap<String, RawRef>,
107    /// Directories that deletions may have left empty (mapped to their storage directory),
108    /// pruned once the batch is done instead of after every ref.
109    emptied_dirs: &'a RefCell<BTreeMap<PathBuf, PathBuf>>,
110}
111
112/// What [`FilesRefStore::prune_deleted_packed_records`] took out of `packed-refs`.
113#[derive(Debug, Default)]
114struct PrunedPackedRecords {
115    /// Logical names of every ref the batch deletes.
116    names: HashSet<String>,
117    /// Removed values of deletions whose expectation the commit-time recheck verifies.
118    removed: HashMap<String, RawRef>,
119    /// Removed records verbatim, per storage directory.
120    records: Vec<(PathBuf, Vec<String>)>,
121}
122
123#[derive(Debug)]
124struct PreparedFilesBatch {
125    updates: Vec<RefUpdate>,
126    case_only_renames: Vec<super::RefCaseOnlyRename>,
127    ref_locks: HashMap<String, PathBuf>,
128    packed_lock: Option<PathBuf>,
129    ref_lock_wait: LockWait,
130    /// Ref/reflog state observed at prepare (must drive commit, not a reload).
131    before_commit: RefBatchState,
132    after_commit: RefBatchState,
133    /// `packed-refs` records an initial transaction adds instead of loose files (see
134    /// [`RefTransaction::initial`]); empty for every other batch.
135    initial_packed_records: Vec<String>,
136}
137
138impl FilesRefStore {
139    /// Open a files backend at `config.git_dir`.
140    #[must_use]
141    pub fn open(config: FilesRefStoreConfig) -> Self {
142        let path = super::paths::packed_refs_path(&config.common_dir);
143        let stamp = Self::packed_refs_stamp(&path);
144        Self {
145            git_dir: config.git_dir,
146            common_dir: config.common_dir,
147            namespace_prefix: config.namespace_prefix,
148            packed_cache: Mutex::new(PackedCacheEntry {
149                stamp,
150                loaded: false,
151                file: None,
152                map: None,
153                map_is_complete: false,
154            }),
155        }
156    }
157
158    /// Git directory this store reads `HEAD` and per-worktree refs from.
159    pub(crate) fn git_dir(&self) -> &Path {
160        &self.git_dir
161    }
162
163    /// Convenience constructor for a simple repository directory.
164    pub fn from_git_dir(git_dir: impl Into<PathBuf>) -> Result<Self> {
165        let git_dir: PathBuf = git_dir.into();
166        let git_dir = git_dir.canonicalize_simplified().unwrap_or(git_dir);
167        let common_dir = resolve_common_dir(&git_dir);
168        Ok(Self::open(FilesRefStoreConfig {
169            git_dir: git_dir.clone(),
170            common_dir,
171            namespace_prefix: crate::ref_namespace::ref_storage_prefix_default(),
172        }))
173    }
174
175    fn packed_refs_stamp(path: &Path) -> PackedRefsStamp {
176        match fs::metadata(path) {
177            Ok(meta) => PackedRefsStamp {
178                len: meta.len(),
179                modified: meta.modified().ok(),
180                identity: file_identity(&meta),
181            },
182            Err(_) => PackedRefsStamp {
183                len: 0,
184                modified: None,
185                identity: None,
186            },
187        }
188    }
189
190    /// Drop the cached `packed-refs` view so the next lookup reads the file again.
191    ///
192    /// Stamps can miss a replacement on filesystems with coarse timestamps or reused inodes,
193    /// so checks whose answer must reflect the file right now (the create-conflict recheck
194    /// under the ref lock) start from a fresh read.
195    fn forget_packed_cache(&self) {
196        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
197        guard.loaded = false;
198        guard.file = None;
199        guard.map = None;
200        guard.map_is_complete = false;
201    }
202
203    fn invalidate_packed_cache_after_write(&self) {
204        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
205        guard.map = None;
206        guard.map_is_complete = false;
207    }
208
209    fn refresh_packed_cache_if_stale(&self) -> Result<()> {
210        let path = super::paths::packed_refs_path(&self.common_dir);
211        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
212        let stamp = Self::packed_refs_stamp(&path);
213        if guard.loaded && guard.stamp == stamp {
214            return Ok(());
215        }
216        guard.file = read_packed_refs_file_view(&self.common_dir)?;
217        guard.map = None;
218        guard.map_is_complete = false;
219        guard.stamp = stamp;
220        guard.loaded = true;
221        Ok(())
222    }
223
224    fn ensure_full_packed_map(&self) -> Result<()> {
225        self.refresh_packed_cache_if_stale()?;
226        let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
227        if guard.map.is_some() && guard.map_is_complete {
228            return Ok(());
229        }
230        let map = if let Some(ref file) = guard.file {
231            let path = super::paths::packed_refs_path(&self.common_dir);
232            file.full_map(&path)?
233        } else {
234            BTreeMap::new()
235        };
236        guard.map = Some(map);
237        guard.map_is_complete = true;
238        Ok(())
239    }
240
241    /// Records of the common dir's `packed-refs` under `prefix` (storage names), read through
242    /// the cached file view so a listing and later lookups index the file once.
243    fn packed_map_for_prefix_filtered(
244        &self,
245        prefix: &str,
246        keep: Option<PackedRecordFilter<'_>>,
247    ) -> Result<HashMap<String, ObjectId>> {
248        self.refresh_packed_cache_if_stale()?;
249        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
250        let Some(ref file) = guard.file else {
251            return Ok(HashMap::new());
252        };
253        let path = super::paths::packed_refs_path(&self.common_dir);
254        let prefix = (!prefix.is_empty()).then_some(prefix);
255        file.map_for_prefix_filtered(&path, prefix, keep)
256    }
257
258    /// The common dir's `packed-refs` view this store reads through, refreshed when the file
259    /// changed (`None` without a `packed-refs`). The copy shares the file's bytes and index.
260    pub(crate) fn packed_refs_view(&self) -> Result<Option<crate::refs::PackedRefsFileView>> {
261        self.refresh_packed_cache_if_stale()?;
262        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
263        Ok(guard.file.clone())
264    }
265
266    fn lookup_packed_oid(&self, storage_name: &str) -> Result<Option<ObjectId>> {
267        self.refresh_packed_cache_if_stale()?;
268        let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
269        let Some(ref file) = guard.file else {
270            return Ok(None);
271        };
272        let path = super::paths::packed_refs_path(&self.common_dir);
273        file.lookup_oid(&path, storage_name)
274    }
275
276    /// Add to `refs` every existing ref that can block creating a new ref of `updates`: its
277    /// ancestors (`refs/heads/a` for `refs/heads/a/b`) and the refs under `<name>/`.
278    ///
279    /// `refs` must already hold every update target that exists. With these neighbors,
280    /// [`verify_create_conflicts`] reaches the same verdict as against the whole store, so a
281    /// create costs a few lookups instead of reading every ref and reflog.
282    fn insert_create_conflict_neighbors(
283        &self,
284        refs: &mut BTreeMap<String, RawRef>,
285        updates: &[RefUpdate],
286    ) -> Result<()> {
287        let creates: Vec<&str> = updates
288            .iter()
289            .filter(|u| u.new_value.is_some() && !u.flags.log_only && !refs.contains_key(&u.name))
290            .map(|u| u.name.as_str())
291            .collect();
292        let packed = self.packed_refs_view()?;
293        // Siblings share their ancestors; look each one up once per batch.
294        let mut not_refs: HashSet<&str> = HashSet::new();
295        for name in creates {
296            for (slash, _) in name.match_indices('/') {
297                let ancestor = &name[..slash];
298                if refs.contains_key(ancestor) || not_refs.contains(ancestor) {
299                    continue;
300                }
301                match self.existing_ref_named_in(ancestor, packed.as_ref(), None)? {
302                    Some(raw) => {
303                        refs.insert(ancestor.to_owned(), raw);
304                    }
305                    None => {
306                        not_refs.insert(ancestor);
307                    }
308                }
309            }
310            for (descendant, raw) in self.descendant_refs_in(name, packed.as_ref())? {
311                refs.entry(descendant).or_insert(raw);
312            }
313        }
314        Ok(())
315    }
316
317    /// Whether `name` can be created next to the refs on disk and the other names of its batch
318    /// (`extras`), under the rules of [`crate::refs::verify_refname_available_for_create`].
319    ///
320    /// Lookups go through this store's packed-refs cache, which revalidates against the file's
321    /// size and mtime, instead of re-reading `packed-refs` for every ancestor. Any file under
322    /// `<name>/` blocks the create, since the directory could not be replaced by the ref file.
323    /// Unreadable refs count as absent, as in the path-based check.
324    ///
325    /// `held_lock` is the path of `name`'s lock file when the caller holds it: every path above
326    /// it is then a directory, so ancestors are not probed for loose files there.
327    fn verify_create_available(
328        &self,
329        name: &str,
330        extras: &BTreeSet<&str>,
331        held_lock: Option<&Path>,
332    ) -> std::result::Result<(), RefnameUnavailable> {
333        let packed = self.packed_refs_view().ok().flatten();
334        self.verify_create_available_in(
335            name,
336            extras,
337            held_lock,
338            packed.as_ref(),
339            &mut HashMap::new(),
340        )
341    }
342
343    /// [`Self::verify_create_available`] against one view of `packed-refs` (`packed`), with
344    /// `ancestors` remembering which ancestor names are refs across the creates of a batch.
345    fn verify_create_available_in(
346        &self,
347        name: &str,
348        extras: &BTreeSet<&str>,
349        held_lock: Option<&Path>,
350        packed: Option<&crate::refs::PackedRefsFileView>,
351        ancestors: &mut HashMap<String, bool>,
352    ) -> std::result::Result<(), RefnameUnavailable> {
353        let mut blocking = BTreeSet::new();
354        for (slash, _) in name.match_indices('/') {
355            let ancestor = &name[..slash];
356            let is_ref = match ancestors.get(ancestor) {
357                Some(&is_ref) => is_ref,
358                None => {
359                    let is_ref = matches!(
360                        self.existing_ref_named_in(ancestor, packed, held_lock),
361                        Ok(Some(_))
362                    );
363                    ancestors.insert(ancestor.to_owned(), is_ref);
364                    is_ref
365                }
366            };
367            if is_ref {
368                blocking.insert(ancestor.to_owned());
369            }
370        }
371        let storage = self.route(name).storage_name;
372        let mut storage_names = BTreeSet::new();
373        for base in self.store_bases() {
374            collect_loose_file_names(&base.join(&storage), &storage, &mut storage_names);
375        }
376        if let Some(packed) = packed.filter(|_| storage.contains('/')) {
377            let path = super::paths::packed_refs_path(&self.common_dir);
378            if let Ok(Some((descendant, _))) = packed.first_descendant(&path, &storage) {
379                storage_names.insert(descendant);
380            }
381        }
382        blocking.extend(storage_names.iter().map(|s| self.logical_name(s)));
383        verify_name_available_for_create(name, &blocking, extras)
384    }
385
386    /// Directories holding loose refs and logs: the common dir, then the per-worktree
387    /// git dir when it differs (a linked worktree). The main worktree yields one entry,
388    /// so its loose refs are not walked twice.
389    fn store_bases(&self) -> impl Iterator<Item = &Path> {
390        let common = self.common_dir.as_path();
391        let git = self.git_dir.as_path();
392        std::iter::once(common).chain((git != common).then_some(git))
393    }
394
395    /// The value of `name` as a full listing would report it: a parseable loose file in either
396    /// store directory, else a visible packed record. Directories and unparseable files are
397    /// not refs.
398    ///
399    /// `packed` is one view of `packed-refs` shared by several lookups, so the file is
400    /// revalidated once. Loose paths above `held_lock`, a lock file the caller holds, are
401    /// directories and are not probed.
402    fn existing_ref_named_in(
403        &self,
404        name: &str,
405        packed: Option<&crate::refs::PackedRefsFileView>,
406        held_lock: Option<&Path>,
407    ) -> Result<Option<RawRef>> {
408        let storage = self.route(name).storage_name;
409        for base in self.store_bases() {
410            let path = base.join(&storage);
411            if held_lock.is_some_and(|lock| lock.starts_with(&path)) || !path.is_file() {
412                continue;
413            }
414            if let Ok(r) = read_ref_file(&path) {
415                return Ok(Some(ref_to_raw(r)));
416            }
417        }
418        let Some(packed) = packed.filter(|_| self.packed_storage_visible(&storage)) else {
419            return Ok(None);
420        };
421        let path = super::paths::packed_refs_path(&self.common_dir);
422        Ok(packed.lookup_oid(&path, &storage)?.map(RawRef::Direct))
423    }
424
425    /// Loose refs under `<name>/` in either store directory, plus the first one in `packed`.
426    fn descendant_refs_in(
427        &self,
428        name: &str,
429        packed: Option<&crate::refs::PackedRefsFileView>,
430    ) -> Result<BTreeMap<String, RawRef>> {
431        let storage = self.route(name).storage_name;
432        let mut storage_map = BTreeMap::new();
433        for base in self.store_bases() {
434            let dir = base.join(&storage);
435            if dir.is_dir() {
436                collect_loose_raw_refs(&dir, &format!("{storage}/"), "", &mut storage_map)?;
437            }
438        }
439        // Refs live under `refs/`, so a one-level name has no packed descendants; skipping it
440        // also avoids scanning from a pseudoref like `ORIG_HEAD` to the end of the file.
441        if let Some(packed) = packed.filter(|_| storage.contains('/')) {
442            let path = super::paths::packed_refs_path(&self.common_dir);
443            if let Some((descendant, oid)) = packed.first_descendant(&path, &storage)? {
444                if self.packed_storage_visible(&descendant) {
445                    storage_map.entry(descendant).or_insert(RawRef::Direct(oid));
446                }
447            }
448        }
449        Ok(storage_map
450            .into_iter()
451            .map(|(storage, value)| (self.logical_name(&storage), value))
452            .collect())
453    }
454
455    /// Load `start` and any further symbolic targets into `refs` (up to [`crate::refs::SYMREF_MAXDEPTH`]).
456    fn insert_symref_peel_chain(
457        &self,
458        refs: &mut BTreeMap<String, RawRef>,
459        start: &str,
460    ) -> Result<()> {
461        let mut name = start.to_owned();
462        for _ in 0..crate::refs::SYMREF_MAXDEPTH {
463            if refs.contains_key(&name) {
464                match refs.get(&name) {
465                    Some(RawRef::Symbolic(next)) => name = next.clone(),
466                    _ => break,
467                }
468                continue;
469            }
470            let Some(raw) = self.read_raw(&name)? else {
471                break;
472            };
473            let advance = match &raw {
474                RawRef::Symbolic(next) => Some(next.clone()),
475                RawRef::Direct(_) => None,
476            };
477            refs.insert(name.clone(), raw);
478            match advance {
479                Some(next) => name = next,
480                None => break,
481            }
482        }
483        Ok(())
484    }
485
486    fn load_batch_state_for_prepare(
487        &self,
488        updates: &[RefUpdate],
489        reflog_renames: &[super::RefReflogRename],
490        case_only_renames: &[super::RefCaseOnlyRename],
491    ) -> Result<RefBatchState> {
492        let mut refs = BTreeMap::new();
493        for update in updates {
494            if let Some(raw) = self.read_raw(&update.name)? {
495                refs.insert(update.name.clone(), raw);
496            }
497        }
498        for rename in case_only_renames {
499            if refs.contains_key(&rename.from) {
500                continue;
501            }
502            if let Some(raw) = self.read_raw(&rename.from)? {
503                refs.insert(rename.from.clone(), raw);
504            }
505        }
506        self.insert_head_for_batch_state(&mut refs)?;
507
508        for update in updates {
509            if matches!(refs.get(&update.name), Some(RawRef::Symbolic(_))) {
510                self.insert_symref_peel_chain(&mut refs, &update.name)?;
511            }
512        }
513        if matches!(refs.get("HEAD"), Some(RawRef::Symbolic(_))) {
514            self.insert_symref_peel_chain(&mut refs, "HEAD")?;
515        }
516
517        let mut reflogs = self.load_reflogs_for_updates(&refs, updates)?;
518        for rename in reflog_renames {
519            if reflogs.contains_key(&rename.from) {
520                continue;
521            }
522            let entries = read_reflog_at_path(&self.reflog_path(&rename.from))?;
523            if !entries.is_empty() {
524                reflogs.insert(rename.from.clone(), entries);
525            }
526        }
527        Ok(RefBatchState { refs, reflogs })
528    }
529
530    /// Make room for a new reflog of `name`: an empty directory tree at its path is removed,
531    /// and one that still holds logs (left behind for `<name>/…` refs) blocks the write.
532    ///
533    /// # Errors
534    ///
535    /// [`RefStoreError::NameUnavailable`] naming the first log under the directory, or
536    /// [`RefStoreError::Corrupt`] when the directory cannot be listed.
537    fn ensure_reflog_path_writable(&self, name: &str) -> StoreResult<()> {
538        let path = self.reflog_path(name);
539        let is_dir = |path: &Path| fs::symlink_metadata(path).is_ok_and(|m| m.is_dir());
540        if !is_dir(&path) {
541            return Ok(());
542        }
543        crate::refs::remove_empty_ref_directory(&path);
544        if !is_dir(&path) {
545            return Ok(());
546        }
547        let storage = self.route(name).storage_name;
548        let mut logs = BTreeSet::new();
549        collect_loose_file_names(&path, &storage, &mut logs);
550        let Some(log) = logs.first() else {
551            return Err(RefStoreError::Corrupt(format!(
552                "cannot write reflog at '{}': unreadable directory in the way",
553                path.display()
554            )));
555        };
556        Err(RefnameUnavailable::DescendantExists {
557            blocking: self.logical_name(log),
558            new_ref: name.to_owned(),
559        }
560        .into())
561    }
562
563    fn read_raw_at_route(&self, route: &RefStorageRoute) -> Result<Option<RawRef>> {
564        let path = route.storage_dir.join(&route.storage_name);
565        match read_ref_file(&path) {
566            Ok(Ref::Direct(oid)) => return Ok(Some(RawRef::Direct(oid))),
567            Ok(Ref::Symbolic(target)) => return Ok(Some(RawRef::Symbolic(target))),
568            Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
569            Err(e) => return Err(e),
570        }
571        if let Some(oid) = self.lookup_packed_oid(&route.storage_name)? {
572            return Ok(Some(RawRef::Direct(oid)));
573        }
574        Ok(None)
575    }
576
577    fn packed_storage_visible(&self, storage: &str) -> bool {
578        match self.namespace_prefix.as_deref() {
579            None => true,
580            Some(prefix) => storage.starts_with(prefix),
581        }
582    }
583
584    /// Prefix for scanning `packed-refs` lines (storage paths, not logical names).
585    fn packed_refs_scan_prefix(&self, logical_prefix: &str) -> String {
586        match self.namespace_prefix.as_deref() {
587            Some(ns) if !logical_prefix.is_empty() => format!("{ns}{logical_prefix}"),
588            Some(ns) => ns.trim_end_matches('/').to_owned(),
589            None => logical_prefix.to_owned(),
590        }
591    }
592
593    /// Raw refs whose logical names start with `logical_prefix`, loose values overriding packed
594    /// ones. `keep`, given a packed record's storage name, id and peel, may leave it out.
595    fn load_raw_map_prefix(
596        &self,
597        logical_prefix: &str,
598        keep: Option<PackedRecordFilter<'_>>,
599    ) -> Result<BTreeMap<String, RawRef>> {
600        let logical_prefix = logical_prefix.trim_end_matches('/');
601        let mut map: BTreeMap<String, RawRef> = BTreeMap::new();
602        if logical_prefix.is_empty() {
603            self.ensure_full_packed_map()?;
604            let cache = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
605            if let Some(packed) = cache.map.as_ref() {
606                for (storage, oid) in packed.iter() {
607                    if !self.packed_storage_visible(storage)
608                        || keep.is_some_and(|keep| !keep(storage, oid, PackedPeel::Unknown))
609                    {
610                        continue;
611                    }
612                    let logical = self.logical_name(storage);
613                    map.insert(logical, RawRef::Direct(*oid));
614                }
615            }
616            drop(cache);
617        } else {
618            for (i, base) in self.store_bases().enumerate() {
619                let packed_prefix = self.packed_refs_scan_prefix(logical_prefix);
620                let packed = if i == 0 {
621                    self.packed_map_for_prefix_filtered(&packed_prefix, keep)?
622                } else {
623                    read_packed_refs_map_for_prefix_filtered(base, &packed_prefix, keep)?
624                };
625                for (storage, oid) in packed {
626                    if !self.packed_storage_visible(&storage) {
627                        continue;
628                    }
629                    let logical = self.logical_name(&storage);
630                    map.insert(logical, RawRef::Direct(oid));
631                }
632            }
633        }
634
635        for base in self.store_bases() {
636            let storage_prefix = self.namespace_prefix.as_deref().unwrap_or("refs/");
637            if self.namespace_prefix.is_some() {
638                let root = base.join(storage_prefix.trim_end_matches('/'));
639                if root.is_dir() {
640                    let mut storage_map = BTreeMap::new();
641                    collect_loose_raw_refs(&root, storage_prefix, "", &mut storage_map)?;
642                    for (storage, value) in storage_map {
643                        let logical = self.logical_name(&storage);
644                        if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
645                            map.insert(logical, value);
646                        }
647                    }
648                }
649            } else {
650                let (walk_root, walk_prefix) = loose_ref_list_walk_root(base, logical_prefix);
651                if walk_root.is_dir() {
652                    let mut storage_map = BTreeMap::new();
653                    collect_loose_raw_refs(
654                        &walk_root,
655                        &walk_prefix,
656                        logical_prefix,
657                        &mut storage_map,
658                    )?;
659                    for (storage, value) in storage_map {
660                        let logical = self.logical_name(&storage);
661                        if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
662                            map.insert(logical, value);
663                        }
664                    }
665                }
666            }
667            if head_matches_prefix(logical_prefix) && base.join("HEAD").is_file() {
668                if let Ok(r) = read_ref_file(&base.join("HEAD")) {
669                    map.insert("HEAD".to_owned(), ref_to_raw(r));
670                }
671            }
672        }
673        Ok(map)
674    }
675
676    /// Reflogs a batch can append to: each update that carries a reflog, and the ref its
677    /// symbolic chain (as recorded in `refs`) ends at. Updates without a reflog touch none,
678    /// so plain deletes and CAS updates skip reading `logs/` entirely.
679    fn load_reflogs_for_updates(
680        &self,
681        refs: &BTreeMap<String, RawRef>,
682        updates: &[RefUpdate],
683    ) -> Result<BTreeMap<String, Vec<ReflogEntry>>> {
684        let mut names = BTreeSet::new();
685        for update in updates.iter().filter(|u| u.reflog.is_some()) {
686            let mut name = update.name.as_str();
687            names.insert(name.to_owned());
688            for _ in 0..crate::refs::SYMREF_MAXDEPTH {
689                let Some(RawRef::Symbolic(next)) = refs.get(name) else {
690                    break;
691                };
692                name = next;
693                names.insert(name.to_owned());
694            }
695        }
696        let mut out = BTreeMap::new();
697        for name in names {
698            let entries = read_reflog_at_path(&self.reflog_path(&name))?;
699            if !entries.is_empty() {
700                out.insert(name, entries);
701            }
702        }
703        Ok(out)
704    }
705
706    fn insert_head_for_batch_state(&self, map: &mut BTreeMap<String, RawRef>) -> Result<()> {
707        if map.contains_key("HEAD") {
708            return Ok(());
709        }
710        for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
711            let head_path = base.join("HEAD");
712            if head_path.is_file() {
713                let raw = read_ref_file(&head_path).map(ref_to_raw)?;
714                map.insert("HEAD".to_owned(), raw);
715                break;
716            }
717        }
718        Ok(())
719    }
720
721    fn loose_ref_file_exists(&self, refname: &str) -> Result<bool> {
722        let route = self.route(refname);
723        let path = route.storage_dir.join(&route.storage_name);
724        Ok(path.is_file())
725    }
726
727    fn route(&self, refname: &str) -> RefStorageRoute {
728        route_ref_storage_in_common(
729            &self.git_dir,
730            &self.common_dir,
731            refname,
732            self.namespace_prefix.as_deref(),
733        )
734    }
735
736    /// Reject updates that would write a loose ref or reflog path Win32 cannot expose to
737    /// system Git (reserved device names and similar). Deletes are left alone.
738    #[cfg(windows)]
739    fn reject_win32_unstorable_ref_paths(&self, txn: &RefTransaction) -> StoreResult<()> {
740        use crate::git_path::win32_storage_path_components_round_trip;
741
742        let storage_ok =
743            |name: &str| win32_storage_path_components_round_trip(&self.route(name).storage_name);
744        for update in txn.updates() {
745            if update.new_value.is_none() && !update.flags.log_only {
746                continue;
747            }
748            if !storage_ok(&update.name) {
749                return Err(RefStoreError::InvalidRefName {
750                    name: update.name.clone(),
751                });
752            }
753        }
754        for rename in txn.reflog_renames() {
755            if !storage_ok(&rename.to) {
756                return Err(RefStoreError::InvalidRefName {
757                    name: rename.to.clone(),
758                });
759            }
760        }
761        for rename in txn.case_only_renames() {
762            if !storage_ok(&rename.to) {
763                return Err(RefStoreError::InvalidRefName {
764                    name: rename.to.clone(),
765                });
766            }
767        }
768        Ok(())
769    }
770
771    fn logical_name(&self, storage: &str) -> String {
772        crate::ref_namespace::logical_ref_name_with_prefix(
773            self.namespace_prefix.as_deref(),
774            storage,
775        )
776    }
777
778    /// Path of the loose reflog file for the logical ref name `logical`.
779    pub(crate) fn reflog_path(&self, logical: &str) -> PathBuf {
780        let route = self.route(logical);
781        route.storage_dir.join("logs").join(&route.storage_name)
782    }
783
784    /// Every parseable entry of `name`'s reflog, oldest first, without the per-entry clone
785    /// [`RefStore::for_each_reflog_entry`] needs; empty when the log is missing.
786    pub(crate) fn read_reflog_entries(&self, name: &str) -> Result<Vec<ReflogEntry>> {
787        read_reflog_at_path(&self.reflog_path(name))
788    }
789
790    fn ref_lock_paths(&self, refname: &str) -> (RefStorageRoute, PathBuf, PathBuf) {
791        let route = self.route(refname);
792        let path = route.storage_dir.join(&route.storage_name);
793        let lock = lock_path_for_ref(&path);
794        (route, path, lock)
795    }
796
797    /// The `packed-refs` records (ref line plus `^` peel line for annotated tags) that store
798    /// an initial transaction's updates, or `None` when the batch must commit as usual: an
799    /// update is not a plain direct create, stores outside the common directory's
800    /// `packed-refs`, or names a ref that exists already.
801    ///
802    /// # Errors
803    ///
804    /// [`RefStoreError::NameUnavailable`] when the batch conflicts with itself or with the
805    /// stored refs (as the regular path would report), and listing failures.
806    fn initial_packed_records(&self, txn: &RefTransaction) -> StoreResult<Option<Vec<String>>> {
807        let updates = txn.updates();
808        if updates.is_empty()
809            || !txn.reflog_renames().is_empty()
810            || !txn.case_only_renames().is_empty()
811        {
812            return Ok(None);
813        }
814        let mut creates = Vec::with_capacity(updates.len());
815        for update in updates {
816            let Some(RawRef::Direct(oid)) = update.new_value else {
817                return Ok(None);
818            };
819            if update.flags.log_only
820                || update.reflog.is_some()
821                || !matches!(update.expected, Expected::Any | Expected::Missing)
822            {
823                return Ok(None);
824            }
825            let route = self.route(&update.name);
826            if route.storage_dir != self.common_dir || route.storage_name != update.name {
827                return Ok(None);
828            }
829            creates.push((update.name.as_str(), oid));
830        }
831        let existing = self
832            .load_raw_map_prefix("", None)
833            .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
834        if updates.iter().any(|u| existing.contains_key(&u.name)) {
835            return Ok(None);
836        }
837        verify_create_conflicts(&existing, updates)?;
838
839        let odb = crate::odb::Odb::new(&self.common_dir.join("objects"))
840            .with_config_git_dir(self.git_dir.clone());
841        let mut peels: HashMap<ObjectId, Option<ObjectId>> = HashMap::new();
842        let records = odb.with_pack_read_context(|| {
843            creates
844                .into_iter()
845                .map(|(name, oid)| {
846                    let peel = *peels
847                        .entry(oid)
848                        .or_insert_with(|| crate::refs::packed_ref_peel(&odb, oid));
849                    match peel {
850                        Some(peel) => format!("{oid} {name}\n^{peel}\n"),
851                        None => format!("{oid} {name}\n"),
852                    }
853                })
854                .collect()
855        });
856        Ok(Some(records))
857    }
858
859    /// Take `packed-refs.lock` for a batch, waiting as `core.packedRefsTimeout` allows.
860    fn acquire_packed_lock(&self) -> StoreResult<PathBuf> {
861        use crate::lockfile::{LockError, LockFile};
862        let packed_path = super::paths::packed_refs_path(&self.common_dir);
863        let lock = lock_path_for_ref(&packed_path);
864        let wait = LockWait::packed_refs_for_git_dir(&self.common_dir);
865        match LockFile::acquire(&lock, wait).map(|guard| guard.detach()) {
866            Ok(()) => Ok(lock),
867            Err(LockError::Held { .. }) => Err(RefStoreError::LockHeld {
868                name: super::paths::PACKED_REFS.to_owned(),
869            }),
870            Err(LockError::Io { source, .. }) => Err(RefStoreError::Corrupt(source.to_string())),
871        }
872    }
873
874    fn needs_packed_lock(&self, updates: &[RefUpdate]) -> bool {
875        updates.iter().any(|u| {
876            if u.new_value.is_some() || u.flags.log_only {
877                return false;
878            }
879            let storage = self.route(&u.name).storage_name;
880            self.lookup_packed_oid(&storage).ok().flatten().is_some()
881        })
882    }
883
884    /// Take the locks of `names` that `prepare` holds, recording them in `ref_locks`: the
885    /// `carried` ref (the first update's, which commit keeps writing under), and every other
886    /// ref whose lock path is not plainly absent right now, so a lock held elsewhere is waited
887    /// for, and a path that cannot hold a lock is reported, before anything is written.
888    ///
889    /// Commit re-takes the other locks one update at a time, so creating and removing each
890    /// uncontended lock here would only add a file create and unlink per ref.
891    ///
892    /// # Errors
893    ///
894    /// The failure of [`Self::lock_ref`]; every lock in `ref_locks` is released first.
895    fn take_prepare_locks<'a>(
896        &self,
897        names: impl IntoIterator<Item = &'a String>,
898        carried: Option<&str>,
899        wait: LockWait,
900        ref_locks: &mut HashMap<String, PathBuf>,
901    ) -> StoreResult<()> {
902        for name in names {
903            if ref_locks.contains_key(name) {
904                continue;
905            }
906            let (_, _, lock) = self.ref_lock_paths(name);
907            // Only a lock path that is really absent can wait for commit. Anything else (a
908            // file where a parent directory belongs, a permission error) must fail here,
909            // before the batch writes any ref.
910            let absent = matches!(
911                fs::symlink_metadata(&lock),
912                Err(err) if err.kind() == io::ErrorKind::NotFound
913            );
914            if carried != Some(name.as_str()) && absent {
915                continue;
916            }
917            if let Err(err) = self.lock_ref(name, &lock, wait) {
918                for acquired in ref_locks.values() {
919                    let _ = fs::remove_file(acquired);
920                }
921                return Err(Self::ref_store_error_from_lib(err));
922            }
923            ref_locks.insert(name.clone(), lock);
924        }
925        Ok(())
926    }
927
928    /// Take `refname`'s lock file at `lock_path`, creating its directory when needed.
929    ///
930    /// # Errors
931    ///
932    /// A directory that cannot be created (a ref file in the way) is reported as a
933    /// [`GitDirPathOperation::CreateDir`] failure; otherwise as [`Self::acquire_ref_lock_mapped`].
934    fn lock_ref(&self, refname: &str, lock_path: &Path, wait: LockWait) -> Result<()> {
935        if let Some(reason) = self.name_unavailable_if_ancestor_is_loose_file(refname) {
936            return Err(Error::RefStore(RefStoreError::NameUnavailable { reason }));
937        }
938        let Err(err) = self.acquire_ref_lock_mapped(lock_path, refname, wait) else {
939            return Ok(());
940        };
941        if let Some(parent) = lock_path.parent() {
942            if parent.is_file() {
943                return Err(Error::RefStore(RefStoreError::NameUnavailable {
944                    reason: RefnameUnavailable::AncestorExists {
945                        blocking: loose_storage_path_to_refname(&self.common_dir, parent),
946                        new_ref: refname.to_owned(),
947                    },
948                }));
949            }
950            if let Err(source) = fs::create_dir_all(parent) {
951                if let Some(reason) = self.name_unavailable_if_ancestor_is_loose_file(refname) {
952                    return Err(Error::RefStore(RefStoreError::NameUnavailable { reason }));
953                }
954                self.io_ref_store(parent, GitDirPathOperation::CreateDir, Err(source))?;
955            }
956        }
957        Err(err)
958    }
959
960    /// Whether a strict ancestor of `refname` is already stored as a loose ref file (blocks
961    /// creating a descendant under it).
962    fn name_unavailable_if_ancestor_is_loose_file(
963        &self,
964        refname: &str,
965    ) -> Option<RefnameUnavailable> {
966        for (slash, _) in refname.match_indices('/').rev() {
967            let ancestor = &refname[..slash];
968            if ancestor == "refs" {
969                continue;
970            }
971            if self.loose_ref_file_exists(ancestor).ok()? {
972                return Some(RefnameUnavailable::AncestorExists {
973                    blocking: ancestor.to_owned(),
974                    new_ref: refname.to_owned(),
975                });
976            }
977        }
978        None
979    }
980
981    fn acquire_ref_lock_mapped(
982        &self,
983        lock_path: &Path,
984        refname: &str,
985        wait: LockWait,
986    ) -> Result<()> {
987        use crate::lockfile::{LockError, LockFile};
988        LockFile::acquire(lock_path, wait)
989            .map(|guard| guard.detach())
990            .map_err(|err| match err {
991                LockError::Held { .. } => Error::RefStore(RefStoreError::LockHeld {
992                    name: refname.to_owned(),
993                }),
994                LockError::Io { source, path } => map_ref_store_path_io(
995                    &self.common_dir,
996                    &self.git_dir,
997                    path.as_path(),
998                    GitDirPathOperation::UpdateRef,
999                    source,
1000                ),
1001            })
1002    }
1003
1004    fn ref_store_error_from_lib(err: Error) -> RefStoreError {
1005        match err {
1006            Error::GitDirPathIo {
1007                path,
1008                operation,
1009                source,
1010            } => RefStoreError::git_dir_path_io(path, operation, source),
1011            Error::RefStore(store_err) => store_err,
1012            other => RefStoreError::Corrupt(other.to_string()),
1013        }
1014    }
1015
1016    fn io_ref_store<T>(
1017        &self,
1018        path: &Path,
1019        operation: GitDirPathOperation,
1020        result: io::Result<T>,
1021    ) -> Result<T> {
1022        io_with_ref_store_path(&self.common_dir, &self.git_dir, path, operation, result)
1023    }
1024
1025    fn release_locks(batch: &PreparedFilesBatch) {
1026        for lock in batch.ref_locks.values() {
1027            let _ = fs::remove_file(lock);
1028        }
1029        if let Some(p) = &batch.packed_lock {
1030            let _ = fs::remove_file(p);
1031        }
1032    }
1033
1034    fn write_loose_value(
1035        common_dir: &Path,
1036        checkout_git_dir: &Path,
1037        refname: &str,
1038        path: &Path,
1039        lock: &Path,
1040        value: &RawRef,
1041    ) -> Result<()> {
1042        let content = match value {
1043            RawRef::Direct(oid) => format!("{oid}\n"),
1044            RawRef::Symbolic(target) => format!("ref: {target}\n"),
1045        };
1046        io_with_ref_store_path(
1047            common_dir,
1048            checkout_git_dir,
1049            lock,
1050            GitDirPathOperation::UpdateRef,
1051            fs::write(lock, content),
1052        )?;
1053        // An empty directory tree left by deleted nested refs may occupy the ref's path.
1054        let renamed = fs::rename(lock, path).or_else(|err| {
1055            if !path.is_dir() {
1056                return Err(err);
1057            }
1058            remove_empty_ref_directory(path);
1059            fs::rename(lock, path)
1060        });
1061        match renamed {
1062            Ok(()) => Ok(()),
1063            Err(err)
1064                if matches!(
1065                    err.kind(),
1066                    io::ErrorKind::IsADirectory | io::ErrorKind::NotADirectory
1067                ) =>
1068            {
1069                let _ = fs::remove_file(lock);
1070                Err(Error::RefStore(RefStoreError::NameUnavailable {
1071                    reason: name_unavailable_after_loose_rename_conflict(refname, path, common_dir),
1072                }))
1073            }
1074            Err(err) => io_with_ref_store_path(
1075                common_dir,
1076                checkout_git_dir,
1077                path,
1078                GitDirPathOperation::UpdateRef,
1079                Err(err),
1080            ),
1081        }
1082    }
1083
1084    fn persist_case_only_loose_ref(
1085        &self,
1086        rename: &super::RefCaseOnlyRename,
1087        after: &RefBatchState,
1088        ctx: CommitContext<'_>,
1089    ) -> Result<()> {
1090        let Some(value) = after.refs.get(&rename.to) else {
1091            return Err(Error::Message(format!(
1092                "case-only rename missing destination ref '{}'",
1093                rename.to
1094            )));
1095        };
1096        let (_, from_path, from_lock) = self.ref_lock_paths(&rename.from);
1097        if let Some(parent) = from_lock.parent() {
1098            fs::create_dir_all(parent).map_err(Error::Io)?;
1099        }
1100        self.acquire_ref_lock_mapped(&from_lock, &rename.from, ctx.ref_lock_wait)?;
1101        if !from_path.is_file() {
1102            self.remove_packed_record(&rename.from, ctx)?;
1103            FilesRefStore::write_loose_value(
1104                &self.common_dir,
1105                &self.git_dir,
1106                &rename.from,
1107                &from_path,
1108                &from_lock,
1109                value,
1110            )?;
1111        }
1112        let (_, to_path, _) = self.ref_lock_paths(&rename.to);
1113        if from_path != to_path {
1114            if let Some(parent) = to_path.parent() {
1115                fs::create_dir_all(parent).map_err(Error::Io)?;
1116            }
1117            fs::rename(&from_path, &to_path).map_err(Error::Io)?;
1118        }
1119        let _ = fs::remove_file(&from_lock);
1120        Ok(())
1121    }
1122
1123    /// Drop `refname`'s `packed-refs` record unless the up-front batch rewrite already did.
1124    fn remove_packed_record(&self, refname: &str, ctx: CommitContext<'_>) -> Result<()> {
1125        let route = self.route(refname);
1126        if !ctx.lock_held {
1127            return remove_packed_ref(&route.storage_dir, &route.storage_name);
1128        }
1129        if ctx.pruned.contains(refname) {
1130            return Ok(());
1131        }
1132        remove_packed_ref_under_lock(&route.storage_dir, &route.storage_name)
1133    }
1134
1135    /// Remove the `packed-refs` records of every ref `before` has and `after` drops, one
1136    /// rewrite per storage directory, ahead of any loose-file change (Git's order, so a
1137    /// deleted loose ref never uncovers a stale packed value).
1138    ///
1139    /// The returned [`PrunedPackedRecords`] covers every pruned name, the removed values the
1140    /// commit-time recheck still needs, and the records themselves for
1141    /// [`Self::restore_unprocessed_packed_records`].
1142    fn prune_deleted_packed_records(
1143        &self,
1144        before: &RefBatchState,
1145        after: &RefBatchState,
1146        updates: &[RefUpdate],
1147    ) -> Result<PrunedPackedRecords> {
1148        let checked: HashSet<&str> = updates
1149            .iter()
1150            .filter(|u| u.new_value.is_none() && !u.flags.log_only && u.expected != Expected::Any)
1151            .map(|u| u.name.as_str())
1152            .collect();
1153        let mut pruned = PrunedPackedRecords::default();
1154        let mut by_dir: BTreeMap<PathBuf, (HashSet<String>, HashMap<String, String>)> =
1155            BTreeMap::new();
1156        for name in before.refs.keys() {
1157            if after.refs.contains_key(name) {
1158                continue;
1159            }
1160            let route = self.route(name);
1161            let (storage_names, checked_names) = by_dir.entry(route.storage_dir).or_default();
1162            if checked.contains(name.as_str()) {
1163                checked_names.insert(route.storage_name.clone(), name.clone());
1164            }
1165            storage_names.insert(route.storage_name);
1166            pruned.names.insert(name.clone());
1167        }
1168        for (dir, (storage_names, checked_names)) in by_dir {
1169            let records = remove_packed_refs_under_lock(&dir, &storage_names)?;
1170            for record in &records {
1171                let Some(logical) = checked_names.get(packed_record_name(record)) else {
1172                    continue;
1173                };
1174                if let Some(oid) = packed_record_oid(record) {
1175                    pruned.removed.insert(logical.clone(), RawRef::Direct(oid));
1176                }
1177            }
1178            if !records.is_empty() {
1179                pruned.records.push((dir, records));
1180            }
1181        }
1182        self.invalidate_packed_cache_after_write();
1183        Ok(pruned)
1184    }
1185
1186    /// After a commit failed part-way, write back the pruned `packed-refs` records of refs the
1187    /// batch never got to (names not in `persisted`), so a failed batch leaves them in place.
1188    fn restore_unprocessed_packed_records(
1189        &self,
1190        pruned: &PrunedPackedRecords,
1191        persisted: &HashSet<String>,
1192    ) -> Result<()> {
1193        let mut unprocessed: HashMap<PathBuf, HashSet<String>> = HashMap::new();
1194        for name in pruned.names.difference(persisted) {
1195            let route = self.route(name);
1196            unprocessed
1197                .entry(route.storage_dir)
1198                .or_default()
1199                .insert(route.storage_name);
1200        }
1201        for (dir, records) in &pruned.records {
1202            let Some(names) = unprocessed.get(dir) else {
1203                continue;
1204            };
1205            let restore: Vec<&str> = records
1206                .iter()
1207                .map(String::as_str)
1208                .filter(|record| names.contains(packed_record_name(record)))
1209                .collect();
1210            restore_packed_records_under_lock(dir, &restore)?;
1211        }
1212        self.invalidate_packed_cache_after_write();
1213        Ok(())
1214    }
1215
1216    fn delete_loose_ref(&self, refname: &str, lock: &Path, ctx: CommitContext<'_>) -> Result<()> {
1217        let route = self.route(refname);
1218        let path = route.storage_dir.join(&route.storage_name);
1219        self.remove_packed_record(refname, ctx)?;
1220        remove_empty_ref_directory(&path);
1221        let _ = fs::remove_file(&path);
1222        let _ = fs::remove_file(lock);
1223        if let Some(parent) = path.parent() {
1224            ctx.emptied_dirs
1225                .borrow_mut()
1226                .entry(parent.to_path_buf())
1227                .or_insert(route.storage_dir);
1228        }
1229        Ok(())
1230    }
1231
1232    /// Replace a reflog file atomically (temp file in the same directory, then rename).
1233    fn persist_reflog_atomic(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
1234        let path = self.reflog_path(name);
1235        if let Some(parent) = path.parent() {
1236            self.io_ref_store(
1237                parent,
1238                GitDirPathOperation::CreateDir,
1239                fs::create_dir_all(parent),
1240            )?;
1241        }
1242        let body: String = if entries.is_empty() {
1243            String::new()
1244        } else {
1245            entries.iter().map(reflog_line).collect()
1246        };
1247        let tmp = path.with_extension("new");
1248        let mut created_tmp = false;
1249        let write_result = (|| -> Result<()> {
1250            let mut file = fs::OpenOptions::new()
1251                .write(true)
1252                .create_new(true)
1253                .open(&tmp)
1254                .map_err(Error::Io)?;
1255            created_tmp = true;
1256            use std::io::Write as _;
1257            file.write_all(body.as_bytes()).map_err(Error::Io)?;
1258            drop(file);
1259            self.io_ref_store(
1260                &path,
1261                GitDirPathOperation::AppendReflog,
1262                fs::rename(&tmp, &path),
1263            )?;
1264            created_tmp = false;
1265            Ok(())
1266        })();
1267        if write_result.is_err() && created_tmp {
1268            let _ = fs::remove_file(&tmp);
1269        }
1270        write_result
1271    }
1272
1273    /// Add `entries` to the end of `name`'s reflog, leaving the lines it has as they are
1274    /// (lines this store can't parse, and messages that are not UTF-8, included), as Git
1275    /// appends to a log. A log whose last line has no line ending gets one first.
1276    ///
1277    /// # Returns
1278    ///
1279    /// `false` when the log is gone, so nothing was written.
1280    fn append_reflog_entries(&self, name: &str, entries: &[ReflogEntry]) -> Result<bool> {
1281        use std::io::{Read, Seek, SeekFrom, Write};
1282
1283        let path = self.reflog_path(name);
1284        let mut file = match fs::OpenOptions::new().read(true).append(true).open(&path) {
1285            Ok(file) => file,
1286            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
1287            Err(e) => {
1288                return self.io_ref_store(&path, GitDirPathOperation::AppendReflog, Err(e));
1289            }
1290        };
1291        let mut body = String::new();
1292        if file.metadata()?.len() > 0 {
1293            let mut last = [0u8; 1];
1294            file.seek(SeekFrom::End(-1))?;
1295            file.read_exact(&mut last)?;
1296            if last[0] != b'\n' {
1297                body.push('\n');
1298            }
1299        }
1300        body.extend(entries.iter().map(reflog_line));
1301        self.io_ref_store(
1302            &path,
1303            GitDirPathOperation::AppendReflog,
1304            file.write_all(body.as_bytes()),
1305        )?;
1306        Ok(true)
1307    }
1308
1309    fn peel_ref_to_oid(&self, start: &str) -> Result<Option<ObjectId>> {
1310        let mut name = start.to_owned();
1311        for _ in 0..crate::refs::SYMREF_MAXDEPTH {
1312            match self.read_raw(&name)? {
1313                Some(RawRef::Direct(oid)) => return Ok(Some(oid)),
1314                Some(RawRef::Symbolic(next)) => name = next,
1315                None => return Ok(None),
1316            }
1317        }
1318        Err(Error::RefStore(RefStoreError::SymrefLoop))
1319    }
1320
1321    /// Verify `update` against disk under its locks: its expectation, and for a create that
1322    /// the name is still free. `held_lock` is `update.name`'s lock file when it is held.
1323    fn recheck_update_at_commit(
1324        &self,
1325        update: &RefUpdate,
1326        batch_extras: &BTreeSet<&str>,
1327        batch_start: &RefBatchState,
1328        state_before: &RefBatchState,
1329        held_lock: Option<&Path>,
1330        ctx: CommitContext<'_>,
1331    ) -> StoreResult<()> {
1332        if update.flags.log_only {
1333            return Ok(());
1334        }
1335        let batch_differs =
1336            batch_start.refs.get(&update.name) != state_before.refs.get(&update.name);
1337        if !super::apply::expected_matches_for_update(state_before, update) {
1338            return Err(RefStoreError::ExpectedMismatch {
1339                name: update.name.clone(),
1340                expected: update.expected.clone(),
1341                actual: state_before.refs.get(&update.name).cloned(),
1342            });
1343        }
1344        if !batch_differs && update.new_value.is_none() && update.expected == Expected::Any {
1345            return Ok(());
1346        }
1347        let actual = if batch_differs {
1348            state_before.refs.get(&update.name).cloned()
1349        } else {
1350            self.read_raw(&update.name)
1351                .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1352                .or_else(|| ctx.removed.get(&update.name).cloned())
1353        };
1354        let actual_ref = actual.as_ref();
1355        if !batch_differs {
1356            let disk_ok = match &update.expected {
1357                Expected::Oid(want)
1358                    if matches!(actual_ref, Some(RawRef::Symbolic(_)))
1359                        && matches!(update.new_value.as_ref(), Some(RawRef::Direct(_)))
1360                        && !update.flags.no_deref =>
1361                {
1362                    self.peel_ref_to_oid(&update.name)
1363                        .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1364                        .is_some_and(|got| got == *want)
1365                }
1366                _ => expected_matches(actual_ref, &update.expected),
1367            };
1368            if !disk_ok {
1369                return Err(RefStoreError::ExpectedMismatch {
1370                    name: update.name.clone(),
1371                    expected: update.expected.clone(),
1372                    actual,
1373                });
1374            }
1375        }
1376        if update.new_value.is_some() && actual_ref.is_none() && !batch_differs {
1377            self.verify_create_available(&update.name, batch_extras, held_lock)?;
1378        }
1379        Ok(())
1380    }
1381
1382    /// Store `value` as `name` (`None` deletes it).
1383    fn persist_one_ref_change(
1384        &self,
1385        name: &str,
1386        value: Option<&RawRef>,
1387        lock: &Path,
1388        ctx: CommitContext<'_>,
1389    ) -> Result<()> {
1390        let (_, path, _) = self.ref_lock_paths(name);
1391        match value {
1392            None => self.delete_loose_ref(name, lock, ctx),
1393            Some(value) => FilesRefStore::write_loose_value(
1394                &self.common_dir,
1395                &self.git_dir,
1396                name,
1397                &path,
1398                lock,
1399                value,
1400            ),
1401        }
1402    }
1403
1404    fn persist_one_ref_name(
1405        &self,
1406        name: &str,
1407        value: Option<&RawRef>,
1408        ctx: CommitContext<'_>,
1409        persisted_refs: &mut HashSet<String>,
1410    ) -> Result<()> {
1411        let (_, _, lock_path) = self.ref_lock_paths(name);
1412        self.lock_ref(name, &lock_path, ctx.ref_lock_wait)?;
1413        let write_result = self.persist_one_ref_change(name, value, &lock_path, ctx);
1414        let _ = fs::remove_file(&lock_path);
1415        write_result?;
1416        persisted_refs.insert(name.to_owned());
1417        Ok(())
1418    }
1419
1420    /// Lock every ref `update` writes (and `update.name` when its expectation must be
1421    /// verified), recheck the expectation against disk under those locks, then write the refs.
1422    ///
1423    /// `running` is the batch state before `update`; `touched` is the one ref the update
1424    /// stores [`RefUpdate::new_value`] into (see [`ref_change_target`]).
1425    ///
1426    /// Holding the write targets' locks across the recheck matters for symbolic refs too: an
1427    /// update to `HEAD` that dereferences to `refs/heads/main` must exclude writers of `main`,
1428    /// not only of `HEAD`. Each successful write consumes its lock; anything left is released.
1429    #[allow(clippy::too_many_arguments)]
1430    fn persist_update_locked(
1431        &self,
1432        update: &RefUpdate,
1433        batch_extras: &BTreeSet<&str>,
1434        before: &RefBatchState,
1435        running: &RefBatchState,
1436        touched: Option<&str>,
1437        ctx: CommitContext<'_>,
1438        persisted_refs: &mut HashSet<String>,
1439        writes_ref: bool,
1440        carried: &mut Option<(String, PathBuf)>,
1441    ) -> Result<()> {
1442        let value_after = |name: &str| {
1443            if touched == Some(name) {
1444                update.new_value.as_ref()
1445            } else {
1446                running.refs.get(name)
1447            }
1448        };
1449        let mut targets: Vec<String> = touched
1450            .filter(|t| *t != update.name && running.refs.get(*t) != update.new_value.as_ref())
1451            .map(str::to_owned)
1452            .into_iter()
1453            .collect();
1454        if writes_ref {
1455            targets.push(update.name.clone());
1456        }
1457
1458        let mut held: BTreeMap<String, PathBuf> = BTreeMap::new();
1459        let release = |held: &BTreeMap<String, PathBuf>| {
1460            for lock in held.values() {
1461                let _ = fs::remove_file(lock);
1462            }
1463        };
1464        let verify_name = (!matches!(update.expected, Expected::Any)).then_some(&update.name);
1465        for name in targets.iter().chain(verify_name) {
1466            if held.contains_key(name) {
1467                continue;
1468            }
1469            if let Some((_, lock)) = carried.take_if(|(carried_name, _)| carried_name == name) {
1470                held.insert(name.clone(), lock);
1471                continue;
1472            }
1473            let (_, _, lock_path) = self.ref_lock_paths(name);
1474            if let Err(err) = self.lock_ref(name, &lock_path, ctx.ref_lock_wait) {
1475                release(&held);
1476                return Err(err);
1477            }
1478            held.insert(name.clone(), lock_path);
1479        }
1480
1481        let held_lock = held.get(&update.name).map(PathBuf::as_path);
1482        if let Err(err) =
1483            self.recheck_update_at_commit(update, batch_extras, before, running, held_lock, ctx)
1484        {
1485            release(&held);
1486            return Err(err.into());
1487        }
1488        #[cfg(test)]
1489        tests::run_after_recheck_hook();
1490
1491        for name in &targets {
1492            let Some(lock_path) = held.remove(name) else {
1493                continue;
1494            };
1495            if let Err(err) = self.persist_one_ref_change(name, value_after(name), &lock_path, ctx)
1496            {
1497                let _ = fs::remove_file(&lock_path);
1498                release(&held);
1499                return Err(err);
1500            }
1501            persisted_refs.insert(name.clone());
1502        }
1503        release(&held);
1504        Ok(())
1505    }
1506
1507    fn persist_state_diff(
1508        &self,
1509        batch: &PreparedFilesBatch,
1510        carried: &mut Option<(String, PathBuf)>,
1511        emptied_dirs: &RefCell<BTreeMap<PathBuf, PathBuf>>,
1512    ) -> Result<()> {
1513        let before = &batch.before_commit;
1514        let after = &batch.after_commit;
1515        let updates = batch.updates.as_slice();
1516        let packed_lock_held = batch.packed_lock.is_some();
1517        let batch_extras: BTreeSet<&str> = updates.iter().map(|u| u.name.as_str()).collect();
1518        if updates.iter().any(|u| {
1519            u.new_value.is_some() && !u.flags.log_only && !before.refs.contains_key(&u.name)
1520        }) {
1521            self.forget_packed_cache();
1522        }
1523
1524        let pruned = if packed_lock_held {
1525            self.prune_deleted_packed_records(before, after, updates)?
1526        } else {
1527            PrunedPackedRecords::default()
1528        };
1529        let ctx = CommitContext {
1530            ref_lock_wait: batch.ref_lock_wait,
1531            lock_held: packed_lock_held,
1532            pruned: &pruned.names,
1533            removed: &pruned.removed,
1534            emptied_dirs,
1535        };
1536
1537        let names: BTreeSet<&String> = before.reflogs.keys().chain(after.reflogs.keys()).collect();
1538        for name in names {
1539            let (old, new) = (before.reflogs.get(name), after.reflogs.get(name));
1540            if old == new {
1541                continue;
1542            }
1543            let under_carried = carried
1544                .as_ref()
1545                .is_some_and(|(carried_name, _)| carried_name == name);
1546            let mut extra_lock = None;
1547            if !under_carried {
1548                let (_, _, lock_path) = self.ref_lock_paths(name);
1549                self.lock_ref(name, &lock_path, ctx.ref_lock_wait)?;
1550                extra_lock = Some(lock_path);
1551            }
1552            let write_result = (|| -> Result<()> {
1553                if let (Some(old), Some(new)) = (old, new) {
1554                    if new.starts_with(old)
1555                        && self.append_reflog_entries(name, &new[old.len()..])?
1556                    {
1557                        return Ok(());
1558                    }
1559                }
1560                self.persist_reflog_atomic(name, new.map(Vec::as_slice).unwrap_or_default())
1561            })();
1562            if let Some(lock_path) = extra_lock {
1563                let _ = fs::remove_file(&lock_path);
1564            }
1565            write_result?;
1566        }
1567
1568        let mut persisted_refs = HashSet::new();
1569        let persisted =
1570            self.persist_batch_refs(batch, &batch_extras, ctx, &mut persisted_refs, carried);
1571        if persisted.is_err() {
1572            // Keep the error that stopped the batch; a failed restore cannot be reported
1573            // alongside it.
1574            let _ = self.restore_unprocessed_packed_records(&pruned, &persisted_refs);
1575        }
1576        persisted?;
1577        self.invalidate_packed_cache_after_write();
1578        Ok(())
1579    }
1580
1581    /// Write every ref change of `batch` (case-only renames, then each update under its locks,
1582    /// then anything the updates left out), recording each written name in `persisted_refs`.
1583    fn persist_batch_refs(
1584        &self,
1585        batch: &PreparedFilesBatch,
1586        batch_extras: &BTreeSet<&str>,
1587        ctx: CommitContext<'_>,
1588        persisted_refs: &mut HashSet<String>,
1589        carried: &mut Option<(String, PathBuf)>,
1590    ) -> Result<()> {
1591        let before = &batch.before_commit;
1592        let after = &batch.after_commit;
1593        let updates = batch.updates.as_slice();
1594        for rename in &batch.case_only_renames {
1595            self.persist_case_only_loose_ref(rename, after, ctx)?;
1596            persisted_refs.insert(rename.from.clone());
1597            persisted_refs.insert(rename.to.clone());
1598        }
1599        let mut running = before.clone();
1600        for (idx, update) in updates.iter().enumerate() {
1601            if update.flags.log_only {
1602                apply_update(&mut running, update).map_err(Error::from)?;
1603                continue;
1604            }
1605            let touched = ref_change_target(&running, update).map_err(Error::from)?;
1606            let name = &update.name;
1607            let b = running.refs.get(name);
1608            let a = if touched.as_deref() == Some(name.as_str()) {
1609                update.new_value.as_ref()
1610            } else {
1611                b
1612            };
1613            let materialize_loose = update.new_value.is_some()
1614                && a.is_some()
1615                && b == a
1616                && !self
1617                    .loose_ref_file_exists(name)
1618                    .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1619            let writes_ref = b != a || materialize_loose;
1620
1621            self.persist_update_locked(
1622                update,
1623                batch_extras,
1624                before,
1625                &running,
1626                touched.as_deref(),
1627                ctx,
1628                persisted_refs,
1629                writes_ref,
1630                carried,
1631            )?;
1632
1633            apply_update(&mut running, update).map_err(Error::from)?;
1634            crate::refs::test_ref_store_batch_pause_after_ref_write(idx, &self.common_dir);
1635        }
1636
1637        for name in before
1638            .refs
1639            .keys()
1640            .chain(after.refs.keys())
1641            .collect::<BTreeSet<_>>()
1642        {
1643            if persisted_refs.contains(name) {
1644                continue;
1645            }
1646            let b = before.refs.get(name);
1647            let a = after.refs.get(name);
1648            if b == a {
1649                continue;
1650            }
1651            self.persist_one_ref_name(name, a, ctx, persisted_refs)?;
1652        }
1653        Ok(())
1654    }
1655}
1656
1657impl FilesRefStore {
1658    /// [`RefStore::for_each_ref`], skipping the `packed-refs` records `keep` rejects.
1659    ///
1660    /// `keep` sees each packed record's storage name, object id and [`PackedPeel`] before
1661    /// anything is allocated for it. A loose ref is listed whatever `keep` says about a packed
1662    /// record of the same name.
1663    pub(crate) fn for_each_ref_filtered(
1664        &self,
1665        prefix: &str,
1666        keep: Option<PackedRecordFilter<'_>>,
1667        f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1668    ) -> Result<()> {
1669        let prefix = prefix.trim_end_matches('/');
1670        let mut map = self.load_raw_map_prefix(prefix, keep)?;
1671        for (name, value) in map.split_off(prefix) {
1672            if !name.starts_with(prefix) {
1673                break;
1674            }
1675            let peeled = match &value {
1676                RawRef::Direct(_) => None,
1677                RawRef::Symbolic(target) => self.resolve(target).ok(),
1678            };
1679            let entry = RefEntry {
1680                name,
1681                value,
1682                peeled,
1683            };
1684            if f(&entry).is_break() {
1685                break;
1686            }
1687        }
1688        Ok(())
1689    }
1690}
1691
1692impl RefStore for FilesRefStore {
1693    fn format(&self) -> RefStorageFormat {
1694        RefStorageFormat::Files
1695    }
1696
1697    fn read_raw(&self, name: &str) -> Result<Option<RawRef>> {
1698        self.read_raw_at_route(&self.route(name))
1699    }
1700
1701    fn for_each_ref(
1702        &self,
1703        prefix: &str,
1704        f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1705    ) -> Result<()> {
1706        self.for_each_ref_filtered(prefix, None, f)
1707    }
1708
1709    fn prepare(
1710        &self,
1711        txn: RefTransaction,
1712    ) -> StoreResult<Box<dyn PreparedRefTransaction + Send + '_>> {
1713        txn.validate()?;
1714
1715        if txn.is_initial() {
1716            if let Some(initial_packed_records) = self.initial_packed_records(&txn)? {
1717                let packed_lock = self.acquire_packed_lock()?;
1718                return Ok(Box::new(FilesPrepared {
1719                    store: self,
1720                    committed: false,
1721                    batch: Some(PreparedFilesBatch {
1722                        updates: Vec::new(),
1723                        case_only_renames: Vec::new(),
1724                        ref_locks: HashMap::new(),
1725                        packed_lock: Some(packed_lock),
1726                        ref_lock_wait: LockWait::Immediately,
1727                        before_commit: RefBatchState::default(),
1728                        after_commit: RefBatchState::default(),
1729                        initial_packed_records,
1730                    }),
1731                    rejections: Vec::new(),
1732                }));
1733            }
1734        }
1735
1736        #[cfg(windows)]
1737        self.reject_win32_unstorable_ref_paths(&txn)?;
1738
1739        let allow_failures = txn.allows_failures();
1740        let (updates, reflog_renames, case_only_renames) = txn.into_parts();
1741        let mut rejections = Vec::new();
1742
1743        let mut preliminary = self
1744            .load_batch_state_for_prepare(&updates, &reflog_renames, &case_only_renames)
1745            .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1746        self.insert_create_conflict_neighbors(&mut preliminary.refs, &updates)
1747            .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1748        let updates = if allow_failures {
1749            let (accepted, part) = partition_updates_allowing_failures(&preliminary, updates)?;
1750            rejections.extend(part);
1751            accepted
1752        } else {
1753            verify_create_conflicts(&preliminary.refs, &updates)?;
1754            updates
1755        };
1756        for rename in &reflog_renames {
1757            self.ensure_reflog_path_writable(&rename.to)?;
1758        }
1759        for rename in &case_only_renames {
1760            self.ensure_reflog_path_writable(&rename.to)?;
1761        }
1762        let lock_names = ref_lock_names_for_updates(&preliminary, &updates);
1763        let ref_lock_wait = LockWait::files_ref_lock_for_git_dir(&self.common_dir);
1764
1765        let carried = updates
1766            .iter()
1767            .find(|u| !u.flags.log_only)
1768            .map(|u| u.name.as_str());
1769        let mut ref_locks: HashMap<String, PathBuf> = HashMap::new();
1770        self.take_prepare_locks(&lock_names, carried, ref_lock_wait, &mut ref_locks)?;
1771
1772        if let Err(err) =
1773            simulate_batch_apply(&preliminary, &updates, &reflog_renames, &case_only_renames)
1774        {
1775            for acquired in ref_locks.values() {
1776                let _ = fs::remove_file(acquired);
1777            }
1778            return Err(err);
1779        }
1780
1781        let extras: BTreeSet<&str> = updates.iter().map(|u| u.name.as_str()).collect();
1782        let packed = self.packed_refs_view().ok().flatten();
1783        let mut ancestors = HashMap::new();
1784        let mut available_updates = Vec::with_capacity(updates.len());
1785        for update in &updates {
1786            if update.new_value.is_none() || update.flags.log_only {
1787                available_updates.push(update.clone());
1788                continue;
1789            }
1790            if preliminary.refs.contains_key(&update.name) {
1791                available_updates.push(update.clone());
1792                continue;
1793            }
1794            if let Err(reason) = self.verify_create_available_in(
1795                &update.name,
1796                &extras,
1797                ref_locks.get(&update.name).map(PathBuf::as_path),
1798                packed.as_ref(),
1799                &mut ancestors,
1800            ) {
1801                let err: RefStoreError = reason.into();
1802                if allow_failures && is_per_ref_rejectable(&err) {
1803                    rejections.push((update.name.clone(), err));
1804                    continue;
1805                }
1806                for acquired in ref_locks.values() {
1807                    let _ = fs::remove_file(acquired);
1808                }
1809                return Err(err);
1810            }
1811            available_updates.push(update.clone());
1812        }
1813        let updates = available_updates;
1814
1815        let before_commit = preliminary;
1816        let mut after_commit = before_commit.clone();
1817        for update in &updates {
1818            apply_update(&mut after_commit, update)?;
1819        }
1820        for rename in &case_only_renames {
1821            apply_case_only_ref_rename(&mut after_commit, rename)?;
1822        }
1823        for rename in &reflog_renames {
1824            apply_reflog_rename(&mut after_commit, rename)?;
1825        }
1826        let mut changed = changed_ref_names(&before_commit.refs, &after_commit.refs);
1827        for update in &updates {
1828            if update.flags.log_only || update.new_value.is_none() {
1829                continue;
1830            }
1831            if before_commit.refs.contains_key(&update.name)
1832                && !self
1833                    .loose_ref_file_exists(&update.name)
1834                    .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1835            {
1836                changed.insert(&update.name);
1837            }
1838        }
1839        // Commit writes reflogs before refs, so a directory where a new log goes must stop
1840        // the batch here, not after some of it is written.
1841        for name in after_commit.reflogs.keys() {
1842            if before_commit.reflogs.contains_key(name) {
1843                continue;
1844            }
1845            if let Err(err) = self.ensure_reflog_path_writable(name) {
1846                for acquired in ref_locks.values() {
1847                    let _ = fs::remove_file(acquired);
1848                }
1849                return Err(err);
1850            }
1851        }
1852
1853        self.take_prepare_locks(changed, carried, ref_lock_wait, &mut ref_locks)?;
1854
1855        let mut packed_lock = None;
1856        if self.needs_packed_lock(&updates) {
1857            match self.acquire_packed_lock() {
1858                Ok(lock) => packed_lock = Some(lock),
1859                Err(err) => {
1860                    for acquired in ref_locks.values() {
1861                        let _ = fs::remove_file(acquired);
1862                    }
1863                    return Err(err);
1864                }
1865            }
1866        }
1867
1868        Ok(Box::new(FilesPrepared {
1869            store: self,
1870            committed: false,
1871            rejections: rejections.clone(),
1872            batch: Some(PreparedFilesBatch {
1873                updates,
1874                case_only_renames,
1875                ref_locks,
1876                packed_lock,
1877                ref_lock_wait,
1878                before_commit,
1879                after_commit,
1880                initial_packed_records: Vec::new(),
1881            }),
1882        }))
1883    }
1884
1885    fn reflog_exists(&self, name: &str) -> Result<bool> {
1886        Ok(self.reflog_path(name).is_file())
1887    }
1888
1889    fn for_each_reflog_entry(
1890        &self,
1891        name: &str,
1892        reverse: bool,
1893        f: &mut dyn FnMut(&ReflogEntry) -> ControlFlow<()>,
1894    ) -> Result<()> {
1895        let path = self.reflog_path(name);
1896        if reverse {
1897            // Newest-first readers usually stop early, so read the log backwards in chunks.
1898            let Some(mut reader) = reflog::ReverseReflogLineReader::open(&path)? else {
1899                return Ok(());
1900            };
1901            while let Some(line) = reader.next_nonempty_line()? {
1902                let Some(entry) = reflog::parse_reflog_line_for_store(&utf8_or_lossy(&line)) else {
1903                    continue;
1904                };
1905                if f(&entry).is_break() {
1906                    break;
1907                }
1908            }
1909            return Ok(());
1910        }
1911        for entry in &read_reflog_at_path(&path)? {
1912            if f(entry).is_break() {
1913                break;
1914            }
1915        }
1916        Ok(())
1917    }
1918
1919    fn create_reflog(&self, name: &str) -> Result<()> {
1920        validate_storable_refname(name).map_err(|e| Error::Message(e.to_string()))?;
1921        let path = self.reflog_path(name);
1922        if let Some(parent) = path.parent() {
1923            self.io_ref_store(
1924                parent,
1925                GitDirPathOperation::CreateDir,
1926                fs::create_dir_all(parent),
1927            )?;
1928        }
1929        self.io_ref_store(
1930            &path,
1931            GitDirPathOperation::AppendReflog,
1932            fs::OpenOptions::new().create(true).append(true).open(&path),
1933        )?;
1934        Ok(())
1935    }
1936
1937    fn delete_reflog(&self, name: &str) -> Result<()> {
1938        let route = self.route(name);
1939        let logs_root = route.storage_dir.join("logs");
1940        let path = logs_root.join(&route.storage_name);
1941        let _ = fs::remove_file(&path);
1942        // Empty parents left under `logs/` would block a later ref whose reflog needs that
1943        // path as a file (deleting `a/b`, then creating `a`).
1944        let mut parent = path.parent();
1945        while let Some(dir) = parent.filter(|d| *d != logs_root && d.starts_with(&logs_root)) {
1946            if fs::remove_dir(dir).is_err() {
1947                break;
1948            }
1949            parent = dir.parent();
1950        }
1951        Ok(())
1952    }
1953
1954    fn replace_reflog(&self, name: &str, entries: Vec<ReflogEntry>) -> Result<()> {
1955        self.rewrite_reflog(name, &mut |_| Ok(entries.clone()))
1956    }
1957
1958    fn rewrite_reflog(
1959        &self,
1960        name: &str,
1961        transform: &mut dyn FnMut(Vec<ReflogEntry>) -> Result<Vec<ReflogEntry>>,
1962    ) -> Result<()> {
1963        let wait = LockWait::files_ref_lock_for_git_dir(&self.common_dir);
1964        let (_, _, lock_path) = self.ref_lock_paths(name);
1965        self.lock_ref(name, &lock_path, wait)?;
1966        let result = (|| -> Result<()> {
1967            let entries = self.read_reflog_entries(name)?;
1968            let rewritten = transform(entries)?;
1969            self.persist_reflog_atomic(name, &rewritten)
1970        })();
1971        let _ = fs::remove_file(&lock_path);
1972        result
1973    }
1974
1975    fn for_each_reflog_ref(&self, f: &mut dyn FnMut(&str) -> ControlFlow<()>) -> Result<()> {
1976        let mut names = BTreeSet::new();
1977        for base in self.store_bases() {
1978            let logs = base.join("logs");
1979            if logs.is_dir() {
1980                collect_reflog_ref_names(&logs, "", self, &mut names)?;
1981            }
1982        }
1983        for name in names {
1984            if f(&name).is_break() {
1985                break;
1986            }
1987        }
1988        Ok(())
1989    }
1990
1991    fn as_files(&self) -> Option<&FilesRefStore> {
1992        Some(self)
1993    }
1994}
1995
1996struct FilesPrepared<'a> {
1997    store: &'a FilesRefStore,
1998    committed: bool,
1999    batch: Option<PreparedFilesBatch>,
2000    rejections: Vec<(String, RefStoreError)>,
2001}
2002
2003impl PreparedRefTransaction for FilesPrepared<'_> {
2004    fn rejected_updates(&self) -> &[(String, RefStoreError)] {
2005        &self.rejections
2006    }
2007
2008    fn commit(mut self: Box<Self>) -> StoreResult<()> {
2009        let Some(mut batch) = self.batch.take() else {
2010            return Ok(());
2011        };
2012
2013        if !batch.initial_packed_records.is_empty() {
2014            let records: Vec<&str> = batch
2015                .initial_packed_records
2016                .iter()
2017                .map(String::as_str)
2018                .collect();
2019            let written = restore_packed_records_under_lock(&self.store.common_dir, &records);
2020            FilesRefStore::release_locks(&batch);
2021            self.store.invalidate_packed_cache_after_write();
2022            written.map_err(FilesRefStore::ref_store_error_from_lib)?;
2023            self.committed = true;
2024            return Ok(());
2025        }
2026
2027        // The first update is written before any other ref, so it keeps the lock it took in
2028        // `prepare` instead of releasing and re-creating it. Any other lock `prepare` took (only
2029        // ones another writer held) is released here; each ref is locked again when its update
2030        // is written, so other writers can touch later refs mid-batch.
2031        let mut ref_locks = std::mem::take(&mut batch.ref_locks);
2032        let mut carried = batch
2033            .updates
2034            .iter()
2035            .find(|u| !u.flags.log_only)
2036            .and_then(|u| ref_locks.remove_entry(&u.name));
2037        for lock in ref_locks.values() {
2038            let _ = fs::remove_file(lock);
2039        }
2040        // The packed cache revalidates against the file's size and mtime on every read, so
2041        // rechecks under the ref locks see concurrent rewrites without a forced re-parse.
2042        let emptied_dirs = RefCell::new(BTreeMap::new());
2043        let persisted = self
2044            .store
2045            .persist_state_diff(&batch, &mut carried, &emptied_dirs);
2046        if let Some((_, lock)) = carried {
2047            let _ = fs::remove_file(lock);
2048        }
2049        // Pruning runs after every lock of the batch is gone: a leftover lock file would make
2050        // its directory look non-empty (issue #1317). Deepest first, so a parent is only
2051        // checked once its emptied children are removed.
2052        let mut emptied_dirs: Vec<_> = emptied_dirs.into_inner().into_iter().collect();
2053        emptied_dirs.sort_by_key(|(dir, _)| std::cmp::Reverse(dir.components().count()));
2054        for (dir, storage_dir) in &emptied_dirs {
2055            prune_empty_ref_dirs(storage_dir, dir);
2056        }
2057        self.store.invalidate_packed_cache_after_write();
2058        if let Err(err) = persisted {
2059            if let Some(p) = &batch.packed_lock {
2060                let _ = fs::remove_file(p);
2061            }
2062            return Err(match err {
2063                Error::RefStore(store_err) => store_err,
2064                other => RefStoreError::Corrupt(other.to_string()),
2065            });
2066        }
2067        if let Some(p) = &batch.packed_lock {
2068            let _ = fs::remove_file(p);
2069        }
2070        self.committed = true;
2071        Ok(())
2072    }
2073
2074    fn abort(mut self: Box<Self>) -> StoreResult<()> {
2075        if let Some(batch) = self.batch.take() {
2076            FilesRefStore::release_locks(&batch);
2077        }
2078        self.store.invalidate_packed_cache_after_write();
2079        self.committed = true;
2080        Ok(())
2081    }
2082}
2083
2084impl Drop for FilesPrepared<'_> {
2085    fn drop(&mut self) {
2086        if self.committed {
2087            return;
2088        }
2089        if let Some(batch) = self.batch.take() {
2090            FilesRefStore::release_locks(&batch);
2091        }
2092        self.store.invalidate_packed_cache_after_write();
2093    }
2094}
2095
2096fn loose_storage_path_to_refname(storage_root: &Path, loose: &Path) -> String {
2097    loose
2098        .strip_prefix(storage_root)
2099        .map(|rel| rel.to_string_lossy().replace('\\', "/"))
2100        .unwrap_or_else(|_| loose.to_string_lossy().into_owned())
2101}
2102
2103fn first_loose_ref_file_under(dir: &Path) -> Option<PathBuf> {
2104    let entries = fs::read_dir(dir).ok()?;
2105    for entry in entries.flatten() {
2106        let path = entry.path();
2107        let file_type = entry.file_type().ok()?;
2108        if file_type.is_dir() {
2109            if let Some(found) = first_loose_ref_file_under(&path) {
2110                return Some(found);
2111            }
2112        } else if file_type.is_file() && path.extension().is_none_or(|e| e != "lock") {
2113            return Some(path);
2114        }
2115    }
2116    None
2117}
2118
2119fn name_unavailable_after_loose_rename_conflict(
2120    refname: &str,
2121    ref_path: &Path,
2122    storage_root: &Path,
2123) -> RefnameUnavailable {
2124    if ref_path.is_dir() {
2125        if let Some(descendant) = first_loose_ref_file_under(ref_path) {
2126            return RefnameUnavailable::DescendantExists {
2127                blocking: loose_storage_path_to_refname(storage_root, &descendant),
2128                new_ref: refname.to_owned(),
2129            };
2130        }
2131    }
2132    if let Some(parent) = ref_path.parent() {
2133        if parent.is_file() {
2134            return RefnameUnavailable::AncestorExists {
2135                blocking: loose_storage_path_to_refname(storage_root, parent),
2136                new_ref: refname.to_owned(),
2137            };
2138        }
2139    }
2140    RefnameUnavailable::DescendantExists {
2141        blocking: format!("{refname}/…"),
2142        new_ref: refname.to_owned(),
2143    }
2144}
2145
2146fn head_matches_prefix(logical_prefix: &str) -> bool {
2147    let prefix = logical_prefix.trim_end_matches('/');
2148    if prefix.is_empty() {
2149        // `git for-each-ref` with no pattern lists refs under `refs/`, not `HEAD`.
2150        return false;
2151    }
2152    "HEAD".starts_with(prefix) || prefix.starts_with("HEAD")
2153}
2154
2155fn ref_to_raw(r: Ref) -> RawRef {
2156    match r {
2157        Ref::Direct(oid) => RawRef::Direct(oid),
2158        Ref::Symbolic(s) => RawRef::Symbolic(s),
2159    }
2160}
2161
2162/// Read every loose ref file below `dir` into `out`, keyed by `prefix` plus its relative path.
2163///
2164/// Only names starting with `want` are read; entries that cannot lead to one are skipped
2165/// without a `stat`, so a partial-name prefix such as `refs/tags/v1` does not read every
2166/// file in `refs/tags/`. An empty `want` reads everything.
2167fn collect_loose_raw_refs(
2168    dir: &Path,
2169    prefix: &str,
2170    want: &str,
2171    out: &mut BTreeMap<String, RawRef>,
2172) -> Result<()> {
2173    let read = match fs::read_dir(dir) {
2174        Ok(r) => r,
2175        Err(e)
2176            if e.kind() == io::ErrorKind::NotFound || e.kind() == io::ErrorKind::NotADirectory =>
2177        {
2178            return Ok(());
2179        }
2180        Err(e) => return Err(Error::Io(e)),
2181    };
2182    for entry in read {
2183        let entry = entry?;
2184        let name_owned = entry.file_name().into_string().map_err(|_| {
2185            Error::Io(io::Error::new(
2186                io::ErrorKind::InvalidData,
2187                "non-utf8 ref path component",
2188            ))
2189        })?;
2190        let refname = format!("{prefix}{name_owned}");
2191        let wanted = refname.starts_with(want);
2192        let leads_to_wanted = || {
2193            want.strip_prefix(refname.as_str())
2194                .is_some_and(|rest| rest.starts_with('/'))
2195        };
2196        if !wanted && !leads_to_wanted() {
2197            continue;
2198        }
2199        let path = entry.path();
2200        // The directory entry's type saves a `stat` and a `readlink` per ref; symlinks and
2201        // unknown types still go through `stat`, which follows symlinks.
2202        let file_type = entry.file_type().ok();
2203        if file_type.is_some_and(|t| t.is_file()) {
2204            if wanted {
2205                if let Ok(r) = crate::refs::read_regular_ref_file(&path) {
2206                    out.insert(refname, ref_to_raw(r));
2207                }
2208            }
2209            continue;
2210        }
2211        if file_type.is_some_and(|t| t.is_dir()) {
2212            collect_loose_raw_refs(&path, &format!("{refname}/"), want, out)?;
2213            continue;
2214        }
2215        let meta = match fs::metadata(&path) {
2216            Ok(m) => m,
2217            // Concurrent delete or an unreadable entry (e.g. dangling symlink): skip it.
2218            Err(_) => continue,
2219        };
2220        if meta.is_dir() {
2221            collect_loose_raw_refs(&path, &format!("{refname}/"), want, out)?;
2222        } else if meta.is_file() && wanted {
2223            if let Ok(r) = read_ref_file(&path) {
2224                out.insert(refname, ref_to_raw(r));
2225            }
2226        }
2227    }
2228    Ok(())
2229}
2230
2231/// Add `<prefix>/<relative path>` for every file below `dir` to `out`; a missing or unreadable
2232/// directory adds nothing.
2233fn collect_loose_file_names(dir: &Path, prefix: &str, out: &mut BTreeSet<String>) {
2234    let Ok(read) = fs::read_dir(dir) else {
2235        return;
2236    };
2237    for entry in read.flatten() {
2238        let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2239        let path = entry.path();
2240        match fs::metadata(&path) {
2241            Ok(meta) if meta.is_dir() => collect_loose_file_names(&path, &name, out),
2242            Ok(meta) if meta.is_file() => {
2243                out.insert(name);
2244            }
2245            _ => {}
2246        }
2247    }
2248}
2249
2250#[cfg(unix)]
2251fn file_identity(meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
2252    use std::os::unix::fs::MetadataExt;
2253    Some((meta.dev(), meta.ino(), meta.ctime(), meta.ctime_nsec()))
2254}
2255
2256#[cfg(not(unix))]
2257fn file_identity(_meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
2258    None
2259}
2260
2261/// The line `entry` is written as in a reflog file.
2262fn reflog_line(entry: &ReflogEntry) -> String {
2263    let (old_hex, new_hex) = crate::refs::reflog_oid_hex_pair(&entry.old_oid, &entry.new_oid);
2264    if entry.message.is_empty() {
2265        format!("{old_hex} {new_hex} {}\n", entry.identity)
2266    } else {
2267        format!(
2268            "{old_hex} {new_hex} {}\t{}\n",
2269            entry.identity, entry.message
2270        )
2271    }
2272}
2273
2274/// The entries of the reflog file at `path` (none when it is missing). A message that is
2275/// not UTF-8, which Git writes for a commit subject in a legacy encoding, is read with
2276/// replacement characters rather than failing the read.
2277fn read_reflog_at_path(path: &Path) -> Result<Vec<ReflogEntry>> {
2278    let content = match fs::read(path) {
2279        Ok(bytes) => bytes,
2280        // A ref being created can collide with a file or directory in `logs/`; that is a
2281        // missing log here, and the ref conflict check reports the collision.
2282        Err(e) if crate::refs::io_error_is_missing_file(&e, path) => return Ok(Vec::new()),
2283        Err(e) => return Err(Error::Io(e)),
2284    };
2285    let text = utf8_or_lossy(&content);
2286    Ok(text
2287        .lines()
2288        .filter_map(reflog::parse_reflog_line_for_store)
2289        .collect())
2290}
2291
2292/// `bytes` as text, validating on the fast path and replacing invalid sequences only when
2293/// there are any (`String::from_utf8_lossy` alone is markedly slower on large logs).
2294fn utf8_or_lossy(bytes: &[u8]) -> Cow<'_, str> {
2295    match std::str::from_utf8(bytes) {
2296        Ok(text) => Cow::Borrowed(text),
2297        Err(_) => String::from_utf8_lossy(bytes),
2298    }
2299}
2300
2301fn changed_ref_names<'a>(
2302    before: &'a BTreeMap<String, RawRef>,
2303    after: &'a BTreeMap<String, RawRef>,
2304) -> BTreeSet<&'a String> {
2305    before
2306        .keys()
2307        .chain(after.keys())
2308        .filter(|name| before.get(*name) != after.get(*name))
2309        .collect()
2310}
2311
2312fn collect_reflog_ref_names(
2313    dir: &Path,
2314    prefix: &str,
2315    store: &FilesRefStore,
2316    out: &mut BTreeSet<String>,
2317) -> Result<()> {
2318    let read = match fs::read_dir(dir) {
2319        Ok(r) => r,
2320        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
2321        Err(e) => return Err(Error::Io(e)),
2322    };
2323    for entry in read {
2324        let entry = entry?;
2325        let name = entry.file_name().to_string_lossy().into_owned();
2326        let refname = format!("{prefix}{name}");
2327        let path = entry.path();
2328        if path.is_dir() {
2329            collect_reflog_ref_names(&path, &format!("{refname}/"), store, out)?;
2330        } else if path.is_file() {
2331            out.insert(store.logical_name(&refname));
2332        }
2333    }
2334    Ok(())
2335}
2336
2337#[cfg(test)]
2338mod tests {
2339    use std::cell::RefCell;
2340
2341    use super::*;
2342    use crate::refs::store::RefUpdateFlags;
2343
2344    // hygiene: test-only commit interleaving hook
2345    thread_local! {
2346        static AFTER_RECHECK: RefCell<Option<Box<dyn FnOnce()>>> = const { RefCell::new(None) };
2347    }
2348
2349    /// Runs (once) the hook installed by [`after_next_recheck`] on this thread.
2350    pub(super) fn run_after_recheck_hook() {
2351        if let Some(hook) = AFTER_RECHECK.with(|slot| slot.borrow_mut().take()) {
2352            hook();
2353        }
2354    }
2355
2356    /// Run `hook` inside the next commit on this thread, right after an update passed its
2357    /// on-disk compare-and-swap recheck and before the ref is written.
2358    fn after_next_recheck(hook: impl FnOnce() + 'static) {
2359        AFTER_RECHECK.with(|slot| *slot.borrow_mut() = Some(Box::new(hook)));
2360    }
2361
2362    fn oid(byte: u8) -> ObjectId {
2363        let mut bytes = [0u8; 20];
2364        bytes[19] = byte;
2365        ObjectId::from_bytes(&bytes).expect("oid")
2366    }
2367
2368    fn store(git_dir: &Path) -> FilesRefStore {
2369        FilesRefStore::open(FilesRefStoreConfig {
2370            git_dir: git_dir.to_path_buf(),
2371            common_dir: git_dir.to_path_buf(),
2372            namespace_prefix: None,
2373        })
2374    }
2375
2376    fn set(
2377        store: &FilesRefStore,
2378        name: &str,
2379        value: ObjectId,
2380        expected: Expected,
2381    ) -> StoreResult<()> {
2382        let txn = RefTransaction::new().update(RefUpdate {
2383            name: name.to_owned(),
2384            new_value: Some(RawRef::Direct(value)),
2385            expected,
2386            reflog: None,
2387            flags: RefUpdateFlags::default(),
2388        })?;
2389        store.prepare(txn)?.commit()
2390    }
2391
2392    fn git_dir() -> tempfile::TempDir {
2393        let dir = tempfile::tempdir().expect("tempdir");
2394        fs::create_dir_all(dir.path().join("refs/heads")).expect("refs");
2395        fs::write(dir.path().join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
2396        dir
2397    }
2398
2399    fn listed_names(store: &FilesRefStore, prefix: &str) -> Vec<String> {
2400        let mut names = Vec::new();
2401        store
2402            .for_each_ref(prefix, &mut |entry| {
2403                names.push(entry.name.clone());
2404                ControlFlow::Continue(())
2405            })
2406            .expect("list");
2407        names
2408    }
2409
2410    #[test]
2411    fn partial_name_prefix_lists_loose_refs_without_visiting_siblings() {
2412        let dir = git_dir();
2413        let tags = dir.path().join("refs/tags");
2414        for name in ["v1", "v10", "v1x/a", "v2/deep", "w"] {
2415            let path = tags.join(name);
2416            fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
2417            fs::write(path, format!("{}\n", oid(1))).expect("ref");
2418        }
2419        // A sibling that cannot be stat'ed fails any walk that looks at it.
2420        #[cfg(unix)]
2421        std::os::unix::fs::symlink(tags.join("missing-target"), tags.join("zzz"))
2422            .expect("dangling symlink");
2423        let store = store(dir.path());
2424
2425        assert_eq!(
2426            listed_names(&store, "refs/tags/v1"),
2427            ["refs/tags/v1", "refs/tags/v10", "refs/tags/v1x/a"]
2428        );
2429        assert_eq!(listed_names(&store, "refs/tags/v2/"), ["refs/tags/v2/deep"]);
2430        assert_eq!(listed_names(&store, "refs/tags/w"), ["refs/tags/w"]);
2431        assert!(listed_names(&store, "refs/tags/x").is_empty());
2432    }
2433
2434    /// Two writers that both expect `expected` must not both succeed: the one that commits
2435    /// first holds the ref lock from its recheck to its write.
2436    fn assert_racing_writer_is_locked_out(expected: Expected, seed: Option<ObjectId>) {
2437        let dir = git_dir();
2438        let name = "refs/notes/commits";
2439        let writer = store(dir.path());
2440        if let Some(seed) = seed {
2441            set(&writer, name, seed, Expected::Missing).expect("seed");
2442        }
2443
2444        let other = store(dir.path());
2445        let racing_expected = expected.clone();
2446        let racer = std::rc::Rc::new(RefCell::new(None));
2447        let racer_slot = racer.clone();
2448        after_next_recheck(move || {
2449            *racer_slot.borrow_mut() = Some(set(&other, name, oid(2), racing_expected));
2450        });
2451        set(&writer, name, oid(3), expected).expect("first committer wins");
2452
2453        let racer = racer.borrow_mut().take().expect("hook ran");
2454        assert!(
2455            matches!(racer, Err(RefStoreError::LockHeld { .. })),
2456            "racing writer must not commit while the ref is locked: {racer:?}"
2457        );
2458        assert_eq!(
2459            writer.read_raw(name).expect("read"),
2460            Some(RawRef::Direct(oid(3)))
2461        );
2462        assert!(
2463            !dir.path().join("refs/notes/commits.lock").exists(),
2464            "the lock is released after the write"
2465        );
2466    }
2467
2468    #[test]
2469    fn expected_oid_update_holds_the_lock_through_the_write() {
2470        assert_racing_writer_is_locked_out(Expected::Oid(oid(1)), Some(oid(1)));
2471    }
2472
2473    #[test]
2474    fn expected_missing_create_holds_the_lock_through_the_write() {
2475        assert_racing_writer_is_locked_out(Expected::Missing, None);
2476    }
2477
2478    #[test]
2479    fn symbolic_update_holds_the_dereferenced_target_lock_through_the_write() {
2480        let dir = git_dir();
2481        let writer = store(dir.path());
2482        set(&writer, "refs/heads/main", oid(1), Expected::Missing).expect("seed main");
2483
2484        let other = store(dir.path());
2485        let racer = std::rc::Rc::new(RefCell::new(None));
2486        let racer_slot = racer.clone();
2487        after_next_recheck(move || {
2488            *racer_slot.borrow_mut() = Some(set(
2489                &other,
2490                "refs/heads/main",
2491                oid(2),
2492                Expected::Oid(oid(1)),
2493            ));
2494        });
2495        set(&writer, "HEAD", oid(3), Expected::Oid(oid(1))).expect("update through HEAD");
2496
2497        let racer = racer.borrow_mut().take().expect("hook ran");
2498        assert!(
2499            matches!(racer, Err(RefStoreError::LockHeld { .. })),
2500            "a direct writer of the HEAD target must be locked out: {racer:?}"
2501        );
2502        assert_eq!(
2503            writer.read_raw("refs/heads/main").expect("read"),
2504            Some(RawRef::Direct(oid(3)))
2505        );
2506        assert_eq!(
2507            writer.read_raw("HEAD").expect("read"),
2508            Some(RawRef::Symbolic("refs/heads/main".to_owned()))
2509        );
2510        for lock in ["HEAD.lock", "refs/heads/main.lock"] {
2511            assert!(!dir.path().join(lock).exists(), "{lock} released");
2512        }
2513    }
2514
2515    #[test]
2516    fn create_availability_matches_the_path_based_check() {
2517        let dir = git_dir();
2518        let path = dir.path();
2519        let hex = oid(1).to_hex();
2520        fs::write(
2521            path.join("packed-refs"),
2522            format!(
2523                "# pack-refs with: peeled fully-peeled sorted \n\
2524                 {hex} refs/heads/p\n\
2525                 {hex} refs/heads/pd/x\n\
2526                 {hex} refs/heads/pd-sibling\n\
2527                 {hex} refs/tags/v1\n"
2528            ),
2529        )
2530        .expect("packed-refs");
2531        for loose in ["refs/heads/l", "refs/heads/ld/y/z"] {
2532            let file = path.join(loose);
2533            fs::create_dir_all(file.parent().expect("parent")).expect("dirs");
2534            fs::write(file, format!("{hex}\n")).expect("loose");
2535        }
2536        let refs = store(path);
2537        let extras: BTreeSet<String> = ["refs/heads/batch/one".to_owned()].into();
2538        for name in [
2539            "refs/heads/new",
2540            "refs/heads/p/child",
2541            "refs/heads/pd",
2542            "refs/heads/pd-sibling/x",
2543            "refs/heads/l/child",
2544            "refs/heads/ld",
2545            "refs/heads/ld/y",
2546            "refs/heads/batch",
2547            "refs/heads/batch/one/two",
2548            "refs/tags/v1/x",
2549            "refs/tags/v2",
2550        ] {
2551            assert_eq!(
2552                refs.verify_create_available(
2553                    name,
2554                    &extras.iter().map(String::as_str).collect(),
2555                    None
2556                ),
2557                crate::refs::verify_refname_available_for_create(
2558                    path,
2559                    name,
2560                    &extras,
2561                    &HashSet::new()
2562                ),
2563                "{name}"
2564            );
2565        }
2566    }
2567
2568    #[test]
2569    fn mapped_packed_refs_are_reread_after_replacement() {
2570        let dir = git_dir();
2571        let path = dir.path();
2572        let records = |value: ObjectId| -> Vec<crate::refs::PackedRefRecord> {
2573            (0..500)
2574                .map(|i| crate::refs::PackedRefRecord {
2575                    refname: format!("refs/tags/t{i:04}"),
2576                    oid: value,
2577                    peeled: None,
2578                })
2579                .collect()
2580        };
2581        crate::refs::write_packed_refs_file(path, &records(oid(1))).expect("pack");
2582        assert!(fs::metadata(path.join("packed-refs")).expect("meta").len() > 8192);
2583        let refs = store(path);
2584        assert_eq!(
2585            refs.read_raw("refs/tags/t0499").expect("read"),
2586            Some(RawRef::Direct(oid(1)))
2587        );
2588        let mut replaced = records(oid(2));
2589        replaced.truncate(10);
2590        crate::refs::write_packed_refs_file(path, &replaced).expect("repack");
2591        assert_eq!(refs.read_raw("refs/tags/t0499").expect("read"), None);
2592        assert_eq!(
2593            refs.read_raw("refs/tags/t0009").expect("read"),
2594            Some(RawRef::Direct(oid(2)))
2595        );
2596    }
2597
2598    /// Replace `packed-refs` by renaming a same-length file over it, then put back the old
2599    /// mtime (as a coarse-mtime filesystem would show it).
2600    fn replace_packed_refs_keeping_len_and_mtime(path: &Path, body: &str) {
2601        let packed = path.join("packed-refs");
2602        let before = fs::metadata(&packed).expect("meta");
2603        assert_eq!(before.len(), body.len() as u64, "same length");
2604        let tmp = path.join("packed-refs.new");
2605        fs::write(&tmp, body).expect("write");
2606        fs::File::options()
2607            .write(true)
2608            .open(&tmp)
2609            .expect("open")
2610            .set_modified(before.modified().expect("mtime"))
2611            .expect("pin mtime");
2612        fs::rename(&tmp, &packed).expect("rename");
2613        let after = fs::metadata(&packed).expect("meta");
2614        assert_eq!(after.len(), before.len());
2615        assert_eq!(after.modified().ok(), before.modified().ok());
2616    }
2617
2618    #[test]
2619    fn create_rechecks_a_packed_refs_replaced_with_same_len_and_mtime() {
2620        assert_create_blocked_by_replaced_packed_descendant(false);
2621    }
2622
2623    #[test]
2624    fn create_rechecks_packed_refs_even_when_the_stamp_cannot_tell() {
2625        assert_create_blocked_by_replaced_packed_descendant(true);
2626    }
2627
2628    /// Prime a store with `refs/heads/blocked-child` packed, swap in an equal-length
2629    /// `packed-refs` holding `refs/heads/blocked/child`, then create `refs/heads/blocked`.
2630    fn assert_create_blocked_by_replaced_packed_descendant(stamp_collides: bool) {
2631        let dir = git_dir();
2632        let path = dir.path();
2633        let hex = oid(1).to_hex();
2634        let header = "# pack-refs with: peeled fully-peeled sorted \n";
2635        fs::write(
2636            path.join("packed-refs"),
2637            format!("{header}{hex} refs/heads/blocked-child\n"),
2638        )
2639        .expect("packed-refs");
2640        let writer = store(path);
2641        assert_eq!(
2642            writer.read_raw("refs/heads/blocked-child").expect("prime"),
2643            Some(RawRef::Direct(oid(1)))
2644        );
2645        replace_packed_refs_keeping_len_and_mtime(
2646            path,
2647            &format!("{header}{hex} refs/heads/blocked/child\n"),
2648        );
2649        if stamp_collides {
2650            // A replacement no stamp can see (coarse timestamps and a reused inode): only a
2651            // fresh read under the ref lock notices it.
2652            let mut guard = writer.packed_cache.lock().expect("cache");
2653            guard.stamp = FilesRefStore::packed_refs_stamp(&path.join("packed-refs"));
2654        }
2655
2656        let err = set(&writer, "refs/heads/blocked", oid(2), Expected::Missing)
2657            .expect_err("packed descendant blocks the create");
2658        assert!(
2659            matches!(
2660                err,
2661                RefStoreError::NameUnavailable {
2662                    reason: RefnameUnavailable::DescendantExists { ref blocking, .. }
2663                } if blocking == "refs/heads/blocked/child"
2664            ),
2665            "{err:?}"
2666        );
2667        assert!(!path.join("refs/heads/blocked").exists());
2668        assert!(!path.join("refs/heads/blocked.lock").exists());
2669    }
2670
2671    #[test]
2672    fn a_ref_whose_reflog_is_not_utf8_moves_and_its_log_keeps_what_it_had() {
2673        let dir = git_dir();
2674        let path = dir.path();
2675        let writer = store(path);
2676        let name = "refs/heads/main";
2677        set(&writer, name, oid(1), Expected::Missing).expect("create");
2678        // Git logs a commit subject in a legacy encoding as it is, and the
2679        // store keeps a line it can't parse.
2680        let log = path.join("logs/refs/heads/main");
2681        fs::create_dir_all(log.parent().expect("parent")).expect("logs");
2682        let mut written = format!(
2683            "{} {} A <a@e> 1700000000 +0000\tcommit: Caf",
2684            oid(0).to_hex(),
2685            oid(1).to_hex()
2686        )
2687        .into_bytes();
2688        written.extend_from_slice(b"\xe9 latin-1\nnot a reflog line\n");
2689        fs::write(&log, &written).expect("log");
2690
2691        let txn = RefTransaction::new()
2692            .update(RefUpdate {
2693                name: name.to_owned(),
2694                new_value: Some(RawRef::Direct(oid(2))),
2695                expected: Expected::Oid(oid(1)),
2696                reflog: Some(super::super::ReflogUpdate {
2697                    identity: "T <t@t> 1700000001 +0000".into(),
2698                    message: "moved".into(),
2699                    time: time::OffsetDateTime::UNIX_EPOCH,
2700                    old_oid: None,
2701                    new_oid: None,
2702                }),
2703                flags: RefUpdateFlags::default(),
2704            })
2705            .expect("txn");
2706        writer
2707            .prepare(txn)
2708            .expect("prepare")
2709            .commit()
2710            .expect("commit");
2711
2712        assert_eq!(
2713            writer.read_raw(name).expect("read"),
2714            Some(RawRef::Direct(oid(2)))
2715        );
2716        let bytes = fs::read(&log).expect("log");
2717        assert!(
2718            bytes.starts_with(&written),
2719            "what the log had stays as written"
2720        );
2721        assert_eq!(
2722            String::from_utf8_lossy(&bytes[written.len()..]),
2723            format!(
2724                "{} {} T <t@t> 1700000001 +0000\tmoved\n",
2725                oid(1).to_hex(),
2726                oid(2).to_hex()
2727            )
2728        );
2729        let mut messages = Vec::new();
2730        writer
2731            .for_each_reflog_entry(name, false, &mut |entry| {
2732                messages.push(entry.message.clone());
2733                ControlFlow::Continue(())
2734            })
2735            .expect("read log");
2736        assert_eq!(messages, ["commit: Caf\u{fffd} latin-1", "moved"]);
2737    }
2738
2739    #[test]
2740    fn create_with_reflog_next_to_a_ref_file_in_logs_reports_the_conflict() {
2741        let dir = git_dir();
2742        let path = dir.path();
2743        let writer = store(path);
2744        set(&writer, "refs/heads/a", oid(1), Expected::Missing).expect("create");
2745        fs::create_dir_all(path.join("logs/refs/heads")).expect("logs");
2746        fs::write(path.join("logs/refs/heads/a"), "").expect("log");
2747        let txn = RefTransaction::new()
2748            .update(RefUpdate {
2749                name: "refs/heads/a/b".to_owned(),
2750                new_value: Some(RawRef::Direct(oid(2))),
2751                expected: Expected::Missing,
2752                reflog: Some(super::super::ReflogUpdate {
2753                    identity: "T <t@t>".into(),
2754                    message: "create".into(),
2755                    time: time::OffsetDateTime::UNIX_EPOCH,
2756                    old_oid: None,
2757                    new_oid: None,
2758                }),
2759                flags: RefUpdateFlags::default(),
2760            })
2761            .expect("txn");
2762        let err = writer.prepare(txn).map(|_| ()).expect_err("conflict");
2763        assert!(
2764            matches!(
2765                err,
2766                RefStoreError::NameUnavailable {
2767                    reason: RefnameUnavailable::AncestorExists { ref blocking, .. }
2768                } if blocking == "refs/heads/a"
2769            ),
2770            "{err:?}"
2771        );
2772        assert!(!path.join("refs/heads/a.lock").exists());
2773    }
2774
2775    fn create_with_reflog(name: &str, value: ObjectId) -> RefTransaction {
2776        RefTransaction::new()
2777            .update(RefUpdate {
2778                name: name.to_owned(),
2779                new_value: Some(RawRef::Direct(value)),
2780                expected: Expected::Missing,
2781                reflog: Some(super::super::ReflogUpdate {
2782                    identity: "T <t@t>".into(),
2783                    message: "branch: Created from main".into(),
2784                    time: time::OffsetDateTime::UNIX_EPOCH,
2785                    old_oid: None,
2786                    new_oid: None,
2787                }),
2788                flags: RefUpdateFlags::default(),
2789            })
2790            .expect("txn")
2791    }
2792
2793    /// Prepare a reflogged create of `refs/heads/rel` and check it names `blocking`, leaving
2794    /// no lock, ref or log behind.
2795    fn assert_rel_create_blocked_by(path: &Path, blocking_ref: &str) {
2796        let writer = store(path);
2797        let err = writer
2798            .prepare(create_with_reflog("refs/heads/rel", oid(2)))
2799            .map(|_| ())
2800            .expect_err("conflict");
2801        assert!(
2802            matches!(
2803                err,
2804                RefStoreError::NameUnavailable {
2805                    reason: RefnameUnavailable::DescendantExists { ref blocking, ref new_ref }
2806                } if blocking == blocking_ref && new_ref == "refs/heads/rel"
2807            ),
2808            "{err:?}"
2809        );
2810        assert_eq!(writer.read_raw("refs/heads/rel").expect("read"), None);
2811        assert!(!path.join("refs/heads/rel.lock").exists());
2812        assert!(!path.join("logs/refs/heads/rel").is_file());
2813    }
2814
2815    #[test]
2816    fn create_with_reflog_over_a_loose_descendant_names_it() {
2817        let dir = git_dir();
2818        let path = dir.path();
2819        store(path)
2820            .prepare(create_with_reflog("refs/heads/rel/1.0", oid(1)))
2821            .expect("prepare seed")
2822            .commit()
2823            .expect("seed");
2824        assert!(path.join("logs/refs/heads/rel/1.0").is_file());
2825        assert_rel_create_blocked_by(path, "refs/heads/rel/1.0");
2826    }
2827
2828    #[test]
2829    fn create_with_reflog_over_a_packed_descendant_with_its_log_dir_names_it() {
2830        let dir = git_dir();
2831        let path = dir.path();
2832        // `pack-refs` leaves `logs/refs/heads/rel/1.0`, and an empty `refs/heads/rel/`.
2833        fs::write(
2834            path.join("packed-refs"),
2835            format!(
2836                "# pack-refs with: peeled fully-peeled sorted \n{} refs/heads/rel/1.0\n",
2837                oid(1)
2838            ),
2839        )
2840        .expect("packed-refs");
2841        fs::create_dir_all(path.join("refs/heads/rel")).expect("empty ref dir");
2842        fs::create_dir_all(path.join("logs/refs/heads/rel")).expect("logs");
2843        fs::write(path.join("logs/refs/heads/rel/1.0"), "").expect("log");
2844        assert_rel_create_blocked_by(path, "refs/heads/rel/1.0");
2845    }
2846
2847    #[test]
2848    fn create_with_reflog_over_a_stray_log_directory_names_the_log() {
2849        let dir = git_dir();
2850        let path = dir.path();
2851        fs::create_dir_all(path.join("logs/refs/heads/rel/deep")).expect("logs");
2852        fs::write(path.join("logs/refs/heads/rel/deep/2.0"), "").expect("log");
2853        assert_rel_create_blocked_by(path, "refs/heads/rel/deep/2.0");
2854        assert!(path.join("logs/refs/heads/rel/deep/2.0").is_file());
2855    }
2856
2857    #[test]
2858    fn create_with_reflog_removes_an_empty_log_directory() {
2859        let dir = git_dir();
2860        let path = dir.path();
2861        fs::create_dir_all(path.join("logs/refs/heads/rel/a/b")).expect("empty logs");
2862        let writer = store(path);
2863        writer
2864            .prepare(create_with_reflog("refs/heads/rel", oid(2)))
2865            .expect("prepare")
2866            .commit()
2867            .expect("commit");
2868        assert_eq!(
2869            writer.read_raw("refs/heads/rel").expect("read"),
2870            Some(RawRef::Direct(oid(2)))
2871        );
2872        assert_eq!(
2873            writer
2874                .read_reflog_entries("refs/heads/rel")
2875                .expect("log")
2876                .len(),
2877            1
2878        );
2879    }
2880
2881    #[test]
2882    fn failed_recheck_releases_the_lock() {
2883        let dir = git_dir();
2884        let name = "refs/heads/main";
2885        let writer = store(dir.path());
2886        set(&writer, name, oid(1), Expected::Missing).expect("create");
2887        let other = store(dir.path());
2888        set(&other, name, oid(2), Expected::Oid(oid(1))).expect("other moves it");
2889
2890        let err = set(&writer, name, oid(3), Expected::Oid(oid(1))).expect_err("stale");
2891        assert!(
2892            matches!(err, RefStoreError::ExpectedMismatch { .. }),
2893            "{err:?}"
2894        );
2895        assert!(!dir.path().join("refs/heads/main.lock").exists());
2896        set(&writer, name, oid(3), Expected::Oid(oid(2))).expect("retry against current value");
2897        assert_eq!(
2898            writer.read_raw(name).expect("read"),
2899            Some(RawRef::Direct(oid(3)))
2900        );
2901    }
2902
2903    fn delete(name: &str, expected: Expected) -> RefUpdate {
2904        RefUpdate {
2905            name: name.to_owned(),
2906            new_value: None,
2907            expected,
2908            reflog: None,
2909            flags: RefUpdateFlags::default(),
2910        }
2911    }
2912
2913    fn commit_all(store: &FilesRefStore, updates: Vec<RefUpdate>) -> StoreResult<()> {
2914        store
2915            .prepare(RefTransaction::from_updates(updates)?)?
2916            .commit()
2917    }
2918
2919    /// `packed-refs` with twenty `refs/remotes/o/x<i>`, a peeled tag and `main`, plus a loose
2920    /// `x5` that overrides its packed value.
2921    fn packed_remote_refs(path: &Path) {
2922        let mut body = String::from("# pack-refs with: peeled fully-peeled sorted \n");
2923        body.push_str(&format!("{} refs/heads/main\n", oid(4)));
2924        let mut remotes: Vec<String> = (0..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
2925        remotes.sort();
2926        for name in &remotes {
2927            body.push_str(&format!("{} {name}\n", oid(1)));
2928        }
2929        body.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
2930        fs::write(path.join("packed-refs"), body).expect("packed-refs");
2931        fs::create_dir_all(path.join("refs/remotes/o")).expect("dir");
2932        fs::write(path.join("refs/remotes/o/x5"), format!("{}\n", oid(9))).expect("loose");
2933    }
2934
2935    #[test]
2936    fn batch_delete_of_packed_refs_rewrites_packed_refs_once_and_keeps_the_rest() {
2937        let dir = git_dir();
2938        let path = dir.path();
2939        packed_remote_refs(path);
2940        let writer = store(path);
2941        let updates = (0..20)
2942            .map(|i| {
2943                let expected = match i {
2944                    0 => Expected::Oid(oid(1)),
2945                    5 => Expected::Oid(oid(9)),
2946                    7 => Expected::Exists,
2947                    _ => Expected::Any,
2948                };
2949                delete(&format!("refs/remotes/o/x{i}"), expected)
2950            })
2951            .collect();
2952        commit_all(&writer, updates).expect("batch delete");
2953
2954        for i in 0..20 {
2955            let name = format!("refs/remotes/o/x{i}");
2956            assert_eq!(writer.read_raw(&name).expect("read"), None, "{name}");
2957        }
2958        let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2959        assert_eq!(
2960            packed,
2961            format!(
2962                "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n{} refs/tags/t\n^{}\n",
2963                oid(4),
2964                oid(2),
2965                oid(3)
2966            )
2967        );
2968        assert!(
2969            path.join("refs/remotes").is_dir(),
2970            "refs/remotes/ namespace kept"
2971        );
2972        assert!(
2973            !path.join("refs/remotes/o").exists(),
2974            "emptied child dirs are pruned"
2975        );
2976        assert!(!path.join("packed-refs.lock").exists());
2977        assert!(path.join("refs/heads").is_dir());
2978    }
2979
2980    #[test]
2981    fn stale_expectation_on_packed_ref_rejects_the_whole_batch() {
2982        let dir = git_dir();
2983        let path = dir.path();
2984        packed_remote_refs(path);
2985        let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
2986        let writer = store(path);
2987        let err = commit_all(
2988            &writer,
2989            vec![
2990                delete("refs/remotes/o/x1", Expected::Any),
2991                delete("refs/remotes/o/x2", Expected::Oid(oid(7))),
2992            ],
2993        )
2994        .expect_err("stale");
2995        assert!(
2996            matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x2"),
2997            "{err:?}"
2998        );
2999        assert_eq!(
3000            fs::read_to_string(path.join("packed-refs")).expect("packed"),
3001            before
3002        );
3003        assert!(!path.join("packed-refs.lock").exists());
3004        assert!(!path.join("refs/remotes/o/x1.lock").exists());
3005    }
3006
3007    #[test]
3008    fn batch_delete_prunes_nested_empty_dirs_deepest_first() {
3009        let dir = git_dir();
3010        let path = dir.path();
3011        let writer = store(path);
3012        for name in ["refs/heads/x/y/z", "refs/heads/x/w", "refs/heads/keep/k"] {
3013            set(&writer, name, oid(1), Expected::Missing).expect("create");
3014        }
3015        commit_all(
3016            &writer,
3017            vec![
3018                delete("refs/heads/x/w", Expected::Oid(oid(1))),
3019                delete("refs/heads/x/y/z", Expected::Any),
3020            ],
3021        )
3022        .expect("delete");
3023        assert!(!path.join("refs/heads/x").exists());
3024        assert!(path.join("refs/heads/keep/k").is_file());
3025    }
3026
3027    #[test]
3028    fn deleting_through_a_symref_removes_the_packed_target() {
3029        let dir = git_dir();
3030        let path = dir.path();
3031        packed_remote_refs(path);
3032        fs::write(path.join("refs/remotes/o/HEAD"), "ref: refs/remotes/o/x3\n").expect("symref");
3033        let writer = store(path);
3034        commit_all(&writer, vec![delete("refs/remotes/o/HEAD", Expected::Any)])
3035            .expect("deref delete");
3036        assert_eq!(writer.read_raw("refs/remotes/o/x3").expect("read"), None);
3037        assert_eq!(
3038            writer.read_raw("refs/remotes/o/HEAD").expect("read"),
3039            Some(RawRef::Symbolic("refs/remotes/o/x3".to_owned()))
3040        );
3041        let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
3042        assert!(!packed.contains("refs/remotes/o/x3\n"), "{packed}");
3043        assert!(packed.contains("refs/remotes/o/x4\n"), "{packed}");
3044    }
3045
3046    #[test]
3047    fn late_commit_time_mismatch_keeps_packed_refs_the_batch_never_reached() {
3048        let dir = git_dir();
3049        let path = dir.path();
3050        packed_remote_refs(path);
3051        let writer = store(path);
3052        let other = store(path);
3053        after_next_recheck(move || {
3054            set(&other, "refs/remotes/o/x9", oid(7), Expected::Any).expect("concurrent write");
3055        });
3056        let updates = (0..20)
3057            .map(|i| {
3058                let expected = if i == 9 {
3059                    Expected::Oid(oid(1))
3060                } else {
3061                    Expected::Any
3062                };
3063                delete(&format!("refs/remotes/o/x{i}"), expected)
3064            })
3065            .collect();
3066        let err = commit_all(&writer, updates).expect_err("x9 moved under the batch");
3067        assert!(
3068            matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x9"),
3069            "{err:?}"
3070        );
3071
3072        for i in 0..9 {
3073            let name = format!("refs/remotes/o/x{i}");
3074            assert_eq!(
3075                writer.read_raw(&name).expect("read"),
3076                None,
3077                "{name} was deleted"
3078            );
3079        }
3080        assert_eq!(
3081            writer.read_raw("refs/remotes/o/x9").expect("read"),
3082            Some(RawRef::Direct(oid(7))),
3083            "the concurrent value survives"
3084        );
3085        for i in 10..20 {
3086            let name = format!("refs/remotes/o/x{i}");
3087            assert_eq!(
3088                writer.read_raw(&name).expect("read"),
3089                Some(RawRef::Direct(oid(1))),
3090                "{name} was never processed"
3091            );
3092        }
3093        let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
3094        let mut expected = format!(
3095            "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n",
3096            oid(4)
3097        );
3098        let mut kept: Vec<String> = (9..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
3099        kept.sort();
3100        for name in kept {
3101            expected.push_str(&format!("{} {name}\n", oid(1)));
3102        }
3103        expected.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
3104        assert_eq!(packed, expected);
3105        assert!(!path.join("packed-refs.lock").exists());
3106        assert!(!path.join("refs/remotes/o/x10.lock").exists());
3107    }
3108
3109    #[test]
3110    fn restore_merges_records_back_in_sorted_order_with_peel_lines() {
3111        let dir = git_dir();
3112        let path = dir.path();
3113        fs::write(
3114            path.join("packed-refs"),
3115            format!(
3116                "# pack-refs with: peeled fully-peeled sorted \n{} refs/heads/b\n{} refs/tags/z\n^{}\n",
3117                oid(1),
3118                oid(2),
3119                oid(3)
3120            ),
3121        )
3122        .expect("packed");
3123        let names: HashSet<String> = ["refs/heads/b", "refs/tags/z"]
3124            .into_iter()
3125            .map(str::to_owned)
3126            .collect();
3127        let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
3128        let removed = remove_packed_refs_under_lock(path, &names).expect("remove");
3129        assert_eq!(removed.len(), 2);
3130        assert_eq!(packed_record_name(&removed[1]), "refs/tags/z");
3131        assert_eq!(packed_record_oid(&removed[1]), Some(oid(2)));
3132        fs::write(
3133            path.join("packed-refs"),
3134            format!(
3135                "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/a\n{} refs/heads/c\n",
3136                oid(5),
3137                oid(6)
3138            ),
3139        )
3140        .expect("packed");
3141        let records: Vec<&str> = removed.iter().map(String::as_str).collect();
3142        restore_packed_records_under_lock(path, &records).expect("restore");
3143        assert_eq!(
3144            fs::read_to_string(path.join("packed-refs")).expect("packed"),
3145            format!(
3146                "# 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",
3147                oid(5),
3148                oid(1),
3149                oid(6),
3150                oid(2),
3151                oid(3)
3152            )
3153        );
3154        assert!(before.contains("refs/tags/z"));
3155    }
3156
3157    /// Refs deleted while listing must not fail the walk (matches Git upload-pack behavior).
3158    #[test]
3159    fn concurrent_loose_ref_delete_during_list_is_tolerated() {
3160        use std::sync::atomic::{AtomicBool, Ordering};
3161        use std::sync::Arc;
3162        use std::thread;
3163        use std::time::Duration;
3164
3165        let dir = git_dir();
3166        set(
3167            &store(dir.path()),
3168            "refs/heads/main",
3169            oid(1),
3170            Expected::Missing,
3171        )
3172        .expect("seed main");
3173
3174        let stop = Arc::new(AtomicBool::new(false));
3175        let writer_dir = dir.path().to_path_buf();
3176        let writer_stop = stop.clone();
3177        let writer = thread::spawn(move || {
3178            use crate::refs::{delete_ref, write_ref};
3179            let c = oid(2);
3180            while !writer_stop.load(Ordering::Relaxed) {
3181                for i in 0..7 {
3182                    let name = format!("refs/heads/d{i}/sub/x");
3183                    let _ = write_ref(&writer_dir, &name, &c);
3184                    let _ = delete_ref(&writer_dir, &name);
3185                }
3186            }
3187        });
3188
3189        let lister = store(dir.path());
3190        let mut errors = 0usize;
3191        for _ in 0..400 {
3192            if lister
3193                .for_each_ref("", &mut |_| ControlFlow::Continue(()))
3194                .is_err()
3195            {
3196                errors += 1;
3197            }
3198        }
3199        stop.store(true, Ordering::Relaxed);
3200        writer.join().expect("writer");
3201        thread::sleep(Duration::from_millis(10));
3202        assert_eq!(
3203            errors, 0,
3204            "listing must tolerate refs deleted between readdir and open"
3205        );
3206    }
3207
3208    fn create(name: &str, value: ObjectId) -> RefUpdate {
3209        RefUpdate {
3210            name: name.to_owned(),
3211            new_value: Some(RawRef::Direct(value)),
3212            expected: Expected::Missing,
3213            reflog: None,
3214            flags: RefUpdateFlags::default(),
3215        }
3216    }
3217
3218    fn commit_initial(store: &FilesRefStore, updates: Vec<RefUpdate>) -> StoreResult<()> {
3219        store
3220            .prepare(RefTransaction::from_updates(updates)?.initial())?
3221            .commit()
3222    }
3223
3224    fn loose_files_under(dir: &Path) -> BTreeSet<String> {
3225        let mut names = BTreeSet::new();
3226        collect_loose_file_names(dir, "", &mut names);
3227        names
3228    }
3229
3230    #[test]
3231    fn initial_transaction_writes_one_sorted_packed_refs_and_no_loose_files() {
3232        let dir = git_dir();
3233        fs::write(
3234            dir.path().join("packed-refs"),
3235            format!(
3236                "# pack-refs with: peeled fully-peeled sorted \n{} refs/tags/kept\n",
3237                oid(7)
3238            ),
3239        )
3240        .expect("packed-refs");
3241        let store = store(dir.path());
3242        let mut updates: Vec<RefUpdate> = (0..500)
3243            .rev()
3244            .map(|i| create(&format!("refs/remotes/origin/b/n{i}"), oid(1)))
3245            .collect();
3246        updates.push(create("refs/tags/v1", oid(2)));
3247        commit_initial(&store, updates).expect("initial batch");
3248
3249        assert!(loose_files_under(&dir.path().join("refs")).is_empty());
3250        assert!(!dir.path().join("packed-refs.lock").exists());
3251        let packed = fs::read_to_string(dir.path().join("packed-refs")).expect("read");
3252        let names: Vec<&str> = packed
3253            .lines()
3254            .skip(1)
3255            .map(|line| line.split_once(' ').expect("record").1)
3256            .collect();
3257        let mut sorted = names.clone();
3258        sorted.sort_unstable();
3259        assert_eq!(names, sorted);
3260        assert_eq!(names.len(), 502);
3261        assert!(names.contains(&"refs/tags/kept"));
3262        assert_eq!(
3263            store.read_raw("refs/remotes/origin/b/n42").expect("read"),
3264            Some(RawRef::Direct(oid(1)))
3265        );
3266        assert_eq!(
3267            listed_names(&store, "refs/tags/"),
3268            ["refs/tags/kept", "refs/tags/v1"]
3269        );
3270    }
3271
3272    #[test]
3273    fn initial_transaction_falls_back_to_loose_refs_when_it_cannot_pack() {
3274        let dir = git_dir();
3275        let store = store(dir.path());
3276        set(&store, "refs/heads/main", oid(1), Expected::Missing).expect("seed");
3277
3278        // An update of an existing ref is not a create.
3279        let updates = vec![
3280            create("refs/heads/topic", oid(2)),
3281            RefUpdate {
3282                expected: Expected::Any,
3283                ..create("refs/heads/main", oid(3))
3284            },
3285        ];
3286        commit_initial(&store, updates).expect("regular batch");
3287        assert_eq!(
3288            loose_files_under(&dir.path().join("refs/heads")),
3289            BTreeSet::from(["/main".to_owned(), "/topic".to_owned()])
3290        );
3291
3292        // A symbolic ref is not a plain create either.
3293        let symref = RefUpdate {
3294            new_value: Some(RawRef::Symbolic("refs/heads/main".to_owned())),
3295            ..create("refs/remotes/origin/HEAD", oid(1))
3296        };
3297        commit_initial(&store, vec![create("refs/tags/t", oid(4)), symref]).expect("symref");
3298        assert!(dir.path().join("refs/remotes/origin/HEAD").is_file());
3299        assert!(dir.path().join("refs/tags/t").is_file());
3300        assert!(!dir.path().join("packed-refs").exists());
3301    }
3302
3303    #[test]
3304    fn initial_transaction_reports_name_conflicts() {
3305        let dir = git_dir();
3306        let store = store(dir.path());
3307        set(&store, "refs/heads/a", oid(1), Expected::Missing).expect("seed");
3308
3309        let err = commit_initial(&store, vec![create("refs/heads/a/b", oid(2))])
3310            .expect_err("stored ancestor");
3311        assert!(matches!(
3312            err,
3313            RefStoreError::NameUnavailable {
3314                reason: RefnameUnavailable::AncestorExists { .. }
3315            }
3316        ));
3317        let err = commit_initial(
3318            &store,
3319            vec![
3320                create("refs/heads/x", oid(2)),
3321                create("refs/heads/x/y", oid(2)),
3322            ],
3323        )
3324        .expect_err("batch conflicts with itself");
3325        assert!(matches!(
3326            err,
3327            RefStoreError::NameUnavailable {
3328                reason: RefnameUnavailable::SameBatch { .. }
3329            }
3330        ));
3331        assert!(!dir.path().join("packed-refs").exists());
3332        assert!(!dir.path().join("packed-refs.lock").exists());
3333    }
3334
3335    #[test]
3336    fn batch_blocked_by_a_file_in_place_of_a_parent_writes_nothing() {
3337        let dir = git_dir();
3338        fs::write(dir.path().join("refs/heads/block"), "not a ref\n").expect("obstruction");
3339        let store = store(dir.path());
3340
3341        let err = commit_all(
3342            &store,
3343            vec![
3344                create("refs/heads/first", oid(1)),
3345                create("refs/heads/block/child", oid(2)),
3346            ],
3347        )
3348        .expect_err("refs/heads/block/child cannot be created");
3349        assert!(
3350            !matches!(err, RefStoreError::ExpectedMismatch { .. }),
3351            "{err:?}"
3352        );
3353        assert!(
3354            !dir.path().join("refs/heads/first").exists(),
3355            "the batch is all-or-nothing: no ref before the failing one is written"
3356        );
3357        assert_eq!(
3358            fs::read_to_string(dir.path().join("refs/heads/block")).expect("read"),
3359            "not a ref\n"
3360        );
3361        assert!(!dir.path().join("refs/heads/first.lock").exists());
3362    }
3363
3364    #[cfg(windows)]
3365    #[test]
3366    fn win32_initial_packed_transaction_accepts_reserved_ref_name() {
3367        let dir = git_dir();
3368        let store = store(dir.path());
3369        commit_initial(&store, vec![create("refs/heads/aux", oid(1))]).expect("packed initial");
3370
3371        assert!(
3372            !dir.path().join("refs/heads/aux").exists(),
3373            "packed-only initial must not write a loose ref"
3374        );
3375        let packed = fs::read_to_string(dir.path().join("packed-refs")).expect("packed-refs");
3376        assert!(
3377            packed.contains("refs/heads/aux"),
3378            "reserved name may live in packed-refs: {packed}"
3379        );
3380    }
3381
3382    #[cfg(windows)]
3383    #[test]
3384    fn win32_initial_loose_fallback_rejects_reserved_ref_name() {
3385        let dir = git_dir();
3386        let store = store(dir.path());
3387        set(&store, "refs/heads/main", oid(1), Expected::Missing).expect("seed");
3388
3389        let err = commit_initial(
3390            &store,
3391            vec![
3392                create("refs/heads/aux", oid(2)),
3393                RefUpdate {
3394                    expected: Expected::Any,
3395                    ..create("refs/heads/main", oid(3))
3396                },
3397            ],
3398        )
3399        .expect_err("loose fallback");
3400        assert!(
3401            matches!(err, RefStoreError::InvalidRefName { .. }),
3402            "{err:?}"
3403        );
3404        assert!(!dir.path().join("refs/heads/aux").exists());
3405    }
3406
3407    #[cfg(windows)]
3408    #[test]
3409    fn win32_rejects_reserved_ref_path_components() {
3410        let dir = git_dir();
3411        let store = store(dir.path());
3412        let oid = oid(9);
3413
3414        for name in [
3415            "refs/heads/aux",
3416            "refs/heads/foo/com1",
3417            "refs/heads/COM1.txt",
3418        ] {
3419            let err = set(&store, name, oid, Expected::Missing).expect_err(name);
3420            assert!(
3421                matches!(err, RefStoreError::InvalidRefName { .. }),
3422                "{name}: {err:?}"
3423            );
3424            let loose = dir.path().join(name);
3425            assert!(!loose.exists(), "{name} must not be written");
3426            assert!(
3427                !dir.path().join(format!("{name}.lock")).exists(),
3428                "lock must not be created for {name}"
3429            );
3430            assert!(
3431                !dir.path().join("logs").join(name).exists(),
3432                "reflog must not be written for {name}"
3433            );
3434        }
3435    }
3436
3437    #[cfg(windows)]
3438    #[test]
3439    fn win32_reftable_still_accepts_reserved_ref_names() {
3440        use super::reftable::ReftableRefStore;
3441
3442        let store = ReftableRefStore::open_ephemeral().expect("reftable");
3443        let txn = RefTransaction::new()
3444            .update(RefUpdate {
3445                name: "refs/heads/aux".to_owned(),
3446                new_value: Some(RawRef::Direct(oid(1))),
3447                expected: Expected::Missing,
3448                reflog: None,
3449                flags: RefUpdateFlags::default(),
3450            })
3451            .expect("txn");
3452        store.prepare(txn).expect("prepare").commit().expect("aux");
3453    }
3454
3455    #[cfg(windows)]
3456    #[test]
3457    fn win32_can_delete_preseeded_reserved_loose_ref() {
3458        let dir = git_dir();
3459        let hex = oid(3).to_hex();
3460        let aux = dir.path().join("refs/heads/aux");
3461        fs::write(&aux, format!("{hex}\n")).expect("seed aux");
3462        let store = store(dir.path());
3463
3464        let txn = RefTransaction::new()
3465            .update(RefUpdate {
3466                name: "refs/heads/aux".to_owned(),
3467                new_value: None,
3468                expected: Expected::Any,
3469                reflog: None,
3470                flags: RefUpdateFlags::default(),
3471            })
3472            .expect("txn");
3473        store
3474            .prepare(txn)
3475            .expect("prepare")
3476            .commit()
3477            .expect("delete");
3478        assert!(!aux.exists());
3479    }
3480}