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