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