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