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