1use std::cell::RefCell;
4use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
5use std::fs;
6use std::io;
7use std::ops::ControlFlow;
8use std::path::{Path, PathBuf};
9use std::sync::Mutex;
10
11use crate::error::{Error, Result};
12use crate::git_dir::io_error::{
13 io_with_ref_store_path, map_ref_store_path_io, GitDirPathOperation,
14};
15use crate::lockfile::LockWait;
16use crate::objects::ObjectId;
17use crate::reflog::{self, ReflogEntry};
18use crate::refs::{
19 lock_path_for_ref, loose_ref_list_walk_root, packed_record_name, packed_record_oid,
20 prune_empty_ref_dirs, read_packed_refs_file_view, read_packed_refs_map_for_prefix_filtered,
21 read_ref_file, remove_empty_ref_directory, remove_packed_ref, remove_packed_ref_under_lock,
22 remove_packed_refs_under_lock, restore_packed_records_under_lock, PackedPeel,
23 PackedRecordFilter, Ref,
24};
25
26use super::apply::{
27 apply_case_only_ref_rename, apply_reflog_rename, apply_update, ref_change_target,
28 ref_lock_names_for_updates, simulate_batch_apply, RefBatchState,
29};
30use super::error::RefStoreError;
31use super::routing::{resolve_common_dir, route_ref_storage_in_common, RefStorageRoute};
32use super::transaction::expected_matches;
33use super::transaction::RefTransaction;
34use super::types::Expected;
35use super::validation::{
36 validate_storable_refname, verify_create_conflicts, verify_name_available_for_create,
37};
38use super::{PreparedRefTransaction, RawRef, RefEntry, RefStorageFormat, RefStore, RefUpdate};
39use crate::refs::RefnameUnavailable;
40
41type StoreResult<T> = std::result::Result<T, RefStoreError>;
42
43#[derive(Debug, Clone)]
45pub struct FilesRefStoreConfig {
46 pub git_dir: PathBuf,
48 pub common_dir: PathBuf,
50 pub namespace_prefix: Option<String>,
52}
53
54#[derive(Debug)]
56pub struct FilesRefStore {
57 git_dir: PathBuf,
58 common_dir: PathBuf,
59 namespace_prefix: Option<String>,
60 packed_cache: Mutex<PackedCacheEntry>,
61 prepared: Mutex<Option<PreparedFilesBatch>>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67struct PackedRefsStamp {
68 len: u64,
69 modified: Option<std::time::SystemTime>,
70 identity: Option<(u64, u64, i64, i64)>,
72}
73
74#[derive(Debug)]
75struct PackedCacheEntry {
76 stamp: PackedRefsStamp,
77 loaded: bool,
78 file: Option<crate::refs::PackedRefsFileView>,
79 map: Option<BTreeMap<String, ObjectId>>,
81 map_is_complete: bool,
82}
83
84#[derive(Debug, Clone, Copy)]
86struct CommitContext<'a> {
87 ref_lock_wait: LockWait,
89 lock_held: bool,
91 pruned: &'a HashSet<String>,
93 removed: &'a HashMap<String, RawRef>,
95 emptied_dirs: &'a RefCell<BTreeMap<PathBuf, PathBuf>>,
98}
99
100#[derive(Debug, Default)]
102struct PrunedPackedRecords {
103 names: HashSet<String>,
105 removed: HashMap<String, RawRef>,
107 records: Vec<(PathBuf, Vec<String>)>,
109}
110
111#[derive(Debug)]
112struct PreparedFilesBatch {
113 updates: Vec<RefUpdate>,
114 case_only_renames: Vec<super::RefCaseOnlyRename>,
115 ref_locks: HashMap<String, PathBuf>,
116 packed_lock: Option<PathBuf>,
117 ref_lock_wait: LockWait,
118 before_commit: RefBatchState,
120 after_commit: RefBatchState,
121}
122
123impl FilesRefStore {
124 #[must_use]
126 pub fn open(config: FilesRefStoreConfig) -> Self {
127 let path = super::paths::packed_refs_path(&config.common_dir);
128 let stamp = Self::packed_refs_stamp(&path);
129 Self {
130 git_dir: config.git_dir,
131 common_dir: config.common_dir,
132 namespace_prefix: config.namespace_prefix,
133 packed_cache: Mutex::new(PackedCacheEntry {
134 stamp,
135 loaded: false,
136 file: None,
137 map: None,
138 map_is_complete: false,
139 }),
140 prepared: Mutex::new(None),
141 }
142 }
143
144 pub(crate) fn git_dir(&self) -> &Path {
146 &self.git_dir
147 }
148
149 pub fn from_git_dir(git_dir: impl Into<PathBuf>) -> Result<Self> {
151 let git_dir: PathBuf = git_dir.into();
152 let git_dir = fs::canonicalize(&git_dir).unwrap_or(git_dir);
153 let common_dir = resolve_common_dir(&git_dir);
154 Ok(Self::open(FilesRefStoreConfig {
155 git_dir: git_dir.clone(),
156 common_dir,
157 namespace_prefix: crate::ref_namespace::ref_storage_prefix_default(),
158 }))
159 }
160
161 fn packed_refs_stamp(path: &Path) -> PackedRefsStamp {
162 match fs::metadata(path) {
163 Ok(meta) => PackedRefsStamp {
164 len: meta.len(),
165 modified: meta.modified().ok(),
166 identity: file_identity(&meta),
167 },
168 Err(_) => PackedRefsStamp {
169 len: 0,
170 modified: None,
171 identity: None,
172 },
173 }
174 }
175
176 fn forget_packed_cache(&self) {
182 let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
183 guard.loaded = false;
184 guard.file = None;
185 guard.map = None;
186 guard.map_is_complete = false;
187 }
188
189 fn invalidate_packed_cache_after_write(&self) {
190 let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
191 guard.map = None;
192 guard.map_is_complete = false;
193 }
194
195 fn refresh_packed_cache_if_stale(&self) -> Result<()> {
196 let path = super::paths::packed_refs_path(&self.common_dir);
197 let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
198 let stamp = Self::packed_refs_stamp(&path);
199 if guard.loaded && guard.stamp == stamp {
200 return Ok(());
201 }
202 guard.file = read_packed_refs_file_view(&self.common_dir)?;
203 guard.map = None;
204 guard.map_is_complete = false;
205 guard.stamp = stamp;
206 guard.loaded = true;
207 Ok(())
208 }
209
210 fn ensure_full_packed_map(&self) -> Result<()> {
211 self.refresh_packed_cache_if_stale()?;
212 let mut guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
213 if guard.map.is_some() && guard.map_is_complete {
214 return Ok(());
215 }
216 let map = if let Some(ref file) = guard.file {
217 let path = super::paths::packed_refs_path(&self.common_dir);
218 file.map_for_prefix(&path, None)?
219 .into_iter()
220 .collect::<BTreeMap<_, _>>()
221 } else {
222 BTreeMap::new()
223 };
224 guard.map = Some(map);
225 guard.map_is_complete = true;
226 Ok(())
227 }
228
229 fn first_packed_descendant(&self, storage_name: &str) -> Result<Option<(String, ObjectId)>> {
230 self.refresh_packed_cache_if_stale()?;
231 let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
232 let Some(ref file) = guard.file else {
233 return Ok(None);
234 };
235 let path = super::paths::packed_refs_path(&self.common_dir);
236 file.first_descendant(&path, storage_name)
237 }
238
239 fn packed_map_for_prefix_filtered(
242 &self,
243 prefix: &str,
244 keep: Option<PackedRecordFilter<'_>>,
245 ) -> Result<HashMap<String, ObjectId>> {
246 self.refresh_packed_cache_if_stale()?;
247 let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
248 let Some(ref file) = guard.file else {
249 return Ok(HashMap::new());
250 };
251 let path = super::paths::packed_refs_path(&self.common_dir);
252 let prefix = (!prefix.is_empty()).then_some(prefix);
253 file.map_for_prefix_filtered(&path, prefix, keep)
254 }
255
256 pub(crate) fn packed_refs_view(&self) -> Result<Option<crate::refs::PackedRefsFileView>> {
259 self.refresh_packed_cache_if_stale()?;
260 let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
261 Ok(guard.file.clone())
262 }
263
264 fn lookup_packed_oid(&self, storage_name: &str) -> Result<Option<ObjectId>> {
265 self.refresh_packed_cache_if_stale()?;
266 let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
267 let Some(ref file) = guard.file else {
268 return Ok(None);
269 };
270 let path = super::paths::packed_refs_path(&self.common_dir);
271 file.lookup_oid(&path, storage_name)
272 }
273
274 fn insert_create_conflict_neighbors(
281 &self,
282 refs: &mut BTreeMap<String, RawRef>,
283 updates: &[RefUpdate],
284 ) -> Result<()> {
285 let creates: Vec<&str> = updates
286 .iter()
287 .filter(|u| u.new_value.is_some() && !u.flags.log_only && !refs.contains_key(&u.name))
288 .map(|u| u.name.as_str())
289 .collect();
290 for name in creates {
291 for (slash, _) in name.match_indices('/') {
292 let ancestor = &name[..slash];
293 if refs.contains_key(ancestor) {
294 continue;
295 }
296 if let Some(raw) = self.read_existing_ref_named(ancestor)? {
297 refs.insert(ancestor.to_owned(), raw);
298 }
299 }
300 for (descendant, raw) in self.descendant_refs(name)? {
301 refs.entry(descendant).or_insert(raw);
302 }
303 }
304 Ok(())
305 }
306
307 fn verify_create_available(
315 &self,
316 name: &str,
317 extras: &BTreeSet<String>,
318 ) -> std::result::Result<(), RefnameUnavailable> {
319 let mut blocking = BTreeSet::new();
320 for (slash, _) in name.match_indices('/') {
321 let ancestor = &name[..slash];
322 if let Ok(Some(_)) = self.read_existing_ref_named(ancestor) {
323 blocking.insert(ancestor.to_owned());
324 }
325 }
326 let storage = self.route(name).storage_name;
327 let mut storage_names = BTreeSet::new();
328 for base in self.store_bases() {
329 collect_loose_file_names(&base.join(&storage), &storage, &mut storage_names);
330 }
331 if storage.contains('/') {
332 if let Ok(Some((packed, _))) = self.first_packed_descendant(&storage) {
333 storage_names.insert(packed);
334 }
335 }
336 blocking.extend(storage_names.iter().map(|s| self.logical_name(s)));
337 verify_name_available_for_create(name, &blocking, extras)
338 }
339
340 fn store_bases(&self) -> impl Iterator<Item = &Path> {
341 let common = self.common_dir.as_path();
342 let git = self.git_dir.as_path();
343 std::iter::once(common).chain((git != common).then_some(git))
344 }
345
346 fn read_existing_ref_named(&self, name: &str) -> Result<Option<RawRef>> {
350 let storage = self.route(name).storage_name;
351 for base in self.store_bases() {
352 let path = base.join(&storage);
353 if !path.is_file() {
354 continue;
355 }
356 if let Ok(r) = read_ref_file(&path) {
357 return Ok(Some(ref_to_raw(r)));
358 }
359 }
360 if !self.packed_storage_visible(&storage) {
361 return Ok(None);
362 }
363 Ok(self.lookup_packed_oid(&storage)?.map(RawRef::Direct))
364 }
365
366 fn descendant_refs(&self, name: &str) -> Result<BTreeMap<String, RawRef>> {
368 let storage = self.route(name).storage_name;
369 let mut storage_map = BTreeMap::new();
370 for base in self.store_bases() {
371 let dir = base.join(&storage);
372 if dir.is_dir() {
373 collect_loose_raw_refs(&dir, &format!("{storage}/"), "", &mut storage_map)?;
374 }
375 }
376 if storage.contains('/') {
379 if let Some((packed, oid)) = self.first_packed_descendant(&storage)? {
380 if self.packed_storage_visible(&packed) {
381 storage_map.entry(packed).or_insert(RawRef::Direct(oid));
382 }
383 }
384 }
385 Ok(storage_map
386 .into_iter()
387 .map(|(storage, value)| (self.logical_name(&storage), value))
388 .collect())
389 }
390
391 fn insert_symref_peel_chain(
393 &self,
394 refs: &mut BTreeMap<String, RawRef>,
395 start: &str,
396 ) -> Result<()> {
397 let mut name = start.to_owned();
398 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
399 if refs.contains_key(&name) {
400 match refs.get(&name) {
401 Some(RawRef::Symbolic(next)) => name = next.clone(),
402 _ => break,
403 }
404 continue;
405 }
406 let Some(raw) = self.read_raw(&name)? else {
407 break;
408 };
409 let advance = match &raw {
410 RawRef::Symbolic(next) => Some(next.clone()),
411 RawRef::Direct(_) => None,
412 };
413 refs.insert(name.clone(), raw);
414 match advance {
415 Some(next) => name = next,
416 None => break,
417 }
418 }
419 Ok(())
420 }
421
422 fn load_batch_state_for_prepare(
423 &self,
424 updates: &[RefUpdate],
425 reflog_renames: &[super::RefReflogRename],
426 case_only_renames: &[super::RefCaseOnlyRename],
427 ) -> Result<RefBatchState> {
428 let mut refs = BTreeMap::new();
429 for update in updates {
430 if let Some(raw) = self.read_raw(&update.name)? {
431 refs.insert(update.name.clone(), raw);
432 }
433 }
434 for rename in case_only_renames {
435 if refs.contains_key(&rename.from) {
436 continue;
437 }
438 if let Some(raw) = self.read_raw(&rename.from)? {
439 refs.insert(rename.from.clone(), raw);
440 }
441 }
442 self.insert_head_for_batch_state(&mut refs)?;
443
444 for update in updates {
445 if matches!(refs.get(&update.name), Some(RawRef::Symbolic(_))) {
446 self.insert_symref_peel_chain(&mut refs, &update.name)?;
447 }
448 }
449 if matches!(refs.get("HEAD"), Some(RawRef::Symbolic(_))) {
450 self.insert_symref_peel_chain(&mut refs, "HEAD")?;
451 }
452
453 let mut reflogs = self.load_reflogs_for_updates(&refs, updates)?;
454 for rename in reflog_renames {
455 if reflogs.contains_key(&rename.from) {
456 continue;
457 }
458 let entries = read_reflog_at_path(&self.reflog_path(&rename.from))?;
459 if !entries.is_empty() {
460 reflogs.insert(rename.from.clone(), entries);
461 }
462 }
463 Ok(RefBatchState { refs, reflogs })
464 }
465
466 fn ensure_reflog_path_writable(&self, name: &str) -> StoreResult<()> {
467 let path = self.reflog_path(name);
468 if path.is_dir() {
469 return Err(RefStoreError::Corrupt(format!(
470 "refusing to write reflog at '{}': path is a directory",
471 path.display()
472 )));
473 }
474 Ok(())
475 }
476
477 fn read_raw_at_route(&self, route: &RefStorageRoute) -> Result<Option<RawRef>> {
478 let path = route.storage_dir.join(&route.storage_name);
479 match read_ref_file(&path) {
480 Ok(Ref::Direct(oid)) => return Ok(Some(RawRef::Direct(oid))),
481 Ok(Ref::Symbolic(target)) => return Ok(Some(RawRef::Symbolic(target))),
482 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
483 Err(e) => return Err(e),
484 }
485 if let Some(oid) = self.lookup_packed_oid(&route.storage_name)? {
486 return Ok(Some(RawRef::Direct(oid)));
487 }
488 Ok(None)
489 }
490
491 fn packed_storage_visible(&self, storage: &str) -> bool {
492 match self.namespace_prefix.as_deref() {
493 None => true,
494 Some(prefix) => storage.starts_with(prefix),
495 }
496 }
497
498 fn packed_refs_scan_prefix(&self, logical_prefix: &str) -> String {
500 match self.namespace_prefix.as_deref() {
501 Some(ns) if !logical_prefix.is_empty() => format!("{ns}{logical_prefix}"),
502 Some(ns) => ns.trim_end_matches('/').to_owned(),
503 None => logical_prefix.to_owned(),
504 }
505 }
506
507 fn load_raw_map_prefix(
510 &self,
511 logical_prefix: &str,
512 keep: Option<PackedRecordFilter<'_>>,
513 ) -> Result<BTreeMap<String, RawRef>> {
514 let logical_prefix = logical_prefix.trim_end_matches('/');
515 let mut map: BTreeMap<String, RawRef> = BTreeMap::new();
516 if logical_prefix.is_empty() {
517 self.ensure_full_packed_map()?;
518 let cache = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
519 if let Some(packed) = cache.map.as_ref() {
520 for (storage, oid) in packed.iter() {
521 if !self.packed_storage_visible(storage)
522 || keep.is_some_and(|keep| !keep(storage, oid, PackedPeel::Unknown))
523 {
524 continue;
525 }
526 let logical = self.logical_name(storage);
527 map.insert(logical, RawRef::Direct(*oid));
528 }
529 }
530 drop(cache);
531 } else {
532 let common = self.common_dir.as_path();
533 let git = self.git_dir.as_path();
534 let bases: [&Path; 2] = if common == git {
535 [common, common]
536 } else {
537 [common, git]
538 };
539 for (i, base) in bases.iter().enumerate() {
540 if i == 1 && *base == bases[0] {
541 continue;
542 }
543 let packed_prefix = self.packed_refs_scan_prefix(logical_prefix);
544 let packed = if i == 0 {
545 self.packed_map_for_prefix_filtered(&packed_prefix, keep)?
546 } else {
547 read_packed_refs_map_for_prefix_filtered(base, &packed_prefix, keep)?
548 };
549 for (storage, oid) in packed {
550 if !self.packed_storage_visible(&storage) {
551 continue;
552 }
553 let logical = self.logical_name(&storage);
554 map.insert(logical, RawRef::Direct(oid));
555 }
556 }
557 }
558
559 for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
560 let storage_prefix = self.namespace_prefix.as_deref().unwrap_or("refs/");
561 if self.namespace_prefix.is_some() {
562 let root = base.join(storage_prefix.trim_end_matches('/'));
563 if root.is_dir() {
564 let mut storage_map = BTreeMap::new();
565 collect_loose_raw_refs(&root, storage_prefix, "", &mut storage_map)?;
566 for (storage, value) in storage_map {
567 let logical = self.logical_name(&storage);
568 if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
569 map.insert(logical, value);
570 }
571 }
572 }
573 } else {
574 let (walk_root, walk_prefix) = loose_ref_list_walk_root(base, logical_prefix);
575 if walk_root.is_dir() {
576 let mut storage_map = BTreeMap::new();
577 collect_loose_raw_refs(
578 &walk_root,
579 &walk_prefix,
580 logical_prefix,
581 &mut storage_map,
582 )?;
583 for (storage, value) in storage_map {
584 let logical = self.logical_name(&storage);
585 if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
586 map.insert(logical, value);
587 }
588 }
589 }
590 }
591 if head_matches_prefix(logical_prefix) && base.join("HEAD").is_file() {
592 if let Ok(r) = read_ref_file(&base.join("HEAD")) {
593 map.insert("HEAD".to_owned(), ref_to_raw(r));
594 }
595 }
596 }
597 Ok(map)
598 }
599
600 fn load_reflogs_for_updates(
604 &self,
605 refs: &BTreeMap<String, RawRef>,
606 updates: &[RefUpdate],
607 ) -> Result<BTreeMap<String, Vec<ReflogEntry>>> {
608 let mut names = BTreeSet::new();
609 for update in updates.iter().filter(|u| u.reflog.is_some()) {
610 let mut name = update.name.as_str();
611 names.insert(name.to_owned());
612 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
613 let Some(RawRef::Symbolic(next)) = refs.get(name) else {
614 break;
615 };
616 name = next;
617 names.insert(name.to_owned());
618 }
619 }
620 let mut out = BTreeMap::new();
621 for name in names {
622 let entries = read_reflog_at_path(&self.reflog_path(&name))?;
623 if !entries.is_empty() {
624 out.insert(name, entries);
625 }
626 }
627 Ok(out)
628 }
629
630 fn insert_head_for_batch_state(&self, map: &mut BTreeMap<String, RawRef>) -> Result<()> {
631 if map.contains_key("HEAD") {
632 return Ok(());
633 }
634 for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
635 let head_path = base.join("HEAD");
636 if head_path.is_file() {
637 let raw = read_ref_file(&head_path).map(ref_to_raw)?;
638 map.insert("HEAD".to_owned(), raw);
639 break;
640 }
641 }
642 Ok(())
643 }
644
645 fn loose_ref_file_exists(&self, refname: &str) -> Result<bool> {
646 let route = self.route(refname);
647 let path = route.storage_dir.join(&route.storage_name);
648 Ok(path.is_file())
649 }
650
651 fn route(&self, refname: &str) -> RefStorageRoute {
652 route_ref_storage_in_common(
653 &self.git_dir,
654 &self.common_dir,
655 refname,
656 self.namespace_prefix.as_deref(),
657 )
658 }
659
660 fn logical_name(&self, storage: &str) -> String {
661 crate::ref_namespace::logical_ref_name_with_prefix(
662 self.namespace_prefix.as_deref(),
663 storage,
664 )
665 }
666
667 fn reflog_path(&self, logical: &str) -> PathBuf {
668 let route = self.route(logical);
669 route.storage_dir.join("logs").join(&route.storage_name)
670 }
671
672 fn ref_lock_paths(&self, refname: &str) -> (RefStorageRoute, PathBuf, PathBuf) {
673 let route = self.route(refname);
674 let path = route.storage_dir.join(&route.storage_name);
675 let lock = lock_path_for_ref(&path);
676 (route, path, lock)
677 }
678
679 fn needs_packed_lock(&self, updates: &[RefUpdate]) -> bool {
680 updates.iter().any(|u| {
681 if u.new_value.is_some() || u.flags.log_only {
682 return false;
683 }
684 let storage = self.route(&u.name).storage_name;
685 self.lookup_packed_oid(&storage).ok().flatten().is_some()
686 })
687 }
688
689 fn acquire_ref_lock_mapped(
690 &self,
691 lock_path: &Path,
692 refname: &str,
693 wait: LockWait,
694 ) -> Result<()> {
695 use crate::lockfile::{LockError, LockFile};
696 LockFile::acquire(lock_path, wait)
697 .map(|guard| guard.detach())
698 .map_err(|err| match err {
699 LockError::Held { .. } => Error::RefStore(RefStoreError::LockHeld {
700 name: refname.to_owned(),
701 }),
702 LockError::Io { source, path } => map_ref_store_path_io(
703 &self.common_dir,
704 &self.git_dir,
705 path.as_path(),
706 GitDirPathOperation::UpdateRef,
707 source,
708 ),
709 })
710 }
711
712 fn ref_store_error_from_lib(err: Error) -> RefStoreError {
713 match err {
714 Error::GitDirPathIo {
715 path,
716 operation,
717 source,
718 } => RefStoreError::git_dir_path_io(path, operation, source),
719 Error::RefStore(store_err) => store_err,
720 other => RefStoreError::Corrupt(other.to_string()),
721 }
722 }
723
724 fn io_ref_store<T>(
725 &self,
726 path: &Path,
727 operation: GitDirPathOperation,
728 result: io::Result<T>,
729 ) -> Result<T> {
730 io_with_ref_store_path(&self.common_dir, &self.git_dir, path, operation, result)
731 }
732
733 fn release_locks(batch: &PreparedFilesBatch) {
734 for lock in batch.ref_locks.values() {
735 let _ = fs::remove_file(lock);
736 }
737 if let Some(p) = &batch.packed_lock {
738 let _ = fs::remove_file(p);
739 }
740 }
741
742 fn write_loose_value(
743 common_dir: &Path,
744 checkout_git_dir: &Path,
745 path: &Path,
746 lock: &Path,
747 value: &RawRef,
748 ) -> Result<()> {
749 remove_empty_ref_directory(path);
750 if let Some(parent) = path.parent() {
751 io_with_ref_store_path(
752 common_dir,
753 checkout_git_dir,
754 parent,
755 GitDirPathOperation::CreateDir,
756 fs::create_dir_all(parent),
757 )?;
758 }
759 let content = match value {
760 RawRef::Direct(oid) => format!("{oid}\n"),
761 RawRef::Symbolic(target) => format!("ref: {target}\n"),
762 };
763 io_with_ref_store_path(
764 common_dir,
765 checkout_git_dir,
766 lock,
767 GitDirPathOperation::UpdateRef,
768 fs::write(lock, content),
769 )?;
770 io_with_ref_store_path(
771 common_dir,
772 checkout_git_dir,
773 path,
774 GitDirPathOperation::UpdateRef,
775 fs::rename(lock, path),
776 )?;
777 Ok(())
778 }
779
780 fn persist_case_only_loose_ref(
781 &self,
782 rename: &super::RefCaseOnlyRename,
783 after: &RefBatchState,
784 ctx: CommitContext<'_>,
785 ) -> Result<()> {
786 let Some(value) = after.refs.get(&rename.to) else {
787 return Err(Error::Message(format!(
788 "case-only rename missing destination ref '{}'",
789 rename.to
790 )));
791 };
792 let (_, from_path, from_lock) = self.ref_lock_paths(&rename.from);
793 if let Some(parent) = from_lock.parent() {
794 fs::create_dir_all(parent).map_err(Error::Io)?;
795 }
796 self.acquire_ref_lock_mapped(&from_lock, &rename.from, ctx.ref_lock_wait)?;
797 if !from_path.is_file() {
798 self.remove_packed_record(&rename.from, ctx)?;
799 FilesRefStore::write_loose_value(
800 &self.common_dir,
801 &self.git_dir,
802 &from_path,
803 &from_lock,
804 value,
805 )?;
806 }
807 let (_, to_path, _) = self.ref_lock_paths(&rename.to);
808 if from_path != to_path {
809 if let Some(parent) = to_path.parent() {
810 fs::create_dir_all(parent).map_err(Error::Io)?;
811 }
812 fs::rename(&from_path, &to_path).map_err(Error::Io)?;
813 }
814 let _ = fs::remove_file(&from_lock);
815 Ok(())
816 }
817
818 fn remove_packed_record(&self, refname: &str, ctx: CommitContext<'_>) -> Result<()> {
820 let route = self.route(refname);
821 if !ctx.lock_held {
822 return remove_packed_ref(&route.storage_dir, &route.storage_name);
823 }
824 if ctx.pruned.contains(refname) {
825 return Ok(());
826 }
827 remove_packed_ref_under_lock(&route.storage_dir, &route.storage_name)
828 }
829
830 fn prune_deleted_packed_records(
838 &self,
839 before: &RefBatchState,
840 after: &RefBatchState,
841 updates: &[RefUpdate],
842 ) -> Result<PrunedPackedRecords> {
843 let checked: HashSet<&str> = updates
844 .iter()
845 .filter(|u| u.new_value.is_none() && !u.flags.log_only && u.expected != Expected::Any)
846 .map(|u| u.name.as_str())
847 .collect();
848 let mut pruned = PrunedPackedRecords::default();
849 let mut by_dir: BTreeMap<PathBuf, (HashSet<String>, HashMap<String, String>)> =
850 BTreeMap::new();
851 for name in before.refs.keys() {
852 if after.refs.contains_key(name) {
853 continue;
854 }
855 let route = self.route(name);
856 let (storage_names, checked_names) = by_dir.entry(route.storage_dir).or_default();
857 if checked.contains(name.as_str()) {
858 checked_names.insert(route.storage_name.clone(), name.clone());
859 }
860 storage_names.insert(route.storage_name);
861 pruned.names.insert(name.clone());
862 }
863 for (dir, (storage_names, checked_names)) in by_dir {
864 let records = remove_packed_refs_under_lock(&dir, &storage_names)?;
865 for record in &records {
866 let Some(logical) = checked_names.get(packed_record_name(record)) else {
867 continue;
868 };
869 if let Some(oid) = packed_record_oid(record) {
870 pruned.removed.insert(logical.clone(), RawRef::Direct(oid));
871 }
872 }
873 if !records.is_empty() {
874 pruned.records.push((dir, records));
875 }
876 }
877 self.invalidate_packed_cache_after_write();
878 Ok(pruned)
879 }
880
881 fn restore_unprocessed_packed_records(
884 &self,
885 pruned: &PrunedPackedRecords,
886 persisted: &HashSet<String>,
887 ) -> Result<()> {
888 let mut unprocessed: HashMap<PathBuf, HashSet<String>> = HashMap::new();
889 for name in pruned.names.difference(persisted) {
890 let route = self.route(name);
891 unprocessed
892 .entry(route.storage_dir)
893 .or_default()
894 .insert(route.storage_name);
895 }
896 for (dir, records) in &pruned.records {
897 let Some(names) = unprocessed.get(dir) else {
898 continue;
899 };
900 let restore: Vec<&str> = records
901 .iter()
902 .map(String::as_str)
903 .filter(|record| names.contains(packed_record_name(record)))
904 .collect();
905 restore_packed_records_under_lock(dir, &restore)?;
906 }
907 self.invalidate_packed_cache_after_write();
908 Ok(())
909 }
910
911 fn delete_loose_ref(&self, refname: &str, lock: &Path, ctx: CommitContext<'_>) -> Result<()> {
912 let route = self.route(refname);
913 let path = route.storage_dir.join(&route.storage_name);
914 self.remove_packed_record(refname, ctx)?;
915 remove_empty_ref_directory(&path);
916 let _ = fs::remove_file(&path);
917 let _ = fs::remove_file(lock);
918 if let Some(parent) = path.parent() {
919 ctx.emptied_dirs
920 .borrow_mut()
921 .entry(parent.to_path_buf())
922 .or_insert(route.storage_dir);
923 }
924 Ok(())
925 }
926
927 fn persist_reflog(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
928 let path = self.reflog_path(name);
929 if entries.is_empty() {
930 if let Some(parent) = path.parent() {
931 self.io_ref_store(
932 parent,
933 GitDirPathOperation::CreateDir,
934 fs::create_dir_all(parent),
935 )?;
936 }
937 self.io_ref_store(
938 &path,
939 GitDirPathOperation::AppendReflog,
940 fs::write(&path, ""),
941 )?;
942 return Ok(());
943 }
944 if let Some(parent) = path.parent() {
945 self.io_ref_store(
946 parent,
947 GitDirPathOperation::CreateDir,
948 fs::create_dir_all(parent),
949 )?;
950 }
951 let mut body = String::new();
952 for entry in entries {
953 let (old_hex, new_hex) =
954 crate::refs::reflog_oid_hex_pair(&entry.old_oid, &entry.new_oid);
955 if entry.message.is_empty() {
956 body.push_str(&format!("{old_hex} {new_hex} {}\n", entry.identity));
957 } else {
958 body.push_str(&format!(
959 "{old_hex} {new_hex} {}\t{}\n",
960 entry.identity, entry.message
961 ));
962 }
963 }
964 self.io_ref_store(
965 &path,
966 GitDirPathOperation::AppendReflog,
967 fs::write(&path, body),
968 )?;
969 Ok(())
970 }
971
972 fn peel_ref_to_oid(&self, start: &str) -> Result<Option<ObjectId>> {
973 let mut name = start.to_owned();
974 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
975 match self.read_raw(&name)? {
976 Some(RawRef::Direct(oid)) => return Ok(Some(oid)),
977 Some(RawRef::Symbolic(next)) => name = next,
978 None => return Ok(None),
979 }
980 }
981 Err(Error::RefStore(RefStoreError::SymrefLoop))
982 }
983
984 fn recheck_update_at_commit(
985 &self,
986 update: &RefUpdate,
987 batch_extras: &BTreeSet<String>,
988 batch_start: &RefBatchState,
989 state_before: &RefBatchState,
990 ctx: CommitContext<'_>,
991 ) -> StoreResult<()> {
992 if update.flags.log_only {
993 return Ok(());
994 }
995 let batch_differs =
996 batch_start.refs.get(&update.name) != state_before.refs.get(&update.name);
997 if !super::apply::expected_matches_for_update(state_before, update) {
998 return Err(RefStoreError::ExpectedMismatch {
999 name: update.name.clone(),
1000 expected: update.expected.clone(),
1001 actual: state_before.refs.get(&update.name).cloned(),
1002 });
1003 }
1004 if !batch_differs && update.new_value.is_none() && update.expected == Expected::Any {
1005 return Ok(());
1006 }
1007 let actual = if batch_differs {
1008 state_before.refs.get(&update.name).cloned()
1009 } else {
1010 self.read_raw(&update.name)
1011 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1012 .or_else(|| ctx.removed.get(&update.name).cloned())
1013 };
1014 let actual_ref = actual.as_ref();
1015 if !batch_differs {
1016 let disk_ok = match &update.expected {
1017 Expected::Oid(want)
1018 if matches!(actual_ref, Some(RawRef::Symbolic(_)))
1019 && matches!(update.new_value.as_ref(), Some(RawRef::Direct(_)))
1020 && !update.flags.no_deref =>
1021 {
1022 self.peel_ref_to_oid(&update.name)
1023 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1024 .is_some_and(|got| got == *want)
1025 }
1026 _ => expected_matches(actual_ref, &update.expected),
1027 };
1028 if !disk_ok {
1029 return Err(RefStoreError::ExpectedMismatch {
1030 name: update.name.clone(),
1031 expected: update.expected.clone(),
1032 actual,
1033 });
1034 }
1035 }
1036 if update.new_value.is_some() && actual_ref.is_none() && !batch_differs {
1037 self.verify_create_available(&update.name, batch_extras)?;
1038 }
1039 Ok(())
1040 }
1041
1042 fn persist_one_ref_change(
1044 &self,
1045 name: &str,
1046 value: Option<&RawRef>,
1047 lock: &Path,
1048 ctx: CommitContext<'_>,
1049 ) -> Result<()> {
1050 let (_, path, _) = self.ref_lock_paths(name);
1051 match value {
1052 None => self.delete_loose_ref(name, lock, ctx),
1053 Some(value) => FilesRefStore::write_loose_value(
1054 &self.common_dir,
1055 &self.git_dir,
1056 &path,
1057 lock,
1058 value,
1059 ),
1060 }
1061 }
1062
1063 fn persist_one_ref_name(
1064 &self,
1065 name: &str,
1066 value: Option<&RawRef>,
1067 ctx: CommitContext<'_>,
1068 persisted_refs: &mut HashSet<String>,
1069 ) -> Result<()> {
1070 let (_, _, lock_path) = self.ref_lock_paths(name);
1071 if let Some(parent) = lock_path.parent() {
1072 self.io_ref_store(
1073 parent,
1074 GitDirPathOperation::CreateDir,
1075 fs::create_dir_all(parent),
1076 )?;
1077 }
1078 self.acquire_ref_lock_mapped(&lock_path, name, ctx.ref_lock_wait)?;
1079 let write_result = self.persist_one_ref_change(name, value, &lock_path, ctx);
1080 let _ = fs::remove_file(&lock_path);
1081 write_result?;
1082 persisted_refs.insert(name.to_owned());
1083 Ok(())
1084 }
1085
1086 #[allow(clippy::too_many_arguments)]
1096 fn persist_update_locked(
1097 &self,
1098 update: &RefUpdate,
1099 batch_extras: &BTreeSet<String>,
1100 before: &RefBatchState,
1101 running: &RefBatchState,
1102 touched: Option<&str>,
1103 ctx: CommitContext<'_>,
1104 persisted_refs: &mut HashSet<String>,
1105 writes_ref: bool,
1106 carried: &mut Option<(String, PathBuf)>,
1107 ) -> Result<()> {
1108 let value_after = |name: &str| {
1109 if touched == Some(name) {
1110 update.new_value.as_ref()
1111 } else {
1112 running.refs.get(name)
1113 }
1114 };
1115 let mut targets: Vec<String> = touched
1116 .filter(|t| *t != update.name && running.refs.get(*t) != update.new_value.as_ref())
1117 .map(str::to_owned)
1118 .into_iter()
1119 .collect();
1120 if writes_ref {
1121 targets.push(update.name.clone());
1122 }
1123
1124 let mut held: BTreeMap<String, PathBuf> = BTreeMap::new();
1125 let release = |held: &BTreeMap<String, PathBuf>| {
1126 for lock in held.values() {
1127 let _ = fs::remove_file(lock);
1128 }
1129 };
1130 let verify_name = (!matches!(update.expected, Expected::Any)).then_some(&update.name);
1131 for name in targets.iter().chain(verify_name) {
1132 if held.contains_key(name) {
1133 continue;
1134 }
1135 if let Some((_, lock)) = carried.take_if(|(carried_name, _)| carried_name == name) {
1136 held.insert(name.clone(), lock);
1137 continue;
1138 }
1139 let (_, _, lock_path) = self.ref_lock_paths(name);
1140 let acquired = match lock_path.parent() {
1141 Some(parent) => self.io_ref_store(
1142 parent,
1143 GitDirPathOperation::CreateDir,
1144 fs::create_dir_all(parent),
1145 ),
1146 None => Ok(()),
1147 }
1148 .and_then(|()| self.acquire_ref_lock_mapped(&lock_path, name, ctx.ref_lock_wait));
1149 if let Err(err) = acquired {
1150 release(&held);
1151 return Err(err);
1152 }
1153 held.insert(name.clone(), lock_path);
1154 }
1155
1156 if let Err(err) = self.recheck_update_at_commit(update, batch_extras, before, running, ctx)
1157 {
1158 release(&held);
1159 return Err(err.into());
1160 }
1161 #[cfg(test)]
1162 tests::run_after_recheck_hook();
1163
1164 for name in &targets {
1165 let Some(lock_path) = held.remove(name) else {
1166 continue;
1167 };
1168 if let Err(err) = self.persist_one_ref_change(name, value_after(name), &lock_path, ctx)
1169 {
1170 let _ = fs::remove_file(&lock_path);
1171 release(&held);
1172 return Err(err);
1173 }
1174 persisted_refs.insert(name.clone());
1175 }
1176 release(&held);
1177 Ok(())
1178 }
1179
1180 fn persist_state_diff(
1181 &self,
1182 batch: &PreparedFilesBatch,
1183 carried: &mut Option<(String, PathBuf)>,
1184 emptied_dirs: &RefCell<BTreeMap<PathBuf, PathBuf>>,
1185 ) -> Result<()> {
1186 let before = &batch.before_commit;
1187 let after = &batch.after_commit;
1188 let updates = batch.updates.as_slice();
1189 let packed_lock_held = batch.packed_lock.is_some();
1190 let batch_extras: BTreeSet<String> = updates.iter().map(|u| u.name.clone()).collect();
1191 if updates.iter().any(|u| {
1192 u.new_value.is_some() && !u.flags.log_only && !before.refs.contains_key(&u.name)
1193 }) {
1194 self.forget_packed_cache();
1195 }
1196
1197 let mut names: BTreeSet<String> = BTreeSet::new();
1198 names.extend(before.refs.keys().cloned());
1199 names.extend(after.refs.keys().cloned());
1200 names.extend(before.reflogs.keys().cloned());
1201 names.extend(after.reflogs.keys().cloned());
1202
1203 for name in names {
1204 if before.reflogs.get(&name) != after.reflogs.get(&name) {
1205 let entries = after.reflogs.get(&name).cloned().unwrap_or_default();
1206 self.persist_reflog(&name, &entries)?;
1207 }
1208 }
1209
1210 let pruned = if packed_lock_held {
1211 self.prune_deleted_packed_records(before, after, updates)?
1212 } else {
1213 PrunedPackedRecords::default()
1214 };
1215 let ctx = CommitContext {
1216 ref_lock_wait: batch.ref_lock_wait,
1217 lock_held: packed_lock_held,
1218 pruned: &pruned.names,
1219 removed: &pruned.removed,
1220 emptied_dirs,
1221 };
1222
1223 let mut persisted_refs = HashSet::new();
1224 let persisted =
1225 self.persist_batch_refs(batch, &batch_extras, ctx, &mut persisted_refs, carried);
1226 if persisted.is_err() {
1227 let _ = self.restore_unprocessed_packed_records(&pruned, &persisted_refs);
1230 }
1231 persisted?;
1232 self.invalidate_packed_cache_after_write();
1233 Ok(())
1234 }
1235
1236 fn persist_batch_refs(
1239 &self,
1240 batch: &PreparedFilesBatch,
1241 batch_extras: &BTreeSet<String>,
1242 ctx: CommitContext<'_>,
1243 persisted_refs: &mut HashSet<String>,
1244 carried: &mut Option<(String, PathBuf)>,
1245 ) -> Result<()> {
1246 let before = &batch.before_commit;
1247 let after = &batch.after_commit;
1248 let updates = batch.updates.as_slice();
1249 for rename in &batch.case_only_renames {
1250 self.persist_case_only_loose_ref(rename, after, ctx)?;
1251 persisted_refs.insert(rename.from.clone());
1252 persisted_refs.insert(rename.to.clone());
1253 }
1254 let mut running = before.clone();
1255 for (idx, update) in updates.iter().enumerate() {
1256 if update.flags.log_only {
1257 apply_update(&mut running, update).map_err(Error::from)?;
1258 continue;
1259 }
1260 let touched = ref_change_target(&running, update).map_err(Error::from)?;
1261 let name = &update.name;
1262 let b = running.refs.get(name);
1263 let a = if touched.as_deref() == Some(name.as_str()) {
1264 update.new_value.as_ref()
1265 } else {
1266 b
1267 };
1268 let materialize_loose = update.new_value.is_some()
1269 && a.is_some()
1270 && b == a
1271 && !self
1272 .loose_ref_file_exists(name)
1273 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1274 let writes_ref = b != a || materialize_loose;
1275
1276 self.persist_update_locked(
1277 update,
1278 batch_extras,
1279 before,
1280 &running,
1281 touched.as_deref(),
1282 ctx,
1283 persisted_refs,
1284 writes_ref,
1285 carried,
1286 )?;
1287
1288 apply_update(&mut running, update).map_err(Error::from)?;
1289 crate::refs::test_ref_store_batch_pause_after_ref_write(idx, &self.common_dir);
1290 }
1291
1292 for name in before
1293 .refs
1294 .keys()
1295 .chain(after.refs.keys())
1296 .collect::<BTreeSet<_>>()
1297 {
1298 if persisted_refs.contains(name) {
1299 continue;
1300 }
1301 let b = before.refs.get(name);
1302 let a = after.refs.get(name);
1303 if b == a {
1304 continue;
1305 }
1306 self.persist_one_ref_name(name, a, ctx, persisted_refs)?;
1307 }
1308 Ok(())
1309 }
1310}
1311
1312impl FilesRefStore {
1313 pub(crate) fn for_each_ref_filtered(
1319 &self,
1320 prefix: &str,
1321 keep: Option<PackedRecordFilter<'_>>,
1322 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1323 ) -> Result<()> {
1324 let prefix = prefix.trim_end_matches('/');
1325 let mut map = self.load_raw_map_prefix(prefix, keep)?;
1326 for (name, value) in map.split_off(prefix) {
1327 if !name.starts_with(prefix) {
1328 break;
1329 }
1330 let peeled = match &value {
1331 RawRef::Direct(_) => None,
1332 RawRef::Symbolic(target) => self.resolve(target).ok(),
1333 };
1334 let entry = RefEntry {
1335 name,
1336 value,
1337 peeled,
1338 };
1339 if f(&entry).is_break() {
1340 break;
1341 }
1342 }
1343 Ok(())
1344 }
1345}
1346
1347impl RefStore for FilesRefStore {
1348 fn format(&self) -> RefStorageFormat {
1349 RefStorageFormat::Files
1350 }
1351
1352 fn read_raw(&self, name: &str) -> Result<Option<RawRef>> {
1353 self.read_raw_at_route(&self.route(name))
1354 }
1355
1356 fn for_each_ref(
1357 &self,
1358 prefix: &str,
1359 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1360 ) -> Result<()> {
1361 self.for_each_ref_filtered(prefix, None, f)
1362 }
1363
1364 fn prepare(
1365 &self,
1366 txn: RefTransaction,
1367 ) -> StoreResult<Box<dyn PreparedRefTransaction + Send + '_>> {
1368 txn.validate()?;
1369 let (updates, reflog_renames, case_only_renames) = txn.into_parts();
1370
1371 let mut prepared_guard = self.prepared.lock().unwrap_or_else(|e| e.into_inner());
1372 if prepared_guard.is_some() {
1373 return Err(RefStoreError::LockHeld {
1374 name: updates
1375 .first()
1376 .map(|u| u.name.clone())
1377 .unwrap_or_else(|| "refs".to_owned()),
1378 });
1379 }
1380
1381 let mut preliminary = self
1382 .load_batch_state_for_prepare(&updates, &reflog_renames, &case_only_renames)
1383 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1384 self.insert_create_conflict_neighbors(&mut preliminary.refs, &updates)
1385 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1386 verify_create_conflicts(&preliminary.refs, &updates)?;
1387 for rename in &reflog_renames {
1388 self.ensure_reflog_path_writable(&rename.to)?;
1389 }
1390 for rename in &case_only_renames {
1391 self.ensure_reflog_path_writable(&rename.to)?;
1392 }
1393 let lock_names = ref_lock_names_for_updates(&preliminary, &updates);
1394 let ref_lock_wait = LockWait::files_ref_lock_for_git_dir(&self.common_dir);
1395
1396 let mut ref_locks: HashMap<String, PathBuf> = HashMap::new();
1397 for name in &lock_names {
1398 let (_, _, lock) = self.ref_lock_paths(name);
1399 if let Some(parent) = lock.parent() {
1400 if let Err(err) = self.io_ref_store(
1401 parent,
1402 GitDirPathOperation::CreateDir,
1403 fs::create_dir_all(parent),
1404 ) {
1405 for acquired in ref_locks.values() {
1406 let _ = fs::remove_file(acquired);
1407 }
1408 return Err(Self::ref_store_error_from_lib(err));
1409 }
1410 }
1411 if let Err(err) = self.acquire_ref_lock_mapped(&lock, name, ref_lock_wait) {
1412 for acquired in ref_locks.values() {
1413 let _ = fs::remove_file(acquired);
1414 }
1415 return Err(Self::ref_store_error_from_lib(err));
1416 }
1417 ref_locks.insert(name.clone(), lock);
1418 }
1419
1420 if let Err(err) =
1421 simulate_batch_apply(&preliminary, &updates, &reflog_renames, &case_only_renames)
1422 {
1423 for acquired in ref_locks.values() {
1424 let _ = fs::remove_file(acquired);
1425 }
1426 return Err(err);
1427 }
1428
1429 let extras: BTreeSet<String> = updates.iter().map(|u| u.name.clone()).collect();
1430 for update in &updates {
1431 if update.new_value.is_none() || update.flags.log_only {
1432 continue;
1433 }
1434 if preliminary.refs.contains_key(&update.name) {
1435 continue;
1436 }
1437 if let Err(reason) = self.verify_create_available(&update.name, &extras) {
1438 for acquired in ref_locks.values() {
1439 let _ = fs::remove_file(acquired);
1440 }
1441 return Err(reason.into());
1442 }
1443 }
1444
1445 let before_commit = preliminary;
1446 let mut after_commit = before_commit.clone();
1447 for update in &updates {
1448 apply_update(&mut after_commit, update)?;
1449 }
1450 for rename in &case_only_renames {
1451 apply_case_only_ref_rename(&mut after_commit, rename)?;
1452 }
1453 for rename in &reflog_renames {
1454 apply_reflog_rename(&mut after_commit, rename)?;
1455 }
1456 let mut changed = changed_ref_names(&before_commit.refs, &after_commit.refs);
1457 for update in &updates {
1458 if update.flags.log_only || update.new_value.is_none() {
1459 continue;
1460 }
1461 if before_commit.refs.contains_key(&update.name)
1462 && !self
1463 .loose_ref_file_exists(&update.name)
1464 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1465 {
1466 changed.insert(update.name.clone());
1467 }
1468 }
1469
1470 for name in &changed {
1471 if ref_locks.contains_key(name) {
1472 continue;
1473 }
1474 let (_, _, lock) = self.ref_lock_paths(name);
1475 if let Some(parent) = lock.parent() {
1476 if let Err(err) = self.io_ref_store(
1477 parent,
1478 GitDirPathOperation::CreateDir,
1479 fs::create_dir_all(parent),
1480 ) {
1481 for acquired in ref_locks.values() {
1482 let _ = fs::remove_file(acquired);
1483 }
1484 return Err(Self::ref_store_error_from_lib(err));
1485 }
1486 }
1487 if let Err(err) = self.acquire_ref_lock_mapped(&lock, name, ref_lock_wait) {
1488 for acquired in ref_locks.values() {
1489 let _ = fs::remove_file(acquired);
1490 }
1491 return Err(Self::ref_store_error_from_lib(err));
1492 }
1493 ref_locks.insert(name.clone(), lock);
1494 }
1495
1496 let mut packed_lock = None;
1497 if self.needs_packed_lock(&updates) {
1498 let packed_path = super::paths::packed_refs_path(&self.common_dir);
1499 let lock = lock_path_for_ref(&packed_path);
1500 use crate::lockfile::{LockError, LockFile};
1501 let wait = LockWait::packed_refs_for_git_dir(&self.common_dir);
1502 if let Err(err) = LockFile::acquire(&lock, wait).map(|guard| guard.detach()) {
1503 for acquired in ref_locks.values() {
1504 let _ = fs::remove_file(acquired);
1505 }
1506 return Err(match err {
1507 LockError::Held { .. } => RefStoreError::LockHeld {
1508 name: super::paths::PACKED_REFS.to_owned(),
1509 },
1510 LockError::Io { source, .. } => RefStoreError::Corrupt(source.to_string()),
1511 });
1512 }
1513 packed_lock = Some(lock);
1514 }
1515
1516 *prepared_guard = Some(PreparedFilesBatch {
1517 updates,
1518 case_only_renames,
1519 ref_locks,
1520 packed_lock,
1521 ref_lock_wait,
1522 before_commit,
1523 after_commit,
1524 });
1525
1526 Ok(Box::new(FilesPrepared {
1527 store: self,
1528 committed: false,
1529 }))
1530 }
1531
1532 fn reflog_exists(&self, name: &str) -> Result<bool> {
1533 Ok(self.reflog_path(name).is_file())
1534 }
1535
1536 fn for_each_reflog_entry(
1537 &self,
1538 name: &str,
1539 reverse: bool,
1540 f: &mut dyn FnMut(&ReflogEntry) -> ControlFlow<()>,
1541 ) -> Result<()> {
1542 let entries = read_reflog_at_path(&self.reflog_path(name))?;
1543 if reverse {
1544 for entry in entries.iter().rev() {
1545 if f(entry).is_break() {
1546 break;
1547 }
1548 }
1549 } else {
1550 for entry in &entries {
1551 if f(entry).is_break() {
1552 break;
1553 }
1554 }
1555 }
1556 Ok(())
1557 }
1558
1559 fn create_reflog(&self, name: &str) -> Result<()> {
1560 validate_storable_refname(name).map_err(|e| Error::Message(e.to_string()))?;
1561 let path = self.reflog_path(name);
1562 if let Some(parent) = path.parent() {
1563 self.io_ref_store(
1564 parent,
1565 GitDirPathOperation::CreateDir,
1566 fs::create_dir_all(parent),
1567 )?;
1568 }
1569 self.io_ref_store(
1570 &path,
1571 GitDirPathOperation::AppendReflog,
1572 fs::OpenOptions::new().create(true).append(true).open(&path),
1573 )?;
1574 Ok(())
1575 }
1576
1577 fn delete_reflog(&self, name: &str) -> Result<()> {
1578 let route = self.route(name);
1579 let logs_root = route.storage_dir.join("logs");
1580 let path = logs_root.join(&route.storage_name);
1581 let _ = fs::remove_file(&path);
1582 let mut parent = path.parent();
1585 while let Some(dir) = parent.filter(|d| *d != logs_root && d.starts_with(&logs_root)) {
1586 if fs::remove_dir(dir).is_err() {
1587 break;
1588 }
1589 parent = dir.parent();
1590 }
1591 Ok(())
1592 }
1593
1594 fn replace_reflog(&self, name: &str, entries: Vec<ReflogEntry>) -> Result<()> {
1595 self.persist_reflog(name, &entries)
1596 }
1597
1598 fn for_each_reflog_ref(&self, f: &mut dyn FnMut(&str) -> ControlFlow<()>) -> Result<()> {
1599 let mut names = BTreeSet::new();
1600 for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
1601 let logs = base.join("logs");
1602 if logs.is_dir() {
1603 collect_reflog_ref_names(&logs, "", self, &mut names)?;
1604 }
1605 }
1606 for name in names {
1607 if f(&name).is_break() {
1608 break;
1609 }
1610 }
1611 Ok(())
1612 }
1613
1614 fn as_files(&self) -> Option<&FilesRefStore> {
1615 Some(self)
1616 }
1617}
1618
1619struct FilesPrepared<'a> {
1620 store: &'a FilesRefStore,
1621 committed: bool,
1622}
1623
1624impl PreparedRefTransaction for FilesPrepared<'_> {
1625 fn commit(mut self: Box<Self>) -> StoreResult<()> {
1626 let mut guard = self
1627 .store
1628 .prepared
1629 .lock()
1630 .unwrap_or_else(|e| e.into_inner());
1631 let Some(mut batch) = guard.take() else {
1632 return Ok(());
1633 };
1634
1635 let mut ref_locks = std::mem::take(&mut batch.ref_locks);
1639 let mut carried = batch
1640 .updates
1641 .iter()
1642 .find(|u| !u.flags.log_only)
1643 .and_then(|u| ref_locks.remove_entry(&u.name));
1644 for lock in ref_locks.values() {
1645 let _ = fs::remove_file(lock);
1646 }
1647 let emptied_dirs = RefCell::new(BTreeMap::new());
1650 let persisted = self
1651 .store
1652 .persist_state_diff(&batch, &mut carried, &emptied_dirs);
1653 if let Some((_, lock)) = carried {
1654 let _ = fs::remove_file(lock);
1655 }
1656 let mut emptied_dirs: Vec<_> = emptied_dirs.into_inner().into_iter().collect();
1660 emptied_dirs.sort_by_key(|(dir, _)| std::cmp::Reverse(dir.components().count()));
1661 for (dir, storage_dir) in &emptied_dirs {
1662 prune_empty_ref_dirs(storage_dir, dir);
1663 }
1664 self.store.invalidate_packed_cache_after_write();
1665 if let Err(err) = persisted {
1666 if let Some(p) = &batch.packed_lock {
1667 let _ = fs::remove_file(p);
1668 }
1669 return Err(match err {
1670 Error::RefStore(store_err) => store_err,
1671 other => RefStoreError::Corrupt(other.to_string()),
1672 });
1673 }
1674 if let Some(p) = &batch.packed_lock {
1675 let _ = fs::remove_file(p);
1676 }
1677 self.committed = true;
1678 Ok(())
1679 }
1680
1681 fn abort(mut self: Box<Self>) -> StoreResult<()> {
1682 let mut guard = self
1683 .store
1684 .prepared
1685 .lock()
1686 .unwrap_or_else(|e| e.into_inner());
1687 if let Some(batch) = guard.take() {
1688 FilesRefStore::release_locks(&batch);
1689 }
1690 self.store.invalidate_packed_cache_after_write();
1691 self.committed = true;
1692 Ok(())
1693 }
1694}
1695
1696impl Drop for FilesPrepared<'_> {
1697 fn drop(&mut self) {
1698 if self.committed {
1699 return;
1700 }
1701 if let Ok(mut guard) = self.store.prepared.lock() {
1702 if let Some(batch) = guard.take() {
1703 FilesRefStore::release_locks(&batch);
1704 }
1705 }
1706 self.store.invalidate_packed_cache_after_write();
1707 }
1708}
1709
1710fn head_matches_prefix(logical_prefix: &str) -> bool {
1711 let prefix = logical_prefix.trim_end_matches('/');
1712 if prefix.is_empty() {
1713 return false;
1715 }
1716 "HEAD".starts_with(prefix) || prefix.starts_with("HEAD")
1717}
1718
1719fn ref_to_raw(r: Ref) -> RawRef {
1720 match r {
1721 Ref::Direct(oid) => RawRef::Direct(oid),
1722 Ref::Symbolic(s) => RawRef::Symbolic(s),
1723 }
1724}
1725
1726fn collect_loose_raw_refs(
1732 dir: &Path,
1733 prefix: &str,
1734 want: &str,
1735 out: &mut BTreeMap<String, RawRef>,
1736) -> Result<()> {
1737 let read = match fs::read_dir(dir) {
1738 Ok(r) => r,
1739 Err(e)
1740 if e.kind() == io::ErrorKind::NotFound || e.kind() == io::ErrorKind::NotADirectory =>
1741 {
1742 return Ok(());
1743 }
1744 Err(e) => return Err(Error::Io(e)),
1745 };
1746 for entry in read {
1747 let entry = entry?;
1748 let name_owned = entry.file_name().into_string().map_err(|_| {
1749 Error::Io(io::Error::new(
1750 io::ErrorKind::InvalidData,
1751 "non-utf8 ref path component",
1752 ))
1753 })?;
1754 let refname = format!("{prefix}{name_owned}");
1755 let wanted = refname.starts_with(want);
1756 let leads_to_wanted = || {
1757 want.strip_prefix(refname.as_str())
1758 .is_some_and(|rest| rest.starts_with('/'))
1759 };
1760 if !wanted && !leads_to_wanted() {
1761 continue;
1762 }
1763 let path = entry.path();
1764 let meta = match fs::metadata(&path) {
1765 Ok(m) => m,
1766 Err(_) => continue,
1768 };
1769 if meta.is_dir() {
1770 collect_loose_raw_refs(&path, &format!("{refname}/"), want, out)?;
1771 } else if meta.is_file() && wanted {
1772 if let Ok(r) = read_ref_file(&path) {
1773 out.insert(refname, ref_to_raw(r));
1774 }
1775 }
1776 }
1777 Ok(())
1778}
1779
1780fn collect_loose_file_names(dir: &Path, prefix: &str, out: &mut BTreeSet<String>) {
1783 let Ok(read) = fs::read_dir(dir) else {
1784 return;
1785 };
1786 for entry in read.flatten() {
1787 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
1788 let path = entry.path();
1789 match fs::metadata(&path) {
1790 Ok(meta) if meta.is_dir() => collect_loose_file_names(&path, &name, out),
1791 Ok(meta) if meta.is_file() => {
1792 out.insert(name);
1793 }
1794 _ => {}
1795 }
1796 }
1797}
1798
1799#[cfg(unix)]
1800fn file_identity(meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
1801 use std::os::unix::fs::MetadataExt;
1802 Some((meta.dev(), meta.ino(), meta.ctime(), meta.ctime_nsec()))
1803}
1804
1805#[cfg(not(unix))]
1806fn file_identity(_meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
1807 None
1808}
1809
1810fn read_reflog_at_path(path: &Path) -> Result<Vec<ReflogEntry>> {
1811 let content = match fs::read_to_string(path) {
1812 Ok(c) => c,
1813 Err(e)
1816 if matches!(
1817 e.kind(),
1818 io::ErrorKind::NotFound
1819 | io::ErrorKind::NotADirectory
1820 | io::ErrorKind::IsADirectory
1821 ) =>
1822 {
1823 return Ok(Vec::new());
1824 }
1825 Err(e) => return Err(Error::Io(e)),
1826 };
1827 Ok(content
1828 .lines()
1829 .filter_map(reflog::parse_reflog_line_for_store)
1830 .collect())
1831}
1832
1833fn changed_ref_names(
1834 before: &BTreeMap<String, RawRef>,
1835 after: &BTreeMap<String, RawRef>,
1836) -> BTreeSet<String> {
1837 let mut names: BTreeSet<String> = BTreeSet::new();
1838 names.extend(before.keys().cloned());
1839 names.extend(after.keys().cloned());
1840 names
1841 .into_iter()
1842 .filter(|name| before.get(name) != after.get(name))
1843 .collect()
1844}
1845
1846fn collect_reflog_ref_names(
1847 dir: &Path,
1848 prefix: &str,
1849 store: &FilesRefStore,
1850 out: &mut BTreeSet<String>,
1851) -> Result<()> {
1852 let read = match fs::read_dir(dir) {
1853 Ok(r) => r,
1854 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1855 Err(e) => return Err(Error::Io(e)),
1856 };
1857 for entry in read {
1858 let entry = entry?;
1859 let name = entry.file_name().to_string_lossy().into_owned();
1860 let refname = format!("{prefix}{name}");
1861 let path = entry.path();
1862 if path.is_dir() {
1863 collect_reflog_ref_names(&path, &format!("{refname}/"), store, out)?;
1864 } else if path.is_file() {
1865 out.insert(store.logical_name(&refname));
1866 }
1867 }
1868 Ok(())
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use std::cell::RefCell;
1874
1875 use super::*;
1876 use crate::refs::store::RefUpdateFlags;
1877
1878 thread_local! {
1880 static AFTER_RECHECK: RefCell<Option<Box<dyn FnOnce()>>> = const { RefCell::new(None) };
1881 }
1882
1883 pub(super) fn run_after_recheck_hook() {
1885 if let Some(hook) = AFTER_RECHECK.with(|slot| slot.borrow_mut().take()) {
1886 hook();
1887 }
1888 }
1889
1890 fn after_next_recheck(hook: impl FnOnce() + 'static) {
1893 AFTER_RECHECK.with(|slot| *slot.borrow_mut() = Some(Box::new(hook)));
1894 }
1895
1896 fn oid(byte: u8) -> ObjectId {
1897 let mut bytes = [0u8; 20];
1898 bytes[19] = byte;
1899 ObjectId::from_bytes(&bytes).expect("oid")
1900 }
1901
1902 fn store(git_dir: &Path) -> FilesRefStore {
1903 FilesRefStore::open(FilesRefStoreConfig {
1904 git_dir: git_dir.to_path_buf(),
1905 common_dir: git_dir.to_path_buf(),
1906 namespace_prefix: None,
1907 })
1908 }
1909
1910 fn set(
1911 store: &FilesRefStore,
1912 name: &str,
1913 value: ObjectId,
1914 expected: Expected,
1915 ) -> StoreResult<()> {
1916 let txn = RefTransaction::new().update(RefUpdate {
1917 name: name.to_owned(),
1918 new_value: Some(RawRef::Direct(value)),
1919 expected,
1920 reflog: None,
1921 flags: RefUpdateFlags::default(),
1922 })?;
1923 store.prepare(txn)?.commit()
1924 }
1925
1926 fn git_dir() -> tempfile::TempDir {
1927 let dir = tempfile::tempdir().expect("tempdir");
1928 fs::create_dir_all(dir.path().join("refs/heads")).expect("refs");
1929 fs::write(dir.path().join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
1930 dir
1931 }
1932
1933 fn listed_names(store: &FilesRefStore, prefix: &str) -> Vec<String> {
1934 let mut names = Vec::new();
1935 store
1936 .for_each_ref(prefix, &mut |entry| {
1937 names.push(entry.name.clone());
1938 ControlFlow::Continue(())
1939 })
1940 .expect("list");
1941 names
1942 }
1943
1944 #[test]
1945 fn partial_name_prefix_lists_loose_refs_without_visiting_siblings() {
1946 let dir = git_dir();
1947 let tags = dir.path().join("refs/tags");
1948 for name in ["v1", "v10", "v1x/a", "v2/deep", "w"] {
1949 let path = tags.join(name);
1950 fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
1951 fs::write(path, format!("{}\n", oid(1))).expect("ref");
1952 }
1953 #[cfg(unix)]
1955 std::os::unix::fs::symlink(tags.join("missing-target"), tags.join("zzz"))
1956 .expect("dangling symlink");
1957 let store = store(dir.path());
1958
1959 assert_eq!(
1960 listed_names(&store, "refs/tags/v1"),
1961 ["refs/tags/v1", "refs/tags/v10", "refs/tags/v1x/a"]
1962 );
1963 assert_eq!(listed_names(&store, "refs/tags/v2/"), ["refs/tags/v2/deep"]);
1964 assert_eq!(listed_names(&store, "refs/tags/w"), ["refs/tags/w"]);
1965 assert!(listed_names(&store, "refs/tags/x").is_empty());
1966 }
1967
1968 fn assert_racing_writer_is_locked_out(expected: Expected, seed: Option<ObjectId>) {
1971 let dir = git_dir();
1972 let name = "refs/notes/commits";
1973 let writer = store(dir.path());
1974 if let Some(seed) = seed {
1975 set(&writer, name, seed, Expected::Missing).expect("seed");
1976 }
1977
1978 let other = store(dir.path());
1979 let racing_expected = expected.clone();
1980 let racer = std::rc::Rc::new(RefCell::new(None));
1981 let racer_slot = racer.clone();
1982 after_next_recheck(move || {
1983 *racer_slot.borrow_mut() = Some(set(&other, name, oid(2), racing_expected));
1984 });
1985 set(&writer, name, oid(3), expected).expect("first committer wins");
1986
1987 let racer = racer.borrow_mut().take().expect("hook ran");
1988 assert!(
1989 matches!(racer, Err(RefStoreError::LockHeld { .. })),
1990 "racing writer must not commit while the ref is locked: {racer:?}"
1991 );
1992 assert_eq!(
1993 writer.read_raw(name).expect("read"),
1994 Some(RawRef::Direct(oid(3)))
1995 );
1996 assert!(
1997 !dir.path().join("refs/notes/commits.lock").exists(),
1998 "the lock is released after the write"
1999 );
2000 }
2001
2002 #[test]
2003 fn expected_oid_update_holds_the_lock_through_the_write() {
2004 assert_racing_writer_is_locked_out(Expected::Oid(oid(1)), Some(oid(1)));
2005 }
2006
2007 #[test]
2008 fn expected_missing_create_holds_the_lock_through_the_write() {
2009 assert_racing_writer_is_locked_out(Expected::Missing, None);
2010 }
2011
2012 #[test]
2013 fn symbolic_update_holds_the_dereferenced_target_lock_through_the_write() {
2014 let dir = git_dir();
2015 let writer = store(dir.path());
2016 set(&writer, "refs/heads/main", oid(1), Expected::Missing).expect("seed main");
2017
2018 let other = store(dir.path());
2019 let racer = std::rc::Rc::new(RefCell::new(None));
2020 let racer_slot = racer.clone();
2021 after_next_recheck(move || {
2022 *racer_slot.borrow_mut() = Some(set(
2023 &other,
2024 "refs/heads/main",
2025 oid(2),
2026 Expected::Oid(oid(1)),
2027 ));
2028 });
2029 set(&writer, "HEAD", oid(3), Expected::Oid(oid(1))).expect("update through HEAD");
2030
2031 let racer = racer.borrow_mut().take().expect("hook ran");
2032 assert!(
2033 matches!(racer, Err(RefStoreError::LockHeld { .. })),
2034 "a direct writer of the HEAD target must be locked out: {racer:?}"
2035 );
2036 assert_eq!(
2037 writer.read_raw("refs/heads/main").expect("read"),
2038 Some(RawRef::Direct(oid(3)))
2039 );
2040 assert_eq!(
2041 writer.read_raw("HEAD").expect("read"),
2042 Some(RawRef::Symbolic("refs/heads/main".to_owned()))
2043 );
2044 for lock in ["HEAD.lock", "refs/heads/main.lock"] {
2045 assert!(!dir.path().join(lock).exists(), "{lock} released");
2046 }
2047 }
2048
2049 #[test]
2050 fn create_availability_matches_the_path_based_check() {
2051 let dir = git_dir();
2052 let path = dir.path();
2053 let hex = oid(1).to_hex();
2054 fs::write(
2055 path.join("packed-refs"),
2056 format!(
2057 "# pack-refs with: peeled fully-peeled sorted \n\
2058 {hex} refs/heads/p\n\
2059 {hex} refs/heads/pd/x\n\
2060 {hex} refs/heads/pd-sibling\n\
2061 {hex} refs/tags/v1\n"
2062 ),
2063 )
2064 .expect("packed-refs");
2065 for loose in ["refs/heads/l", "refs/heads/ld/y/z"] {
2066 let file = path.join(loose);
2067 fs::create_dir_all(file.parent().expect("parent")).expect("dirs");
2068 fs::write(file, format!("{hex}\n")).expect("loose");
2069 }
2070 let refs = store(path);
2071 let extras: BTreeSet<String> = ["refs/heads/batch/one".to_owned()].into();
2072 for name in [
2073 "refs/heads/new",
2074 "refs/heads/p/child",
2075 "refs/heads/pd",
2076 "refs/heads/pd-sibling/x",
2077 "refs/heads/l/child",
2078 "refs/heads/ld",
2079 "refs/heads/ld/y",
2080 "refs/heads/batch",
2081 "refs/heads/batch/one/two",
2082 "refs/tags/v1/x",
2083 "refs/tags/v2",
2084 ] {
2085 assert_eq!(
2086 refs.verify_create_available(name, &extras),
2087 crate::refs::verify_refname_available_for_create(
2088 path,
2089 name,
2090 &extras,
2091 &HashSet::new()
2092 ),
2093 "{name}"
2094 );
2095 }
2096 }
2097
2098 #[test]
2099 fn mapped_packed_refs_are_reread_after_replacement() {
2100 let dir = git_dir();
2101 let path = dir.path();
2102 let records = |value: ObjectId| -> Vec<crate::refs::PackedRefRecord> {
2103 (0..500)
2104 .map(|i| crate::refs::PackedRefRecord {
2105 refname: format!("refs/tags/t{i:04}"),
2106 oid: value,
2107 peeled: None,
2108 })
2109 .collect()
2110 };
2111 crate::refs::write_packed_refs_file(path, &records(oid(1))).expect("pack");
2112 assert!(fs::metadata(path.join("packed-refs")).expect("meta").len() > 8192);
2113 let refs = store(path);
2114 assert_eq!(
2115 refs.read_raw("refs/tags/t0499").expect("read"),
2116 Some(RawRef::Direct(oid(1)))
2117 );
2118 let mut replaced = records(oid(2));
2119 replaced.truncate(10);
2120 crate::refs::write_packed_refs_file(path, &replaced).expect("repack");
2121 assert_eq!(refs.read_raw("refs/tags/t0499").expect("read"), None);
2122 assert_eq!(
2123 refs.read_raw("refs/tags/t0009").expect("read"),
2124 Some(RawRef::Direct(oid(2)))
2125 );
2126 }
2127
2128 fn replace_packed_refs_keeping_len_and_mtime(path: &Path, body: &str) {
2131 let packed = path.join("packed-refs");
2132 let before = fs::metadata(&packed).expect("meta");
2133 assert_eq!(before.len(), body.len() as u64, "same length");
2134 let tmp = path.join("packed-refs.new");
2135 fs::write(&tmp, body).expect("write");
2136 fs::File::options()
2137 .write(true)
2138 .open(&tmp)
2139 .expect("open")
2140 .set_modified(before.modified().expect("mtime"))
2141 .expect("pin mtime");
2142 fs::rename(&tmp, &packed).expect("rename");
2143 let after = fs::metadata(&packed).expect("meta");
2144 assert_eq!(after.len(), before.len());
2145 assert_eq!(after.modified().ok(), before.modified().ok());
2146 }
2147
2148 #[test]
2149 fn create_rechecks_a_packed_refs_replaced_with_same_len_and_mtime() {
2150 assert_create_blocked_by_replaced_packed_descendant(false);
2151 }
2152
2153 #[test]
2154 fn create_rechecks_packed_refs_even_when_the_stamp_cannot_tell() {
2155 assert_create_blocked_by_replaced_packed_descendant(true);
2156 }
2157
2158 fn assert_create_blocked_by_replaced_packed_descendant(stamp_collides: bool) {
2161 let dir = git_dir();
2162 let path = dir.path();
2163 let hex = oid(1).to_hex();
2164 let header = "# pack-refs with: peeled fully-peeled sorted \n";
2165 fs::write(
2166 path.join("packed-refs"),
2167 format!("{header}{hex} refs/heads/blocked-child\n"),
2168 )
2169 .expect("packed-refs");
2170 let writer = store(path);
2171 assert_eq!(
2172 writer.read_raw("refs/heads/blocked-child").expect("prime"),
2173 Some(RawRef::Direct(oid(1)))
2174 );
2175 replace_packed_refs_keeping_len_and_mtime(
2176 path,
2177 &format!("{header}{hex} refs/heads/blocked/child\n"),
2178 );
2179 if stamp_collides {
2180 let mut guard = writer.packed_cache.lock().expect("cache");
2183 guard.stamp = FilesRefStore::packed_refs_stamp(&path.join("packed-refs"));
2184 }
2185
2186 let err = set(&writer, "refs/heads/blocked", oid(2), Expected::Missing)
2187 .expect_err("packed descendant blocks the create");
2188 assert!(
2189 matches!(
2190 err,
2191 RefStoreError::NameUnavailable {
2192 reason: RefnameUnavailable::DescendantExists { ref blocking, .. }
2193 } if blocking == "refs/heads/blocked/child"
2194 ),
2195 "{err:?}"
2196 );
2197 assert!(!path.join("refs/heads/blocked").exists());
2198 assert!(!path.join("refs/heads/blocked.lock").exists());
2199 }
2200
2201 #[test]
2202 fn create_with_reflog_next_to_a_ref_file_in_logs_reports_the_conflict() {
2203 let dir = git_dir();
2204 let path = dir.path();
2205 let writer = store(path);
2206 set(&writer, "refs/heads/a", oid(1), Expected::Missing).expect("create");
2207 fs::create_dir_all(path.join("logs/refs/heads")).expect("logs");
2208 fs::write(path.join("logs/refs/heads/a"), "").expect("log");
2209 let txn = RefTransaction::new()
2210 .update(RefUpdate {
2211 name: "refs/heads/a/b".to_owned(),
2212 new_value: Some(RawRef::Direct(oid(2))),
2213 expected: Expected::Missing,
2214 reflog: Some(super::super::ReflogUpdate {
2215 identity: "T <t@t>".into(),
2216 message: "create".into(),
2217 time: time::OffsetDateTime::UNIX_EPOCH,
2218 old_oid: None,
2219 new_oid: None,
2220 }),
2221 flags: RefUpdateFlags::default(),
2222 })
2223 .expect("txn");
2224 let err = writer.prepare(txn).map(|_| ()).expect_err("conflict");
2225 assert!(
2226 matches!(
2227 err,
2228 RefStoreError::NameUnavailable {
2229 reason: RefnameUnavailable::AncestorExists { ref blocking, .. }
2230 } if blocking == "refs/heads/a"
2231 ),
2232 "{err:?}"
2233 );
2234 assert!(!path.join("refs/heads/a.lock").exists());
2235 }
2236
2237 #[test]
2238 fn failed_recheck_releases_the_lock() {
2239 let dir = git_dir();
2240 let name = "refs/heads/main";
2241 let writer = store(dir.path());
2242 set(&writer, name, oid(1), Expected::Missing).expect("create");
2243 let other = store(dir.path());
2244 set(&other, name, oid(2), Expected::Oid(oid(1))).expect("other moves it");
2245
2246 let err = set(&writer, name, oid(3), Expected::Oid(oid(1))).expect_err("stale");
2247 assert!(
2248 matches!(err, RefStoreError::ExpectedMismatch { .. }),
2249 "{err:?}"
2250 );
2251 assert!(!dir.path().join("refs/heads/main.lock").exists());
2252 set(&writer, name, oid(3), Expected::Oid(oid(2))).expect("retry against current value");
2253 assert_eq!(
2254 writer.read_raw(name).expect("read"),
2255 Some(RawRef::Direct(oid(3)))
2256 );
2257 }
2258
2259 fn delete(name: &str, expected: Expected) -> RefUpdate {
2260 RefUpdate {
2261 name: name.to_owned(),
2262 new_value: None,
2263 expected,
2264 reflog: None,
2265 flags: RefUpdateFlags::default(),
2266 }
2267 }
2268
2269 fn commit_all(store: &FilesRefStore, updates: Vec<RefUpdate>) -> StoreResult<()> {
2270 store
2271 .prepare(RefTransaction::from_updates(updates)?)?
2272 .commit()
2273 }
2274
2275 fn packed_remote_refs(path: &Path) {
2278 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted \n");
2279 body.push_str(&format!("{} refs/heads/main\n", oid(4)));
2280 let mut remotes: Vec<String> = (0..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
2281 remotes.sort();
2282 for name in &remotes {
2283 body.push_str(&format!("{} {name}\n", oid(1)));
2284 }
2285 body.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
2286 fs::write(path.join("packed-refs"), body).expect("packed-refs");
2287 fs::create_dir_all(path.join("refs/remotes/o")).expect("dir");
2288 fs::write(path.join("refs/remotes/o/x5"), format!("{}\n", oid(9))).expect("loose");
2289 }
2290
2291 #[test]
2292 fn batch_delete_of_packed_refs_rewrites_packed_refs_once_and_keeps_the_rest() {
2293 let dir = git_dir();
2294 let path = dir.path();
2295 packed_remote_refs(path);
2296 let writer = store(path);
2297 let updates = (0..20)
2298 .map(|i| {
2299 let expected = match i {
2300 0 => Expected::Oid(oid(1)),
2301 5 => Expected::Oid(oid(9)),
2302 7 => Expected::Exists,
2303 _ => Expected::Any,
2304 };
2305 delete(&format!("refs/remotes/o/x{i}"), expected)
2306 })
2307 .collect();
2308 commit_all(&writer, updates).expect("batch delete");
2309
2310 for i in 0..20 {
2311 let name = format!("refs/remotes/o/x{i}");
2312 assert_eq!(writer.read_raw(&name).expect("read"), None, "{name}");
2313 }
2314 let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2315 assert_eq!(
2316 packed,
2317 format!(
2318 "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n{} refs/tags/t\n^{}\n",
2319 oid(4),
2320 oid(2),
2321 oid(3)
2322 )
2323 );
2324 assert!(
2325 !path.join("refs/remotes").exists(),
2326 "emptied dirs are pruned"
2327 );
2328 assert!(!path.join("packed-refs.lock").exists());
2329 assert!(path.join("refs/heads").is_dir());
2330 }
2331
2332 #[test]
2333 fn stale_expectation_on_packed_ref_rejects_the_whole_batch() {
2334 let dir = git_dir();
2335 let path = dir.path();
2336 packed_remote_refs(path);
2337 let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
2338 let writer = store(path);
2339 let err = commit_all(
2340 &writer,
2341 vec![
2342 delete("refs/remotes/o/x1", Expected::Any),
2343 delete("refs/remotes/o/x2", Expected::Oid(oid(7))),
2344 ],
2345 )
2346 .expect_err("stale");
2347 assert!(
2348 matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x2"),
2349 "{err:?}"
2350 );
2351 assert_eq!(
2352 fs::read_to_string(path.join("packed-refs")).expect("packed"),
2353 before
2354 );
2355 assert!(!path.join("packed-refs.lock").exists());
2356 assert!(!path.join("refs/remotes/o/x1.lock").exists());
2357 }
2358
2359 #[test]
2360 fn batch_delete_prunes_nested_empty_dirs_deepest_first() {
2361 let dir = git_dir();
2362 let path = dir.path();
2363 let writer = store(path);
2364 for name in ["refs/heads/x/y/z", "refs/heads/x/w", "refs/heads/keep/k"] {
2365 set(&writer, name, oid(1), Expected::Missing).expect("create");
2366 }
2367 commit_all(
2368 &writer,
2369 vec![
2370 delete("refs/heads/x/w", Expected::Oid(oid(1))),
2371 delete("refs/heads/x/y/z", Expected::Any),
2372 ],
2373 )
2374 .expect("delete");
2375 assert!(!path.join("refs/heads/x").exists());
2376 assert!(path.join("refs/heads/keep/k").is_file());
2377 }
2378
2379 #[test]
2380 fn deleting_through_a_symref_removes_the_packed_target() {
2381 let dir = git_dir();
2382 let path = dir.path();
2383 packed_remote_refs(path);
2384 fs::write(path.join("refs/remotes/o/HEAD"), "ref: refs/remotes/o/x3\n").expect("symref");
2385 let writer = store(path);
2386 commit_all(&writer, vec![delete("refs/remotes/o/HEAD", Expected::Any)])
2387 .expect("deref delete");
2388 assert_eq!(writer.read_raw("refs/remotes/o/x3").expect("read"), None);
2389 assert_eq!(
2390 writer.read_raw("refs/remotes/o/HEAD").expect("read"),
2391 Some(RawRef::Symbolic("refs/remotes/o/x3".to_owned()))
2392 );
2393 let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2394 assert!(!packed.contains("refs/remotes/o/x3\n"), "{packed}");
2395 assert!(packed.contains("refs/remotes/o/x4\n"), "{packed}");
2396 }
2397
2398 #[test]
2399 fn late_commit_time_mismatch_keeps_packed_refs_the_batch_never_reached() {
2400 let dir = git_dir();
2401 let path = dir.path();
2402 packed_remote_refs(path);
2403 let writer = store(path);
2404 let other = store(path);
2405 after_next_recheck(move || {
2406 set(&other, "refs/remotes/o/x9", oid(7), Expected::Any).expect("concurrent write");
2407 });
2408 let updates = (0..20)
2409 .map(|i| {
2410 let expected = if i == 9 {
2411 Expected::Oid(oid(1))
2412 } else {
2413 Expected::Any
2414 };
2415 delete(&format!("refs/remotes/o/x{i}"), expected)
2416 })
2417 .collect();
2418 let err = commit_all(&writer, updates).expect_err("x9 moved under the batch");
2419 assert!(
2420 matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x9"),
2421 "{err:?}"
2422 );
2423
2424 for i in 0..9 {
2425 let name = format!("refs/remotes/o/x{i}");
2426 assert_eq!(
2427 writer.read_raw(&name).expect("read"),
2428 None,
2429 "{name} was deleted"
2430 );
2431 }
2432 assert_eq!(
2433 writer.read_raw("refs/remotes/o/x9").expect("read"),
2434 Some(RawRef::Direct(oid(7))),
2435 "the concurrent value survives"
2436 );
2437 for i in 10..20 {
2438 let name = format!("refs/remotes/o/x{i}");
2439 assert_eq!(
2440 writer.read_raw(&name).expect("read"),
2441 Some(RawRef::Direct(oid(1))),
2442 "{name} was never processed"
2443 );
2444 }
2445 let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2446 let mut expected = format!(
2447 "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n",
2448 oid(4)
2449 );
2450 let mut kept: Vec<String> = (9..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
2451 kept.sort();
2452 for name in kept {
2453 expected.push_str(&format!("{} {name}\n", oid(1)));
2454 }
2455 expected.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
2456 assert_eq!(packed, expected);
2457 assert!(!path.join("packed-refs.lock").exists());
2458 assert!(!path.join("refs/remotes/o/x10.lock").exists());
2459 }
2460
2461 #[test]
2462 fn restore_merges_records_back_in_sorted_order_with_peel_lines() {
2463 let dir = git_dir();
2464 let path = dir.path();
2465 fs::write(
2466 path.join("packed-refs"),
2467 format!(
2468 "# pack-refs with: peeled fully-peeled sorted \n{} refs/heads/b\n{} refs/tags/z\n^{}\n",
2469 oid(1),
2470 oid(2),
2471 oid(3)
2472 ),
2473 )
2474 .expect("packed");
2475 let names: HashSet<String> = ["refs/heads/b", "refs/tags/z"]
2476 .into_iter()
2477 .map(str::to_owned)
2478 .collect();
2479 let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
2480 let removed = remove_packed_refs_under_lock(path, &names).expect("remove");
2481 assert_eq!(removed.len(), 2);
2482 assert_eq!(packed_record_name(&removed[1]), "refs/tags/z");
2483 assert_eq!(packed_record_oid(&removed[1]), Some(oid(2)));
2484 fs::write(
2485 path.join("packed-refs"),
2486 format!(
2487 "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/a\n{} refs/heads/c\n",
2488 oid(5),
2489 oid(6)
2490 ),
2491 )
2492 .expect("packed");
2493 let records: Vec<&str> = removed.iter().map(String::as_str).collect();
2494 restore_packed_records_under_lock(path, &records).expect("restore");
2495 assert_eq!(
2496 fs::read_to_string(path.join("packed-refs")).expect("packed"),
2497 format!(
2498 "# 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",
2499 oid(5),
2500 oid(1),
2501 oid(6),
2502 oid(2),
2503 oid(3)
2504 )
2505 );
2506 assert!(before.contains("refs/tags/z"));
2507 }
2508
2509 #[test]
2511 fn concurrent_loose_ref_delete_during_list_is_tolerated() {
2512 use std::sync::atomic::{AtomicBool, Ordering};
2513 use std::sync::Arc;
2514 use std::thread;
2515 use std::time::Duration;
2516
2517 let dir = git_dir();
2518 set(
2519 &store(dir.path()),
2520 "refs/heads/main",
2521 oid(1),
2522 Expected::Missing,
2523 )
2524 .expect("seed main");
2525
2526 let stop = Arc::new(AtomicBool::new(false));
2527 let writer_dir = dir.path().to_path_buf();
2528 let writer_stop = stop.clone();
2529 let writer = thread::spawn(move || {
2530 use crate::refs::{delete_ref, write_ref};
2531 let c = oid(2);
2532 while !writer_stop.load(Ordering::Relaxed) {
2533 for i in 0..7 {
2534 let name = format!("refs/heads/d{i}/sub/x");
2535 let _ = write_ref(&writer_dir, &name, &c);
2536 let _ = delete_ref(&writer_dir, &name);
2537 }
2538 }
2539 });
2540
2541 let lister = store(dir.path());
2542 let mut errors = 0usize;
2543 for _ in 0..400 {
2544 if lister
2545 .for_each_ref("", &mut |_| ControlFlow::Continue(()))
2546 .is_err()
2547 {
2548 errors += 1;
2549 }
2550 }
2551 stop.store(true, Ordering::Relaxed);
2552 writer.join().expect("writer");
2553 thread::sleep(Duration::from_millis(10));
2554 assert_eq!(
2555 errors, 0,
2556 "listing must tolerate refs deleted between readdir and open"
2557 );
2558 }
2559}