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 fn lookup_packed_oid(&self, storage_name: &str) -> Result<Option<ObjectId>> {
267 self.refresh_packed_cache_if_stale()?;
268 let guard = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
269 let Some(ref file) = guard.file else {
270 return Ok(None);
271 };
272 let path = super::paths::packed_refs_path(&self.common_dir);
273 file.lookup_oid(&path, storage_name)
274 }
275
276 fn insert_create_conflict_neighbors(
283 &self,
284 refs: &mut BTreeMap<String, RawRef>,
285 updates: &[RefUpdate],
286 ) -> Result<()> {
287 let creates: Vec<&str> = updates
288 .iter()
289 .filter(|u| u.new_value.is_some() && !u.flags.log_only && !refs.contains_key(&u.name))
290 .map(|u| u.name.as_str())
291 .collect();
292 let packed = self.packed_refs_view()?;
293 let mut not_refs: HashSet<&str> = HashSet::new();
295 for name in creates {
296 for (slash, _) in name.match_indices('/') {
297 let ancestor = &name[..slash];
298 if refs.contains_key(ancestor) || not_refs.contains(ancestor) {
299 continue;
300 }
301 match self.existing_ref_named_in(ancestor, packed.as_ref(), None)? {
302 Some(raw) => {
303 refs.insert(ancestor.to_owned(), raw);
304 }
305 None => {
306 not_refs.insert(ancestor);
307 }
308 }
309 }
310 for (descendant, raw) in self.descendant_refs_in(name, packed.as_ref())? {
311 refs.entry(descendant).or_insert(raw);
312 }
313 }
314 Ok(())
315 }
316
317 fn verify_create_available(
328 &self,
329 name: &str,
330 extras: &BTreeSet<&str>,
331 held_lock: Option<&Path>,
332 ) -> std::result::Result<(), RefnameUnavailable> {
333 let packed = self.packed_refs_view().ok().flatten();
334 self.verify_create_available_in(
335 name,
336 extras,
337 held_lock,
338 packed.as_ref(),
339 &mut HashMap::new(),
340 )
341 }
342
343 fn verify_create_available_in(
346 &self,
347 name: &str,
348 extras: &BTreeSet<&str>,
349 held_lock: Option<&Path>,
350 packed: Option<&crate::refs::PackedRefsFileView>,
351 ancestors: &mut HashMap<String, bool>,
352 ) -> std::result::Result<(), RefnameUnavailable> {
353 let mut blocking = BTreeSet::new();
354 for (slash, _) in name.match_indices('/') {
355 let ancestor = &name[..slash];
356 let is_ref = match ancestors.get(ancestor) {
357 Some(&is_ref) => is_ref,
358 None => {
359 let is_ref = matches!(
360 self.existing_ref_named_in(ancestor, packed, held_lock),
361 Ok(Some(_))
362 );
363 ancestors.insert(ancestor.to_owned(), is_ref);
364 is_ref
365 }
366 };
367 if is_ref {
368 blocking.insert(ancestor.to_owned());
369 }
370 }
371 let storage = self.route(name).storage_name;
372 let mut storage_names = BTreeSet::new();
373 for base in self.store_bases() {
374 collect_loose_file_names(&base.join(&storage), &storage, &mut storage_names);
375 }
376 if let Some(packed) = packed.filter(|_| storage.contains('/')) {
377 let path = super::paths::packed_refs_path(&self.common_dir);
378 if let Ok(Some((descendant, _))) = packed.first_descendant(&path, &storage) {
379 storage_names.insert(descendant);
380 }
381 }
382 blocking.extend(storage_names.iter().map(|s| self.logical_name(s)));
383 verify_name_available_for_create(name, &blocking, extras)
384 }
385
386 fn store_bases(&self) -> impl Iterator<Item = &Path> {
390 let common = self.common_dir.as_path();
391 let git = self.git_dir.as_path();
392 std::iter::once(common).chain((git != common).then_some(git))
393 }
394
395 fn existing_ref_named_in(
403 &self,
404 name: &str,
405 packed: Option<&crate::refs::PackedRefsFileView>,
406 held_lock: Option<&Path>,
407 ) -> Result<Option<RawRef>> {
408 let storage = self.route(name).storage_name;
409 for base in self.store_bases() {
410 let path = base.join(&storage);
411 if held_lock.is_some_and(|lock| lock.starts_with(&path)) || !path.is_file() {
412 continue;
413 }
414 if let Ok(r) = read_ref_file(&path) {
415 return Ok(Some(ref_to_raw(r)));
416 }
417 }
418 let Some(packed) = packed.filter(|_| self.packed_storage_visible(&storage)) else {
419 return Ok(None);
420 };
421 let path = super::paths::packed_refs_path(&self.common_dir);
422 Ok(packed.lookup_oid(&path, &storage)?.map(RawRef::Direct))
423 }
424
425 fn descendant_refs_in(
427 &self,
428 name: &str,
429 packed: Option<&crate::refs::PackedRefsFileView>,
430 ) -> Result<BTreeMap<String, RawRef>> {
431 let storage = self.route(name).storage_name;
432 let mut storage_map = BTreeMap::new();
433 for base in self.store_bases() {
434 let dir = base.join(&storage);
435 if dir.is_dir() {
436 collect_loose_raw_refs(&dir, &format!("{storage}/"), "", &mut storage_map)?;
437 }
438 }
439 if let Some(packed) = packed.filter(|_| storage.contains('/')) {
442 let path = super::paths::packed_refs_path(&self.common_dir);
443 if let Some((descendant, oid)) = packed.first_descendant(&path, &storage)? {
444 if self.packed_storage_visible(&descendant) {
445 storage_map.entry(descendant).or_insert(RawRef::Direct(oid));
446 }
447 }
448 }
449 Ok(storage_map
450 .into_iter()
451 .map(|(storage, value)| (self.logical_name(&storage), value))
452 .collect())
453 }
454
455 fn insert_symref_peel_chain(
457 &self,
458 refs: &mut BTreeMap<String, RawRef>,
459 start: &str,
460 ) -> Result<()> {
461 let mut name = start.to_owned();
462 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
463 if refs.contains_key(&name) {
464 match refs.get(&name) {
465 Some(RawRef::Symbolic(next)) => name = next.clone(),
466 _ => break,
467 }
468 continue;
469 }
470 let Some(raw) = self.read_raw(&name)? else {
471 break;
472 };
473 let advance = match &raw {
474 RawRef::Symbolic(next) => Some(next.clone()),
475 RawRef::Direct(_) => None,
476 };
477 refs.insert(name.clone(), raw);
478 match advance {
479 Some(next) => name = next,
480 None => break,
481 }
482 }
483 Ok(())
484 }
485
486 fn load_batch_state_for_prepare(
487 &self,
488 updates: &[RefUpdate],
489 reflog_renames: &[super::RefReflogRename],
490 case_only_renames: &[super::RefCaseOnlyRename],
491 ) -> Result<RefBatchState> {
492 let mut refs = BTreeMap::new();
493 for update in updates {
494 if let Some(raw) = self.read_raw(&update.name)? {
495 refs.insert(update.name.clone(), raw);
496 }
497 }
498 for rename in case_only_renames {
499 if refs.contains_key(&rename.from) {
500 continue;
501 }
502 if let Some(raw) = self.read_raw(&rename.from)? {
503 refs.insert(rename.from.clone(), raw);
504 }
505 }
506 self.insert_head_for_batch_state(&mut refs)?;
507
508 for update in updates {
509 if matches!(refs.get(&update.name), Some(RawRef::Symbolic(_))) {
510 self.insert_symref_peel_chain(&mut refs, &update.name)?;
511 }
512 }
513 if matches!(refs.get("HEAD"), Some(RawRef::Symbolic(_))) {
514 self.insert_symref_peel_chain(&mut refs, "HEAD")?;
515 }
516
517 let mut reflogs = self.load_reflogs_for_updates(&refs, updates)?;
518 for rename in reflog_renames {
519 if reflogs.contains_key(&rename.from) {
520 continue;
521 }
522 let entries = read_reflog_at_path(&self.reflog_path(&rename.from))?;
523 if !entries.is_empty() {
524 reflogs.insert(rename.from.clone(), entries);
525 }
526 }
527 Ok(RefBatchState { refs, reflogs })
528 }
529
530 fn ensure_reflog_path_writable(&self, name: &str) -> StoreResult<()> {
538 let path = self.reflog_path(name);
539 let is_dir = |path: &Path| fs::symlink_metadata(path).is_ok_and(|m| m.is_dir());
540 if !is_dir(&path) {
541 return Ok(());
542 }
543 crate::refs::remove_empty_ref_directory(&path);
544 if !is_dir(&path) {
545 return Ok(());
546 }
547 let storage = self.route(name).storage_name;
548 let mut logs = BTreeSet::new();
549 collect_loose_file_names(&path, &storage, &mut logs);
550 let Some(log) = logs.first() else {
551 return Err(RefStoreError::Corrupt(format!(
552 "cannot write reflog at '{}': unreadable directory in the way",
553 path.display()
554 )));
555 };
556 Err(RefnameUnavailable::DescendantExists {
557 blocking: self.logical_name(log),
558 new_ref: name.to_owned(),
559 }
560 .into())
561 }
562
563 fn read_raw_at_route(&self, route: &RefStorageRoute) -> Result<Option<RawRef>> {
564 let path = route.storage_dir.join(&route.storage_name);
565 match read_ref_file(&path) {
566 Ok(Ref::Direct(oid)) => return Ok(Some(RawRef::Direct(oid))),
567 Ok(Ref::Symbolic(target)) => return Ok(Some(RawRef::Symbolic(target))),
568 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
569 Err(e) => return Err(e),
570 }
571 if let Some(oid) = self.lookup_packed_oid(&route.storage_name)? {
572 return Ok(Some(RawRef::Direct(oid)));
573 }
574 Ok(None)
575 }
576
577 fn packed_storage_visible(&self, storage: &str) -> bool {
578 match self.namespace_prefix.as_deref() {
579 None => true,
580 Some(prefix) => storage.starts_with(prefix),
581 }
582 }
583
584 fn packed_refs_scan_prefix(&self, logical_prefix: &str) -> String {
586 match self.namespace_prefix.as_deref() {
587 Some(ns) if !logical_prefix.is_empty() => format!("{ns}{logical_prefix}"),
588 Some(ns) => ns.trim_end_matches('/').to_owned(),
589 None => logical_prefix.to_owned(),
590 }
591 }
592
593 fn load_raw_map_prefix(
596 &self,
597 logical_prefix: &str,
598 keep: Option<PackedRecordFilter<'_>>,
599 ) -> Result<BTreeMap<String, RawRef>> {
600 let logical_prefix = logical_prefix.trim_end_matches('/');
601 let mut map: BTreeMap<String, RawRef> = BTreeMap::new();
602 if logical_prefix.is_empty() {
603 self.ensure_full_packed_map()?;
604 let cache = self.packed_cache.lock().unwrap_or_else(|e| e.into_inner());
605 if let Some(packed) = cache.map.as_ref() {
606 for (storage, oid) in packed.iter() {
607 if !self.packed_storage_visible(storage)
608 || keep.is_some_and(|keep| !keep(storage, oid, PackedPeel::Unknown))
609 {
610 continue;
611 }
612 let logical = self.logical_name(storage);
613 map.insert(logical, RawRef::Direct(*oid));
614 }
615 }
616 drop(cache);
617 } else {
618 for (i, base) in self.store_bases().enumerate() {
619 let packed_prefix = self.packed_refs_scan_prefix(logical_prefix);
620 let packed = if i == 0 {
621 self.packed_map_for_prefix_filtered(&packed_prefix, keep)?
622 } else {
623 read_packed_refs_map_for_prefix_filtered(base, &packed_prefix, keep)?
624 };
625 for (storage, oid) in packed {
626 if !self.packed_storage_visible(&storage) {
627 continue;
628 }
629 let logical = self.logical_name(&storage);
630 map.insert(logical, RawRef::Direct(oid));
631 }
632 }
633 }
634
635 for base in self.store_bases() {
636 let storage_prefix = self.namespace_prefix.as_deref().unwrap_or("refs/");
637 if self.namespace_prefix.is_some() {
638 let root = base.join(storage_prefix.trim_end_matches('/'));
639 if root.is_dir() {
640 let mut storage_map = BTreeMap::new();
641 collect_loose_raw_refs(&root, storage_prefix, "", &mut storage_map)?;
642 for (storage, value) in storage_map {
643 let logical = self.logical_name(&storage);
644 if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
645 map.insert(logical, value);
646 }
647 }
648 }
649 } else {
650 let (walk_root, walk_prefix) = loose_ref_list_walk_root(base, logical_prefix);
651 if walk_root.is_dir() {
652 let mut storage_map = BTreeMap::new();
653 collect_loose_raw_refs(
654 &walk_root,
655 &walk_prefix,
656 logical_prefix,
657 &mut storage_map,
658 )?;
659 for (storage, value) in storage_map {
660 let logical = self.logical_name(&storage);
661 if logical_prefix.is_empty() || logical.starts_with(logical_prefix) {
662 map.insert(logical, value);
663 }
664 }
665 }
666 }
667 if head_matches_prefix(logical_prefix) && base.join("HEAD").is_file() {
668 if let Ok(r) = read_ref_file(&base.join("HEAD")) {
669 map.insert("HEAD".to_owned(), ref_to_raw(r));
670 }
671 }
672 }
673 Ok(map)
674 }
675
676 fn load_reflogs_for_updates(
680 &self,
681 refs: &BTreeMap<String, RawRef>,
682 updates: &[RefUpdate],
683 ) -> Result<BTreeMap<String, Vec<ReflogEntry>>> {
684 let mut names = BTreeSet::new();
685 for update in updates.iter().filter(|u| u.reflog.is_some()) {
686 let mut name = update.name.as_str();
687 names.insert(name.to_owned());
688 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
689 let Some(RawRef::Symbolic(next)) = refs.get(name) else {
690 break;
691 };
692 name = next;
693 names.insert(name.to_owned());
694 }
695 }
696 let mut out = BTreeMap::new();
697 for name in names {
698 let entries = read_reflog_at_path(&self.reflog_path(&name))?;
699 if !entries.is_empty() {
700 out.insert(name, entries);
701 }
702 }
703 Ok(out)
704 }
705
706 fn insert_head_for_batch_state(&self, map: &mut BTreeMap<String, RawRef>) -> Result<()> {
707 if map.contains_key("HEAD") {
708 return Ok(());
709 }
710 for base in [self.common_dir.as_path(), self.git_dir.as_path()] {
711 let head_path = base.join("HEAD");
712 if head_path.is_file() {
713 let raw = read_ref_file(&head_path).map(ref_to_raw)?;
714 map.insert("HEAD".to_owned(), raw);
715 break;
716 }
717 }
718 Ok(())
719 }
720
721 fn loose_ref_file_exists(&self, refname: &str) -> Result<bool> {
722 let route = self.route(refname);
723 let path = route.storage_dir.join(&route.storage_name);
724 Ok(path.is_file())
725 }
726
727 fn route(&self, refname: &str) -> RefStorageRoute {
728 route_ref_storage_in_common(
729 &self.git_dir,
730 &self.common_dir,
731 refname,
732 self.namespace_prefix.as_deref(),
733 )
734 }
735
736 #[cfg(windows)]
739 fn reject_win32_unstorable_ref_paths(&self, txn: &RefTransaction) -> StoreResult<()> {
740 use crate::git_path::win32_storage_path_components_round_trip;
741
742 let storage_ok =
743 |name: &str| win32_storage_path_components_round_trip(&self.route(name).storage_name);
744 for update in txn.updates() {
745 if update.new_value.is_none() && !update.flags.log_only {
746 continue;
747 }
748 if !storage_ok(&update.name) {
749 return Err(RefStoreError::InvalidRefName {
750 name: update.name.clone(),
751 });
752 }
753 }
754 for rename in txn.reflog_renames() {
755 if !storage_ok(&rename.to) {
756 return Err(RefStoreError::InvalidRefName {
757 name: rename.to.clone(),
758 });
759 }
760 }
761 for rename in txn.case_only_renames() {
762 if !storage_ok(&rename.to) {
763 return Err(RefStoreError::InvalidRefName {
764 name: rename.to.clone(),
765 });
766 }
767 }
768 Ok(())
769 }
770
771 fn logical_name(&self, storage: &str) -> String {
772 crate::ref_namespace::logical_ref_name_with_prefix(
773 self.namespace_prefix.as_deref(),
774 storage,
775 )
776 }
777
778 pub(crate) fn reflog_path(&self, logical: &str) -> PathBuf {
780 let route = self.route(logical);
781 route.storage_dir.join("logs").join(&route.storage_name)
782 }
783
784 pub(crate) fn read_reflog_entries(&self, name: &str) -> Result<Vec<ReflogEntry>> {
787 read_reflog_at_path(&self.reflog_path(name))
788 }
789
790 fn ref_lock_paths(&self, refname: &str) -> (RefStorageRoute, PathBuf, PathBuf) {
791 let route = self.route(refname);
792 let path = route.storage_dir.join(&route.storage_name);
793 let lock = lock_path_for_ref(&path);
794 (route, path, lock)
795 }
796
797 fn initial_packed_records(&self, txn: &RefTransaction) -> StoreResult<Option<Vec<String>>> {
807 let updates = txn.updates();
808 if updates.is_empty()
809 || !txn.reflog_renames().is_empty()
810 || !txn.case_only_renames().is_empty()
811 {
812 return Ok(None);
813 }
814 let mut creates = Vec::with_capacity(updates.len());
815 for update in updates {
816 let Some(RawRef::Direct(oid)) = update.new_value else {
817 return Ok(None);
818 };
819 if update.flags.log_only
820 || update.reflog.is_some()
821 || !matches!(update.expected, Expected::Any | Expected::Missing)
822 {
823 return Ok(None);
824 }
825 let route = self.route(&update.name);
826 if route.storage_dir != self.common_dir || route.storage_name != update.name {
827 return Ok(None);
828 }
829 creates.push((update.name.as_str(), oid));
830 }
831 let existing = self
832 .load_raw_map_prefix("", None)
833 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
834 if updates.iter().any(|u| existing.contains_key(&u.name)) {
835 return Ok(None);
836 }
837 verify_create_conflicts(&existing, updates)?;
838
839 let odb = crate::odb::Odb::new(&self.common_dir.join("objects"))
840 .with_config_git_dir(self.git_dir.clone());
841 let mut peels: HashMap<ObjectId, Option<ObjectId>> = HashMap::new();
842 let records = odb.with_pack_read_context(|| {
843 creates
844 .into_iter()
845 .map(|(name, oid)| {
846 let peel = *peels
847 .entry(oid)
848 .or_insert_with(|| crate::refs::packed_ref_peel(&odb, oid));
849 match peel {
850 Some(peel) => format!("{oid} {name}\n^{peel}\n"),
851 None => format!("{oid} {name}\n"),
852 }
853 })
854 .collect()
855 });
856 Ok(Some(records))
857 }
858
859 fn acquire_packed_lock(&self) -> StoreResult<PathBuf> {
861 use crate::lockfile::{LockError, LockFile};
862 let packed_path = super::paths::packed_refs_path(&self.common_dir);
863 let lock = lock_path_for_ref(&packed_path);
864 let wait = LockWait::packed_refs_for_git_dir(&self.common_dir);
865 match LockFile::acquire(&lock, wait).map(|guard| guard.detach()) {
866 Ok(()) => Ok(lock),
867 Err(LockError::Held { .. }) => Err(RefStoreError::LockHeld {
868 name: super::paths::PACKED_REFS.to_owned(),
869 }),
870 Err(LockError::Io { source, .. }) => Err(RefStoreError::Corrupt(source.to_string())),
871 }
872 }
873
874 fn needs_packed_lock(&self, updates: &[RefUpdate]) -> bool {
875 updates.iter().any(|u| {
876 if u.new_value.is_some() || u.flags.log_only {
877 return false;
878 }
879 let storage = self.route(&u.name).storage_name;
880 self.lookup_packed_oid(&storage).ok().flatten().is_some()
881 })
882 }
883
884 fn take_prepare_locks<'a>(
896 &self,
897 names: impl IntoIterator<Item = &'a String>,
898 carried: Option<&str>,
899 wait: LockWait,
900 ref_locks: &mut HashMap<String, PathBuf>,
901 ) -> StoreResult<()> {
902 for name in names {
903 if ref_locks.contains_key(name) {
904 continue;
905 }
906 let (_, _, lock) = self.ref_lock_paths(name);
907 let absent = matches!(
911 fs::symlink_metadata(&lock),
912 Err(err) if err.kind() == io::ErrorKind::NotFound
913 );
914 if carried != Some(name.as_str()) && absent {
915 continue;
916 }
917 if let Err(err) = self.lock_ref(name, &lock, wait) {
918 for acquired in ref_locks.values() {
919 let _ = fs::remove_file(acquired);
920 }
921 return Err(Self::ref_store_error_from_lib(err));
922 }
923 ref_locks.insert(name.clone(), lock);
924 }
925 Ok(())
926 }
927
928 fn lock_ref(&self, refname: &str, lock_path: &Path, wait: LockWait) -> Result<()> {
935 if let Some(reason) = self.name_unavailable_if_ancestor_is_loose_file(refname) {
936 return Err(Error::RefStore(RefStoreError::NameUnavailable { reason }));
937 }
938 let Err(err) = self.acquire_ref_lock_mapped(lock_path, refname, wait) else {
939 return Ok(());
940 };
941 if let Some(parent) = lock_path.parent() {
942 if parent.is_file() {
943 return Err(Error::RefStore(RefStoreError::NameUnavailable {
944 reason: RefnameUnavailable::AncestorExists {
945 blocking: loose_storage_path_to_refname(&self.common_dir, parent),
946 new_ref: refname.to_owned(),
947 },
948 }));
949 }
950 if let Err(source) = fs::create_dir_all(parent) {
951 if let Some(reason) = self.name_unavailable_if_ancestor_is_loose_file(refname) {
952 return Err(Error::RefStore(RefStoreError::NameUnavailable { reason }));
953 }
954 self.io_ref_store(parent, GitDirPathOperation::CreateDir, Err(source))?;
955 }
956 }
957 Err(err)
958 }
959
960 fn name_unavailable_if_ancestor_is_loose_file(
963 &self,
964 refname: &str,
965 ) -> Option<RefnameUnavailable> {
966 for (slash, _) in refname.match_indices('/').rev() {
967 let ancestor = &refname[..slash];
968 if ancestor == "refs" {
969 continue;
970 }
971 if self.loose_ref_file_exists(ancestor).ok()? {
972 return Some(RefnameUnavailable::AncestorExists {
973 blocking: ancestor.to_owned(),
974 new_ref: refname.to_owned(),
975 });
976 }
977 }
978 None
979 }
980
981 fn acquire_ref_lock_mapped(
982 &self,
983 lock_path: &Path,
984 refname: &str,
985 wait: LockWait,
986 ) -> Result<()> {
987 use crate::lockfile::{LockError, LockFile};
988 LockFile::acquire(lock_path, wait)
989 .map(|guard| guard.detach())
990 .map_err(|err| match err {
991 LockError::Held { .. } => Error::RefStore(RefStoreError::LockHeld {
992 name: refname.to_owned(),
993 }),
994 LockError::Io { source, path } => map_ref_store_path_io(
995 &self.common_dir,
996 &self.git_dir,
997 path.as_path(),
998 GitDirPathOperation::UpdateRef,
999 source,
1000 ),
1001 })
1002 }
1003
1004 fn ref_store_error_from_lib(err: Error) -> RefStoreError {
1005 match err {
1006 Error::GitDirPathIo {
1007 path,
1008 operation,
1009 source,
1010 } => RefStoreError::git_dir_path_io(path, operation, source),
1011 Error::RefStore(store_err) => store_err,
1012 other => RefStoreError::Corrupt(other.to_string()),
1013 }
1014 }
1015
1016 fn io_ref_store<T>(
1017 &self,
1018 path: &Path,
1019 operation: GitDirPathOperation,
1020 result: io::Result<T>,
1021 ) -> Result<T> {
1022 io_with_ref_store_path(&self.common_dir, &self.git_dir, path, operation, result)
1023 }
1024
1025 fn release_locks(batch: &PreparedFilesBatch) {
1026 for lock in batch.ref_locks.values() {
1027 let _ = fs::remove_file(lock);
1028 }
1029 if let Some(p) = &batch.packed_lock {
1030 let _ = fs::remove_file(p);
1031 }
1032 }
1033
1034 fn write_loose_value(
1035 common_dir: &Path,
1036 checkout_git_dir: &Path,
1037 refname: &str,
1038 path: &Path,
1039 lock: &Path,
1040 value: &RawRef,
1041 ) -> Result<()> {
1042 let content = match value {
1043 RawRef::Direct(oid) => format!("{oid}\n"),
1044 RawRef::Symbolic(target) => format!("ref: {target}\n"),
1045 };
1046 io_with_ref_store_path(
1047 common_dir,
1048 checkout_git_dir,
1049 lock,
1050 GitDirPathOperation::UpdateRef,
1051 fs::write(lock, content),
1052 )?;
1053 let renamed = fs::rename(lock, path).or_else(|err| {
1055 if !path.is_dir() {
1056 return Err(err);
1057 }
1058 remove_empty_ref_directory(path);
1059 fs::rename(lock, path)
1060 });
1061 match renamed {
1062 Ok(()) => Ok(()),
1063 Err(err)
1064 if matches!(
1065 err.kind(),
1066 io::ErrorKind::IsADirectory | io::ErrorKind::NotADirectory
1067 ) =>
1068 {
1069 let _ = fs::remove_file(lock);
1070 Err(Error::RefStore(RefStoreError::NameUnavailable {
1071 reason: name_unavailable_after_loose_rename_conflict(refname, path, common_dir),
1072 }))
1073 }
1074 Err(err) => io_with_ref_store_path(
1075 common_dir,
1076 checkout_git_dir,
1077 path,
1078 GitDirPathOperation::UpdateRef,
1079 Err(err),
1080 ),
1081 }
1082 }
1083
1084 fn persist_case_only_loose_ref(
1085 &self,
1086 rename: &super::RefCaseOnlyRename,
1087 after: &RefBatchState,
1088 ctx: CommitContext<'_>,
1089 ) -> Result<()> {
1090 let Some(value) = after.refs.get(&rename.to) else {
1091 return Err(Error::Message(format!(
1092 "case-only rename missing destination ref '{}'",
1093 rename.to
1094 )));
1095 };
1096 let (_, from_path, from_lock) = self.ref_lock_paths(&rename.from);
1097 if let Some(parent) = from_lock.parent() {
1098 fs::create_dir_all(parent).map_err(Error::Io)?;
1099 }
1100 self.acquire_ref_lock_mapped(&from_lock, &rename.from, ctx.ref_lock_wait)?;
1101 if !from_path.is_file() {
1102 self.remove_packed_record(&rename.from, ctx)?;
1103 FilesRefStore::write_loose_value(
1104 &self.common_dir,
1105 &self.git_dir,
1106 &rename.from,
1107 &from_path,
1108 &from_lock,
1109 value,
1110 )?;
1111 }
1112 let (_, to_path, _) = self.ref_lock_paths(&rename.to);
1113 if from_path != to_path {
1114 if let Some(parent) = to_path.parent() {
1115 fs::create_dir_all(parent).map_err(Error::Io)?;
1116 }
1117 fs::rename(&from_path, &to_path).map_err(Error::Io)?;
1118 }
1119 let _ = fs::remove_file(&from_lock);
1120 Ok(())
1121 }
1122
1123 fn remove_packed_record(&self, refname: &str, ctx: CommitContext<'_>) -> Result<()> {
1125 let route = self.route(refname);
1126 if !ctx.lock_held {
1127 return remove_packed_ref(&route.storage_dir, &route.storage_name);
1128 }
1129 if ctx.pruned.contains(refname) {
1130 return Ok(());
1131 }
1132 remove_packed_ref_under_lock(&route.storage_dir, &route.storage_name)
1133 }
1134
1135 fn prune_deleted_packed_records(
1143 &self,
1144 before: &RefBatchState,
1145 after: &RefBatchState,
1146 updates: &[RefUpdate],
1147 ) -> Result<PrunedPackedRecords> {
1148 let checked: HashSet<&str> = updates
1149 .iter()
1150 .filter(|u| u.new_value.is_none() && !u.flags.log_only && u.expected != Expected::Any)
1151 .map(|u| u.name.as_str())
1152 .collect();
1153 let mut pruned = PrunedPackedRecords::default();
1154 let mut by_dir: BTreeMap<PathBuf, (HashSet<String>, HashMap<String, String>)> =
1155 BTreeMap::new();
1156 for name in before.refs.keys() {
1157 if after.refs.contains_key(name) {
1158 continue;
1159 }
1160 let route = self.route(name);
1161 let (storage_names, checked_names) = by_dir.entry(route.storage_dir).or_default();
1162 if checked.contains(name.as_str()) {
1163 checked_names.insert(route.storage_name.clone(), name.clone());
1164 }
1165 storage_names.insert(route.storage_name);
1166 pruned.names.insert(name.clone());
1167 }
1168 for (dir, (storage_names, checked_names)) in by_dir {
1169 let records = remove_packed_refs_under_lock(&dir, &storage_names)?;
1170 for record in &records {
1171 let Some(logical) = checked_names.get(packed_record_name(record)) else {
1172 continue;
1173 };
1174 if let Some(oid) = packed_record_oid(record) {
1175 pruned.removed.insert(logical.clone(), RawRef::Direct(oid));
1176 }
1177 }
1178 if !records.is_empty() {
1179 pruned.records.push((dir, records));
1180 }
1181 }
1182 self.invalidate_packed_cache_after_write();
1183 Ok(pruned)
1184 }
1185
1186 fn restore_unprocessed_packed_records(
1189 &self,
1190 pruned: &PrunedPackedRecords,
1191 persisted: &HashSet<String>,
1192 ) -> Result<()> {
1193 let mut unprocessed: HashMap<PathBuf, HashSet<String>> = HashMap::new();
1194 for name in pruned.names.difference(persisted) {
1195 let route = self.route(name);
1196 unprocessed
1197 .entry(route.storage_dir)
1198 .or_default()
1199 .insert(route.storage_name);
1200 }
1201 for (dir, records) in &pruned.records {
1202 let Some(names) = unprocessed.get(dir) else {
1203 continue;
1204 };
1205 let restore: Vec<&str> = records
1206 .iter()
1207 .map(String::as_str)
1208 .filter(|record| names.contains(packed_record_name(record)))
1209 .collect();
1210 restore_packed_records_under_lock(dir, &restore)?;
1211 }
1212 self.invalidate_packed_cache_after_write();
1213 Ok(())
1214 }
1215
1216 fn delete_loose_ref(&self, refname: &str, lock: &Path, ctx: CommitContext<'_>) -> Result<()> {
1217 let route = self.route(refname);
1218 let path = route.storage_dir.join(&route.storage_name);
1219 self.remove_packed_record(refname, ctx)?;
1220 remove_empty_ref_directory(&path);
1221 let _ = fs::remove_file(&path);
1222 let _ = fs::remove_file(lock);
1223 if let Some(parent) = path.parent() {
1224 ctx.emptied_dirs
1225 .borrow_mut()
1226 .entry(parent.to_path_buf())
1227 .or_insert(route.storage_dir);
1228 }
1229 Ok(())
1230 }
1231
1232 fn persist_reflog_atomic(&self, name: &str, entries: &[ReflogEntry]) -> Result<()> {
1234 let path = self.reflog_path(name);
1235 if let Some(parent) = path.parent() {
1236 self.io_ref_store(
1237 parent,
1238 GitDirPathOperation::CreateDir,
1239 fs::create_dir_all(parent),
1240 )?;
1241 }
1242 let body: String = if entries.is_empty() {
1243 String::new()
1244 } else {
1245 entries.iter().map(reflog_line).collect()
1246 };
1247 let tmp = path.with_extension("new");
1248 let mut created_tmp = false;
1249 let write_result = (|| -> Result<()> {
1250 let mut file = fs::OpenOptions::new()
1251 .write(true)
1252 .create_new(true)
1253 .open(&tmp)
1254 .map_err(Error::Io)?;
1255 created_tmp = true;
1256 use std::io::Write as _;
1257 file.write_all(body.as_bytes()).map_err(Error::Io)?;
1258 drop(file);
1259 self.io_ref_store(
1260 &path,
1261 GitDirPathOperation::AppendReflog,
1262 fs::rename(&tmp, &path),
1263 )?;
1264 created_tmp = false;
1265 Ok(())
1266 })();
1267 if write_result.is_err() && created_tmp {
1268 let _ = fs::remove_file(&tmp);
1269 }
1270 write_result
1271 }
1272
1273 fn append_reflog_entries(&self, name: &str, entries: &[ReflogEntry]) -> Result<bool> {
1281 use std::io::{Read, Seek, SeekFrom, Write};
1282
1283 let path = self.reflog_path(name);
1284 let mut file = match fs::OpenOptions::new().read(true).append(true).open(&path) {
1285 Ok(file) => file,
1286 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
1287 Err(e) => {
1288 return self.io_ref_store(&path, GitDirPathOperation::AppendReflog, Err(e));
1289 }
1290 };
1291 let mut body = String::new();
1292 if file.metadata()?.len() > 0 {
1293 let mut last = [0u8; 1];
1294 file.seek(SeekFrom::End(-1))?;
1295 file.read_exact(&mut last)?;
1296 if last[0] != b'\n' {
1297 body.push('\n');
1298 }
1299 }
1300 body.extend(entries.iter().map(reflog_line));
1301 self.io_ref_store(
1302 &path,
1303 GitDirPathOperation::AppendReflog,
1304 file.write_all(body.as_bytes()),
1305 )?;
1306 Ok(true)
1307 }
1308
1309 fn peel_ref_to_oid(&self, start: &str) -> Result<Option<ObjectId>> {
1310 let mut name = start.to_owned();
1311 for _ in 0..crate::refs::SYMREF_MAXDEPTH {
1312 match self.read_raw(&name)? {
1313 Some(RawRef::Direct(oid)) => return Ok(Some(oid)),
1314 Some(RawRef::Symbolic(next)) => name = next,
1315 None => return Ok(None),
1316 }
1317 }
1318 Err(Error::RefStore(RefStoreError::SymrefLoop))
1319 }
1320
1321 fn recheck_update_at_commit(
1324 &self,
1325 update: &RefUpdate,
1326 batch_extras: &BTreeSet<&str>,
1327 batch_start: &RefBatchState,
1328 state_before: &RefBatchState,
1329 held_lock: Option<&Path>,
1330 ctx: CommitContext<'_>,
1331 ) -> StoreResult<()> {
1332 if update.flags.log_only {
1333 return Ok(());
1334 }
1335 let batch_differs =
1336 batch_start.refs.get(&update.name) != state_before.refs.get(&update.name);
1337 if !super::apply::expected_matches_for_update(state_before, update) {
1338 return Err(RefStoreError::ExpectedMismatch {
1339 name: update.name.clone(),
1340 expected: update.expected.clone(),
1341 actual: state_before.refs.get(&update.name).cloned(),
1342 });
1343 }
1344 if !batch_differs && update.new_value.is_none() && update.expected == Expected::Any {
1345 return Ok(());
1346 }
1347 let actual = if batch_differs {
1348 state_before.refs.get(&update.name).cloned()
1349 } else {
1350 self.read_raw(&update.name)
1351 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1352 .or_else(|| ctx.removed.get(&update.name).cloned())
1353 };
1354 let actual_ref = actual.as_ref();
1355 if !batch_differs {
1356 let disk_ok = match &update.expected {
1357 Expected::Oid(want)
1358 if matches!(actual_ref, Some(RawRef::Symbolic(_)))
1359 && matches!(update.new_value.as_ref(), Some(RawRef::Direct(_)))
1360 && !update.flags.no_deref =>
1361 {
1362 self.peel_ref_to_oid(&update.name)
1363 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1364 .is_some_and(|got| got == *want)
1365 }
1366 _ => expected_matches(actual_ref, &update.expected),
1367 };
1368 if !disk_ok {
1369 return Err(RefStoreError::ExpectedMismatch {
1370 name: update.name.clone(),
1371 expected: update.expected.clone(),
1372 actual,
1373 });
1374 }
1375 }
1376 if update.new_value.is_some() && actual_ref.is_none() && !batch_differs {
1377 self.verify_create_available(&update.name, batch_extras, held_lock)?;
1378 }
1379 Ok(())
1380 }
1381
1382 fn persist_one_ref_change(
1384 &self,
1385 name: &str,
1386 value: Option<&RawRef>,
1387 lock: &Path,
1388 ctx: CommitContext<'_>,
1389 ) -> Result<()> {
1390 let (_, path, _) = self.ref_lock_paths(name);
1391 match value {
1392 None => self.delete_loose_ref(name, lock, ctx),
1393 Some(value) => FilesRefStore::write_loose_value(
1394 &self.common_dir,
1395 &self.git_dir,
1396 name,
1397 &path,
1398 lock,
1399 value,
1400 ),
1401 }
1402 }
1403
1404 fn persist_one_ref_name(
1405 &self,
1406 name: &str,
1407 value: Option<&RawRef>,
1408 ctx: CommitContext<'_>,
1409 persisted_refs: &mut HashSet<String>,
1410 ) -> Result<()> {
1411 let (_, _, lock_path) = self.ref_lock_paths(name);
1412 self.lock_ref(name, &lock_path, ctx.ref_lock_wait)?;
1413 let write_result = self.persist_one_ref_change(name, value, &lock_path, ctx);
1414 let _ = fs::remove_file(&lock_path);
1415 write_result?;
1416 persisted_refs.insert(name.to_owned());
1417 Ok(())
1418 }
1419
1420 #[allow(clippy::too_many_arguments)]
1430 fn persist_update_locked(
1431 &self,
1432 update: &RefUpdate,
1433 batch_extras: &BTreeSet<&str>,
1434 before: &RefBatchState,
1435 running: &RefBatchState,
1436 touched: Option<&str>,
1437 ctx: CommitContext<'_>,
1438 persisted_refs: &mut HashSet<String>,
1439 writes_ref: bool,
1440 carried: &mut Option<(String, PathBuf)>,
1441 ) -> Result<()> {
1442 let value_after = |name: &str| {
1443 if touched == Some(name) {
1444 update.new_value.as_ref()
1445 } else {
1446 running.refs.get(name)
1447 }
1448 };
1449 let mut targets: Vec<String> = touched
1450 .filter(|t| *t != update.name && running.refs.get(*t) != update.new_value.as_ref())
1451 .map(str::to_owned)
1452 .into_iter()
1453 .collect();
1454 if writes_ref {
1455 targets.push(update.name.clone());
1456 }
1457
1458 let mut held: BTreeMap<String, PathBuf> = BTreeMap::new();
1459 let release = |held: &BTreeMap<String, PathBuf>| {
1460 for lock in held.values() {
1461 let _ = fs::remove_file(lock);
1462 }
1463 };
1464 let verify_name = (!matches!(update.expected, Expected::Any)).then_some(&update.name);
1465 for name in targets.iter().chain(verify_name) {
1466 if held.contains_key(name) {
1467 continue;
1468 }
1469 if let Some((_, lock)) = carried.take_if(|(carried_name, _)| carried_name == name) {
1470 held.insert(name.clone(), lock);
1471 continue;
1472 }
1473 let (_, _, lock_path) = self.ref_lock_paths(name);
1474 if let Err(err) = self.lock_ref(name, &lock_path, ctx.ref_lock_wait) {
1475 release(&held);
1476 return Err(err);
1477 }
1478 held.insert(name.clone(), lock_path);
1479 }
1480
1481 let held_lock = held.get(&update.name).map(PathBuf::as_path);
1482 if let Err(err) =
1483 self.recheck_update_at_commit(update, batch_extras, before, running, held_lock, ctx)
1484 {
1485 release(&held);
1486 return Err(err.into());
1487 }
1488 #[cfg(test)]
1489 tests::run_after_recheck_hook();
1490
1491 for name in &targets {
1492 let Some(lock_path) = held.remove(name) else {
1493 continue;
1494 };
1495 if let Err(err) = self.persist_one_ref_change(name, value_after(name), &lock_path, ctx)
1496 {
1497 let _ = fs::remove_file(&lock_path);
1498 release(&held);
1499 return Err(err);
1500 }
1501 persisted_refs.insert(name.clone());
1502 }
1503 release(&held);
1504 Ok(())
1505 }
1506
1507 fn persist_state_diff(
1508 &self,
1509 batch: &PreparedFilesBatch,
1510 carried: &mut Option<(String, PathBuf)>,
1511 emptied_dirs: &RefCell<BTreeMap<PathBuf, PathBuf>>,
1512 ) -> Result<()> {
1513 let before = &batch.before_commit;
1514 let after = &batch.after_commit;
1515 let updates = batch.updates.as_slice();
1516 let packed_lock_held = batch.packed_lock.is_some();
1517 let batch_extras: BTreeSet<&str> = updates.iter().map(|u| u.name.as_str()).collect();
1518 if updates.iter().any(|u| {
1519 u.new_value.is_some() && !u.flags.log_only && !before.refs.contains_key(&u.name)
1520 }) {
1521 self.forget_packed_cache();
1522 }
1523
1524 let pruned = if packed_lock_held {
1525 self.prune_deleted_packed_records(before, after, updates)?
1526 } else {
1527 PrunedPackedRecords::default()
1528 };
1529 let ctx = CommitContext {
1530 ref_lock_wait: batch.ref_lock_wait,
1531 lock_held: packed_lock_held,
1532 pruned: &pruned.names,
1533 removed: &pruned.removed,
1534 emptied_dirs,
1535 };
1536
1537 let names: BTreeSet<&String> = before.reflogs.keys().chain(after.reflogs.keys()).collect();
1538 for name in names {
1539 let (old, new) = (before.reflogs.get(name), after.reflogs.get(name));
1540 if old == new {
1541 continue;
1542 }
1543 let under_carried = carried
1544 .as_ref()
1545 .is_some_and(|(carried_name, _)| carried_name == name);
1546 let mut extra_lock = None;
1547 if !under_carried {
1548 let (_, _, lock_path) = self.ref_lock_paths(name);
1549 self.lock_ref(name, &lock_path, ctx.ref_lock_wait)?;
1550 extra_lock = Some(lock_path);
1551 }
1552 let write_result = (|| -> Result<()> {
1553 if let (Some(old), Some(new)) = (old, new) {
1554 if new.starts_with(old)
1555 && self.append_reflog_entries(name, &new[old.len()..])?
1556 {
1557 return Ok(());
1558 }
1559 }
1560 self.persist_reflog_atomic(name, new.map(Vec::as_slice).unwrap_or_default())
1561 })();
1562 if let Some(lock_path) = extra_lock {
1563 let _ = fs::remove_file(&lock_path);
1564 }
1565 write_result?;
1566 }
1567
1568 let mut persisted_refs = HashSet::new();
1569 let persisted =
1570 self.persist_batch_refs(batch, &batch_extras, ctx, &mut persisted_refs, carried);
1571 if persisted.is_err() {
1572 let _ = self.restore_unprocessed_packed_records(&pruned, &persisted_refs);
1575 }
1576 persisted?;
1577 self.invalidate_packed_cache_after_write();
1578 Ok(())
1579 }
1580
1581 fn persist_batch_refs(
1584 &self,
1585 batch: &PreparedFilesBatch,
1586 batch_extras: &BTreeSet<&str>,
1587 ctx: CommitContext<'_>,
1588 persisted_refs: &mut HashSet<String>,
1589 carried: &mut Option<(String, PathBuf)>,
1590 ) -> Result<()> {
1591 let before = &batch.before_commit;
1592 let after = &batch.after_commit;
1593 let updates = batch.updates.as_slice();
1594 for rename in &batch.case_only_renames {
1595 self.persist_case_only_loose_ref(rename, after, ctx)?;
1596 persisted_refs.insert(rename.from.clone());
1597 persisted_refs.insert(rename.to.clone());
1598 }
1599 let mut running = before.clone();
1600 for (idx, update) in updates.iter().enumerate() {
1601 if update.flags.log_only {
1602 apply_update(&mut running, update).map_err(Error::from)?;
1603 continue;
1604 }
1605 let touched = ref_change_target(&running, update).map_err(Error::from)?;
1606 let name = &update.name;
1607 let b = running.refs.get(name);
1608 let a = if touched.as_deref() == Some(name.as_str()) {
1609 update.new_value.as_ref()
1610 } else {
1611 b
1612 };
1613 let materialize_loose = update.new_value.is_some()
1614 && a.is_some()
1615 && b == a
1616 && !self
1617 .loose_ref_file_exists(name)
1618 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1619 let writes_ref = b != a || materialize_loose;
1620
1621 self.persist_update_locked(
1622 update,
1623 batch_extras,
1624 before,
1625 &running,
1626 touched.as_deref(),
1627 ctx,
1628 persisted_refs,
1629 writes_ref,
1630 carried,
1631 )?;
1632
1633 apply_update(&mut running, update).map_err(Error::from)?;
1634 crate::refs::test_ref_store_batch_pause_after_ref_write(idx, &self.common_dir);
1635 }
1636
1637 for name in before
1638 .refs
1639 .keys()
1640 .chain(after.refs.keys())
1641 .collect::<BTreeSet<_>>()
1642 {
1643 if persisted_refs.contains(name) {
1644 continue;
1645 }
1646 let b = before.refs.get(name);
1647 let a = after.refs.get(name);
1648 if b == a {
1649 continue;
1650 }
1651 self.persist_one_ref_name(name, a, ctx, persisted_refs)?;
1652 }
1653 Ok(())
1654 }
1655}
1656
1657impl FilesRefStore {
1658 pub(crate) fn for_each_ref_filtered(
1664 &self,
1665 prefix: &str,
1666 keep: Option<PackedRecordFilter<'_>>,
1667 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1668 ) -> Result<()> {
1669 let prefix = prefix.trim_end_matches('/');
1670 let mut map = self.load_raw_map_prefix(prefix, keep)?;
1671 for (name, value) in map.split_off(prefix) {
1672 if !name.starts_with(prefix) {
1673 break;
1674 }
1675 let peeled = match &value {
1676 RawRef::Direct(_) => None,
1677 RawRef::Symbolic(target) => self.resolve(target).ok(),
1678 };
1679 let entry = RefEntry {
1680 name,
1681 value,
1682 peeled,
1683 };
1684 if f(&entry).is_break() {
1685 break;
1686 }
1687 }
1688 Ok(())
1689 }
1690}
1691
1692impl RefStore for FilesRefStore {
1693 fn format(&self) -> RefStorageFormat {
1694 RefStorageFormat::Files
1695 }
1696
1697 fn read_raw(&self, name: &str) -> Result<Option<RawRef>> {
1698 self.read_raw_at_route(&self.route(name))
1699 }
1700
1701 fn for_each_ref(
1702 &self,
1703 prefix: &str,
1704 f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
1705 ) -> Result<()> {
1706 self.for_each_ref_filtered(prefix, None, f)
1707 }
1708
1709 fn prepare(
1710 &self,
1711 txn: RefTransaction,
1712 ) -> StoreResult<Box<dyn PreparedRefTransaction + Send + '_>> {
1713 txn.validate()?;
1714
1715 if txn.is_initial() {
1716 if let Some(initial_packed_records) = self.initial_packed_records(&txn)? {
1717 let packed_lock = self.acquire_packed_lock()?;
1718 return Ok(Box::new(FilesPrepared {
1719 store: self,
1720 committed: false,
1721 batch: Some(PreparedFilesBatch {
1722 updates: Vec::new(),
1723 case_only_renames: Vec::new(),
1724 ref_locks: HashMap::new(),
1725 packed_lock: Some(packed_lock),
1726 ref_lock_wait: LockWait::Immediately,
1727 before_commit: RefBatchState::default(),
1728 after_commit: RefBatchState::default(),
1729 initial_packed_records,
1730 }),
1731 rejections: Vec::new(),
1732 }));
1733 }
1734 }
1735
1736 #[cfg(windows)]
1737 self.reject_win32_unstorable_ref_paths(&txn)?;
1738
1739 let allow_failures = txn.allows_failures();
1740 let (updates, reflog_renames, case_only_renames) = txn.into_parts();
1741 let mut rejections = Vec::new();
1742
1743 let mut preliminary = self
1744 .load_batch_state_for_prepare(&updates, &reflog_renames, &case_only_renames)
1745 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1746 self.insert_create_conflict_neighbors(&mut preliminary.refs, &updates)
1747 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?;
1748 let updates = if allow_failures {
1749 let (accepted, part) = partition_updates_allowing_failures(&preliminary, updates)?;
1750 rejections.extend(part);
1751 accepted
1752 } else {
1753 verify_create_conflicts(&preliminary.refs, &updates)?;
1754 updates
1755 };
1756 for rename in &reflog_renames {
1757 self.ensure_reflog_path_writable(&rename.to)?;
1758 }
1759 for rename in &case_only_renames {
1760 self.ensure_reflog_path_writable(&rename.to)?;
1761 }
1762 let lock_names = ref_lock_names_for_updates(&preliminary, &updates);
1763 let ref_lock_wait = LockWait::files_ref_lock_for_git_dir(&self.common_dir);
1764
1765 let carried = updates
1766 .iter()
1767 .find(|u| !u.flags.log_only)
1768 .map(|u| u.name.as_str());
1769 let mut ref_locks: HashMap<String, PathBuf> = HashMap::new();
1770 self.take_prepare_locks(&lock_names, carried, ref_lock_wait, &mut ref_locks)?;
1771
1772 if let Err(err) =
1773 simulate_batch_apply(&preliminary, &updates, &reflog_renames, &case_only_renames)
1774 {
1775 for acquired in ref_locks.values() {
1776 let _ = fs::remove_file(acquired);
1777 }
1778 return Err(err);
1779 }
1780
1781 let extras: BTreeSet<&str> = updates.iter().map(|u| u.name.as_str()).collect();
1782 let packed = self.packed_refs_view().ok().flatten();
1783 let mut ancestors = HashMap::new();
1784 let mut available_updates = Vec::with_capacity(updates.len());
1785 for update in &updates {
1786 if update.new_value.is_none() || update.flags.log_only {
1787 available_updates.push(update.clone());
1788 continue;
1789 }
1790 if preliminary.refs.contains_key(&update.name) {
1791 available_updates.push(update.clone());
1792 continue;
1793 }
1794 if let Err(reason) = self.verify_create_available_in(
1795 &update.name,
1796 &extras,
1797 ref_locks.get(&update.name).map(PathBuf::as_path),
1798 packed.as_ref(),
1799 &mut ancestors,
1800 ) {
1801 let err: RefStoreError = reason.into();
1802 if allow_failures && is_per_ref_rejectable(&err) {
1803 rejections.push((update.name.clone(), err));
1804 continue;
1805 }
1806 for acquired in ref_locks.values() {
1807 let _ = fs::remove_file(acquired);
1808 }
1809 return Err(err);
1810 }
1811 available_updates.push(update.clone());
1812 }
1813 let updates = available_updates;
1814
1815 let before_commit = preliminary;
1816 let mut after_commit = before_commit.clone();
1817 for update in &updates {
1818 apply_update(&mut after_commit, update)?;
1819 }
1820 for rename in &case_only_renames {
1821 apply_case_only_ref_rename(&mut after_commit, rename)?;
1822 }
1823 for rename in &reflog_renames {
1824 apply_reflog_rename(&mut after_commit, rename)?;
1825 }
1826 let mut changed = changed_ref_names(&before_commit.refs, &after_commit.refs);
1827 for update in &updates {
1828 if update.flags.log_only || update.new_value.is_none() {
1829 continue;
1830 }
1831 if before_commit.refs.contains_key(&update.name)
1832 && !self
1833 .loose_ref_file_exists(&update.name)
1834 .map_err(|e| RefStoreError::Corrupt(e.to_string()))?
1835 {
1836 changed.insert(&update.name);
1837 }
1838 }
1839 for name in after_commit.reflogs.keys() {
1842 if before_commit.reflogs.contains_key(name) {
1843 continue;
1844 }
1845 if let Err(err) = self.ensure_reflog_path_writable(name) {
1846 for acquired in ref_locks.values() {
1847 let _ = fs::remove_file(acquired);
1848 }
1849 return Err(err);
1850 }
1851 }
1852
1853 self.take_prepare_locks(changed, carried, ref_lock_wait, &mut ref_locks)?;
1854
1855 let mut packed_lock = None;
1856 if self.needs_packed_lock(&updates) {
1857 match self.acquire_packed_lock() {
1858 Ok(lock) => packed_lock = Some(lock),
1859 Err(err) => {
1860 for acquired in ref_locks.values() {
1861 let _ = fs::remove_file(acquired);
1862 }
1863 return Err(err);
1864 }
1865 }
1866 }
1867
1868 Ok(Box::new(FilesPrepared {
1869 store: self,
1870 committed: false,
1871 rejections: rejections.clone(),
1872 batch: Some(PreparedFilesBatch {
1873 updates,
1874 case_only_renames,
1875 ref_locks,
1876 packed_lock,
1877 ref_lock_wait,
1878 before_commit,
1879 after_commit,
1880 initial_packed_records: Vec::new(),
1881 }),
1882 }))
1883 }
1884
1885 fn reflog_exists(&self, name: &str) -> Result<bool> {
1886 Ok(self.reflog_path(name).is_file())
1887 }
1888
1889 fn for_each_reflog_entry(
1890 &self,
1891 name: &str,
1892 reverse: bool,
1893 f: &mut dyn FnMut(&ReflogEntry) -> ControlFlow<()>,
1894 ) -> Result<()> {
1895 let path = self.reflog_path(name);
1896 if reverse {
1897 let Some(mut reader) = reflog::ReverseReflogLineReader::open(&path)? else {
1899 return Ok(());
1900 };
1901 while let Some(line) = reader.next_nonempty_line()? {
1902 let Some(entry) = reflog::parse_reflog_line_for_store(&utf8_or_lossy(&line)) else {
1903 continue;
1904 };
1905 if f(&entry).is_break() {
1906 break;
1907 }
1908 }
1909 return Ok(());
1910 }
1911 for entry in &read_reflog_at_path(&path)? {
1912 if f(entry).is_break() {
1913 break;
1914 }
1915 }
1916 Ok(())
1917 }
1918
1919 fn create_reflog(&self, name: &str) -> Result<()> {
1920 validate_storable_refname(name).map_err(|e| Error::Message(e.to_string()))?;
1921 let path = self.reflog_path(name);
1922 if let Some(parent) = path.parent() {
1923 self.io_ref_store(
1924 parent,
1925 GitDirPathOperation::CreateDir,
1926 fs::create_dir_all(parent),
1927 )?;
1928 }
1929 self.io_ref_store(
1930 &path,
1931 GitDirPathOperation::AppendReflog,
1932 fs::OpenOptions::new().create(true).append(true).open(&path),
1933 )?;
1934 Ok(())
1935 }
1936
1937 fn delete_reflog(&self, name: &str) -> Result<()> {
1938 let route = self.route(name);
1939 let logs_root = route.storage_dir.join("logs");
1940 let path = logs_root.join(&route.storage_name);
1941 let _ = fs::remove_file(&path);
1942 let mut parent = path.parent();
1945 while let Some(dir) = parent.filter(|d| *d != logs_root && d.starts_with(&logs_root)) {
1946 if fs::remove_dir(dir).is_err() {
1947 break;
1948 }
1949 parent = dir.parent();
1950 }
1951 Ok(())
1952 }
1953
1954 fn replace_reflog(&self, name: &str, entries: Vec<ReflogEntry>) -> Result<()> {
1955 self.rewrite_reflog(name, &mut |_| Ok(entries.clone()))
1956 }
1957
1958 fn rewrite_reflog(
1959 &self,
1960 name: &str,
1961 transform: &mut dyn FnMut(Vec<ReflogEntry>) -> Result<Vec<ReflogEntry>>,
1962 ) -> Result<()> {
1963 let wait = LockWait::files_ref_lock_for_git_dir(&self.common_dir);
1964 let (_, _, lock_path) = self.ref_lock_paths(name);
1965 self.lock_ref(name, &lock_path, wait)?;
1966 let result = (|| -> Result<()> {
1967 let entries = self.read_reflog_entries(name)?;
1968 let rewritten = transform(entries)?;
1969 self.persist_reflog_atomic(name, &rewritten)
1970 })();
1971 let _ = fs::remove_file(&lock_path);
1972 result
1973 }
1974
1975 fn for_each_reflog_ref(&self, f: &mut dyn FnMut(&str) -> ControlFlow<()>) -> Result<()> {
1976 let mut names = BTreeSet::new();
1977 for base in self.store_bases() {
1978 let logs = base.join("logs");
1979 if logs.is_dir() {
1980 collect_reflog_ref_names(&logs, "", self, &mut names)?;
1981 }
1982 }
1983 for name in names {
1984 if f(&name).is_break() {
1985 break;
1986 }
1987 }
1988 Ok(())
1989 }
1990
1991 fn as_files(&self) -> Option<&FilesRefStore> {
1992 Some(self)
1993 }
1994}
1995
1996struct FilesPrepared<'a> {
1997 store: &'a FilesRefStore,
1998 committed: bool,
1999 batch: Option<PreparedFilesBatch>,
2000 rejections: Vec<(String, RefStoreError)>,
2001}
2002
2003impl PreparedRefTransaction for FilesPrepared<'_> {
2004 fn rejected_updates(&self) -> &[(String, RefStoreError)] {
2005 &self.rejections
2006 }
2007
2008 fn commit(mut self: Box<Self>) -> StoreResult<()> {
2009 let Some(mut batch) = self.batch.take() else {
2010 return Ok(());
2011 };
2012
2013 if !batch.initial_packed_records.is_empty() {
2014 let records: Vec<&str> = batch
2015 .initial_packed_records
2016 .iter()
2017 .map(String::as_str)
2018 .collect();
2019 let written = restore_packed_records_under_lock(&self.store.common_dir, &records);
2020 FilesRefStore::release_locks(&batch);
2021 self.store.invalidate_packed_cache_after_write();
2022 written.map_err(FilesRefStore::ref_store_error_from_lib)?;
2023 self.committed = true;
2024 return Ok(());
2025 }
2026
2027 let mut ref_locks = std::mem::take(&mut batch.ref_locks);
2032 let mut carried = batch
2033 .updates
2034 .iter()
2035 .find(|u| !u.flags.log_only)
2036 .and_then(|u| ref_locks.remove_entry(&u.name));
2037 for lock in ref_locks.values() {
2038 let _ = fs::remove_file(lock);
2039 }
2040 let emptied_dirs = RefCell::new(BTreeMap::new());
2043 let persisted = self
2044 .store
2045 .persist_state_diff(&batch, &mut carried, &emptied_dirs);
2046 if let Some((_, lock)) = carried {
2047 let _ = fs::remove_file(lock);
2048 }
2049 let mut emptied_dirs: Vec<_> = emptied_dirs.into_inner().into_iter().collect();
2053 emptied_dirs.sort_by_key(|(dir, _)| std::cmp::Reverse(dir.components().count()));
2054 for (dir, storage_dir) in &emptied_dirs {
2055 prune_empty_ref_dirs(storage_dir, dir);
2056 }
2057 self.store.invalidate_packed_cache_after_write();
2058 if let Err(err) = persisted {
2059 if let Some(p) = &batch.packed_lock {
2060 let _ = fs::remove_file(p);
2061 }
2062 return Err(match err {
2063 Error::RefStore(store_err) => store_err,
2064 other => RefStoreError::Corrupt(other.to_string()),
2065 });
2066 }
2067 if let Some(p) = &batch.packed_lock {
2068 let _ = fs::remove_file(p);
2069 }
2070 self.committed = true;
2071 Ok(())
2072 }
2073
2074 fn abort(mut self: Box<Self>) -> StoreResult<()> {
2075 if let Some(batch) = self.batch.take() {
2076 FilesRefStore::release_locks(&batch);
2077 }
2078 self.store.invalidate_packed_cache_after_write();
2079 self.committed = true;
2080 Ok(())
2081 }
2082}
2083
2084impl Drop for FilesPrepared<'_> {
2085 fn drop(&mut self) {
2086 if self.committed {
2087 return;
2088 }
2089 if let Some(batch) = self.batch.take() {
2090 FilesRefStore::release_locks(&batch);
2091 }
2092 self.store.invalidate_packed_cache_after_write();
2093 }
2094}
2095
2096fn loose_storage_path_to_refname(storage_root: &Path, loose: &Path) -> String {
2097 loose
2098 .strip_prefix(storage_root)
2099 .map(|rel| rel.to_string_lossy().replace('\\', "/"))
2100 .unwrap_or_else(|_| loose.to_string_lossy().into_owned())
2101}
2102
2103fn first_loose_ref_file_under(dir: &Path) -> Option<PathBuf> {
2104 let entries = fs::read_dir(dir).ok()?;
2105 for entry in entries.flatten() {
2106 let path = entry.path();
2107 let file_type = entry.file_type().ok()?;
2108 if file_type.is_dir() {
2109 if let Some(found) = first_loose_ref_file_under(&path) {
2110 return Some(found);
2111 }
2112 } else if file_type.is_file() && path.extension().is_none_or(|e| e != "lock") {
2113 return Some(path);
2114 }
2115 }
2116 None
2117}
2118
2119fn name_unavailable_after_loose_rename_conflict(
2120 refname: &str,
2121 ref_path: &Path,
2122 storage_root: &Path,
2123) -> RefnameUnavailable {
2124 if ref_path.is_dir() {
2125 if let Some(descendant) = first_loose_ref_file_under(ref_path) {
2126 return RefnameUnavailable::DescendantExists {
2127 blocking: loose_storage_path_to_refname(storage_root, &descendant),
2128 new_ref: refname.to_owned(),
2129 };
2130 }
2131 }
2132 if let Some(parent) = ref_path.parent() {
2133 if parent.is_file() {
2134 return RefnameUnavailable::AncestorExists {
2135 blocking: loose_storage_path_to_refname(storage_root, parent),
2136 new_ref: refname.to_owned(),
2137 };
2138 }
2139 }
2140 RefnameUnavailable::DescendantExists {
2141 blocking: format!("{refname}/…"),
2142 new_ref: refname.to_owned(),
2143 }
2144}
2145
2146fn head_matches_prefix(logical_prefix: &str) -> bool {
2147 let prefix = logical_prefix.trim_end_matches('/');
2148 if prefix.is_empty() {
2149 return false;
2151 }
2152 "HEAD".starts_with(prefix) || prefix.starts_with("HEAD")
2153}
2154
2155fn ref_to_raw(r: Ref) -> RawRef {
2156 match r {
2157 Ref::Direct(oid) => RawRef::Direct(oid),
2158 Ref::Symbolic(s) => RawRef::Symbolic(s),
2159 }
2160}
2161
2162fn collect_loose_raw_refs(
2168 dir: &Path,
2169 prefix: &str,
2170 want: &str,
2171 out: &mut BTreeMap<String, RawRef>,
2172) -> Result<()> {
2173 let read = match fs::read_dir(dir) {
2174 Ok(r) => r,
2175 Err(e)
2176 if e.kind() == io::ErrorKind::NotFound || e.kind() == io::ErrorKind::NotADirectory =>
2177 {
2178 return Ok(());
2179 }
2180 Err(e) => return Err(Error::Io(e)),
2181 };
2182 for entry in read {
2183 let entry = entry?;
2184 let name_owned = entry.file_name().into_string().map_err(|_| {
2185 Error::Io(io::Error::new(
2186 io::ErrorKind::InvalidData,
2187 "non-utf8 ref path component",
2188 ))
2189 })?;
2190 let refname = format!("{prefix}{name_owned}");
2191 let wanted = refname.starts_with(want);
2192 let leads_to_wanted = || {
2193 want.strip_prefix(refname.as_str())
2194 .is_some_and(|rest| rest.starts_with('/'))
2195 };
2196 if !wanted && !leads_to_wanted() {
2197 continue;
2198 }
2199 let path = entry.path();
2200 let file_type = entry.file_type().ok();
2203 if file_type.is_some_and(|t| t.is_file()) {
2204 if wanted {
2205 if let Ok(r) = crate::refs::read_regular_ref_file(&path) {
2206 out.insert(refname, ref_to_raw(r));
2207 }
2208 }
2209 continue;
2210 }
2211 if file_type.is_some_and(|t| t.is_dir()) {
2212 collect_loose_raw_refs(&path, &format!("{refname}/"), want, out)?;
2213 continue;
2214 }
2215 let meta = match fs::metadata(&path) {
2216 Ok(m) => m,
2217 Err(_) => continue,
2219 };
2220 if meta.is_dir() {
2221 collect_loose_raw_refs(&path, &format!("{refname}/"), want, out)?;
2222 } else if meta.is_file() && wanted {
2223 if let Ok(r) = read_ref_file(&path) {
2224 out.insert(refname, ref_to_raw(r));
2225 }
2226 }
2227 }
2228 Ok(())
2229}
2230
2231fn collect_loose_file_names(dir: &Path, prefix: &str, out: &mut BTreeSet<String>) {
2234 let Ok(read) = fs::read_dir(dir) else {
2235 return;
2236 };
2237 for entry in read.flatten() {
2238 let name = format!("{prefix}/{}", entry.file_name().to_string_lossy());
2239 let path = entry.path();
2240 match fs::metadata(&path) {
2241 Ok(meta) if meta.is_dir() => collect_loose_file_names(&path, &name, out),
2242 Ok(meta) if meta.is_file() => {
2243 out.insert(name);
2244 }
2245 _ => {}
2246 }
2247 }
2248}
2249
2250#[cfg(unix)]
2251fn file_identity(meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
2252 use std::os::unix::fs::MetadataExt;
2253 Some((meta.dev(), meta.ino(), meta.ctime(), meta.ctime_nsec()))
2254}
2255
2256#[cfg(not(unix))]
2257fn file_identity(_meta: &fs::Metadata) -> Option<(u64, u64, i64, i64)> {
2258 None
2259}
2260
2261fn reflog_line(entry: &ReflogEntry) -> String {
2263 let (old_hex, new_hex) = crate::refs::reflog_oid_hex_pair(&entry.old_oid, &entry.new_oid);
2264 if entry.message.is_empty() {
2265 format!("{old_hex} {new_hex} {}\n", entry.identity)
2266 } else {
2267 format!(
2268 "{old_hex} {new_hex} {}\t{}\n",
2269 entry.identity, entry.message
2270 )
2271 }
2272}
2273
2274fn read_reflog_at_path(path: &Path) -> Result<Vec<ReflogEntry>> {
2278 let content = match fs::read(path) {
2279 Ok(bytes) => bytes,
2280 Err(e) if crate::refs::io_error_is_missing_file(&e, path) => return Ok(Vec::new()),
2283 Err(e) => return Err(Error::Io(e)),
2284 };
2285 let text = utf8_or_lossy(&content);
2286 Ok(text
2287 .lines()
2288 .filter_map(reflog::parse_reflog_line_for_store)
2289 .collect())
2290}
2291
2292fn utf8_or_lossy(bytes: &[u8]) -> Cow<'_, str> {
2295 match std::str::from_utf8(bytes) {
2296 Ok(text) => Cow::Borrowed(text),
2297 Err(_) => String::from_utf8_lossy(bytes),
2298 }
2299}
2300
2301fn changed_ref_names<'a>(
2302 before: &'a BTreeMap<String, RawRef>,
2303 after: &'a BTreeMap<String, RawRef>,
2304) -> BTreeSet<&'a String> {
2305 before
2306 .keys()
2307 .chain(after.keys())
2308 .filter(|name| before.get(*name) != after.get(*name))
2309 .collect()
2310}
2311
2312fn collect_reflog_ref_names(
2313 dir: &Path,
2314 prefix: &str,
2315 store: &FilesRefStore,
2316 out: &mut BTreeSet<String>,
2317) -> Result<()> {
2318 let read = match fs::read_dir(dir) {
2319 Ok(r) => r,
2320 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
2321 Err(e) => return Err(Error::Io(e)),
2322 };
2323 for entry in read {
2324 let entry = entry?;
2325 let name = entry.file_name().to_string_lossy().into_owned();
2326 let refname = format!("{prefix}{name}");
2327 let path = entry.path();
2328 if path.is_dir() {
2329 collect_reflog_ref_names(&path, &format!("{refname}/"), store, out)?;
2330 } else if path.is_file() {
2331 out.insert(store.logical_name(&refname));
2332 }
2333 }
2334 Ok(())
2335}
2336
2337#[cfg(test)]
2338mod tests {
2339 use std::cell::RefCell;
2340
2341 use super::*;
2342 use crate::refs::store::RefUpdateFlags;
2343
2344 thread_local! {
2346 static AFTER_RECHECK: RefCell<Option<Box<dyn FnOnce()>>> = const { RefCell::new(None) };
2347 }
2348
2349 pub(super) fn run_after_recheck_hook() {
2351 if let Some(hook) = AFTER_RECHECK.with(|slot| slot.borrow_mut().take()) {
2352 hook();
2353 }
2354 }
2355
2356 fn after_next_recheck(hook: impl FnOnce() + 'static) {
2359 AFTER_RECHECK.with(|slot| *slot.borrow_mut() = Some(Box::new(hook)));
2360 }
2361
2362 fn oid(byte: u8) -> ObjectId {
2363 let mut bytes = [0u8; 20];
2364 bytes[19] = byte;
2365 ObjectId::from_bytes(&bytes).expect("oid")
2366 }
2367
2368 fn store(git_dir: &Path) -> FilesRefStore {
2369 FilesRefStore::open(FilesRefStoreConfig {
2370 git_dir: git_dir.to_path_buf(),
2371 common_dir: git_dir.to_path_buf(),
2372 namespace_prefix: None,
2373 })
2374 }
2375
2376 fn set(
2377 store: &FilesRefStore,
2378 name: &str,
2379 value: ObjectId,
2380 expected: Expected,
2381 ) -> StoreResult<()> {
2382 let txn = RefTransaction::new().update(RefUpdate {
2383 name: name.to_owned(),
2384 new_value: Some(RawRef::Direct(value)),
2385 expected,
2386 reflog: None,
2387 flags: RefUpdateFlags::default(),
2388 })?;
2389 store.prepare(txn)?.commit()
2390 }
2391
2392 fn git_dir() -> tempfile::TempDir {
2393 let dir = tempfile::tempdir().expect("tempdir");
2394 fs::create_dir_all(dir.path().join("refs/heads")).expect("refs");
2395 fs::write(dir.path().join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
2396 dir
2397 }
2398
2399 fn listed_names(store: &FilesRefStore, prefix: &str) -> Vec<String> {
2400 let mut names = Vec::new();
2401 store
2402 .for_each_ref(prefix, &mut |entry| {
2403 names.push(entry.name.clone());
2404 ControlFlow::Continue(())
2405 })
2406 .expect("list");
2407 names
2408 }
2409
2410 #[test]
2411 fn partial_name_prefix_lists_loose_refs_without_visiting_siblings() {
2412 let dir = git_dir();
2413 let tags = dir.path().join("refs/tags");
2414 for name in ["v1", "v10", "v1x/a", "v2/deep", "w"] {
2415 let path = tags.join(name);
2416 fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
2417 fs::write(path, format!("{}\n", oid(1))).expect("ref");
2418 }
2419 #[cfg(unix)]
2421 std::os::unix::fs::symlink(tags.join("missing-target"), tags.join("zzz"))
2422 .expect("dangling symlink");
2423 let store = store(dir.path());
2424
2425 assert_eq!(
2426 listed_names(&store, "refs/tags/v1"),
2427 ["refs/tags/v1", "refs/tags/v10", "refs/tags/v1x/a"]
2428 );
2429 assert_eq!(listed_names(&store, "refs/tags/v2/"), ["refs/tags/v2/deep"]);
2430 assert_eq!(listed_names(&store, "refs/tags/w"), ["refs/tags/w"]);
2431 assert!(listed_names(&store, "refs/tags/x").is_empty());
2432 }
2433
2434 fn assert_racing_writer_is_locked_out(expected: Expected, seed: Option<ObjectId>) {
2437 let dir = git_dir();
2438 let name = "refs/notes/commits";
2439 let writer = store(dir.path());
2440 if let Some(seed) = seed {
2441 set(&writer, name, seed, Expected::Missing).expect("seed");
2442 }
2443
2444 let other = store(dir.path());
2445 let racing_expected = expected.clone();
2446 let racer = std::rc::Rc::new(RefCell::new(None));
2447 let racer_slot = racer.clone();
2448 after_next_recheck(move || {
2449 *racer_slot.borrow_mut() = Some(set(&other, name, oid(2), racing_expected));
2450 });
2451 set(&writer, name, oid(3), expected).expect("first committer wins");
2452
2453 let racer = racer.borrow_mut().take().expect("hook ran");
2454 assert!(
2455 matches!(racer, Err(RefStoreError::LockHeld { .. })),
2456 "racing writer must not commit while the ref is locked: {racer:?}"
2457 );
2458 assert_eq!(
2459 writer.read_raw(name).expect("read"),
2460 Some(RawRef::Direct(oid(3)))
2461 );
2462 assert!(
2463 !dir.path().join("refs/notes/commits.lock").exists(),
2464 "the lock is released after the write"
2465 );
2466 }
2467
2468 #[test]
2469 fn expected_oid_update_holds_the_lock_through_the_write() {
2470 assert_racing_writer_is_locked_out(Expected::Oid(oid(1)), Some(oid(1)));
2471 }
2472
2473 #[test]
2474 fn expected_missing_create_holds_the_lock_through_the_write() {
2475 assert_racing_writer_is_locked_out(Expected::Missing, None);
2476 }
2477
2478 #[test]
2479 fn symbolic_update_holds_the_dereferenced_target_lock_through_the_write() {
2480 let dir = git_dir();
2481 let writer = store(dir.path());
2482 set(&writer, "refs/heads/main", oid(1), Expected::Missing).expect("seed main");
2483
2484 let other = store(dir.path());
2485 let racer = std::rc::Rc::new(RefCell::new(None));
2486 let racer_slot = racer.clone();
2487 after_next_recheck(move || {
2488 *racer_slot.borrow_mut() = Some(set(
2489 &other,
2490 "refs/heads/main",
2491 oid(2),
2492 Expected::Oid(oid(1)),
2493 ));
2494 });
2495 set(&writer, "HEAD", oid(3), Expected::Oid(oid(1))).expect("update through HEAD");
2496
2497 let racer = racer.borrow_mut().take().expect("hook ran");
2498 assert!(
2499 matches!(racer, Err(RefStoreError::LockHeld { .. })),
2500 "a direct writer of the HEAD target must be locked out: {racer:?}"
2501 );
2502 assert_eq!(
2503 writer.read_raw("refs/heads/main").expect("read"),
2504 Some(RawRef::Direct(oid(3)))
2505 );
2506 assert_eq!(
2507 writer.read_raw("HEAD").expect("read"),
2508 Some(RawRef::Symbolic("refs/heads/main".to_owned()))
2509 );
2510 for lock in ["HEAD.lock", "refs/heads/main.lock"] {
2511 assert!(!dir.path().join(lock).exists(), "{lock} released");
2512 }
2513 }
2514
2515 #[test]
2516 fn create_availability_matches_the_path_based_check() {
2517 let dir = git_dir();
2518 let path = dir.path();
2519 let hex = oid(1).to_hex();
2520 fs::write(
2521 path.join("packed-refs"),
2522 format!(
2523 "# pack-refs with: peeled fully-peeled sorted \n\
2524 {hex} refs/heads/p\n\
2525 {hex} refs/heads/pd/x\n\
2526 {hex} refs/heads/pd-sibling\n\
2527 {hex} refs/tags/v1\n"
2528 ),
2529 )
2530 .expect("packed-refs");
2531 for loose in ["refs/heads/l", "refs/heads/ld/y/z"] {
2532 let file = path.join(loose);
2533 fs::create_dir_all(file.parent().expect("parent")).expect("dirs");
2534 fs::write(file, format!("{hex}\n")).expect("loose");
2535 }
2536 let refs = store(path);
2537 let extras: BTreeSet<String> = ["refs/heads/batch/one".to_owned()].into();
2538 for name in [
2539 "refs/heads/new",
2540 "refs/heads/p/child",
2541 "refs/heads/pd",
2542 "refs/heads/pd-sibling/x",
2543 "refs/heads/l/child",
2544 "refs/heads/ld",
2545 "refs/heads/ld/y",
2546 "refs/heads/batch",
2547 "refs/heads/batch/one/two",
2548 "refs/tags/v1/x",
2549 "refs/tags/v2",
2550 ] {
2551 assert_eq!(
2552 refs.verify_create_available(
2553 name,
2554 &extras.iter().map(String::as_str).collect(),
2555 None
2556 ),
2557 crate::refs::verify_refname_available_for_create(
2558 path,
2559 name,
2560 &extras,
2561 &HashSet::new()
2562 ),
2563 "{name}"
2564 );
2565 }
2566 }
2567
2568 #[test]
2569 fn mapped_packed_refs_are_reread_after_replacement() {
2570 let dir = git_dir();
2571 let path = dir.path();
2572 let records = |value: ObjectId| -> Vec<crate::refs::PackedRefRecord> {
2573 (0..500)
2574 .map(|i| crate::refs::PackedRefRecord {
2575 refname: format!("refs/tags/t{i:04}"),
2576 oid: value,
2577 peeled: None,
2578 })
2579 .collect()
2580 };
2581 crate::refs::write_packed_refs_file(path, &records(oid(1))).expect("pack");
2582 assert!(fs::metadata(path.join("packed-refs")).expect("meta").len() > 8192);
2583 let refs = store(path);
2584 assert_eq!(
2585 refs.read_raw("refs/tags/t0499").expect("read"),
2586 Some(RawRef::Direct(oid(1)))
2587 );
2588 let mut replaced = records(oid(2));
2589 replaced.truncate(10);
2590 crate::refs::write_packed_refs_file(path, &replaced).expect("repack");
2591 assert_eq!(refs.read_raw("refs/tags/t0499").expect("read"), None);
2592 assert_eq!(
2593 refs.read_raw("refs/tags/t0009").expect("read"),
2594 Some(RawRef::Direct(oid(2)))
2595 );
2596 }
2597
2598 fn replace_packed_refs_keeping_len_and_mtime(path: &Path, body: &str) {
2601 let packed = path.join("packed-refs");
2602 let before = fs::metadata(&packed).expect("meta");
2603 assert_eq!(before.len(), body.len() as u64, "same length");
2604 let tmp = path.join("packed-refs.new");
2605 fs::write(&tmp, body).expect("write");
2606 fs::File::options()
2607 .write(true)
2608 .open(&tmp)
2609 .expect("open")
2610 .set_modified(before.modified().expect("mtime"))
2611 .expect("pin mtime");
2612 fs::rename(&tmp, &packed).expect("rename");
2613 let after = fs::metadata(&packed).expect("meta");
2614 assert_eq!(after.len(), before.len());
2615 assert_eq!(after.modified().ok(), before.modified().ok());
2616 }
2617
2618 #[test]
2619 fn create_rechecks_a_packed_refs_replaced_with_same_len_and_mtime() {
2620 assert_create_blocked_by_replaced_packed_descendant(false);
2621 }
2622
2623 #[test]
2624 fn create_rechecks_packed_refs_even_when_the_stamp_cannot_tell() {
2625 assert_create_blocked_by_replaced_packed_descendant(true);
2626 }
2627
2628 fn assert_create_blocked_by_replaced_packed_descendant(stamp_collides: bool) {
2631 let dir = git_dir();
2632 let path = dir.path();
2633 let hex = oid(1).to_hex();
2634 let header = "# pack-refs with: peeled fully-peeled sorted \n";
2635 fs::write(
2636 path.join("packed-refs"),
2637 format!("{header}{hex} refs/heads/blocked-child\n"),
2638 )
2639 .expect("packed-refs");
2640 let writer = store(path);
2641 assert_eq!(
2642 writer.read_raw("refs/heads/blocked-child").expect("prime"),
2643 Some(RawRef::Direct(oid(1)))
2644 );
2645 replace_packed_refs_keeping_len_and_mtime(
2646 path,
2647 &format!("{header}{hex} refs/heads/blocked/child\n"),
2648 );
2649 if stamp_collides {
2650 let mut guard = writer.packed_cache.lock().expect("cache");
2653 guard.stamp = FilesRefStore::packed_refs_stamp(&path.join("packed-refs"));
2654 }
2655
2656 let err = set(&writer, "refs/heads/blocked", oid(2), Expected::Missing)
2657 .expect_err("packed descendant blocks the create");
2658 assert!(
2659 matches!(
2660 err,
2661 RefStoreError::NameUnavailable {
2662 reason: RefnameUnavailable::DescendantExists { ref blocking, .. }
2663 } if blocking == "refs/heads/blocked/child"
2664 ),
2665 "{err:?}"
2666 );
2667 assert!(!path.join("refs/heads/blocked").exists());
2668 assert!(!path.join("refs/heads/blocked.lock").exists());
2669 }
2670
2671 #[test]
2672 fn a_ref_whose_reflog_is_not_utf8_moves_and_its_log_keeps_what_it_had() {
2673 let dir = git_dir();
2674 let path = dir.path();
2675 let writer = store(path);
2676 let name = "refs/heads/main";
2677 set(&writer, name, oid(1), Expected::Missing).expect("create");
2678 let log = path.join("logs/refs/heads/main");
2681 fs::create_dir_all(log.parent().expect("parent")).expect("logs");
2682 let mut written = format!(
2683 "{} {} A <a@e> 1700000000 +0000\tcommit: Caf",
2684 oid(0).to_hex(),
2685 oid(1).to_hex()
2686 )
2687 .into_bytes();
2688 written.extend_from_slice(b"\xe9 latin-1\nnot a reflog line\n");
2689 fs::write(&log, &written).expect("log");
2690
2691 let txn = RefTransaction::new()
2692 .update(RefUpdate {
2693 name: name.to_owned(),
2694 new_value: Some(RawRef::Direct(oid(2))),
2695 expected: Expected::Oid(oid(1)),
2696 reflog: Some(super::super::ReflogUpdate {
2697 identity: "T <t@t> 1700000001 +0000".into(),
2698 message: "moved".into(),
2699 time: time::OffsetDateTime::UNIX_EPOCH,
2700 old_oid: None,
2701 new_oid: None,
2702 }),
2703 flags: RefUpdateFlags::default(),
2704 })
2705 .expect("txn");
2706 writer
2707 .prepare(txn)
2708 .expect("prepare")
2709 .commit()
2710 .expect("commit");
2711
2712 assert_eq!(
2713 writer.read_raw(name).expect("read"),
2714 Some(RawRef::Direct(oid(2)))
2715 );
2716 let bytes = fs::read(&log).expect("log");
2717 assert!(
2718 bytes.starts_with(&written),
2719 "what the log had stays as written"
2720 );
2721 assert_eq!(
2722 String::from_utf8_lossy(&bytes[written.len()..]),
2723 format!(
2724 "{} {} T <t@t> 1700000001 +0000\tmoved\n",
2725 oid(1).to_hex(),
2726 oid(2).to_hex()
2727 )
2728 );
2729 let mut messages = Vec::new();
2730 writer
2731 .for_each_reflog_entry(name, false, &mut |entry| {
2732 messages.push(entry.message.clone());
2733 ControlFlow::Continue(())
2734 })
2735 .expect("read log");
2736 assert_eq!(messages, ["commit: Caf\u{fffd} latin-1", "moved"]);
2737 }
2738
2739 #[test]
2740 fn create_with_reflog_next_to_a_ref_file_in_logs_reports_the_conflict() {
2741 let dir = git_dir();
2742 let path = dir.path();
2743 let writer = store(path);
2744 set(&writer, "refs/heads/a", oid(1), Expected::Missing).expect("create");
2745 fs::create_dir_all(path.join("logs/refs/heads")).expect("logs");
2746 fs::write(path.join("logs/refs/heads/a"), "").expect("log");
2747 let txn = RefTransaction::new()
2748 .update(RefUpdate {
2749 name: "refs/heads/a/b".to_owned(),
2750 new_value: Some(RawRef::Direct(oid(2))),
2751 expected: Expected::Missing,
2752 reflog: Some(super::super::ReflogUpdate {
2753 identity: "T <t@t>".into(),
2754 message: "create".into(),
2755 time: time::OffsetDateTime::UNIX_EPOCH,
2756 old_oid: None,
2757 new_oid: None,
2758 }),
2759 flags: RefUpdateFlags::default(),
2760 })
2761 .expect("txn");
2762 let err = writer.prepare(txn).map(|_| ()).expect_err("conflict");
2763 assert!(
2764 matches!(
2765 err,
2766 RefStoreError::NameUnavailable {
2767 reason: RefnameUnavailable::AncestorExists { ref blocking, .. }
2768 } if blocking == "refs/heads/a"
2769 ),
2770 "{err:?}"
2771 );
2772 assert!(!path.join("refs/heads/a.lock").exists());
2773 }
2774
2775 fn create_with_reflog(name: &str, value: ObjectId) -> RefTransaction {
2776 RefTransaction::new()
2777 .update(RefUpdate {
2778 name: name.to_owned(),
2779 new_value: Some(RawRef::Direct(value)),
2780 expected: Expected::Missing,
2781 reflog: Some(super::super::ReflogUpdate {
2782 identity: "T <t@t>".into(),
2783 message: "branch: Created from main".into(),
2784 time: time::OffsetDateTime::UNIX_EPOCH,
2785 old_oid: None,
2786 new_oid: None,
2787 }),
2788 flags: RefUpdateFlags::default(),
2789 })
2790 .expect("txn")
2791 }
2792
2793 fn assert_rel_create_blocked_by(path: &Path, blocking_ref: &str) {
2796 let writer = store(path);
2797 let err = writer
2798 .prepare(create_with_reflog("refs/heads/rel", oid(2)))
2799 .map(|_| ())
2800 .expect_err("conflict");
2801 assert!(
2802 matches!(
2803 err,
2804 RefStoreError::NameUnavailable {
2805 reason: RefnameUnavailable::DescendantExists { ref blocking, ref new_ref }
2806 } if blocking == blocking_ref && new_ref == "refs/heads/rel"
2807 ),
2808 "{err:?}"
2809 );
2810 assert_eq!(writer.read_raw("refs/heads/rel").expect("read"), None);
2811 assert!(!path.join("refs/heads/rel.lock").exists());
2812 assert!(!path.join("logs/refs/heads/rel").is_file());
2813 }
2814
2815 #[test]
2816 fn create_with_reflog_over_a_loose_descendant_names_it() {
2817 let dir = git_dir();
2818 let path = dir.path();
2819 store(path)
2820 .prepare(create_with_reflog("refs/heads/rel/1.0", oid(1)))
2821 .expect("prepare seed")
2822 .commit()
2823 .expect("seed");
2824 assert!(path.join("logs/refs/heads/rel/1.0").is_file());
2825 assert_rel_create_blocked_by(path, "refs/heads/rel/1.0");
2826 }
2827
2828 #[test]
2829 fn create_with_reflog_over_a_packed_descendant_with_its_log_dir_names_it() {
2830 let dir = git_dir();
2831 let path = dir.path();
2832 fs::write(
2834 path.join("packed-refs"),
2835 format!(
2836 "# pack-refs with: peeled fully-peeled sorted \n{} refs/heads/rel/1.0\n",
2837 oid(1)
2838 ),
2839 )
2840 .expect("packed-refs");
2841 fs::create_dir_all(path.join("refs/heads/rel")).expect("empty ref dir");
2842 fs::create_dir_all(path.join("logs/refs/heads/rel")).expect("logs");
2843 fs::write(path.join("logs/refs/heads/rel/1.0"), "").expect("log");
2844 assert_rel_create_blocked_by(path, "refs/heads/rel/1.0");
2845 }
2846
2847 #[test]
2848 fn create_with_reflog_over_a_stray_log_directory_names_the_log() {
2849 let dir = git_dir();
2850 let path = dir.path();
2851 fs::create_dir_all(path.join("logs/refs/heads/rel/deep")).expect("logs");
2852 fs::write(path.join("logs/refs/heads/rel/deep/2.0"), "").expect("log");
2853 assert_rel_create_blocked_by(path, "refs/heads/rel/deep/2.0");
2854 assert!(path.join("logs/refs/heads/rel/deep/2.0").is_file());
2855 }
2856
2857 #[test]
2858 fn create_with_reflog_removes_an_empty_log_directory() {
2859 let dir = git_dir();
2860 let path = dir.path();
2861 fs::create_dir_all(path.join("logs/refs/heads/rel/a/b")).expect("empty logs");
2862 let writer = store(path);
2863 writer
2864 .prepare(create_with_reflog("refs/heads/rel", oid(2)))
2865 .expect("prepare")
2866 .commit()
2867 .expect("commit");
2868 assert_eq!(
2869 writer.read_raw("refs/heads/rel").expect("read"),
2870 Some(RawRef::Direct(oid(2)))
2871 );
2872 assert_eq!(
2873 writer
2874 .read_reflog_entries("refs/heads/rel")
2875 .expect("log")
2876 .len(),
2877 1
2878 );
2879 }
2880
2881 #[test]
2882 fn failed_recheck_releases_the_lock() {
2883 let dir = git_dir();
2884 let name = "refs/heads/main";
2885 let writer = store(dir.path());
2886 set(&writer, name, oid(1), Expected::Missing).expect("create");
2887 let other = store(dir.path());
2888 set(&other, name, oid(2), Expected::Oid(oid(1))).expect("other moves it");
2889
2890 let err = set(&writer, name, oid(3), Expected::Oid(oid(1))).expect_err("stale");
2891 assert!(
2892 matches!(err, RefStoreError::ExpectedMismatch { .. }),
2893 "{err:?}"
2894 );
2895 assert!(!dir.path().join("refs/heads/main.lock").exists());
2896 set(&writer, name, oid(3), Expected::Oid(oid(2))).expect("retry against current value");
2897 assert_eq!(
2898 writer.read_raw(name).expect("read"),
2899 Some(RawRef::Direct(oid(3)))
2900 );
2901 }
2902
2903 fn delete(name: &str, expected: Expected) -> RefUpdate {
2904 RefUpdate {
2905 name: name.to_owned(),
2906 new_value: None,
2907 expected,
2908 reflog: None,
2909 flags: RefUpdateFlags::default(),
2910 }
2911 }
2912
2913 fn commit_all(store: &FilesRefStore, updates: Vec<RefUpdate>) -> StoreResult<()> {
2914 store
2915 .prepare(RefTransaction::from_updates(updates)?)?
2916 .commit()
2917 }
2918
2919 fn packed_remote_refs(path: &Path) {
2922 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted \n");
2923 body.push_str(&format!("{} refs/heads/main\n", oid(4)));
2924 let mut remotes: Vec<String> = (0..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
2925 remotes.sort();
2926 for name in &remotes {
2927 body.push_str(&format!("{} {name}\n", oid(1)));
2928 }
2929 body.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
2930 fs::write(path.join("packed-refs"), body).expect("packed-refs");
2931 fs::create_dir_all(path.join("refs/remotes/o")).expect("dir");
2932 fs::write(path.join("refs/remotes/o/x5"), format!("{}\n", oid(9))).expect("loose");
2933 }
2934
2935 #[test]
2936 fn batch_delete_of_packed_refs_rewrites_packed_refs_once_and_keeps_the_rest() {
2937 let dir = git_dir();
2938 let path = dir.path();
2939 packed_remote_refs(path);
2940 let writer = store(path);
2941 let updates = (0..20)
2942 .map(|i| {
2943 let expected = match i {
2944 0 => Expected::Oid(oid(1)),
2945 5 => Expected::Oid(oid(9)),
2946 7 => Expected::Exists,
2947 _ => Expected::Any,
2948 };
2949 delete(&format!("refs/remotes/o/x{i}"), expected)
2950 })
2951 .collect();
2952 commit_all(&writer, updates).expect("batch delete");
2953
2954 for i in 0..20 {
2955 let name = format!("refs/remotes/o/x{i}");
2956 assert_eq!(writer.read_raw(&name).expect("read"), None, "{name}");
2957 }
2958 let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
2959 assert_eq!(
2960 packed,
2961 format!(
2962 "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n{} refs/tags/t\n^{}\n",
2963 oid(4),
2964 oid(2),
2965 oid(3)
2966 )
2967 );
2968 assert!(
2969 path.join("refs/remotes").is_dir(),
2970 "refs/remotes/ namespace kept"
2971 );
2972 assert!(
2973 !path.join("refs/remotes/o").exists(),
2974 "emptied child dirs are pruned"
2975 );
2976 assert!(!path.join("packed-refs.lock").exists());
2977 assert!(path.join("refs/heads").is_dir());
2978 }
2979
2980 #[test]
2981 fn stale_expectation_on_packed_ref_rejects_the_whole_batch() {
2982 let dir = git_dir();
2983 let path = dir.path();
2984 packed_remote_refs(path);
2985 let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
2986 let writer = store(path);
2987 let err = commit_all(
2988 &writer,
2989 vec![
2990 delete("refs/remotes/o/x1", Expected::Any),
2991 delete("refs/remotes/o/x2", Expected::Oid(oid(7))),
2992 ],
2993 )
2994 .expect_err("stale");
2995 assert!(
2996 matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x2"),
2997 "{err:?}"
2998 );
2999 assert_eq!(
3000 fs::read_to_string(path.join("packed-refs")).expect("packed"),
3001 before
3002 );
3003 assert!(!path.join("packed-refs.lock").exists());
3004 assert!(!path.join("refs/remotes/o/x1.lock").exists());
3005 }
3006
3007 #[test]
3008 fn batch_delete_prunes_nested_empty_dirs_deepest_first() {
3009 let dir = git_dir();
3010 let path = dir.path();
3011 let writer = store(path);
3012 for name in ["refs/heads/x/y/z", "refs/heads/x/w", "refs/heads/keep/k"] {
3013 set(&writer, name, oid(1), Expected::Missing).expect("create");
3014 }
3015 commit_all(
3016 &writer,
3017 vec![
3018 delete("refs/heads/x/w", Expected::Oid(oid(1))),
3019 delete("refs/heads/x/y/z", Expected::Any),
3020 ],
3021 )
3022 .expect("delete");
3023 assert!(!path.join("refs/heads/x").exists());
3024 assert!(path.join("refs/heads/keep/k").is_file());
3025 }
3026
3027 #[test]
3028 fn deleting_through_a_symref_removes_the_packed_target() {
3029 let dir = git_dir();
3030 let path = dir.path();
3031 packed_remote_refs(path);
3032 fs::write(path.join("refs/remotes/o/HEAD"), "ref: refs/remotes/o/x3\n").expect("symref");
3033 let writer = store(path);
3034 commit_all(&writer, vec![delete("refs/remotes/o/HEAD", Expected::Any)])
3035 .expect("deref delete");
3036 assert_eq!(writer.read_raw("refs/remotes/o/x3").expect("read"), None);
3037 assert_eq!(
3038 writer.read_raw("refs/remotes/o/HEAD").expect("read"),
3039 Some(RawRef::Symbolic("refs/remotes/o/x3".to_owned()))
3040 );
3041 let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
3042 assert!(!packed.contains("refs/remotes/o/x3\n"), "{packed}");
3043 assert!(packed.contains("refs/remotes/o/x4\n"), "{packed}");
3044 }
3045
3046 #[test]
3047 fn late_commit_time_mismatch_keeps_packed_refs_the_batch_never_reached() {
3048 let dir = git_dir();
3049 let path = dir.path();
3050 packed_remote_refs(path);
3051 let writer = store(path);
3052 let other = store(path);
3053 after_next_recheck(move || {
3054 set(&other, "refs/remotes/o/x9", oid(7), Expected::Any).expect("concurrent write");
3055 });
3056 let updates = (0..20)
3057 .map(|i| {
3058 let expected = if i == 9 {
3059 Expected::Oid(oid(1))
3060 } else {
3061 Expected::Any
3062 };
3063 delete(&format!("refs/remotes/o/x{i}"), expected)
3064 })
3065 .collect();
3066 let err = commit_all(&writer, updates).expect_err("x9 moved under the batch");
3067 assert!(
3068 matches!(err, RefStoreError::ExpectedMismatch { ref name, .. } if name == "refs/remotes/o/x9"),
3069 "{err:?}"
3070 );
3071
3072 for i in 0..9 {
3073 let name = format!("refs/remotes/o/x{i}");
3074 assert_eq!(
3075 writer.read_raw(&name).expect("read"),
3076 None,
3077 "{name} was deleted"
3078 );
3079 }
3080 assert_eq!(
3081 writer.read_raw("refs/remotes/o/x9").expect("read"),
3082 Some(RawRef::Direct(oid(7))),
3083 "the concurrent value survives"
3084 );
3085 for i in 10..20 {
3086 let name = format!("refs/remotes/o/x{i}");
3087 assert_eq!(
3088 writer.read_raw(&name).expect("read"),
3089 Some(RawRef::Direct(oid(1))),
3090 "{name} was never processed"
3091 );
3092 }
3093 let packed = fs::read_to_string(path.join("packed-refs")).expect("packed");
3094 let mut expected = format!(
3095 "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/main\n",
3096 oid(4)
3097 );
3098 let mut kept: Vec<String> = (9..20).map(|i| format!("refs/remotes/o/x{i}")).collect();
3099 kept.sort();
3100 for name in kept {
3101 expected.push_str(&format!("{} {name}\n", oid(1)));
3102 }
3103 expected.push_str(&format!("{} refs/tags/t\n^{}\n", oid(2), oid(3)));
3104 assert_eq!(packed, expected);
3105 assert!(!path.join("packed-refs.lock").exists());
3106 assert!(!path.join("refs/remotes/o/x10.lock").exists());
3107 }
3108
3109 #[test]
3110 fn restore_merges_records_back_in_sorted_order_with_peel_lines() {
3111 let dir = git_dir();
3112 let path = dir.path();
3113 fs::write(
3114 path.join("packed-refs"),
3115 format!(
3116 "# pack-refs with: peeled fully-peeled sorted \n{} refs/heads/b\n{} refs/tags/z\n^{}\n",
3117 oid(1),
3118 oid(2),
3119 oid(3)
3120 ),
3121 )
3122 .expect("packed");
3123 let names: HashSet<String> = ["refs/heads/b", "refs/tags/z"]
3124 .into_iter()
3125 .map(str::to_owned)
3126 .collect();
3127 let before = fs::read_to_string(path.join("packed-refs")).expect("packed");
3128 let removed = remove_packed_refs_under_lock(path, &names).expect("remove");
3129 assert_eq!(removed.len(), 2);
3130 assert_eq!(packed_record_name(&removed[1]), "refs/tags/z");
3131 assert_eq!(packed_record_oid(&removed[1]), Some(oid(2)));
3132 fs::write(
3133 path.join("packed-refs"),
3134 format!(
3135 "# pack-refs with: peeled fully-peeled sorted\n{} refs/heads/a\n{} refs/heads/c\n",
3136 oid(5),
3137 oid(6)
3138 ),
3139 )
3140 .expect("packed");
3141 let records: Vec<&str> = removed.iter().map(String::as_str).collect();
3142 restore_packed_records_under_lock(path, &records).expect("restore");
3143 assert_eq!(
3144 fs::read_to_string(path.join("packed-refs")).expect("packed"),
3145 format!(
3146 "# 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",
3147 oid(5),
3148 oid(1),
3149 oid(6),
3150 oid(2),
3151 oid(3)
3152 )
3153 );
3154 assert!(before.contains("refs/tags/z"));
3155 }
3156
3157 #[test]
3159 fn concurrent_loose_ref_delete_during_list_is_tolerated() {
3160 use std::sync::atomic::{AtomicBool, Ordering};
3161 use std::sync::Arc;
3162 use std::thread;
3163 use std::time::Duration;
3164
3165 let dir = git_dir();
3166 set(
3167 &store(dir.path()),
3168 "refs/heads/main",
3169 oid(1),
3170 Expected::Missing,
3171 )
3172 .expect("seed main");
3173
3174 let stop = Arc::new(AtomicBool::new(false));
3175 let writer_dir = dir.path().to_path_buf();
3176 let writer_stop = stop.clone();
3177 let writer = thread::spawn(move || {
3178 use crate::refs::{delete_ref, write_ref};
3179 let c = oid(2);
3180 while !writer_stop.load(Ordering::Relaxed) {
3181 for i in 0..7 {
3182 let name = format!("refs/heads/d{i}/sub/x");
3183 let _ = write_ref(&writer_dir, &name, &c);
3184 let _ = delete_ref(&writer_dir, &name);
3185 }
3186 }
3187 });
3188
3189 let lister = store(dir.path());
3190 let mut errors = 0usize;
3191 for _ in 0..400 {
3192 if lister
3193 .for_each_ref("", &mut |_| ControlFlow::Continue(()))
3194 .is_err()
3195 {
3196 errors += 1;
3197 }
3198 }
3199 stop.store(true, Ordering::Relaxed);
3200 writer.join().expect("writer");
3201 thread::sleep(Duration::from_millis(10));
3202 assert_eq!(
3203 errors, 0,
3204 "listing must tolerate refs deleted between readdir and open"
3205 );
3206 }
3207
3208 fn create(name: &str, value: ObjectId) -> RefUpdate {
3209 RefUpdate {
3210 name: name.to_owned(),
3211 new_value: Some(RawRef::Direct(value)),
3212 expected: Expected::Missing,
3213 reflog: None,
3214 flags: RefUpdateFlags::default(),
3215 }
3216 }
3217
3218 fn commit_initial(store: &FilesRefStore, updates: Vec<RefUpdate>) -> StoreResult<()> {
3219 store
3220 .prepare(RefTransaction::from_updates(updates)?.initial())?
3221 .commit()
3222 }
3223
3224 fn loose_files_under(dir: &Path) -> BTreeSet<String> {
3225 let mut names = BTreeSet::new();
3226 collect_loose_file_names(dir, "", &mut names);
3227 names
3228 }
3229
3230 #[test]
3231 fn initial_transaction_writes_one_sorted_packed_refs_and_no_loose_files() {
3232 let dir = git_dir();
3233 fs::write(
3234 dir.path().join("packed-refs"),
3235 format!(
3236 "# pack-refs with: peeled fully-peeled sorted \n{} refs/tags/kept\n",
3237 oid(7)
3238 ),
3239 )
3240 .expect("packed-refs");
3241 let store = store(dir.path());
3242 let mut updates: Vec<RefUpdate> = (0..500)
3243 .rev()
3244 .map(|i| create(&format!("refs/remotes/origin/b/n{i}"), oid(1)))
3245 .collect();
3246 updates.push(create("refs/tags/v1", oid(2)));
3247 commit_initial(&store, updates).expect("initial batch");
3248
3249 assert!(loose_files_under(&dir.path().join("refs")).is_empty());
3250 assert!(!dir.path().join("packed-refs.lock").exists());
3251 let packed = fs::read_to_string(dir.path().join("packed-refs")).expect("read");
3252 let names: Vec<&str> = packed
3253 .lines()
3254 .skip(1)
3255 .map(|line| line.split_once(' ').expect("record").1)
3256 .collect();
3257 let mut sorted = names.clone();
3258 sorted.sort_unstable();
3259 assert_eq!(names, sorted);
3260 assert_eq!(names.len(), 502);
3261 assert!(names.contains(&"refs/tags/kept"));
3262 assert_eq!(
3263 store.read_raw("refs/remotes/origin/b/n42").expect("read"),
3264 Some(RawRef::Direct(oid(1)))
3265 );
3266 assert_eq!(
3267 listed_names(&store, "refs/tags/"),
3268 ["refs/tags/kept", "refs/tags/v1"]
3269 );
3270 }
3271
3272 #[test]
3273 fn initial_transaction_falls_back_to_loose_refs_when_it_cannot_pack() {
3274 let dir = git_dir();
3275 let store = store(dir.path());
3276 set(&store, "refs/heads/main", oid(1), Expected::Missing).expect("seed");
3277
3278 let updates = vec![
3280 create("refs/heads/topic", oid(2)),
3281 RefUpdate {
3282 expected: Expected::Any,
3283 ..create("refs/heads/main", oid(3))
3284 },
3285 ];
3286 commit_initial(&store, updates).expect("regular batch");
3287 assert_eq!(
3288 loose_files_under(&dir.path().join("refs/heads")),
3289 BTreeSet::from(["/main".to_owned(), "/topic".to_owned()])
3290 );
3291
3292 let symref = RefUpdate {
3294 new_value: Some(RawRef::Symbolic("refs/heads/main".to_owned())),
3295 ..create("refs/remotes/origin/HEAD", oid(1))
3296 };
3297 commit_initial(&store, vec![create("refs/tags/t", oid(4)), symref]).expect("symref");
3298 assert!(dir.path().join("refs/remotes/origin/HEAD").is_file());
3299 assert!(dir.path().join("refs/tags/t").is_file());
3300 assert!(!dir.path().join("packed-refs").exists());
3301 }
3302
3303 #[test]
3304 fn initial_transaction_reports_name_conflicts() {
3305 let dir = git_dir();
3306 let store = store(dir.path());
3307 set(&store, "refs/heads/a", oid(1), Expected::Missing).expect("seed");
3308
3309 let err = commit_initial(&store, vec![create("refs/heads/a/b", oid(2))])
3310 .expect_err("stored ancestor");
3311 assert!(matches!(
3312 err,
3313 RefStoreError::NameUnavailable {
3314 reason: RefnameUnavailable::AncestorExists { .. }
3315 }
3316 ));
3317 let err = commit_initial(
3318 &store,
3319 vec![
3320 create("refs/heads/x", oid(2)),
3321 create("refs/heads/x/y", oid(2)),
3322 ],
3323 )
3324 .expect_err("batch conflicts with itself");
3325 assert!(matches!(
3326 err,
3327 RefStoreError::NameUnavailable {
3328 reason: RefnameUnavailable::SameBatch { .. }
3329 }
3330 ));
3331 assert!(!dir.path().join("packed-refs").exists());
3332 assert!(!dir.path().join("packed-refs.lock").exists());
3333 }
3334
3335 #[test]
3336 fn batch_blocked_by_a_file_in_place_of_a_parent_writes_nothing() {
3337 let dir = git_dir();
3338 fs::write(dir.path().join("refs/heads/block"), "not a ref\n").expect("obstruction");
3339 let store = store(dir.path());
3340
3341 let err = commit_all(
3342 &store,
3343 vec![
3344 create("refs/heads/first", oid(1)),
3345 create("refs/heads/block/child", oid(2)),
3346 ],
3347 )
3348 .expect_err("refs/heads/block/child cannot be created");
3349 assert!(
3350 !matches!(err, RefStoreError::ExpectedMismatch { .. }),
3351 "{err:?}"
3352 );
3353 assert!(
3354 !dir.path().join("refs/heads/first").exists(),
3355 "the batch is all-or-nothing: no ref before the failing one is written"
3356 );
3357 assert_eq!(
3358 fs::read_to_string(dir.path().join("refs/heads/block")).expect("read"),
3359 "not a ref\n"
3360 );
3361 assert!(!dir.path().join("refs/heads/first.lock").exists());
3362 }
3363
3364 #[cfg(windows)]
3365 #[test]
3366 fn win32_initial_packed_transaction_accepts_reserved_ref_name() {
3367 let dir = git_dir();
3368 let store = store(dir.path());
3369 commit_initial(&store, vec![create("refs/heads/aux", oid(1))]).expect("packed initial");
3370
3371 assert!(
3372 !dir.path().join("refs/heads/aux").exists(),
3373 "packed-only initial must not write a loose ref"
3374 );
3375 let packed = fs::read_to_string(dir.path().join("packed-refs")).expect("packed-refs");
3376 assert!(
3377 packed.contains("refs/heads/aux"),
3378 "reserved name may live in packed-refs: {packed}"
3379 );
3380 }
3381
3382 #[cfg(windows)]
3383 #[test]
3384 fn win32_initial_loose_fallback_rejects_reserved_ref_name() {
3385 let dir = git_dir();
3386 let store = store(dir.path());
3387 set(&store, "refs/heads/main", oid(1), Expected::Missing).expect("seed");
3388
3389 let err = commit_initial(
3390 &store,
3391 vec![
3392 create("refs/heads/aux", oid(2)),
3393 RefUpdate {
3394 expected: Expected::Any,
3395 ..create("refs/heads/main", oid(3))
3396 },
3397 ],
3398 )
3399 .expect_err("loose fallback");
3400 assert!(
3401 matches!(err, RefStoreError::InvalidRefName { .. }),
3402 "{err:?}"
3403 );
3404 assert!(!dir.path().join("refs/heads/aux").exists());
3405 }
3406
3407 #[cfg(windows)]
3408 #[test]
3409 fn win32_rejects_reserved_ref_path_components() {
3410 let dir = git_dir();
3411 let store = store(dir.path());
3412 let oid = oid(9);
3413
3414 for name in [
3415 "refs/heads/aux",
3416 "refs/heads/foo/com1",
3417 "refs/heads/COM1.txt",
3418 ] {
3419 let err = set(&store, name, oid, Expected::Missing).expect_err(name);
3420 assert!(
3421 matches!(err, RefStoreError::InvalidRefName { .. }),
3422 "{name}: {err:?}"
3423 );
3424 let loose = dir.path().join(name);
3425 assert!(!loose.exists(), "{name} must not be written");
3426 assert!(
3427 !dir.path().join(format!("{name}.lock")).exists(),
3428 "lock must not be created for {name}"
3429 );
3430 assert!(
3431 !dir.path().join("logs").join(name).exists(),
3432 "reflog must not be written for {name}"
3433 );
3434 }
3435 }
3436
3437 #[cfg(windows)]
3438 #[test]
3439 fn win32_reftable_still_accepts_reserved_ref_names() {
3440 use super::reftable::ReftableRefStore;
3441
3442 let store = ReftableRefStore::open_ephemeral().expect("reftable");
3443 let txn = RefTransaction::new()
3444 .update(RefUpdate {
3445 name: "refs/heads/aux".to_owned(),
3446 new_value: Some(RawRef::Direct(oid(1))),
3447 expected: Expected::Missing,
3448 reflog: None,
3449 flags: RefUpdateFlags::default(),
3450 })
3451 .expect("txn");
3452 store.prepare(txn).expect("prepare").commit().expect("aux");
3453 }
3454
3455 #[cfg(windows)]
3456 #[test]
3457 fn win32_can_delete_preseeded_reserved_loose_ref() {
3458 let dir = git_dir();
3459 let hex = oid(3).to_hex();
3460 let aux = dir.path().join("refs/heads/aux");
3461 fs::write(&aux, format!("{hex}\n")).expect("seed aux");
3462 let store = store(dir.path());
3463
3464 let txn = RefTransaction::new()
3465 .update(RefUpdate {
3466 name: "refs/heads/aux".to_owned(),
3467 new_value: None,
3468 expected: Expected::Any,
3469 reflog: None,
3470 flags: RefUpdateFlags::default(),
3471 })
3472 .expect("txn");
3473 store
3474 .prepare(txn)
3475 .expect("prepare")
3476 .commit()
3477 .expect("delete");
3478 assert!(!aux.exists());
3479 }
3480}