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