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