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