1pub(crate) mod handle;
41pub mod listing;
42pub mod query_reachability;
43pub(crate) mod shorten;
44pub mod store;
45pub mod version_sort;
46
47pub use shorten::{
48 expand_ref_dwim_full_name, peel_symref_to_terminal_ref, shorten_unambiguous,
49 warn_ambiguous_refs_from_config,
50};
51
52mod domain_reexports;
53mod on_store;
54pub use domain_reexports::*;
55pub use handle::References;
56pub use on_store::update_branch_for_commit_on_store;
57pub(crate) use on_store::{
58 append_reflog_on_store, delete_ref_on_store, reflog_wanted,
59 update_detached_head_for_commit_on_store, verify_new_tip, write_symbolic_ref_on_store,
60};
61
62pub mod gc {
64 pub use crate::gc::*;
65}
66
67use crate::git_path::CanonicalizeExt;
68use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
69use std::fs;
70use std::io;
71use std::ops::ControlFlow;
72use std::path::{Path, PathBuf};
73use store::{
74 open_ref_store, Expected, PreparedRefTransaction, RawRef as StoreRawRef, RefCaseOnlyRename,
75 RefEntry, RefReflogRename, RefStorageFormat, RefStore, RefStoreError, RefTransaction,
76 RefUpdate, RefUpdateFlags, ReflogUpdate,
77};
78
79use crate::check_ref_format::{check_refname_format_borrowed, RefNameOptions};
80use crate::config::ConfigSet;
81use crate::error::{Error, RefLockError, Result};
82use crate::lockfile::LockWait;
83use crate::objects::{HashAlgo, ObjectId};
84use crate::pack;
85
86pub const SYMREF_MAXDEPTH: usize = 5;
88
89fn map_public_ref_store_error(err: Error, refname: &str) -> Error {
90 match err {
91 Error::RefStore(store::RefStoreError::SymrefLoop) => {
92 Error::InvalidRef(format!("ref symlink too deep: {refname}"))
93 }
94 other => other,
95 }
96}
97
98fn files_legacy_write_precheck(git_dir: &Path, refname: &str) -> Result<()> {
102 let storage_dir = ref_storage_dir(git_dir, refname);
103 let stor = crate::ref_namespace::storage_ref_name(refname);
104 let path = storage_dir.join(stor);
105 remove_empty_ref_directory(&path);
106 if fs::symlink_metadata(&path)
107 .map(|m| m.file_type().is_dir())
108 .unwrap_or(false)
109 {
110 return Err(directory_in_the_way(git_dir, refname, &path));
111 }
112 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
113 return Err(Error::InvalidRef(format!(
114 "cannot update ref '{refname}': reference broken"
115 )));
116 }
117 Ok(())
118}
119
120fn directory_in_the_way(git_dir: &Path, refname: &str, path: &Path) -> Error {
127 match verify_refname_available_for_create(git_dir, refname, &BTreeSet::new(), &HashSet::new()) {
128 Err(reason) => Error::RefStore(store::RefStoreError::NameUnavailable { reason }),
129 Ok(()) => RefLockError::DirectoryInTheWay {
130 refname: refname.to_owned(),
131 path: ref_path_for_display(path),
132 }
133 .into(),
134 }
135}
136
137fn legacy_ref_update_flags(refname: &str) -> RefUpdateFlags {
138 let mut flags = RefUpdateFlags::default();
139 if refname == "HEAD" {
141 flags.no_deref = true;
142 }
143 flags
144}
145
146pub(crate) fn commit_store_update(git_dir: &Path, update: RefUpdate) -> Result<()> {
147 commit_ref_update_on_store(&store::open_checked_ref_store(git_dir)?, update)
148}
149
150pub fn commit_ref_update_on_store(store: &dyn RefStore, update: RefUpdate) -> Result<()> {
157 commit_ref_update_on_store_with_wait(store, update, || default_ref_lock_wait(store))
158}
159
160pub(crate) fn commit_ref_update_on_store_with_wait(
163 store: &dyn RefStore,
164 update: RefUpdate,
165 wait: impl FnOnce() -> LockWait,
166) -> Result<()> {
167 if let Some(ref log) = update.reflog {
168 validate_reflog_update(log)?;
169 }
170 let refname = update.name.clone();
171 let txn = RefTransaction::new()
172 .update(update)
173 .map_err(crate::error::Error::from)?;
174 let prepared = prepare_retrying(store, txn, wait)
175 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))?;
176 prepared
177 .commit()
178 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))
179}
180
181pub fn prepare_with_lock_wait(
233 store: &dyn RefStore,
234 txn: RefTransaction,
235 wait: LockWait,
236) -> std::result::Result<Box<dyn PreparedRefTransaction + Send + '_>, RefStoreError> {
237 prepare_retrying(store, txn, || wait)
238}
239
240fn prepare_retrying(
241 store: &dyn RefStore,
242 txn: RefTransaction,
243 wait: impl FnOnce() -> LockWait,
244) -> std::result::Result<Box<dyn PreparedRefTransaction + Send + '_>, RefStoreError> {
245 use crate::lockfile::{backoff_delay, LockClock, SystemLockClock, SystemLockJitter};
246 let clock = &SystemLockClock;
247 let jitter = &SystemLockJitter;
248 let key = Path::new("refs");
249 let start = clock.now();
250
251 let mut err = match store.prepare_for(txn.clone(), LockWait::Immediately) {
252 Ok(prepared) => return Ok(prepared),
253 Err(err) if matches!(err, RefStoreError::LockHeld { .. }) => err,
254 other => return other,
255 };
256
257 let wait_policy = wait();
258 let deadline = match wait_policy {
259 LockWait::Immediately => return Err(err),
260 LockWait::Backoff { budget } => Some(start + budget),
261 LockWait::Indefinitely => None,
262 };
263 let mut tries: u32 = 0;
264 loop {
265 if deadline.is_some_and(|deadline| clock.now() >= deadline) {
266 return Err(err);
267 }
268 let delay = backoff_delay(key, tries, jitter);
269 tries = tries.saturating_add(1);
270 clock.sleep(match deadline {
271 Some(deadline) => delay.min(deadline.saturating_duration_since(clock.now())),
272 None => delay,
273 });
274 match store.prepare_for(txn.clone(), wait_policy.remaining_from(start, clock.now())) {
275 Ok(prepared) => return Ok(prepared),
276 Err(held) if matches!(held, RefStoreError::LockHeld { .. }) => err = held,
277 other => return other,
278 }
279 }
280}
281
282fn default_ref_lock_wait(store: &dyn RefStore) -> LockWait {
284 store.ref_lock_wait()
285}
286
287pub fn update_direct_ref_on_store(
295 store: &dyn RefStore,
296 git_dir: &Path,
297 refname: &str,
298 oid: &ObjectId,
299) -> Result<()> {
300 store_write_value_on_store(
301 store,
302 git_dir,
303 refname,
304 Some(StoreRawRef::Direct(*oid)),
305 Expected::Any,
306 None,
307 )
308}
309
310pub(crate) fn store_write_value_on_store(
311 store: &dyn RefStore,
312 git_dir: &Path,
313 refname: &str,
314 new_value: Option<StoreRawRef>,
315 expected: Expected,
316 reflog: Option<ReflogUpdate>,
317) -> Result<()> {
318 ensure_refname_safe_for_storage(refname)?;
319 if new_value.is_some() && store.format() == RefStorageFormat::Files {
320 files_legacy_write_precheck(git_dir, refname)?;
321 }
322 commit_ref_update_on_store(
323 store,
324 RefUpdate {
325 name: refname.to_owned(),
326 new_value,
327 expected,
328 reflog,
329 flags: legacy_ref_update_flags(refname),
330 },
331 )
332}
333
334pub fn commit_ref_store_batch(git_dir: &Path, updates: &[RefUpdate]) -> Result<()> {
354 if updates.is_empty() {
355 return Ok(());
356 }
357 commit_ref_updates_on_store(&store::open_checked_ref_store(git_dir)?, updates, &[])
358}
359
360pub fn commit_ref_updates_non_atomic(
366 git_dir: &Path,
367 updates: Vec<RefUpdate>,
368) -> Result<Vec<RefUpdateCommitOutcome>> {
369 commit_ref_updates_non_atomic_on_store(&store::open_checked_ref_store(git_dir)?, updates)
370}
371
372pub fn commit_ref_updates_on_store(
381 store: &dyn RefStore,
382 updates: &[RefUpdate],
383 reflog_renames: &[RefReflogRename],
384) -> Result<()> {
385 commit_ref_batch_on_store(store, updates, reflog_renames, &[])
386}
387
388pub(crate) fn commit_ref_batch_on_store(
394 store: &dyn RefStore,
395 updates: &[RefUpdate],
396 reflog_renames: &[RefReflogRename],
397 case_only_renames: &[RefCaseOnlyRename],
398) -> Result<()> {
399 if updates.is_empty() && reflog_renames.is_empty() && case_only_renames.is_empty() {
400 return Ok(());
401 }
402 commit_owned_ref_batch_on_store(
403 store,
404 updates.to_vec(),
405 reflog_renames.to_vec(),
406 case_only_renames.to_vec(),
407 false,
408 false,
409 || default_ref_lock_wait(store),
410 )
411}
412
413#[derive(Debug, Clone, PartialEq, Eq)]
415pub struct RefUpdateCommitOutcome {
416 pub name: String,
418 pub result: std::result::Result<(), store::RefStoreError>,
420}
421
422pub(crate) fn commit_ref_updates_non_atomic_on_store(
433 store: &dyn RefStore,
434 updates: Vec<RefUpdate>,
435) -> Result<Vec<RefUpdateCommitOutcome>> {
436 if updates.is_empty() {
437 return Ok(Vec::new());
438 }
439 let order: Vec<String> = updates.iter().map(|u| u.name.clone()).collect();
440 let (deletes, rest): (Vec<_>, Vec<_>) = updates
441 .into_iter()
442 .partition(|u| u.new_value.is_none() && !u.flags.log_only);
443 let mut results: std::collections::HashMap<String, store::RefStoreError> =
444 std::collections::HashMap::new();
445 for phase in [deletes, rest] {
446 if phase.is_empty() {
447 continue;
448 }
449 for outcome in commit_ref_updates_allowing_failures_on_store(store, phase)? {
450 if let Err(err) = outcome.result {
451 results.insert(outcome.name, err);
452 }
453 }
454 }
455 Ok(order
456 .into_iter()
457 .map(|name| RefUpdateCommitOutcome {
458 result: results.remove(&name).map_or(Ok(()), Err),
459 name,
460 })
461 .collect())
462}
463
464pub(crate) fn commit_ref_updates_allowing_failures_on_store(
471 store: &dyn RefStore,
472 updates: Vec<RefUpdate>,
473) -> Result<Vec<RefUpdateCommitOutcome>> {
474 if updates.is_empty() {
475 return Ok(Vec::new());
476 }
477 let order: Vec<String> = updates.iter().map(|u| u.name.clone()).collect();
478 for update in &updates {
479 if let Some(ref log) = update.reflog {
480 validate_reflog_update(log)?;
481 }
482 }
483 let label = updates
484 .first()
485 .map(|u| u.name.clone())
486 .unwrap_or_else(|| "refs".to_owned());
487 let txn = RefTransaction::from_updates(updates).map_err(|e| {
488 let name = match &e {
489 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
490 _ => label.clone(),
491 };
492 map_public_ref_store_error(Error::from(e), &name)
493 })?;
494 let txn = txn.allowing_failures();
495 let prepared = prepare_retrying(store, txn, || default_ref_lock_wait(store))
496 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
497 let rejections: std::collections::HashMap<String, store::RefStoreError> = prepared
498 .rejected_updates()
499 .iter()
500 .map(|(name, err)| (name.clone(), err.clone()))
501 .collect();
502 prepared
503 .commit()
504 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
505
506 Ok(order
507 .into_iter()
508 .map(|name| {
509 let result = rejections.get(&name).cloned().map_or(Ok(()), Err);
510 RefUpdateCommitOutcome { name, result }
511 })
512 .collect())
513}
514
515pub(crate) fn commit_ref_updates_on_store_with_wait(
518 store: &dyn RefStore,
519 updates: &[RefUpdate],
520 wait: impl FnOnce() -> LockWait,
521) -> Result<()> {
522 if updates.is_empty() {
523 return Ok(());
524 }
525 commit_owned_ref_batch_on_store(
526 store,
527 updates.to_vec(),
528 Vec::new(),
529 Vec::new(),
530 false,
531 false,
532 wait,
533 )
534}
535
536pub(crate) fn commit_owned_ref_updates_on_store(
542 store: &dyn RefStore,
543 updates: Vec<RefUpdate>,
544) -> Result<()> {
545 if updates.is_empty() {
546 return Ok(());
547 }
548 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new(), false, false, || {
549 default_ref_lock_wait(store)
550 })
551}
552
553pub(crate) fn commit_initial_clone_ref_updates_on_store(
562 store: &dyn RefStore,
563 updates: Vec<RefUpdate>,
564) -> Result<()> {
565 commit_initial_clone_ref_updates_on_store_with_wait(store, updates, || {
566 default_ref_lock_wait(store)
567 })
568}
569
570pub(crate) fn commit_initial_ref_updates_on_store_with_wait(
573 store: &dyn RefStore,
574 updates: Vec<RefUpdate>,
575 wait: impl FnOnce() -> LockWait,
576) -> Result<()> {
577 commit_initial_ref_updates_on_store_with_wait_impl(store, updates, wait, false)
578}
579
580pub(crate) fn commit_initial_clone_ref_updates_on_store_with_wait(
583 store: &dyn RefStore,
584 updates: Vec<RefUpdate>,
585 wait: impl FnOnce() -> LockWait,
586) -> Result<()> {
587 commit_initial_ref_updates_on_store_with_wait_impl(store, updates, wait, true)
588}
589
590fn commit_initial_ref_updates_on_store_with_wait_impl(
591 store: &dyn RefStore,
592 updates: Vec<RefUpdate>,
593 wait: impl FnOnce() -> LockWait,
594 allow_ignorecase_same_batch_refs: bool,
595) -> Result<()> {
596 if updates.is_empty() {
597 return Ok(());
598 }
599 commit_owned_ref_batch_on_store(
600 store,
601 updates,
602 Vec::new(),
603 Vec::new(),
604 true,
605 allow_ignorecase_same_batch_refs,
606 wait,
607 )
608}
609
610fn commit_owned_ref_batch_on_store(
611 store: &dyn RefStore,
612 updates: Vec<RefUpdate>,
613 reflog_renames: Vec<RefReflogRename>,
614 case_only_renames: Vec<RefCaseOnlyRename>,
615 initial: bool,
616 initial_ignorecase_same_batch_refs: bool,
617 wait: impl FnOnce() -> LockWait,
618) -> Result<()> {
619 for update in &updates {
620 if let Some(ref log) = update.reflog {
621 validate_reflog_update(log)?;
622 }
623 }
624 for rename in &reflog_renames {
625 validate_reflog_update(&rename.append)?;
626 }
627 let label = updates
628 .first()
629 .map(|u| u.name.clone())
630 .or_else(|| reflog_renames.first().map(|r| r.from.clone()))
631 .or_else(|| case_only_renames.first().map(|r| r.from.clone()))
632 .unwrap_or_else(|| "refs".to_owned());
633 let mut txn = RefTransaction::from_updates(updates).map_err(|e| {
634 let name = match &e {
635 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
636 _ => label.clone(),
637 };
638 map_public_ref_store_error(Error::from(e), &name)
639 })?;
640 for rename in reflog_renames {
641 txn = txn.with_reflog_rename(rename);
642 }
643 for rename in case_only_renames {
644 txn = txn.with_case_only_rename(rename);
645 }
646 if initial {
647 txn = txn.initial();
648 if initial_ignorecase_same_batch_refs {
649 txn = txn.initial_ignorecase_same_batch_refs();
650 }
651 }
652 let prepared = prepare_retrying(store, txn, wait)
653 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
654 prepared
655 .commit()
656 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))
657}
658
659fn store_write_value(
660 git_dir: &Path,
661 refname: &str,
662 new_value: Option<StoreRawRef>,
663 expected: Expected,
664 reflog: Option<ReflogUpdate>,
665) -> Result<()> {
666 store_write_value_on_store(
667 &store::open_checked_ref_store(git_dir)?,
668 git_dir,
669 refname,
670 new_value,
671 expected,
672 reflog,
673 )
674}
675
676pub(crate) fn collect_resolved_refs(
677 store: &dyn RefStore,
678 prefix: &str,
679) -> Result<Vec<(String, ObjectId)>> {
680 collect_resolved_ref_entries(store, |f| store.for_each_ref(prefix, f))
681}
682
683fn collect_resolved_ref_entries(
685 store: &dyn RefStore,
686 for_each: impl FnOnce(&mut dyn FnMut(&RefEntry) -> ControlFlow<()>) -> Result<()>,
687) -> Result<Vec<(String, ObjectId)>> {
688 let mut results = Vec::new();
689 for_each(&mut |entry: &RefEntry| {
690 let oid = match &entry.value {
691 StoreRawRef::Direct(oid) => Some(*oid),
692 StoreRawRef::Symbolic(target) => entry
693 .peeled
694 .or_else(|| store.resolve(target).ok())
695 .or_else(|| store.resolve(&entry.name).ok()),
696 };
697 if let Some(oid) = oid {
698 results.push((entry.name.clone(), oid));
699 }
700 ControlFlow::Continue(())
701 })?;
702 Ok(results)
703}
704
705#[derive(Debug, Clone, PartialEq, Eq)]
707pub enum Ref {
708 Direct(ObjectId),
710 Symbolic(String),
712}
713
714pub fn read_ref_file(path: &Path) -> Result<Ref> {
721 let meta = fs::symlink_metadata(path).map_err(|e| loose_ref_io_error(e, path))?;
723 if meta.file_type().is_symlink() {
724 if let Ok(target) = fs::read_link(path) {
725 return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
726 }
727 }
728 if meta.is_dir() {
729 return Err(Error::Io(io::Error::new(
730 io::ErrorKind::NotFound,
731 "loose ref path is a directory",
732 )));
733 }
734 read_regular_ref_file(path)
735}
736
737pub(crate) fn read_regular_ref_file(path: &Path) -> Result<Ref> {
739 let content = fs::read_to_string(path).map_err(|e| loose_ref_io_error(e, path))?;
740 parse_ref_content(content.trim_end_matches('\n'))
741}
742
743fn loose_ref_io_error(e: io::Error, path: &Path) -> Error {
747 if io_error_is_missing_file(&e, path) {
748 return Error::Io(io::Error::new(io::ErrorKind::NotFound, e));
749 }
750 Error::Io(e)
751}
752
753pub(crate) fn io_error_is_missing_file(err: &io::Error, path: &Path) -> bool {
761 match err.kind() {
762 io::ErrorKind::NotFound | io::ErrorKind::IsADirectory | io::ErrorKind::NotADirectory => {
763 true
764 }
765 io::ErrorKind::PermissionDenied => fs::symlink_metadata(path).is_ok_and(|m| m.is_dir()),
766 _ => matches!(err.raw_os_error(), Some(libc::EISDIR | libc::ENOTDIR)),
767 }
768}
769
770pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
772 if let Some(target) = content.strip_prefix("ref: ") {
773 Ok(Ref::Symbolic(target.trim().to_owned()))
774 } else if let Some(token) = content.split_whitespace().next() {
775 if ObjectId::is_full_hex(token) {
776 let oid: ObjectId = token.parse()?;
777 Ok(Ref::Direct(oid))
778 } else if content == "unknown-oid" {
779 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
780 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
781 Ok(Ref::Direct(oid))
782 } else {
783 Err(Error::InvalidRef(content.to_owned()))
784 }
785 } else {
786 Err(Error::InvalidRef(content.to_owned()))
787 }
788}
789
790pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
810 open_ref_store(git_dir)?
811 .resolve(refname)
812 .map_err(|err| map_public_ref_store_error(err, refname))
813}
814
815pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
820 let commondir_file = git_dir.join("commondir");
821 if !commondir_file.is_file() {
822 return None;
823 }
824 let raw = fs::read_to_string(commondir_file).ok()?;
825 let rel = raw.trim();
826 let path = if Path::new(rel).is_absolute() {
829 PathBuf::from(rel)
830 } else {
831 git_dir.join(rel)
832 };
833 path.canonicalize_simplified().ok()
834}
835
836#[derive(Debug, Clone, Copy, PartialEq, Eq)]
842pub enum RawRefLookup {
843 Exists,
845 NotFound,
847 IsDirectory,
849}
850
851pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
872 let store = open_ref_store(git_dir)?;
873 match store.read_raw(refname)? {
874 Some(_) => Ok(RawRefLookup::Exists),
875 None => {
876 let (storage_dir, stor_name) =
877 crate::worktree_ref::resolve_ref_storage(git_dir, refname);
878 let path = storage_dir.join(&stor_name);
879 if fs::symlink_metadata(&path)
880 .map(|m| m.is_dir())
881 .unwrap_or(false)
882 {
883 Ok(RawRefLookup::IsDirectory)
884 } else {
885 Ok(RawRefLookup::NotFound)
886 }
887 }
888 }
889}
890
891#[must_use]
893pub fn lock_path_for_ref(path: &Path) -> PathBuf {
894 let mut name = path
895 .file_name()
896 .map(|s| s.to_os_string())
897 .unwrap_or_default();
898 name.push(".lock");
899 match path.parent() {
900 Some(parent) => parent.join(&name),
901 None => PathBuf::from(name),
902 }
903}
904
905fn first_packed_descendant_ref(store: &Path, refname: &str) -> Result<Option<String>> {
907 let leaf_dir = format!("{refname}/");
908 let map = read_packed_refs_map_for_prefix(store, refname)?;
909 let sorted: BTreeSet<String> = map.keys().cloned().collect();
910 Ok(sorted
911 .range(leaf_dir.clone()..)
912 .next()
913 .filter(|name| name.starts_with(&leaf_dir))
914 .cloned())
915}
916
917fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
918 Ok(lookup_packed_ref(git_dir, refname)?.is_some())
919}
920
921pub(crate) fn packed_refname_namespace_conflict(
926 sorted_packed: &BTreeSet<String>,
927 candidate: &str,
928) -> bool {
929 let segments: Vec<&str> = candidate.split('/').filter(|s| !s.is_empty()).collect();
930 if segments.len() > 1 {
931 let mut dirname = String::new();
932 for part in &segments[..segments.len() - 1] {
933 if !dirname.is_empty() {
934 dirname.push('/');
935 }
936 dirname.push_str(part);
937 if sorted_packed.contains(&dirname) {
938 return true;
939 }
940 }
941 }
942
943 let leaf_dir = format!("{candidate}/");
944 if let Some(existing) = sorted_packed.range(leaf_dir.clone()..).next() {
945 if existing.starts_with(&leaf_dir) {
946 return true;
947 }
948 }
949 false
950}
951
952fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
953 Ok(PackedRefs::load(git_dir)?.has_namespace_conflict(refname))
954}
955
956pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
972 if refname == "HEAD" {
973 return Ok(false);
974 }
975 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
976 return Ok(false);
977 }
978 let storage_dir = ref_storage_dir(git_dir, refname);
979 packed_ref_name_exists(&storage_dir, refname)
980}
981
982#[derive(Debug, Clone, PartialEq, Eq)]
984pub enum RefnameUnavailable {
985 AncestorExists {
987 blocking: String,
989 new_ref: String,
991 },
992 DescendantExists {
994 blocking: String,
996 new_ref: String,
998 },
999 SameBatch {
1001 refname: String,
1003 other: String,
1005 },
1006}
1007
1008impl RefnameUnavailable {
1009 #[must_use]
1011 pub fn refname(&self) -> &str {
1012 match self {
1013 RefnameUnavailable::AncestorExists { new_ref, .. }
1014 | RefnameUnavailable::DescendantExists { new_ref, .. } => new_ref,
1015 RefnameUnavailable::SameBatch { refname, .. } => refname,
1016 }
1017 }
1018
1019 #[must_use]
1021 pub fn lock_message_suffix(&self) -> String {
1022 match self {
1023 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
1024 format!("'{blocking}' exists; cannot create '{new_ref}'")
1025 }
1026 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
1027 format!("'{blocking}' exists; cannot create '{new_ref}'")
1028 }
1029 RefnameUnavailable::SameBatch { refname, other } => {
1030 format!("cannot process '{refname}' and '{other}' at the same time")
1031 }
1032 }
1033 }
1034}
1035
1036fn find_descendant_in_sorted_extras(
1037 dirname_with_slash: &str,
1038 extras: &BTreeSet<String>,
1039) -> Option<String> {
1040 let start = extras
1041 .range(dirname_with_slash.to_string()..)
1042 .next()
1043 .cloned()?;
1044 if start.starts_with(dirname_with_slash) {
1045 Some(start)
1046 } else {
1047 None
1048 }
1049}
1050
1051#[must_use]
1055pub fn is_valid_fetch_advertised_ref(name: &str) -> bool {
1056 crate::check_ref_format::is_valid_fetch_advertised_ref(name)
1057}
1058
1059#[must_use]
1064pub fn is_valid_ls_refs_advertised_name(name: &str) -> bool {
1065 name == "HEAD" || is_valid_fetch_advertised_ref(name)
1066}
1067
1068#[must_use]
1070pub fn is_valid_storable_ref_name(name: &str) -> bool {
1071 is_valid_fetch_advertised_ref(name)
1072}
1073
1074#[must_use]
1076pub fn is_valid_advertised_symref_target(name: &str) -> bool {
1077 crate::check_ref_format::is_valid_advertised_symref_target(name)
1078}
1079
1080pub fn verify_refname_available_for_create(
1093 git_dir: &Path,
1094 refname: &str,
1095 extras: &BTreeSet<String>,
1096 skip: &HashSet<String>,
1097) -> std::result::Result<(), RefnameUnavailable> {
1098 let git_dir = git_dir
1101 .canonicalize_simplified()
1102 .unwrap_or_else(|_| git_dir.to_path_buf());
1103 let mut seen_dirnames: HashSet<String> = HashSet::new();
1104 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
1105 if segments.len() <= 1 {
1106 } else {
1108 let mut dirname = String::new();
1109 for part in &segments[..segments.len() - 1] {
1110 if !dirname.is_empty() {
1111 dirname.push('/');
1112 }
1113 dirname.push_str(part);
1114
1115 if !seen_dirnames.insert(dirname.clone()) {
1116 continue;
1117 }
1118
1119 if skip.contains(&dirname) {
1120 continue;
1121 }
1122
1123 match read_raw_ref(&git_dir, &dirname) {
1124 Ok(RawRefLookup::Exists) => {
1125 return Err(RefnameUnavailable::AncestorExists {
1126 blocking: dirname.clone(),
1127 new_ref: refname.to_owned(),
1128 });
1129 }
1130 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
1133 Err(_) => {}
1134 }
1135
1136 if extras.contains(&dirname) {
1137 return Err(RefnameUnavailable::SameBatch {
1138 refname: refname.to_owned(),
1139 other: dirname.clone(),
1140 });
1141 }
1142 }
1143 }
1144
1145 let mut leaf_dir = String::with_capacity(refname.len() + 1);
1146 leaf_dir.push_str(refname);
1147 leaf_dir.push('/');
1148
1149 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1155 let may_have_loose_descendants = packed_dir.join("reftable").is_dir()
1156 || git_dir.join(refname).is_dir()
1157 || packed_dir.join(refname).is_dir();
1158 let under = if may_have_loose_descendants {
1159 list_refs(&git_dir, &leaf_dir).unwrap_or_default()
1160 } else {
1161 Vec::new()
1162 };
1163 if under.is_empty() {
1164 if let Ok(Some(name)) = first_packed_descendant_ref(&packed_dir, refname) {
1165 if !skip.contains(&name) {
1166 return Err(RefnameUnavailable::DescendantExists {
1167 blocking: name,
1168 new_ref: refname.to_owned(),
1169 });
1170 }
1171 }
1172 if packed_dir != git_dir {
1173 if let Ok(Some(name)) = first_packed_descendant_ref(&git_dir, refname) {
1174 if !skip.contains(&name) {
1175 return Err(RefnameUnavailable::DescendantExists {
1176 blocking: name,
1177 new_ref: refname.to_owned(),
1178 });
1179 }
1180 }
1181 }
1182 }
1183 if under.is_empty()
1184 && fs::symlink_metadata(git_dir.join(refname))
1185 .map(|m| m.is_dir())
1186 .unwrap_or(false)
1187 {
1188 let mut blocking: Option<String> = None;
1189 let dir_path = git_dir.join(refname);
1190 if let Ok(read) = fs::read_dir(&dir_path) {
1191 for entry in read.flatten() {
1192 let path = entry.path();
1193 let Ok(meta) = fs::metadata(&path) else {
1194 continue;
1195 };
1196 let name = entry.file_name().to_string_lossy().into_owned();
1197 if !meta.is_file() || name.ends_with(".lock") {
1198 continue;
1199 }
1200 let full = format!("{refname}/{name}");
1201 blocking = Some(full);
1202 break;
1203 }
1204 }
1205 if let Some(b) = blocking {
1206 if !skip.contains(&b) {
1207 return Err(RefnameUnavailable::DescendantExists {
1208 blocking: b,
1209 new_ref: refname.to_owned(),
1210 });
1211 }
1212 }
1213 }
1214
1215 for (existing, _) in under {
1216 if skip.contains(&existing) {
1217 continue;
1218 }
1219 return Err(RefnameUnavailable::DescendantExists {
1220 blocking: existing,
1221 new_ref: refname.to_owned(),
1222 });
1223 }
1224
1225 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
1226 if !skip.contains(&extra) {
1227 return Err(RefnameUnavailable::SameBatch {
1228 refname: refname.to_owned(),
1229 other: extra,
1230 });
1231 }
1232 }
1233
1234 Ok(())
1235}
1236
1237#[derive(Clone, Debug)]
1239pub struct RefBatchItem {
1240 pub name: String,
1242 pub new_oid: Option<ObjectId>,
1244}
1245
1246pub fn verify_ref_transaction_batch(
1257 git_dir: &Path,
1258 items: &[RefBatchItem],
1259) -> std::result::Result<(), RefnameUnavailable> {
1260 if let Some((parent, child)) =
1261 batch_refname_prefix_conflict(items.iter().map(|i| i.name.as_str()))
1262 {
1263 return Err(RefnameUnavailable::SameBatch {
1264 refname: child,
1265 other: parent,
1266 });
1267 }
1268
1269 if crate::ref_storage::detect_format(git_dir).is_ok_and(|f| f.is_reftable()) {
1270 return crate::reftable::verify_ref_transaction_batch_reftable(git_dir, items);
1271 }
1272
1273 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
1274 let empty_skip = HashSet::new();
1275 for item in items {
1276 let Some(_oid) = item.new_oid else {
1277 continue;
1278 };
1279 if resolve_ref(git_dir, &item.name).is_ok() {
1280 continue;
1281 }
1282 verify_refname_available_for_create(git_dir, &item.name, &extras, &empty_skip)?;
1283 }
1284 Ok(())
1285}
1286
1287pub(crate) fn batch_refname_prefix_conflict<'a>(
1289 names: impl Iterator<Item = &'a str>,
1290) -> Option<(String, String)> {
1291 let mut sorted: Vec<&str> = names.collect();
1292 sorted.sort_unstable();
1293 for pair in sorted.windows(2) {
1294 let (a, b) = (pair[0], pair[1]);
1295 if refname_is_strict_prefix(a, b) {
1296 return Some((a.to_owned(), b.to_owned()));
1297 }
1298 if refname_is_strict_prefix(b, a) {
1299 return Some((b.to_owned(), a.to_owned()));
1300 }
1301 }
1302 None
1303}
1304
1305fn refname_is_strict_prefix(parent: &str, child: &str) -> bool {
1306 child.len() > parent.len()
1307 && child.as_bytes().get(parent.len()) == Some(&b'/')
1308 && child.starts_with(parent)
1309}
1310
1311fn packed_refs_unexpected_line(path: &Path, line: impl Into<String>) -> Error {
1312 Error::PackedRefsUnexpectedLine {
1313 path: path.display().to_string(),
1314 line: line.into(),
1315 }
1316}
1317
1318fn packed_refs_unterminated_line(path: &Path, line: impl Into<String>) -> Error {
1319 Error::PackedRefsUnexpectedLine {
1320 path: path.display().to_string(),
1321 line: line.into(),
1322 }
1323}
1324
1325fn parse_packed_ref_peel_line(path: &Path, line: &str, expect_peel: bool) -> Result<ObjectId> {
1327 if !expect_peel {
1328 return Err(packed_refs_unexpected_line(path, line));
1329 }
1330 line.strip_prefix('^')
1331 .and_then(|inner| ObjectId::from_hex(inner).ok())
1332 .ok_or_else(|| packed_refs_unexpected_line(path, line))
1333}
1334
1335fn packed_line_oid(line: &str) -> Option<(usize, ObjectId)> {
1337 let bytes = line.as_bytes();
1338 let hex_len = [HashAlgo::Sha1.hex_len(), HashAlgo::Sha256.hex_len()]
1339 .into_iter()
1340 .find(|&len| bytes.get(len) == Some(&b' '))?;
1341 let oid = ObjectId::from_hex(&line[..hex_len]).ok()?;
1343 Some((hex_len, oid))
1344}
1345
1346fn packed_ref_data_line_refname(line: &str) -> Option<&str> {
1347 let bytes = line.as_bytes();
1348 let mut j = 0usize;
1349 while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
1350 j += 1;
1351 }
1352 if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
1353 return None;
1354 }
1355 let refname = &line[j + 1..];
1356 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
1357 return None;
1358 }
1359 Some(refname)
1360}
1361
1362fn parse_packed_ref_data_line(
1363 path: &Path,
1364 line: &str,
1365 ref_opts: &RefNameOptions,
1366) -> Result<(String, ObjectId)> {
1367 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, ref_opts)?;
1368 Ok((refname.to_owned(), oid))
1369}
1370
1371fn parse_packed_ref_data_line_borrowed<'a>(
1372 path: &Path,
1373 line: &'a str,
1374 ref_opts: &RefNameOptions,
1375) -> Result<(&'a str, ObjectId)> {
1376 let Some((hex_len, oid)) = packed_line_oid(line) else {
1377 return Err(packed_refs_unexpected_line(path, line));
1378 };
1379 let refname = &line[hex_len + 1..];
1380 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
1381 return Err(packed_refs_unexpected_line(path, line));
1382 }
1383 if check_refname_format_borrowed(refname, ref_opts).is_err() {
1384 return Err(packed_refs_unexpected_line(path, line));
1385 }
1386 Ok((refname, oid))
1387}
1388
1389enum StreamRecordsError {
1408 OutOfOrder,
1409 Packed(Error),
1410}
1411
1412#[derive(Debug, Clone)]
1413pub(crate) struct PackedRefsFileView {
1414 data: std::sync::Arc<crate::pack_map::PackData>,
1415 sorted: bool,
1418 hex_len: usize,
1420 body_start: usize,
1422 peel_trait: PackedPeelTrait,
1424}
1425
1426impl PackedRefsFileView {
1427 #[cfg(test)]
1429 fn from_bytes(path: &Path, data: Vec<u8>, hash_algo: crate::objects::HashAlgo) -> Result<Self> {
1430 Self::from_data(path, crate::pack_map::PackData::from_owned(data), hash_algo)
1431 }
1432
1433 fn from_data(
1442 path: &Path,
1443 data: std::sync::Arc<crate::pack_map::PackData>,
1444 hash_algo: crate::objects::HashAlgo,
1445 ) -> Result<Self> {
1446 let hex_len = hash_algo.hex_len();
1447 if data.is_empty() {
1448 return Ok(Self {
1449 data,
1450 sorted: false,
1451 hex_len,
1452 body_start: 0,
1453 peel_trait: PackedPeelTrait::None,
1454 });
1455 }
1456 if !data.ends_with(b"\n") {
1457 let tail = std::str::from_utf8(&data)
1458 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1459 .unwrap_or("<invalid utf-8>");
1460 return Err(packed_refs_unterminated_line(path, tail));
1461 }
1462 let text = std::str::from_utf8(&data[..packed_refs_leading_comments_end(&data)])
1465 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1466 let body_start = if data.starts_with(b"#") {
1469 memchr::memchr(b'\n', &data).map_or(data.len(), |nl| nl + 1)
1470 } else {
1471 0
1472 };
1473 let body = &data[body_start..];
1474 if let Some(rest) = body.strip_suffix(b"\n") {
1475 let last_line = memchr::memrchr(b'\n', rest).map_or(rest, |nl| &rest[nl + 1..]);
1476 if last_line.len() < hex_len + 1 {
1477 return Err(packed_refs_unexpected_line(
1478 path,
1479 String::from_utf8_lossy(last_line),
1480 ));
1481 }
1482 }
1483 Ok(Self {
1484 sorted: packed_refs_file_declares_sorted(text),
1485 hex_len,
1486 body_start,
1487 peel_trait: packed_refs_peel_trait(text),
1488 data,
1489 })
1490 }
1491
1492 fn record_name(&self, line_start: usize) -> &[u8] {
1494 let end = packed_line_end(&self.data, line_start);
1495 &self.data[line_start + self.hex_len + 1..end]
1496 }
1497
1498 fn record_cmp(&self, line_start: usize, name: &str) -> std::cmp::Ordering {
1499 self.record_name(line_start).cmp(name.as_bytes())
1500 }
1501
1502 fn next_record(&self, from: usize, limit: usize) -> Option<usize> {
1505 let hex_len = self.hex_len;
1506 let mut start = from;
1507 while start < limit {
1508 let rest = &self.data[start..];
1509 if rest[0] == b'^' && rest.get(hex_len + 1) == Some(&b'\n') {
1512 start += hex_len + 2;
1513 continue;
1514 }
1515 let end = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
1516 if packed_line_is_record(&rest[..end], hex_len) {
1517 return Some(start);
1518 }
1519 start += end + 1;
1520 }
1521 None
1522 }
1523
1524 fn prev_record(&self, before: usize) -> Option<usize> {
1526 let mut end = before;
1527 while end > 0 {
1528 let line_end = end - 1;
1529 let start = memchr::memrchr(b'\n', &self.data[..line_end]).map_or(0, |nl| nl + 1);
1530 if packed_line_is_record(&self.data[start..line_end], self.hex_len) {
1531 return Some(start);
1532 }
1533 end = start;
1534 }
1535 None
1536 }
1537
1538 fn records(&self) -> impl Iterator<Item = usize> + '_ {
1540 let len = self.data.len();
1541 std::iter::successors(self.next_record(0, len), move |&start| {
1542 self.next_record(packed_line_end(&self.data, start) + 1, len)
1543 })
1544 }
1545
1546 fn search(&self, name: &str) -> std::result::Result<usize, usize> {
1554 let len = self.data.len();
1555 let mut lo = self.body_start;
1556 let mut hi = len;
1557 while lo < hi {
1558 let mid = lo + (hi - lo) / 2;
1559 let line = memchr::memrchr(b'\n', &self.data[lo..mid]).map_or(lo, |nl| lo + nl + 1);
1560 let Some(record) = self.next_record(line, hi) else {
1561 hi = line;
1562 continue;
1563 };
1564 match self.record_cmp(record, name) {
1565 std::cmp::Ordering::Less => lo = packed_line_end(&self.data, record) + 1,
1566 std::cmp::Ordering::Greater => hi = record,
1567 std::cmp::Ordering::Equal => return Ok(record),
1568 }
1569 }
1570 Err(self.next_record(lo, len).unwrap_or(len))
1571 }
1572
1573 fn lower_bound(&self, name: &str) -> usize {
1575 let (Ok(start) | Err(start)) = self.search(name);
1576 start
1577 }
1578
1579 fn lookup_entry(
1581 &self,
1582 path: &Path,
1583 refname: &str,
1584 ) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1585 let ref_opts = RefNameOptions {
1586 allow_onelevel: false,
1587 refspec_pattern: false,
1588 normalize: false,
1589 };
1590 let line_start = if self.sorted {
1591 self.search(refname).ok()
1592 } else {
1593 self.records()
1594 .find(|&start| self.record_cmp(start, refname).is_eq())
1595 };
1596 let Some(start) = line_start else {
1597 return Ok(None);
1598 };
1599 let (name, oid) = parse_packed_line_at(path, &self.data, start, &ref_opts)?;
1600 debug_assert_eq!(name, refname);
1601 let line_end = packed_line_end(&self.data, start);
1602 if line_end + 1 < self.data.len() && self.data[line_end + 1] == b'^' {
1603 let peel_line = packed_line_as_str(&self.data, line_end + 1);
1604 parse_packed_ref_peel_line(path, peel_line, true)?;
1605 }
1606 let peel = peel_oid_following_line(&self.data, start);
1607 Ok(Some((oid, peel)))
1608 }
1609
1610 fn lookup_oid(&self, path: &Path, refname: &str) -> Result<Option<ObjectId>> {
1611 Ok(self.lookup_entry(path, refname)?.map(|(oid, _)| oid))
1612 }
1613
1614 pub(crate) fn first_descendant(
1618 &self,
1619 path: &Path,
1620 refname: &str,
1621 ) -> Result<Option<(String, ObjectId)>> {
1622 let leaf_dir = format!("{refname}/");
1623 Ok(self
1624 .map_for_prefix(path, Some(refname))?
1625 .into_iter()
1626 .filter(|(name, _)| name.starts_with(&leaf_dir))
1627 .min_by(|a, b| a.0.cmp(&b.0)))
1628 }
1629
1630 fn map_for_prefix(
1641 &self,
1642 path: &Path,
1643 prefix: Option<&str>,
1644 ) -> Result<HashMap<String, ObjectId>> {
1645 self.map_for_prefix_filtered(path, prefix, None)
1646 }
1647
1648 pub(crate) fn full_map(&self, path: &Path) -> Result<BTreeMap<String, ObjectId>> {
1651 let (mut records, in_order) =
1652 self.parse_region::<Vec<_>>(path, 0, self.data.len(), None, None)?;
1653 if !in_order {
1654 records.sort_by(|a, b| a.0.cmp(&b.0));
1655 }
1656 records.dedup_by(|later, earlier| {
1658 if later.0 != earlier.0 {
1659 return false;
1660 }
1661 earlier.1 = later.1;
1662 true
1663 });
1664 Ok(records.into_iter().collect())
1665 }
1666
1667 fn map_for_prefix_filtered(
1670 &self,
1671 path: &Path,
1672 prefix: Option<&str>,
1673 keep: Option<PackedRecordFilter<'_>>,
1674 ) -> Result<HashMap<String, ObjectId>> {
1675 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1676 let range_prefix = prefix_trim.filter(|p| self.sorted && !p.is_empty() && *p != "refs");
1677 if let Some((begin, end)) = range_prefix.and_then(|prefix| self.prefix_region(prefix)) {
1678 let (map, in_order) = self.parse_region(path, begin, end, prefix_trim, keep)?;
1679 if in_order {
1680 return Ok(map);
1681 }
1682 }
1683 let (map, _) = self.parse_region(path, 0, self.data.len(), prefix_trim, keep)?;
1685 Ok(map)
1686 }
1687
1688 pub(crate) fn for_each_record(
1699 &self,
1700 path: &Path,
1701 prefix: Option<&str>,
1702 keep: Option<PackedRecordFilter<'_>>,
1703 mut f: impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1704 ) -> Result<ControlFlow<()>> {
1705 if !self.sorted {
1706 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1707 }
1708 if keep.is_some() {
1709 return self.for_each_kept_record(path, prefix, keep, f);
1710 }
1711 let mut prove = |_: &str, _: ObjectId, _: PackedPeel| ControlFlow::Continue(());
1712 match self.walk_records(path, prefix, keep, false, &mut prove) {
1713 Err(StreamRecordsError::OutOfOrder) => {
1714 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1715 }
1716 Err(StreamRecordsError::Packed(e)) => return Err(e),
1717 Ok(_) => {}
1718 }
1719 match self.walk_records(path, prefix, keep, true, &mut f) {
1720 Ok(flow) => Ok(flow),
1721 Err(StreamRecordsError::OutOfOrder) => {
1722 self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f)
1723 }
1724 Err(StreamRecordsError::Packed(e)) => Err(e),
1725 }
1726 }
1727
1728 fn for_each_kept_record(
1735 &self,
1736 path: &Path,
1737 prefix: Option<&str>,
1738 keep: Option<PackedRecordFilter<'_>>,
1739 mut f: impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1740 ) -> Result<ControlFlow<()>> {
1741 let mut kept = Vec::new();
1742 let mut hold = |name, oid, peel| {
1743 kept.push((name, oid, peel));
1744 ControlFlow::Continue(())
1745 };
1746 match self.walk_records(path, prefix, keep, true, &mut hold) {
1747 Ok(_) => {}
1748 Err(StreamRecordsError::OutOfOrder) => {
1749 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1750 }
1751 Err(StreamRecordsError::Packed(e)) => return Err(e),
1752 }
1753 for (name, oid, peel) in kept {
1754 if f(name, oid, peel).is_break() {
1755 return Ok(ControlFlow::Break(()));
1756 }
1757 }
1758 Ok(ControlFlow::Continue(()))
1759 }
1760
1761 fn for_each_record_fallback(
1762 &self,
1763 path: &Path,
1764 prefix: Option<&str>,
1765 keep: Option<PackedRecordFilter<'_>>,
1766 already_emitted: &BTreeSet<String>,
1767 f: &mut impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1768 ) -> Result<ControlFlow<()>> {
1769 let map = self.map_for_prefix_filtered(path, prefix, keep)?;
1770 let mut names: Vec<String> = map.keys().cloned().collect();
1771 names.sort();
1772 for name in names {
1773 if already_emitted.contains(&name) {
1774 continue;
1775 }
1776 let oid = map[&name];
1777 if f(&name, oid, PackedPeel::Unknown).is_break() {
1778 return Ok(ControlFlow::Break(()));
1779 }
1780 }
1781 Ok(ControlFlow::Continue(()))
1782 }
1783
1784 fn walk_records<'s>(
1785 &'s self,
1786 path: &Path,
1787 prefix: Option<&str>,
1788 keep: Option<PackedRecordFilter<'_>>,
1789 emit: bool,
1790 f: &mut impl FnMut(&'s str, ObjectId, PackedPeel) -> ControlFlow<()>,
1791 ) -> std::result::Result<ControlFlow<()>, StreamRecordsError> {
1792 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1793 let prefix_upper = if self.sorted {
1794 prefix_trim.and_then(list_prefix_upper_bound)
1795 } else {
1796 None
1797 };
1798 let ref_opts = RefNameOptions {
1799 allow_onelevel: false,
1800 refspec_pattern: false,
1801 normalize: false,
1802 };
1803 let (mut pos, end) = self.record_scan_range(prefix_trim, prefix_upper.as_deref());
1804 let mut prev_name = "";
1805 let mut expect_peel = false;
1806 let mut pending: Option<(&'s str, ObjectId, Option<ObjectId>)> = None;
1807 let mut flush = |pending: &mut Option<(&'s str, ObjectId, Option<ObjectId>)>| {
1808 let Some((refname, oid, peeled)) = pending.take() else {
1809 return Ok(ControlFlow::Continue(()));
1810 };
1811 let peel = if self.peel_trait.covers(refname) {
1812 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1813 } else {
1814 PackedPeel::Unknown
1815 };
1816 if keep.is_some_and(|keep| !keep(refname, &oid, peel)) {
1817 return Ok(ControlFlow::Continue(()));
1818 }
1819 if !emit {
1820 return Ok(ControlFlow::Continue(()));
1821 }
1822 if f(refname, oid, peel).is_break() {
1823 Ok(ControlFlow::Break(()))
1824 } else {
1825 Ok(ControlFlow::Continue(()))
1826 }
1827 };
1828
1829 while pos < end {
1830 let line_end = packed_line_end(&self.data, pos).min(end);
1831 if self.data[pos..line_end].contains(&b'\r') {
1832 return Err(StreamRecordsError::Packed(packed_refs_unexpected_line(
1833 path,
1834 "<contains carriage return>",
1835 )));
1836 }
1837 let line = std::str::from_utf8(&self.data[pos..line_end]).map_err(|_| {
1838 StreamRecordsError::Packed(packed_refs_unexpected_line(path, "<invalid utf-8>"))
1839 })?;
1840 if line.is_empty() {
1841 if pos + 1 < end {
1842 return Err(StreamRecordsError::Packed(packed_refs_unexpected_line(
1843 path, line,
1844 )));
1845 }
1846 break;
1847 }
1848 if line.starts_with('#') {
1849 if flush(&mut pending)?.is_break() {
1850 return Ok(ControlFlow::Break(()));
1851 }
1852 expect_peel = false;
1853 pos = line_end + 1;
1854 continue;
1855 }
1856 if line.starts_with('^') {
1857 let peel = parse_packed_ref_peel_line(path, line, expect_peel)
1858 .map_err(StreamRecordsError::Packed)?;
1859 if let Some((_, _, stored)) = pending.as_mut() {
1860 *stored = Some(peel);
1861 }
1862 expect_peel = false;
1863 pos = line_end + 1;
1864 continue;
1865 }
1866 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)
1867 .map_err(StreamRecordsError::Packed)?;
1868 if self.sorted && !prev_name.is_empty() && refname < prev_name {
1869 return Err(StreamRecordsError::OutOfOrder);
1870 }
1871 if flush(&mut pending)?.is_break() {
1872 return Ok(ControlFlow::Break(()));
1873 }
1874 prev_name = refname;
1875 let in_prefix = prefix_trim.is_none_or(|p| ref_name_matches_list_prefix(refname, p));
1876 if in_prefix {
1877 pending = Some((refname, oid, None));
1878 } else if self.sorted {
1879 if let Some(upper) = prefix_upper.as_deref() {
1880 if refname >= upper {
1881 break;
1882 }
1883 }
1884 }
1885 expect_peel = true;
1886 pos = line_end + 1;
1887 }
1888 if flush(&mut pending)?.is_break() {
1889 return Ok(ControlFlow::Break(()));
1890 }
1891 Ok(ControlFlow::Continue(()))
1892 }
1893
1894 fn record_scan_range(
1895 &self,
1896 prefix_trim: Option<&str>,
1897 prefix_upper: Option<&str>,
1898 ) -> (usize, usize) {
1899 let len = self.data.len();
1900 if self.sorted {
1901 if let Some(p) = prefix_trim.filter(|p| !p.is_empty() && *p != "refs") {
1902 let first = self.lower_bound(p);
1903 let end = prefix_upper.map_or(len, |upper| self.lower_bound(upper));
1904 if end < first {
1905 return (len, len);
1906 }
1907 return (first, end);
1908 }
1909 }
1910 (self.body_start, len)
1911 }
1912
1913 fn prefix_region(&self, prefix: &str) -> Option<(usize, usize)> {
1921 let len = self.data.len();
1922 let first = self.lower_bound(prefix);
1923 let upper = list_prefix_upper_bound(prefix);
1924 let end = upper
1925 .as_deref()
1926 .map_or(len, |upper| self.lower_bound(upper));
1927 if end < first || (first < end && self.record_cmp(first, prefix).is_lt()) {
1928 return None;
1929 }
1930 if let Some(upper) = upper.as_deref() {
1931 if end < len && self.record_cmp(end, upper).is_lt() {
1932 return None;
1933 }
1934 }
1935 let begin = match self.prev_record(first) {
1936 Some(prev) => {
1937 if !self.record_cmp(prev, prefix).is_lt() {
1938 return None;
1939 }
1940 prev
1941 }
1942 None => 0,
1943 };
1944 let end = if end < len {
1945 let after = packed_line_end(&self.data, end) + 1;
1946 if self.data.get(after) == Some(&b'^') {
1947 packed_line_end(&self.data, after) + 1
1948 } else {
1949 after
1950 }
1951 } else {
1952 end
1953 };
1954 Some((begin, end))
1955 }
1956
1957 fn parse_region<C: Default + Extend<(String, ObjectId)>>(
1961 &self,
1962 path: &Path,
1963 begin: usize,
1964 end: usize,
1965 prefix: Option<&str>,
1966 keep: Option<PackedRecordFilter<'_>>,
1967 ) -> Result<(C, bool)> {
1968 let bytes = &self.data[begin..end];
1969 if memchr::memchr(b'\r', bytes).is_some() {
1970 return Err(packed_refs_unexpected_line(
1971 path,
1972 "<contains carriage return>",
1973 ));
1974 }
1975 let region = std::str::from_utf8(bytes)
1976 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1977 parse_packed_refs_region(path, region, prefix, keep, self.peel_trait)
1978 }
1979}
1980
1981#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1984pub(crate) enum PackedPeel {
1985 Peeled(ObjectId),
1987 NotTag,
1989 Unknown,
1991}
1992
1993pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
1996
1997fn parse_packed_refs_region<C: Default + Extend<(String, ObjectId)>>(
2003 path: &Path,
2004 region: &str,
2005 prefix: Option<&str>,
2006 keep: Option<PackedRecordFilter<'_>>,
2007 peel_trait: PackedPeelTrait,
2008) -> Result<(C, bool)> {
2009 let ref_opts = RefNameOptions {
2010 allow_onelevel: false,
2011 refspec_pattern: false,
2012 normalize: false,
2013 };
2014 let mut map = C::default();
2015 let mut in_order = true;
2016 let mut prev_name = "";
2017 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
2019 let flush = |map: &mut C, pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
2020 let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
2021 return;
2022 };
2023 let peel = if peel_trait.covers(refname) {
2024 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
2025 } else {
2026 PackedPeel::Unknown
2027 };
2028 if keep(refname, &oid, peel) {
2029 map.extend(Some((refname.to_owned(), oid)));
2030 }
2031 };
2032 let mut expect_peel = false;
2033 let mut lines = region.split('\n').peekable();
2034 while let Some(line) = lines.next() {
2035 if line.is_empty() {
2036 if lines.peek().is_some() {
2037 return Err(packed_refs_unexpected_line(path, line));
2038 }
2039 break;
2040 }
2041 if line.starts_with('#') {
2042 flush(&mut map, &mut pending);
2043 expect_peel = false;
2044 continue;
2045 }
2046 if line.starts_with('^') {
2047 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
2048 if let Some((_, _, peeled)) = pending.as_mut() {
2049 *peeled = Some(peel);
2050 }
2051 expect_peel = false;
2052 continue;
2053 }
2054 flush(&mut map, &mut pending);
2055 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
2056 in_order &= prev_name <= refname;
2057 prev_name = refname;
2058 if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
2059 if keep.is_some() {
2060 pending = Some((refname, oid, None));
2061 } else {
2062 map.extend(Some((refname.to_owned(), oid)));
2063 }
2064 }
2065 expect_peel = true;
2066 }
2067 flush(&mut map, &mut pending);
2068 Ok((map, in_order))
2069}
2070
2071fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
2072 let packed_path = store::paths::packed_refs_path(store);
2073 let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
2076 Ok(d) => d,
2077 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
2078 Err(e) => return Err(e),
2079 };
2080 let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
2081 .map(|format| format.hash_algo())
2082 .unwrap_or(crate::objects::HashAlgo::Sha1);
2083 Ok(Some(PackedRefsFileView::from_data(
2084 &packed_path,
2085 data,
2086 hash_algo,
2087 )?))
2088}
2089
2090fn packed_line_is_record(line: &[u8], hex_len: usize) -> bool {
2093 line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^')
2094}
2095
2096fn packed_line_end(data: &[u8], line_start: usize) -> usize {
2097 memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
2098}
2099
2100fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
2101 std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
2102}
2103
2104fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
2105 let line_end = packed_line_end(data, line_start);
2106 if line_end >= data.len() || data[line_end] != b'\n' {
2107 return None;
2108 }
2109 let peel_start = line_end + 1;
2110 if peel_start >= data.len() || data[peel_start] != b'^' {
2111 return None;
2112 }
2113 let peel_end = packed_line_end(data, peel_start);
2114 let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
2115 ObjectId::from_hex(inner.trim()).ok()
2116}
2117
2118fn parse_packed_line_at(
2119 path: &Path,
2120 data: &[u8],
2121 line_start: usize,
2122 ref_opts: &RefNameOptions,
2123) -> Result<(String, ObjectId)> {
2124 let line = packed_line_as_str(data, line_start);
2125 parse_packed_ref_data_line(path, line, ref_opts)
2126}
2127
2128pub(crate) fn parse_packed_refs_to_map(
2129 path: &Path,
2130 data: &[u8],
2131) -> Result<HashMap<String, ObjectId>> {
2132 parse_packed_refs_to_map_with_prefix(path, data, None)
2133}
2134
2135fn packed_refs_leading_comments_end(data: &[u8]) -> usize {
2137 let mut end = 0;
2138 while data.get(end) == Some(&b'#') {
2139 match memchr::memchr(b'\n', &data[end..]) {
2140 Some(nl) => end += nl + 1,
2141 None => return data.len(),
2142 }
2143 }
2144 end
2145}
2146
2147fn packed_refs_file_declares_sorted(text: &str) -> bool {
2149 for line in text.split('\n') {
2150 if line.is_empty() {
2151 break;
2152 }
2153 if !line.starts_with('#') {
2154 break;
2155 }
2156 let body = line[1..].trim();
2157 let Some(traits) = body.strip_prefix("pack-refs with:") else {
2158 continue;
2159 };
2160 return traits.split_whitespace().any(|t| t == "sorted");
2161 }
2162 false
2163}
2164
2165fn parse_packed_refs_to_map_with_prefix(
2166 path: &Path,
2167 data: &[u8],
2168 prefix_filter: Option<&str>,
2169) -> Result<HashMap<String, ObjectId>> {
2170 let mut map = HashMap::new();
2171 visit_packed_refs_with_prefix(path, data, prefix_filter, |refname, oid| {
2172 map.insert(refname.to_owned(), oid);
2173 })?;
2174 Ok(map)
2175}
2176
2177fn visit_packed_refs_with_prefix<'a>(
2179 path: &Path,
2180 data: &'a [u8],
2181 prefix_filter: Option<&str>,
2182 mut visit: impl FnMut(&'a str, ObjectId),
2183) -> Result<()> {
2184 scan_packed_refs(path, data, prefix_filter, |line| {
2185 if let PackedLine::Record(name, oid) = line {
2186 visit(name, oid);
2187 }
2188 })
2189}
2190
2191enum PackedLine<'a> {
2193 Record(&'a str, ObjectId),
2195 Peel(ObjectId),
2197}
2198
2199#[derive(Clone, Copy)]
2201struct PackedEntry<'a> {
2202 name: &'a str,
2203 oid: ObjectId,
2204 peel: Option<ObjectId>,
2205}
2206
2207fn packed_entries_with_peel<'a>(path: &Path, data: &'a [u8]) -> Result<Vec<PackedEntry<'a>>> {
2211 let mut entries: Vec<PackedEntry<'a>> = Vec::with_capacity(data.len() / 64);
2212 scan_packed_refs(path, data, None, |line| match line {
2213 PackedLine::Record(name, oid) => entries.push(PackedEntry {
2214 name,
2215 oid,
2216 peel: None,
2217 }),
2218 PackedLine::Peel(peel) => {
2219 if let Some(entry) = entries.last_mut() {
2220 entry.peel = Some(peel);
2221 }
2222 }
2223 })?;
2224 Ok(entries)
2225}
2226
2227fn scan_packed_refs<'a>(
2230 path: &Path,
2231 data: &'a [u8],
2232 prefix_filter: Option<&str>,
2233 mut visit: impl FnMut(PackedLine<'a>),
2234) -> Result<()> {
2235 if data.is_empty() {
2236 return Ok(());
2237 }
2238 if data.contains(&b'\r') {
2239 return Err(packed_refs_unexpected_line(
2240 path,
2241 "<contains carriage return>",
2242 ));
2243 }
2244 if !data.ends_with(b"\n") {
2245 let tail = std::str::from_utf8(data)
2246 .map(|s| s.rsplit('\n').next().unwrap_or(s))
2247 .unwrap_or("<invalid utf-8>");
2248 return Err(packed_refs_unterminated_line(path, tail));
2249 }
2250 let text = std::str::from_utf8(data)
2251 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
2252
2253 let ref_opts = RefNameOptions {
2254 allow_onelevel: false,
2255 refspec_pattern: false,
2256 normalize: false,
2257 };
2258
2259 let mut expect_peel = false;
2260 let mut peel_follows_visited = false;
2261 let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
2262 let packed_sorted = packed_refs_file_declares_sorted(text);
2263 let prefix_upper = if packed_sorted {
2264 prefix_trim.and_then(list_prefix_upper_bound)
2265 } else {
2266 None
2267 };
2268 let mut lines = text.split('\n').peekable();
2269 while let Some(line) = lines.next() {
2270 if line.is_empty() {
2271 if lines.peek().is_some() {
2272 return Err(packed_refs_unexpected_line(path, line));
2273 }
2274 break;
2275 }
2276 if line.starts_with('#') {
2277 expect_peel = false;
2278 continue;
2279 }
2280 if line.starts_with('^') {
2281 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
2282 if peel_follows_visited {
2283 visit(PackedLine::Peel(peel));
2284 }
2285 expect_peel = false;
2286 peel_follows_visited = false;
2287 continue;
2288 }
2289 peel_follows_visited = false;
2290
2291 if let Some(prefix) = prefix_trim {
2292 let Some(refname) = packed_ref_data_line_refname(line) else {
2293 return Err(packed_refs_unexpected_line(path, line));
2294 };
2295 if packed_sorted {
2296 if refname < prefix {
2297 expect_peel = true;
2298 continue;
2299 }
2300 if let Some(upper) = &prefix_upper {
2301 if refname >= upper.as_str() {
2302 break;
2303 }
2304 }
2305 }
2306 if !ref_name_matches_list_prefix(refname, prefix) {
2307 expect_peel = true;
2308 continue;
2309 }
2310 }
2311 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
2312 visit(PackedLine::Record(refname, oid));
2313 expect_peel = true;
2314 peel_follows_visited = true;
2315 }
2316 Ok(())
2317}
2318
2319pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
2320 read_packed_refs_map_for_prefix(store, "")
2321}
2322
2323pub(crate) fn read_packed_refs_map_for_prefix(
2324 store: &Path,
2325 prefix: &str,
2326) -> Result<HashMap<String, ObjectId>> {
2327 read_packed_refs_map_for_prefix_filtered(store, prefix, None)
2328}
2329
2330pub(crate) fn read_packed_refs_map_for_prefix_filtered(
2332 store: &Path,
2333 prefix: &str,
2334 keep: Option<PackedRecordFilter<'_>>,
2335) -> Result<HashMap<String, ObjectId>> {
2336 let Some(view) = read_packed_refs_file_view(store)? else {
2337 return Ok(HashMap::new());
2338 };
2339 let packed_path = store::paths::packed_refs_path(store);
2340 let filter = if prefix.is_empty() {
2341 None
2342 } else {
2343 Some(prefix)
2344 };
2345 view.map_for_prefix_filtered(&packed_path, filter, keep)
2346}
2347
2348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2350enum PackedPeelTrait {
2351 None,
2353 Tags,
2355 All,
2357}
2358
2359impl PackedPeelTrait {
2360 fn covers(self, refname: &str) -> bool {
2362 match self {
2363 Self::None => false,
2364 Self::Tags => refname.starts_with("refs/tags/"),
2365 Self::All => true,
2366 }
2367 }
2368}
2369
2370pub(crate) struct PackedRefPeels {
2380 view: Option<PackedRefsFileView>,
2381 unsorted: Option<HashMap<Box<[u8]>, usize>>,
2383 peel_trait: PackedPeelTrait,
2384 next: std::cell::Cell<usize>,
2387}
2388
2389impl PackedRefPeels {
2390 pub fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
2400 if format != RefStorageFormat::Files {
2401 return Ok(Self::from_view(None));
2402 }
2403 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2404 Ok(Self::from_view(read_packed_refs_file_view(&store)?))
2405 }
2406
2407 pub(crate) fn for_store(store: &dyn store::RefStore, git_dir: &Path) -> Result<Self> {
2416 match store.as_files() {
2417 Some(files) => Ok(Self::from_view(files.packed_refs_view()?)),
2418 None => Self::load(git_dir, store.format()),
2419 }
2420 }
2421
2422 fn from_view(view: Option<PackedRefsFileView>) -> Self {
2423 let empty = Self {
2424 view: None,
2425 unsorted: None,
2426 peel_trait: PackedPeelTrait::None,
2427 next: std::cell::Cell::new(0),
2428 };
2429 let Some(view) = view else {
2430 return empty;
2431 };
2432 let peel_trait = view.peel_trait;
2433 let unsorted = (!view.sorted).then(|| {
2434 view.records()
2435 .map(|start| (Box::<[u8]>::from(view.record_name(start)), start))
2436 .collect()
2437 });
2438 let first = view
2439 .next_record(view.body_start, view.data.len())
2440 .unwrap_or(view.data.len());
2441 Self {
2442 view: Some(view),
2443 unsorted,
2444 peel_trait,
2445 next: std::cell::Cell::new(first),
2446 }
2447 }
2448
2449 pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
2461 if !self.peel_trait.covers(refname) {
2462 return None;
2463 }
2464 self.record(refname, oid)?.peeled
2465 }
2466
2467 pub(crate) fn record(&self, refname: &str, oid: &ObjectId) -> Option<PackedRecord> {
2473 let view = self.view.as_ref()?;
2474 let start = match &self.unsorted {
2475 Some(map) => *map.get(refname.as_bytes())?,
2476 None => {
2477 let len = view.data.len();
2478 let hint = self.next.get();
2479 let start = if hint < len && view.record_cmp(hint, refname).is_eq() {
2480 hint
2481 } else {
2482 view.search(refname).ok()?
2483 };
2484 let after = packed_line_end(&view.data, start) + 1;
2485 self.next.set(view.next_record(after, len).unwrap_or(len));
2486 start
2487 }
2488 };
2489 let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
2490 let packed_oid = ObjectId::from_hex(hex).ok()?;
2491 (packed_oid == *oid).then(|| PackedRecord {
2492 peeled: self
2493 .peel_trait
2494 .covers(refname)
2495 .then(|| peel_oid_following_line(&view.data, start)),
2496 })
2497 }
2498}
2499
2500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2502pub(crate) struct PackedRecord {
2503 pub(crate) peeled: Option<Option<ObjectId>>,
2505}
2506
2507fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
2509 let Some(header) = text.strip_prefix("# pack-refs with: ") else {
2510 return PackedPeelTrait::None;
2511 };
2512 let traits = header.split('\n').next().unwrap_or_default();
2513 let mut peel_trait = PackedPeelTrait::None;
2514 for t in traits.split_whitespace() {
2515 match t {
2516 "fully-peeled" => return PackedPeelTrait::All,
2517 "peeled" => peel_trait = PackedPeelTrait::Tags,
2518 _ => {}
2519 }
2520 }
2521 peel_trait
2522}
2523
2524fn acquire_packed_refs_lock(
2527 git_dir: &Path,
2528 wait: crate::lockfile::LockWait,
2529) -> Result<crate::lockfile::LockFile> {
2530 use crate::lockfile::{LockError, LockFile};
2531 let lock = lock_path_for_ref(&store::paths::packed_refs_path(git_dir));
2532 LockFile::acquire(&lock, wait).map_err(|err| match err {
2533 LockError::Held { .. } => {
2534 let lock_path = git_dir
2535 .canonicalize_simplified()
2536 .map(|d| d.join("packed-refs.lock"))
2537 .unwrap_or(lock);
2538 Error::from(RefLockError::PackedRefsLockHeld {
2539 lock_path: lock_path.display().to_string(),
2540 })
2541 }
2542 LockError::Io { source, .. } => Error::Io(source),
2543 })
2544}
2545
2546pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
2548 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2549 return Ok(None);
2550 };
2551 let packed_path = store::paths::packed_refs_path(git_dir);
2552 view.lookup_oid(&packed_path, refname)
2553}
2554
2555#[cfg(test)]
2557pub(crate) fn lookup_packed_ref_with_peel(
2558 git_dir: &Path,
2559 refname: &str,
2560) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
2561 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2562 return Ok(None);
2563 };
2564 let packed_path = store::paths::packed_refs_path(git_dir);
2565 view.lookup_entry(&packed_path, refname)
2566}
2567
2568#[cfg(test)]
2571pub(crate) fn test_commit(git_dir: &Path, message: &str) -> ObjectId {
2572 let common = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2573 let body = format!(
2574 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
2575 author T <t@example.com> 0 +0000\n\
2576 committer T <t@example.com> 0 +0000\n\n{message}\n"
2577 );
2578 crate::odb::Odb::new(&common.join("objects"))
2579 .write(crate::objects::ObjectKind::Commit, body.as_bytes())
2580 .unwrap_or_else(|err| panic!("store test commit {message:?}: {err}"))
2581}
2582
2583pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
2598 store_write_value(
2599 git_dir,
2600 refname,
2601 Some(StoreRawRef::Symbolic(target.to_owned())),
2602 Expected::Any,
2603 None,
2604 )
2605}
2606
2607pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
2608 if let Some(rest) = refname.strip_prefix("refs/") {
2609 if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
2610 return Err(Error::InvalidRef(format!(
2611 "refusing to update ref with bad name '{refname}'"
2612 )));
2613 }
2614 return Ok(());
2615 }
2616 if crate::check_ref_format::refname_is_safe(refname) {
2617 Ok(())
2618 } else {
2619 Err(Error::InvalidRef(format!(
2620 "refusing to update ref with bad name '{refname}'"
2621 )))
2622 }
2623}
2624
2625pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
2648 store_write_value(
2649 git_dir,
2650 refname,
2651 Some(StoreRawRef::Direct(*oid)),
2652 Expected::Any,
2653 None,
2654 )
2655}
2656
2657#[must_use]
2659pub(crate) fn ref_update_flags(refname: &str) -> RefUpdateFlags {
2660 legacy_ref_update_flags(refname)
2661}
2662
2663pub(crate) fn create_ref_with_reflog_on_store(
2667 store: &dyn RefStore,
2668 git_dir: &Path,
2669 config: &ConfigSet,
2670 refname: &str,
2671 oid: &ObjectId,
2672 identity: &str,
2673 message: &str,
2674) -> Result<()> {
2675 let reflog =
2676 should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
2677 identity: identity.to_owned(),
2678 message: message.to_owned(),
2679 time: time::OffsetDateTime::now_utc(),
2680 old_oid: None,
2681 new_oid: None,
2682 });
2683 store_write_value_on_store(
2684 store,
2685 git_dir,
2686 refname,
2687 Some(StoreRawRef::Direct(*oid)),
2688 Expected::Missing,
2689 reflog,
2690 )
2691}
2692
2693fn write_ref_at_storage(
2696 git_dir: &Path,
2697 storage_dir: &Path,
2698 refname: &str,
2699 oid: &ObjectId,
2700) -> Result<()> {
2701 let stor = crate::ref_namespace::storage_ref_name(refname);
2702 let path = storage_dir.join(stor);
2703 remove_empty_ref_directory(&path);
2710 if fs::symlink_metadata(&path)
2713 .map(|m| m.file_type().is_dir())
2714 .unwrap_or(false)
2715 {
2716 return Err(directory_in_the_way(git_dir, refname, &path));
2717 }
2718 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
2719 return Err(Error::InvalidRef(format!(
2720 "cannot update ref '{refname}': reference broken"
2721 )));
2722 }
2723 if let Some(parent) = path.parent() {
2724 crate::git_dir::io_error::io_with_git_path(
2725 git_dir,
2726 parent,
2727 crate::git_dir::GitDirPathOperation::CreateDir,
2728 fs::create_dir_all(parent),
2729 )?;
2730 }
2731 let content = format!("{oid}\n");
2732 let lock_path = lock_path_for_ref(&path);
2735 use crate::lockfile::{LockError, LockFile, LockWait};
2736 use std::io::Write as _;
2737 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
2738 let mut lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
2739 held @ LockError::Held { .. } => Error::from(held),
2740 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2741 git_dir,
2742 &lock_path,
2743 crate::git_dir::GitDirPathOperation::UpdateRef,
2744 source,
2745 ),
2746 })?;
2747 crate::git_dir::io_error::io_with_git_path(
2748 git_dir,
2749 &path,
2750 crate::git_dir::GitDirPathOperation::UpdateRef,
2751 lock.write_all(content.as_bytes()),
2752 )?;
2753 lock.commit_to(&path).map_err(|err| match err {
2754 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2755 git_dir,
2756 &path,
2757 crate::git_dir::GitDirPathOperation::UpdateRef,
2758 source,
2759 ),
2760 held @ LockError::Held { .. } => Error::from(held),
2761 })?;
2762 Ok(())
2763}
2764
2765pub fn write_ref_cas(
2779 git_dir: &Path,
2780 refname: &str,
2781 oid: &ObjectId,
2782 expected_old: ObjectId,
2783) -> Result<()> {
2784 store_write_value(
2785 git_dir,
2786 refname,
2787 Some(StoreRawRef::Direct(*oid)),
2788 Expected::Oid(expected_old),
2789 None,
2790 )
2791}
2792
2793pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
2806 store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
2807}
2808
2809fn prune_loose_ref_cas(
2819 git_dir: &Path,
2820 refname: &str,
2821 expected: ObjectId,
2822 wait: crate::lockfile::LockWait,
2823) -> Result<bool> {
2824 ensure_refname_safe_for_storage(refname)?;
2825 let storage_dir = ref_storage_dir(git_dir, refname);
2826 let stor = crate::ref_namespace::storage_ref_name(refname);
2827 let path = storage_dir.join(&stor);
2828 let lock_path = lock_path_for_ref(&path);
2829 use crate::lockfile::LockFile;
2830 let _lock = LockFile::acquire(&lock_path, wait)?;
2831 if !path.is_file() {
2832 return Ok(true);
2833 }
2834 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
2835 return Ok(false);
2836 };
2837 if oid != expected {
2838 return Ok(false);
2839 }
2840 fs::remove_file(&path).map_err(Error::Io)?;
2841 Ok(true)
2842}
2843
2844pub struct PackedRefs {
2860 map: HashMap<String, ObjectId>,
2861 sorted_names: BTreeSet<String>,
2862}
2863
2864fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
2865 let sorted_names = map.keys().cloned().collect();
2866 PackedRefs { map, sorted_names }
2867}
2868
2869impl PackedRefs {
2870 pub fn load(git_dir: &Path) -> Result<Self> {
2879 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2880 let map = read_packed_refs_map(&store)?;
2881 Ok(packed_refs_from_map(map))
2882 }
2883
2884 #[must_use]
2886 pub fn get(&self, refname: &str) -> Option<ObjectId> {
2887 self.map.get(refname).copied()
2888 }
2889
2890 #[must_use]
2894 pub fn has_namespace_conflict(&self, refname: &str) -> bool {
2895 packed_refname_namespace_conflict(&self.sorted_names, refname)
2896 }
2897
2898 pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
2901 self.map.insert(refname.to_owned(), oid);
2902 self.sorted_names.insert(refname.to_owned());
2903 }
2904}
2905
2906pub fn resolve_ref_cached(
2916 git_dir: &Path,
2917 refname: &str,
2918 packed: &PackedRefs,
2919) -> Result<Option<ObjectId>> {
2920 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2921 return Ok(resolve_ref(git_dir, refname).ok());
2922 }
2923 resolve_files_ref_cached(git_dir, refname, packed)
2924}
2925
2926fn resolve_files_ref_cached(
2927 git_dir: &Path,
2928 refname: &str,
2929 packed: &PackedRefs,
2930) -> Result<Option<ObjectId>> {
2931 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
2932 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
2933 let try_names: Vec<&str> = if storage_owned != stor_name {
2934 vec![storage_owned.as_str(), stor_name.as_str()]
2935 } else {
2936 vec![stor_name.as_str()]
2937 };
2938
2939 for name in &try_names {
2940 let path = store.join(name);
2941 match read_ref_file(&path) {
2942 Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
2943 Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
2944 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
2945 Err(e) => return Err(e),
2946 }
2947 }
2948
2949 for name in &try_names {
2950 if let Some(oid) = packed.get(name) {
2951 return Ok(Some(oid));
2952 }
2953 }
2954 Ok(None)
2955}
2956
2957pub fn write_ref_cached(
2966 git_dir: &Path,
2967 refname: &str,
2968 oid: &ObjectId,
2969 packed: &mut PackedRefs,
2970) -> Result<()> {
2971 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2972 return write_ref(git_dir, refname, oid);
2973 }
2974 ensure_refname_safe_for_storage(refname)?;
2975 if packed.has_namespace_conflict(refname) {
2976 return Err(Error::InvalidRef(format!(
2977 "cannot update ref '{refname}': reference namespace conflict"
2978 )));
2979 }
2980 let storage_dir = ref_storage_dir(git_dir, refname);
2981 write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
2982 packed.record_loose_write(refname, *oid);
2983 Ok(())
2984}
2985
2986fn ref_path_for_display(path: &Path) -> String {
2991 let cwd = crate::environment::Environment::empty().cwd.clone();
2992 if let Ok(rel) = path.strip_prefix(&cwd) {
2993 return rel.to_string_lossy().into_owned();
2994 }
2995 if let (Ok(cwd_c), Ok(path_c)) = (
2999 cwd.canonicalize_simplified(),
3000 path.canonicalize_simplified(),
3001 ) {
3002 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
3003 return rel.to_string_lossy().into_owned();
3004 }
3005 }
3006 path.to_string_lossy().into_owned()
3007}
3008
3009pub(crate) fn remove_empty_ref_directory(path: &Path) {
3015 match fs::symlink_metadata(path) {
3016 Ok(meta) if meta.file_type().is_dir() => {}
3017 _ => return,
3018 }
3019 if dir_tree_has_files(path) {
3022 return;
3023 }
3024 let _ = remove_dir_tree(path);
3025}
3026
3027pub(crate) fn dir_tree_has_files(dir: &Path) -> bool {
3029 let Ok(entries) = fs::read_dir(dir) else {
3030 return true;
3032 };
3033 for entry in entries.flatten() {
3034 match entry.file_type() {
3035 Ok(ft) if ft.is_dir() => {
3036 if dir_tree_has_files(&entry.path()) {
3037 return true;
3038 }
3039 }
3040 Ok(_) => return true, Err(_) => return true,
3042 }
3043 }
3044 false
3045}
3046
3047pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
3049 if let Some(dir) = ref_path.parent() {
3050 prune_empty_ref_dirs(storage_dir, dir);
3051 }
3052}
3053
3054pub(crate) fn prune_empty_ref_dirs(storage_dir: &Path, dir: &Path) {
3059 let refs_root = storage_dir.join("refs");
3060 let mut current = Some(dir);
3061 while let Some(dir) = current {
3062 if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
3063 break;
3064 }
3065 if dir
3066 .parent()
3067 .is_some_and(|parent| parent == refs_root.as_path())
3068 {
3069 break;
3070 }
3071 if dir_tree_has_files(dir) {
3072 break;
3073 }
3074 if remove_dir_tree(dir).is_err() {
3075 break;
3076 }
3077 current = dir.parent();
3078 }
3079}
3080
3081fn remove_dir_tree(dir: &Path) -> io::Result<()> {
3083 for entry in fs::read_dir(dir)? {
3084 let entry = entry?;
3085 if entry.file_type()?.is_dir() {
3086 remove_dir_tree(&entry.path())?;
3087 }
3088 }
3089 fs::remove_dir(dir)
3090}
3091
3092pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
3108 if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
3109 return Err(store::RefStoreError::not_found(refname).into());
3110 }
3111 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3112 let store = open_ref_store(git_dir)?;
3113 let mut flags = legacy_ref_update_flags(refname);
3114 if refname != "HEAD" {
3115 if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
3116 flags.no_deref = true;
3117 }
3118 }
3119 return commit_store_update(
3120 git_dir,
3121 RefUpdate {
3122 name: refname.to_owned(),
3123 new_value: None,
3124 expected: Expected::Exists,
3125 reflog: None,
3126 flags,
3127 },
3128 )
3129 .map_err(|e| map_public_ref_store_error(e, refname));
3130 }
3131 ensure_refname_safe_for_storage(refname)?;
3132 let storage_dir = ref_storage_dir(git_dir, refname);
3133 let stor = crate::ref_namespace::storage_ref_name(refname);
3134 let path = storage_dir.join(&stor);
3135 let lock = lock_path_for_ref(&path);
3136 if fs::symlink_metadata(&lock).is_ok() {
3137 return Err(Error::from(crate::lockfile::LockError::Held { path: lock }));
3138 }
3139
3140 remove_packed_ref(&storage_dir, &stor)?;
3146
3147 remove_empty_ref_directory(&path);
3151 match fs::remove_file(&path) {
3152 Ok(()) => {}
3153 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
3154 Err(e)
3155 if e.kind() == io::ErrorKind::NotADirectory
3156 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
3157 Err(e)
3160 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
3161 Err(e) => return Err(Error::Io(e)),
3162 }
3163
3164 prune_empty_loose_ref_parents(&storage_dir, &path);
3165
3166 let log_path = storage_dir.join("logs").join(&stor);
3167
3168 let _ = fs::remove_file(&log_path);
3174
3175 let logs_root = storage_dir.join("logs");
3176 let mut parent = log_path.parent();
3177 while let Some(p) = parent {
3178 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
3179 break;
3180 }
3181 if fs::remove_dir(p).is_err() {
3182 break; }
3184 parent = p.parent();
3185 }
3186
3187 Ok(())
3188}
3189
3190pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
3192 remove_packed_refs_inner(git_dir, &|name| name == refname, false, false).map(drop)
3193}
3194
3195pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
3196 remove_packed_refs_inner(git_dir, &|name| name == refname, false, true).map(drop)
3197}
3198
3199pub(crate) fn remove_packed_refs_under_lock(
3209 git_dir: &Path,
3210 refnames: &HashSet<String>,
3211) -> Result<Vec<String>> {
3212 if refnames.is_empty() {
3213 return Ok(Vec::new());
3214 }
3215 remove_packed_refs_inner(git_dir, &|name| refnames.contains(name), true, true)
3216}
3217
3218pub(crate) fn packed_record_name(record: &str) -> &str {
3220 let line = record.lines().next().unwrap_or("");
3221 line.split_once(' ').map_or("", |(_, name)| name.trim())
3222}
3223
3224pub(crate) fn packed_record_oid(record: &str) -> Option<ObjectId> {
3226 let line = record.lines().next()?;
3227 ObjectId::from_hex(line.split_once(' ')?.0).ok()
3228}
3229
3230pub(crate) fn restore_packed_records_under_lock(git_dir: &Path, records: &[&str]) -> Result<()> {
3237 if records.is_empty() {
3238 return Ok(());
3239 }
3240 let packed_path = store::paths::packed_refs_path(git_dir);
3241 let content = match fs::read(&packed_path) {
3242 Ok(c) => c,
3243 Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
3244 Err(e) => return Err(Error::Io(e)),
3245 };
3246 parse_packed_refs_to_map(&packed_path, &content)?;
3247 let content = String::from_utf8(content)
3248 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3249
3250 let mut blocks: Vec<String> = Vec::new();
3251 for line in content.lines() {
3252 if line.starts_with('#') {
3253 continue;
3254 }
3255 match blocks.last_mut() {
3256 Some(block) if line.starts_with('^') => block.push_str(line),
3257 _ => blocks.push(line.to_owned()),
3258 }
3259 if let Some(block) = blocks.last_mut() {
3260 block.push('\n');
3261 }
3262 }
3263 blocks.extend(records.iter().map(|r| (*r).to_owned()));
3264 blocks.sort_by(|a, b| packed_record_name(a).cmp(packed_record_name(b)));
3265
3266 let mut out =
3267 String::with_capacity(content.len() + records.iter().map(|r| r.len()).sum::<usize>());
3268 out.push_str(PACKED_REFS_HEADER);
3269 for block in &blocks {
3270 out.push_str(block);
3271 }
3272 replace_packed_refs_file(git_dir, &packed_path, &out, true)
3273}
3274
3275const PACKED_REFS_HEADER: &str = "# pack-refs with: peeled fully-peeled sorted\n";
3276
3277fn remove_packed_refs_inner(
3278 git_dir: &Path,
3279 remove: &dyn Fn(&str) -> bool,
3280 keep_removed: bool,
3281 lock_already_held: bool,
3282) -> Result<Vec<String>> {
3283 let mut removed: Vec<String> = Vec::new();
3284 let packed_path = store::paths::packed_refs_path(git_dir);
3285 let content = match fs::read(&packed_path) {
3286 Ok(c) => c,
3287 Err(e)
3288 if e.kind() == io::ErrorKind::NotFound
3289 || e.kind() == io::ErrorKind::NotADirectory
3290 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3291 {
3292 return Ok(removed);
3293 }
3294 Err(e) => return Err(Error::Io(e)),
3295 };
3296 let mut has_removed_record = false;
3299 visit_packed_refs_with_prefix(&packed_path, &content, None, |name, _| {
3300 has_removed_record |= remove(name);
3301 })?;
3302 if !has_removed_record {
3303 return Ok(removed);
3304 }
3305
3306 let content = String::from_utf8(content)
3307 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3308
3309 let mut out = String::with_capacity(content.len());
3310 let mut skip_peeled = false;
3311 let mut changed = false;
3312 let mut header_written = false;
3315
3316 for line in content.lines() {
3317 if skip_peeled {
3318 if line.starts_with('^') {
3319 if let Some(record) = removed.last_mut().filter(|_| keep_removed) {
3320 record.push_str(line);
3321 record.push('\n');
3322 }
3323 changed = true;
3324 continue;
3325 }
3326 skip_peeled = false;
3327 }
3328
3329 if line.starts_with('#') {
3330 continue;
3332 }
3333 if line.starts_with('^') {
3334 out.push_str(line);
3335 out.push('\n');
3336 continue;
3337 }
3338
3339 if !header_written {
3341 out.insert_str(0, PACKED_REFS_HEADER);
3342 header_written = true;
3343 }
3344
3345 let name = line.split_once(' ').map_or("", |(_, name)| name.trim());
3346 if remove(name) {
3347 if keep_removed {
3348 removed.push(format!("{line}\n"));
3349 }
3350 changed = true;
3351 skip_peeled = true;
3352 continue;
3353 }
3354
3355 out.push_str(line);
3356 out.push('\n');
3357 }
3358
3359 if changed {
3360 replace_packed_refs_file(git_dir, &packed_path, &out, lock_already_held)?;
3361 }
3362
3363 Ok(removed)
3364}
3365
3366fn replace_packed_refs_file(
3368 git_dir: &Path,
3369 packed_path: &Path,
3370 body: &str,
3371 lock_already_held: bool,
3372) -> Result<()> {
3373 let _packed_lock = if !lock_already_held {
3380 Some(acquire_packed_refs_lock(
3381 git_dir,
3382 crate::lockfile::LockWait::packed_refs_for_git_dir(git_dir),
3383 )?)
3384 } else {
3385 None
3386 };
3387 write_packed_refs_tmp_and_rename(packed_path, body)
3388}
3389
3390fn write_packed_refs_tmp_and_rename(packed_path: &Path, body: &str) -> Result<()> {
3393 let tmp = packed_path.with_extension("new");
3394 let mut created_tmp = false;
3395 let write_result = (|| -> Result<()> {
3396 let mut file = std::fs::OpenOptions::new()
3398 .write(true)
3399 .create_new(true)
3400 .open(&tmp)
3401 .map_err(Error::Io)?;
3402 created_tmp = true;
3403 use std::io::Write as _;
3404 file.write_all(body.as_bytes()).map_err(Error::Io)?;
3405 drop(file);
3406 fs::rename(&tmp, packed_path).map_err(Error::Io)?;
3407 created_tmp = false; Ok(())
3409 })();
3410
3411 if write_result.is_err() && created_tmp {
3412 let _ = fs::remove_file(&tmp);
3413 }
3414 write_result
3415}
3416
3417pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
3430 read_symbolic_ref(git_dir, "HEAD")
3431}
3432
3433pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
3445 match open_ref_store(git_dir)?.read_raw(refname)? {
3446 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3447 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3448 }
3449}
3450
3451pub fn read_symbolic_ref_for_repository(
3453 repo: &crate::repo::Repository,
3454 refname: &str,
3455) -> Result<Option<String>> {
3456 match repo.refs().read_raw(refname)? {
3457 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3458 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3459 }
3460}
3461
3462#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3464pub enum LogRefsConfig {
3465 Unset,
3467 None,
3469 Normal,
3471 Always,
3473}
3474
3475impl LogRefsConfig {
3476 pub(crate) fn parse(raw: &str) -> Option<Self> {
3478 match raw.trim().to_ascii_lowercase().as_str() {
3479 "always" => Some(Self::Always),
3480 "1" | "true" | "yes" | "on" => Some(Self::Normal),
3481 "0" | "false" | "no" | "off" | "never" => Some(Self::None),
3482 _ => None,
3483 }
3484 }
3485}
3486
3487pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3491 ConfigSet::load(
3492 &crate::environment::Environment::empty(),
3493 Some(git_dir),
3494 true,
3495 )
3496 .map(|cfg| cfg.log_refs_config())
3497 .unwrap_or(LogRefsConfig::Unset)
3498}
3499
3500pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3504 ConfigSet::load(
3505 &crate::environment::Environment::empty(),
3506 Some(git_dir),
3507 true,
3508 )
3509 .map(|cfg| cfg.effective_log_refs_config())
3510 .unwrap_or(LogRefsConfig::Normal)
3511}
3512
3513#[must_use]
3516pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
3517 match mode {
3518 LogRefsConfig::Always => true,
3519 LogRefsConfig::Normal => {
3520 refname == "HEAD"
3521 || refname.starts_with("refs/heads/")
3522 || refname.starts_with("refs/remotes/")
3523 || refname.starts_with("refs/notes/")
3524 }
3525 LogRefsConfig::None | LogRefsConfig::Unset => false,
3526 }
3527}
3528
3529#[must_use]
3531pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
3532 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
3533}
3534
3535#[must_use]
3537pub(crate) fn should_autocreate_reflog_with_config(
3538 config: &ConfigSet,
3539 _git_dir: &Path,
3540 refname: &str,
3541) -> bool {
3542 should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
3543}
3544
3545fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
3571 let Ok(rel) = target.strip_prefix(logs_root) else {
3572 return;
3573 };
3574 let mut cur = logs_root.to_path_buf();
3575 for component in rel.components() {
3576 cur.push(component);
3577 match fs::symlink_metadata(&cur) {
3578 Ok(meta) if meta.file_type().is_dir() => {}
3579 Ok(_) => {
3580 let _ = fs::remove_file(&cur);
3583 }
3584 Err(_) => break, }
3586 }
3587}
3588
3589pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
3592 let width = if !old.is_zero() {
3593 old.algo().hex_len()
3594 } else if !new.is_zero() {
3595 new.algo().hex_len()
3596 } else {
3597 old.algo().hex_len()
3598 };
3599 let render = |oid: &ObjectId| {
3600 if oid.is_zero() {
3601 "0".repeat(width)
3602 } else {
3603 oid.to_hex()
3604 }
3605 };
3606 (render(old), render(new))
3607}
3608
3609fn map_reflog_store_validation(err: store::RefStoreError) -> Error {
3610 match err {
3611 store::RefStoreError::InvalidReflog(sig) => Error::InvalidSignature(sig),
3612 other => Error::from(other),
3613 }
3614}
3615
3616fn validate_reflog_message(message: &str) -> Result<()> {
3617 store::validate_reflog_message(message).map_err(map_reflog_store_validation)
3618}
3619
3620fn validate_reflog_identity_string(identity: &str) -> Result<()> {
3621 store::validate_reflog_identity_string(identity).map_err(map_reflog_store_validation)
3622}
3623
3624fn validate_reflog_update(log: &ReflogUpdate) -> Result<()> {
3626 store::validate_reflog_update(log).map_err(map_reflog_store_validation)
3627}
3628
3629pub fn append_reflog(
3638 git_dir: &Path,
3639 refname: &str,
3640 old_oid: &ObjectId,
3641 new_oid: &ObjectId,
3642 identity: &str,
3643 message: &str,
3644 force_create: bool,
3645) -> Result<()> {
3646 append_reflog_with_config(
3647 git_dir,
3648 refname,
3649 old_oid,
3650 new_oid,
3651 identity,
3652 message,
3653 force_create,
3654 None,
3655 )
3656}
3657
3658#[allow(clippy::too_many_arguments)]
3660pub(crate) fn append_reflog_with_config(
3661 git_dir: &Path,
3662 refname: &str,
3663 old_oid: &ObjectId,
3664 new_oid: &ObjectId,
3665 identity: &str,
3666 message: &str,
3667 force_create: bool,
3668 config: Option<&ConfigSet>,
3669) -> Result<()> {
3670 validate_reflog_identity_string(identity)?;
3671 validate_reflog_message(message)?;
3672 test_inject_reflog_fail(refname)?;
3673 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3674 return crate::reftable::reftable_append_reflog(
3675 git_dir,
3676 refname,
3677 old_oid,
3678 new_oid,
3679 identity,
3680 message,
3681 force_create,
3682 );
3683 }
3684 let storage_dir = ref_storage_dir(git_dir, refname);
3685 let stor = crate::ref_namespace::storage_ref_name(refname);
3686 let log_path = storage_dir.join("logs").join(&stor);
3687 let may_create = force_create
3688 || match config {
3689 Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
3690 None => should_autocreate_reflog(git_dir, refname),
3691 };
3692 if !may_create && !log_path.exists() {
3693 return Ok(());
3694 }
3695 if let Some(parent) = log_path.parent() {
3696 let logs_root = storage_dir.join("logs");
3701 clear_conflicting_reflog_files(&logs_root, parent);
3702 crate::git_dir::io_error::io_with_git_path(
3703 git_dir,
3704 parent,
3705 crate::git_dir::GitDirPathOperation::CreateDir,
3706 fs::create_dir_all(parent),
3707 )?;
3708 }
3709 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
3714 let line = if message.is_empty() {
3715 format!("{old_hex} {new_hex} {identity}\n")
3716 } else {
3717 format!("{old_hex} {new_hex} {identity}\t{message}\n")
3718 };
3719 let mut file = fs::OpenOptions::new()
3720 .create(true)
3721 .append(true)
3722 .open(&log_path)
3723 .map_err(|source| {
3724 crate::git_dir::io_error::map_git_dir_io(
3725 git_dir,
3726 &log_path,
3727 crate::git_dir::GitDirPathOperation::AppendReflog,
3728 source,
3729 )
3730 })?;
3731 use io::Write;
3732 crate::git_dir::io_error::io_with_git_path(
3733 git_dir,
3734 &log_path,
3735 crate::git_dir::GitDirPathOperation::AppendReflog,
3736 file.write_all(line.as_bytes()),
3737 )?;
3738 Ok(())
3739}
3740
3741fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
3742 if all {
3743 b"*"
3744 } else {
3745 b"refs/tags/*"
3746 }
3747}
3748
3749fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
3750 crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
3751}
3752
3753fn collect_loose_ref_files(
3754 dir: &Path,
3755 prefix: &str,
3756 out: &mut Vec<(String, PathBuf)>,
3757) -> Result<()> {
3758 let read = match fs::read_dir(dir) {
3759 Ok(r) => r,
3760 Err(e)
3761 if e.kind() == io::ErrorKind::NotFound
3762 || e.kind() == io::ErrorKind::NotADirectory
3763 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3764 {
3765 return Ok(());
3766 }
3767 Err(e) => return Err(Error::Io(e)),
3768 };
3769 for entry in read {
3770 let entry = entry?;
3771 let name = entry.file_name().to_string_lossy().to_string();
3772 let refname = format!("{prefix}{name}");
3773 let path = entry.path();
3774 let file_type = match entry.file_type() {
3776 Ok(ft) if ft.is_symlink() => fs::metadata(&path).map(|m| m.file_type()),
3777 other => other,
3778 };
3779 let Ok(file_type) = file_type else {
3780 continue;
3781 };
3782 if file_type.is_dir() {
3783 collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
3784 } else if file_type.is_file() {
3785 out.push((refname, path));
3786 }
3787 }
3788 Ok(())
3789}
3790
3791fn read_packed_refs_bytes(packed_path: &Path) -> Result<Vec<u8>> {
3793 if !packed_path.is_file() {
3794 return Ok(Vec::new());
3795 }
3796 Ok(fs::read(packed_path)?)
3797}
3798
3799fn merged_packed_refs_body(
3803 mut existing: Vec<PackedEntry<'_>>,
3804 loose: &[(String, ObjectId, Option<ObjectId>)],
3805) -> String {
3806 if !existing.windows(2).all(|w| w[0].name < w[1].name) {
3807 existing.sort_by(|a, b| a.name.cmp(b.name));
3808 existing.dedup_by(|later, earlier| {
3809 if later.name != earlier.name {
3810 return false;
3811 }
3812 *earlier = *later;
3813 true
3814 });
3815 }
3816 let estimate = |name: &str| name.len() + 2 + HashAlgo::Sha256.hex_len();
3818 let capacity = PACKED_REFS_HEADER.len()
3819 + existing.iter().map(|e| estimate(e.name)).sum::<usize>()
3820 + loose.iter().map(|(name, ..)| estimate(name)).sum::<usize>();
3821 let mut body = String::with_capacity(capacity);
3822 body.push_str(PACKED_REFS_HEADER);
3823
3824 let mut packed = existing.iter().peekable();
3825 for (name, oid, peel) in loose {
3826 while let Some(entry) = packed.next_if(|e| e.name < name.as_str()) {
3827 push_packed_record(&mut body, entry.name, &entry.oid, entry.peel.as_ref());
3828 }
3829 packed.next_if(|e| e.name == name.as_str());
3830 push_packed_record(&mut body, name, oid, peel.as_ref());
3831 }
3832 for entry in packed {
3833 push_packed_record(&mut body, entry.name, &entry.oid, entry.peel.as_ref());
3834 }
3835 body
3836}
3837
3838pub(crate) fn pack_refs_for_repository(
3840 repo: &crate::repo::Repository,
3841 all: bool,
3842 dry_run: bool,
3843) -> Result<(PathBuf, usize, usize)> {
3844 let git_dir = repo.git_dir();
3845 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3846 return pack_refs_reftable(git_dir, dry_run);
3847 }
3848 let config = repo.config().ok();
3849 pack_refs_files(git_dir, repo.odb(), config.as_deref(), all, dry_run)
3850}
3851
3852fn pack_refs_reftable(git_dir: &Path, dry_run: bool) -> Result<(PathBuf, usize, usize)> {
3853 use crate::reftable::ReftableStack;
3854 let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3855 let rt_dir = store_git_dir.join("reftable");
3856 let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
3857 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3858 .unwrap_or(0);
3859 if dry_run {
3860 let tables_pruned = tables_before.saturating_sub(1);
3861 return Ok((rt_dir, 0, tables_pruned));
3862 }
3863 let mut stack = ReftableStack::open(&store_git_dir)?;
3864 stack.compact()?;
3865 let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
3866 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3867 .unwrap_or(0);
3868 Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
3869}
3870
3871fn pack_refs_files(
3872 git_dir: &Path,
3873 odb: &crate::odb::Odb,
3874 config: Option<&crate::config::ConfigSet>,
3875 all: bool,
3876 dry_run: bool,
3877) -> Result<(PathBuf, usize, usize)> {
3878 pack_refs_files_matching(
3879 git_dir,
3880 odb,
3881 config,
3882 |name| ref_matches_pack_include(name, all),
3883 dry_run,
3884 false,
3885 )
3886}
3887
3888fn pack_refs_files_matching(
3891 git_dir: &Path,
3892 odb: &crate::odb::Odb,
3893 config: Option<&crate::config::ConfigSet>,
3894 include: impl Fn(&str) -> bool,
3895 dry_run: bool,
3896 allow_missing: bool,
3897) -> Result<(PathBuf, usize, usize)> {
3898 use crate::lockfile::LockWait;
3899
3900 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3901 let packed_path = store::paths::packed_refs_path(&storage_git_dir);
3902
3903 let mut loose_files = Vec::new();
3904 collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
3905
3906 let mut loose: Vec<(String, ObjectId, Option<ObjectId>)> = Vec::new();
3907 for (refname, path) in loose_files {
3908 if crate::worktree_ref::is_per_worktree_ref(&refname) {
3909 continue;
3910 }
3911 if !include(&refname) {
3912 continue;
3913 }
3914 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
3915 continue;
3916 };
3917 let kind = odb
3919 .try_read_info_once(&oid)
3920 .ok()
3921 .flatten()
3922 .map(|info| info.kind);
3923 if kind.is_none() && !allow_missing {
3924 continue;
3925 }
3926 let peel = if kind == Some(crate::objects::ObjectKind::Tag) {
3927 packed_ref_peel(odb, oid)
3928 } else {
3929 None
3930 };
3931 loose.push((refname, oid, peel));
3932 }
3933 loose.sort_by(|a, b| a.0.cmp(&b.0));
3934 let packed_loose = loose.len();
3935
3936 if packed_loose == 0 || dry_run {
3937 let data = read_packed_refs_bytes(&packed_path)?;
3939 visit_packed_refs_with_prefix(&packed_path, &data, None, |_, _| {})?;
3940 return Ok((packed_path, packed_loose, packed_loose));
3941 }
3942
3943 {
3944 let _packed_lock =
3946 acquire_packed_refs_lock(&storage_git_dir, LockWait::packed_refs(config))?;
3947 let data = read_packed_refs_bytes(&packed_path)?;
3948 let existing = packed_entries_with_peel(&packed_path, &data)?;
3949 let body = merged_packed_refs_body(existing, &loose);
3950 write_packed_refs_tmp_and_rename(&packed_path, &body)?;
3951 }
3952
3953 #[cfg(test)]
3954 pack_refs_test_hooks::run_before_prune();
3955
3956 let mut pruned = 0usize;
3957 let mut parents = HashSet::new();
3958 let wait = LockWait::files_ref_lock(config);
3959 for (refname, expected, _) in &loose {
3960 match prune_loose_ref_cas(git_dir, refname, *expected, wait) {
3961 Ok(true) => {
3962 pruned += 1;
3963 let path = storage_git_dir.join(crate::ref_namespace::storage_ref_name(refname));
3964 if let Some(parent) = path.parent() {
3965 parents.insert(parent.to_path_buf());
3966 }
3967 }
3968 Ok(false) => {
3969 remove_packed_ref(git_dir, refname)?;
3970 }
3971 Err(e) => return Err(e),
3972 }
3973 }
3974
3975 for parent in &parents {
3978 prune_empty_ref_dirs(&storage_git_dir, parent);
3979 }
3980
3981 Ok((packed_path, packed_loose, pruned))
3982}
3983
3984#[cfg(test)]
3986mod pack_refs_test_hooks {
3987 use std::cell::RefCell;
3988
3989 thread_local! {
3990 static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
3991 }
3992
3993 pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
3994 BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
3995 }
3996
3997 pub fn run_before_prune() {
3998 BEFORE_PRUNE.with(|c| {
3999 if let Some(f) = c.borrow_mut().take() {
4000 f();
4001 }
4002 });
4003 }
4004}
4005
4006#[cfg(any(test, feature = "test-support"))]
4007#[path = "inject.rs"]
4008pub(crate) mod inject;
4009
4010fn test_inject_reflog_fail(refname: &str) -> Result<()> {
4011 #[cfg(any(test, feature = "test-support"))]
4012 {
4013 inject::reflog_fail(refname)
4014 }
4015 #[cfg(not(any(test, feature = "test-support")))]
4016 {
4017 let _ = refname;
4018 Ok(())
4019 }
4020}
4021
4022pub struct BranchCommitRefUpdate<'a> {
4035 pub branch_ref: &'a str,
4037 pub expected_old: Option<ObjectId>,
4039 pub new_oid: ObjectId,
4041 pub identity: &'a str,
4043 pub reflog_message: &'a str,
4045}
4046
4047pub fn verify_branch_commit_cas(
4057 expected: Option<ObjectId>,
4058 current: Option<ObjectId>,
4059 refname: &str,
4060) -> Result<()> {
4061 verify_branch_cas(expected, current, refname)
4062}
4063
4064fn verify_branch_cas(
4065 expected: Option<ObjectId>,
4066 current: Option<ObjectId>,
4067 refname: &str,
4068) -> Result<()> {
4069 if expected == current {
4070 return Ok(());
4071 }
4072 Err(store::RefStoreError::ExpectedMismatch {
4073 name: refname.to_owned(),
4074 expected: expected.map_or(Expected::Missing, Expected::Oid),
4075 actual: current.map(StoreRawRef::Direct),
4076 }
4077 .into())
4078}
4079
4080fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
4081 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
4082 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
4083 let try_names: Vec<&str> = if storage_owned != stor_name {
4084 vec![storage_owned.as_str(), stor_name.as_str()]
4085 } else {
4086 vec![stor_name.as_str()]
4087 };
4088 for name in try_names {
4089 let path = store.join(name);
4090 if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
4091 return Ok(Some(oid));
4092 }
4093 if let Some(oid) = lookup_packed_ref(&store, name)? {
4094 return Ok(Some(oid));
4095 }
4096 }
4097 Ok(None)
4098}
4099
4100fn test_branch_ref_pause_before_commit() {
4101 #[cfg(any(test, feature = "test-support"))]
4102 {
4103 inject::branch_ref_pause_before_commit();
4104 }
4105}
4106
4107pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
4108 #[cfg(any(test, feature = "test-support"))]
4109 {
4110 inject::ref_store_batch_pause_after_ref_write(idx, git_dir);
4111 }
4112 #[cfg(not(any(test, feature = "test-support")))]
4113 {
4114 let _ = (idx, git_dir);
4115 }
4116}
4117
4118pub(crate) struct BranchCommitRefLock {
4122 lock: Option<crate::lockfile::LockFile>,
4123 disarmed: bool,
4124}
4125
4126impl BranchCommitRefLock {
4127 pub fn acquire(
4133 git_dir: &Path,
4134 branch_ref: &str,
4135 expected_old: Option<ObjectId>,
4136 ) -> Result<Self> {
4137 ensure_refname_safe_for_storage(branch_ref)?;
4138 let storage_dir = ref_storage_dir(git_dir, branch_ref);
4139 if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
4140 return Err(Error::InvalidRef(format!(
4141 "cannot update ref '{}': reference namespace conflict",
4142 branch_ref
4143 )));
4144 }
4145 let stor = crate::ref_namespace::storage_ref_name(branch_ref);
4146 let path = storage_dir.join(&stor);
4147 remove_empty_ref_directory(&path);
4148 if fs::symlink_metadata(&path)
4149 .map(|m| m.file_type().is_dir())
4150 .unwrap_or(false)
4151 {
4152 return Err(directory_in_the_way(git_dir, branch_ref, &path));
4153 }
4154 if let Some(parent) = path.parent() {
4155 crate::git_dir::io_error::io_with_git_path(
4156 git_dir,
4157 parent,
4158 crate::git_dir::GitDirPathOperation::CreateDir,
4159 fs::create_dir_all(parent),
4160 )?;
4161 }
4162
4163 let lock_path = lock_path_for_ref(&path);
4164 use crate::lockfile::{LockError, LockFile, LockWait};
4165 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4166 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4167 held @ LockError::Held { .. } => Error::from(held),
4168 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4169 git_dir,
4170 path.as_path(),
4171 crate::git_dir::GitDirPathOperation::UpdateRef,
4172 source,
4173 ),
4174 })?;
4175
4176 let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
4177 verify_branch_cas(expected_old, current, branch_ref)?;
4178
4179 Ok(Self {
4180 lock: Some(lock),
4181 disarmed: false,
4182 })
4183 }
4184
4185 pub fn write_reflogs(
4195 &mut self,
4196 git_dir: &Path,
4197 update: &BranchCommitRefUpdate<'_>,
4198 reflog_old: &ObjectId,
4199 config: &ConfigSet,
4200 ) -> Result<()> {
4201 write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
4202 }
4203
4204 pub fn finish_ref_commit(
4212 mut self,
4213 git_dir: &Path,
4214 update: &BranchCommitRefUpdate<'_>,
4215 ) -> Result<()> {
4216 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4217 self.disarmed = true;
4218 Ok(())
4219 }
4220
4221 pub fn commit(
4227 mut self,
4228 git_dir: &Path,
4229 update: &BranchCommitRefUpdate<'_>,
4230 reflog_old: &ObjectId,
4231 config: &ConfigSet,
4232 ) -> Result<()> {
4233 write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
4234 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4235 self.disarmed = true;
4236 Ok(())
4237 }
4238}
4239
4240impl Drop for BranchCommitRefLock {
4241 fn drop(&mut self) {
4242 drop(self.lock.take());
4243 }
4244}
4245
4246fn write_branch_commit_reflogs(
4247 git_dir: &Path,
4248 lock: &mut BranchCommitRefLock,
4249 update: &BranchCommitRefUpdate<'_>,
4250 reflog_old: &ObjectId,
4251 config: &ConfigSet,
4252) -> Result<()> {
4253 let mut branch_reflog_written = false;
4254 if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
4255 match append_reflog_with_config(
4256 git_dir,
4257 update.branch_ref,
4258 reflog_old,
4259 &update.new_oid,
4260 update.identity,
4261 update.reflog_message,
4262 false,
4263 Some(config),
4264 ) {
4265 Ok(()) => branch_reflog_written = true,
4266 Err(err) => {
4267 drop(lock.lock.take());
4268 return Err(err);
4269 }
4270 }
4271 }
4272
4273 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4274 if let Err(err) = append_reflog_with_config(
4275 git_dir,
4276 "HEAD",
4277 reflog_old,
4278 &update.new_oid,
4279 update.identity,
4280 update.reflog_message,
4281 false,
4282 Some(config),
4283 ) {
4284 if branch_reflog_written {
4285 let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
4286 }
4287 drop(lock.lock.take());
4288 return Err(err);
4289 }
4290 }
4291
4292 Ok(())
4293}
4294
4295fn finish_branch_commit_ref_lock_files(
4296 git_dir: &Path,
4297 lock: &mut BranchCommitRefLock,
4298 update: &BranchCommitRefUpdate<'_>,
4299) -> Result<()> {
4300 test_branch_ref_pause_before_commit();
4301
4302 let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
4303 let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
4304 let path = storage_dir.join(&stor);
4305 let content = format!("{}\n", update.new_oid);
4306 use crate::lockfile::LockError;
4307 use std::io::Write as _;
4308 let mut file_lock = lock
4309 .lock
4310 .take()
4311 .ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
4312 crate::git_dir::io_error::io_with_git_path(
4313 git_dir,
4314 &path,
4315 crate::git_dir::GitDirPathOperation::UpdateRef,
4316 file_lock.write_all(content.as_bytes()),
4317 )?;
4318 file_lock.commit_to(&path).map_err(|err| match err {
4319 LockError::Io {
4320 source,
4321 path: lock_path,
4322 } => crate::git_dir::io_error::map_git_dir_io(
4323 git_dir,
4324 lock_path.as_path(),
4325 crate::git_dir::GitDirPathOperation::UpdateRef,
4326 source,
4327 ),
4328 held @ LockError::Held { .. } => Error::from(held),
4329 })?;
4330 Ok(())
4331}
4332
4333pub(crate) fn update_branch_for_commit_files(
4334 git_dir: &Path,
4335 update: &BranchCommitRefUpdate<'_>,
4336 reflog_old: &ObjectId,
4337 config: &ConfigSet,
4338) -> Result<()> {
4339 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4340 lock.commit(git_dir, update, reflog_old, config)
4341}
4342
4343pub(crate) fn update_branch_for_commit_files_with_reference_transaction_hooks(
4346 git_dir: &Path,
4347 update: &BranchCommitRefUpdate<'_>,
4348 reflog_old: &ObjectId,
4349 config: &ConfigSet,
4350 ctx: &crate::hooks::RefHookContext,
4351 lines: &[store::reference_transaction::ReferenceTransactionLine],
4352) -> Result<()> {
4353 use crate::hooks::ReferenceTransactionPhase;
4354 use store::reference_transaction::stdin_from_lines;
4355
4356 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4357 if lines.is_empty() {
4358 return lock.commit(git_dir, update, reflog_old, config);
4359 }
4360 let stdin = stdin_from_lines(lines);
4361 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4362 .map_err(Error::Hook)?;
4363 match lock.commit(git_dir, update, reflog_old, config) {
4364 Ok(()) => {
4365 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4366 Ok(())
4367 }
4368 Err(err) => {
4369 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4370 Err(err)
4371 }
4372 }
4373}
4374
4375pub(crate) struct DetachedHeadRefLock {
4377 lock: Option<crate::lockfile::LockFile>,
4378 disarmed: bool,
4379}
4380
4381impl DetachedHeadRefLock {
4382 fn acquire(git_dir: &Path, expected_old: Option<ObjectId>) -> Result<Self> {
4383 let current = resolve_ref(git_dir, "HEAD").ok();
4384 verify_branch_cas(expected_old, current, "HEAD")?;
4385 let path = git_dir.join("HEAD");
4386 let lock_path = lock_path_for_ref(&path);
4387 use crate::lockfile::{LockError, LockFile, LockWait};
4388 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4389 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4390 held @ LockError::Held { .. } => Error::from(held),
4391 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4392 git_dir,
4393 path.as_path(),
4394 crate::git_dir::GitDirPathOperation::UpdateRef,
4395 source,
4396 ),
4397 })?;
4398 Ok(Self {
4399 lock: Some(lock),
4400 disarmed: false,
4401 })
4402 }
4403
4404 fn commit(
4405 mut self,
4406 git_dir: &Path,
4407 new_oid: ObjectId,
4408 reflog_old: &ObjectId,
4409 identity: &str,
4410 reflog_message: &str,
4411 config: &ConfigSet,
4412 ) -> Result<()> {
4413 let path = git_dir.join("HEAD");
4414 let content = format!("{new_oid}\n");
4415 use crate::lockfile::LockError;
4416 use std::io::Write as _;
4417 let mut file_lock = self
4418 .lock
4419 .take()
4420 .ok_or_else(|| Error::Message("HEAD ref lock already released".into()))?;
4421 crate::git_dir::io_error::io_with_git_path(
4422 git_dir,
4423 &path,
4424 crate::git_dir::GitDirPathOperation::UpdateRef,
4425 file_lock.write_all(content.as_bytes()),
4426 )?;
4427 file_lock.commit_to(&path).map_err(|err| match err {
4428 LockError::Io {
4429 source,
4430 path: lock_path,
4431 } => crate::git_dir::io_error::map_git_dir_io(
4432 git_dir,
4433 lock_path.as_path(),
4434 crate::git_dir::GitDirPathOperation::UpdateRef,
4435 source,
4436 ),
4437 held @ LockError::Held { .. } => Error::from(held),
4438 })?;
4439 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4440 if let Err(err) = append_reflog_with_config(
4441 git_dir,
4442 "HEAD",
4443 reflog_old,
4444 &new_oid,
4445 identity,
4446 reflog_message,
4447 false,
4448 Some(config),
4449 ) {
4450 let _ = write_ref_cas(git_dir, "HEAD", reflog_old, new_oid);
4451 return Err(err);
4452 }
4453 }
4454 self.disarmed = true;
4455 Ok(())
4456 }
4457}
4458
4459impl Drop for DetachedHeadRefLock {
4460 fn drop(&mut self) {
4461 drop(self.lock.take());
4462 }
4463}
4464
4465#[allow(clippy::too_many_arguments)]
4466pub(crate) fn update_detached_head_for_commit_files_with_reference_transaction_hooks(
4467 git_dir: &Path,
4468 expected_old: Option<ObjectId>,
4469 new_oid: ObjectId,
4470 identity: &str,
4471 reflog_message: &str,
4472 config: &ConfigSet,
4473 ctx: &crate::hooks::RefHookContext,
4474 lines: &[store::reference_transaction::ReferenceTransactionLine],
4475) -> Result<()> {
4476 use crate::hooks::ReferenceTransactionPhase;
4477 use store::reference_transaction::stdin_from_lines;
4478
4479 let lock = DetachedHeadRefLock::acquire(git_dir, expected_old)?;
4480 if lines.is_empty() {
4481 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4482 return lock.commit(
4483 git_dir,
4484 new_oid,
4485 &reflog_old,
4486 identity,
4487 reflog_message,
4488 config,
4489 );
4490 }
4491 let stdin = stdin_from_lines(lines);
4492 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4493 .map_err(Error::Hook)?;
4494 let reflog_old = expected_old.unwrap_or_else(|| ctx.null_oid());
4495 match lock.commit(
4496 git_dir,
4497 new_oid,
4498 &reflog_old,
4499 identity,
4500 reflog_message,
4501 config,
4502 ) {
4503 Ok(()) => {
4504 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4505 Ok(())
4506 }
4507 Err(err) => {
4508 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4509 Err(err)
4510 }
4511 }
4512}
4513
4514pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
4522 let config = ConfigSet::load(
4523 &crate::environment::Environment::empty(),
4524 Some(git_dir),
4525 true,
4526 )
4527 .unwrap_or_default();
4528 update_branch_for_commit_with_config(git_dir, update, &config)
4529}
4530
4531pub(crate) fn update_branch_for_commit_with_config(
4540 git_dir: &Path,
4541 update: &BranchCommitRefUpdate<'_>,
4542 config: &ConfigSet,
4543) -> Result<()> {
4544 update_branch_for_commit_on_store(&store::open_checked_ref_store(git_dir)?, update, config)
4545}
4546
4547pub(crate) fn update_detached_head_for_commit_files(
4550 git_dir: &Path,
4551 expected_old: Option<ObjectId>,
4552 new_oid: ObjectId,
4553 identity: &str,
4554 reflog_message: &str,
4555 config: &ConfigSet,
4556) -> Result<()> {
4557 let current = resolve_ref(git_dir, "HEAD").ok();
4558 verify_branch_cas(expected_old, current, "HEAD")?;
4559 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4560
4561 write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
4562 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4563 if let Err(err) = append_reflog_with_config(
4564 git_dir,
4565 "HEAD",
4566 &reflog_old,
4567 &new_oid,
4568 identity,
4569 reflog_message,
4570 false,
4571 Some(config),
4572 ) {
4573 let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
4574 return Err(err);
4575 }
4576 }
4577 Ok(())
4578}
4579
4580#[must_use]
4586pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
4587 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
4588 store.join("logs").join(stor_name)
4589}
4590
4591fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
4592 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
4593}
4594
4595fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
4604 if prefix.is_empty() {
4605 return String::new();
4606 }
4607 if prefix.ends_with('/') {
4608 return prefix.to_string();
4609 }
4610 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
4611 if candidate.is_file() {
4612 prefix.to_string()
4613 } else {
4614 format!("{prefix}/")
4615 }
4616}
4617
4618pub(crate) fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
4621 odb.peel_stored_tag_target(oid).ok().flatten()
4622}
4623
4624pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
4635 pack_imported_refs_for_clone(git_dir, Some(remote))
4636}
4637
4638pub(crate) fn pack_imported_refs_for_clone(git_dir: &Path, remote: Option<&str>) -> Result<()> {
4640 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4641 return Ok(());
4642 }
4643 let prefix = remote.map(|name| format!("refs/remotes/{name}/"));
4644 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
4645 let odb = crate::odb::Odb::new(&storage_git_dir.join("objects"))
4646 .with_config_git_dir(git_dir.to_path_buf());
4647 let config = crate::config::ConfigSet::load(
4648 &crate::environment::Environment::empty(),
4649 Some(git_dir),
4650 true,
4651 )
4652 .ok();
4653 odb.with_pack_read_context(|| {
4654 pack_refs_files_matching(
4655 git_dir,
4656 &odb,
4657 config.as_ref(),
4658 |name| {
4659 name.starts_with("refs/tags/")
4660 || prefix
4661 .as_ref()
4662 .is_some_and(|prefix| name.starts_with(prefix))
4663 },
4664 false,
4665 true,
4667 )
4668 })?;
4669 Ok(())
4670}
4671
4672fn push_packed_record(body: &mut String, name: &str, oid: &ObjectId, peel: Option<&ObjectId>) {
4674 oid.push_hex_to(body);
4675 body.push(' ');
4676 body.push_str(name);
4677 body.push('\n');
4678 if let Some(peel) = peel {
4679 body.push('^');
4680 peel.push_hex_to(body);
4681 body.push('\n');
4682 }
4683}
4684
4685#[derive(Debug, Clone, PartialEq, Eq)]
4687pub struct PackedRefRecord {
4688 pub refname: String,
4690 pub oid: ObjectId,
4692 pub peeled: Option<ObjectId>,
4694}
4695
4696pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
4706 let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
4707 sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
4708 for rec in &sorted {
4709 ensure_refname_safe_for_storage(&rec.refname)?;
4710 }
4711 let mut body = String::from(PACKED_REFS_HEADER);
4712 for rec in &sorted {
4713 push_packed_record(&mut body, &rec.refname, &rec.oid, rec.peeled.as_ref());
4714 }
4715 let packed_path = store::paths::packed_refs_path(git_dir);
4716 replace_packed_refs_file(git_dir, &packed_path, &body, false)?;
4717 for rec in &sorted {
4718 let storage_dir = ref_storage_dir(git_dir, &rec.refname);
4719 let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
4720 let loose = storage_dir.join(stor);
4721 if loose.is_file() {
4722 fs::remove_file(&loose).map_err(Error::Io)?;
4723 }
4724 if let Some(parent) = loose.parent() {
4725 prune_empty_ref_dirs(&storage_dir, parent);
4726 }
4727 }
4728 Ok(())
4729}
4730
4731pub fn list_refs_for_repository(
4742 repo: &crate::repo::Repository,
4743 prefix: &str,
4744) -> Result<Vec<(String, ObjectId)>> {
4745 let prefix_norm = normalize_list_refs_prefix(repo.git_dir(), prefix);
4746 list_store_refs_with_name_prefix(repo, &prefix_norm)
4747}
4748
4749fn list_store_refs_with_name_prefix(
4751 repo: &crate::repo::Repository,
4752 prefix: &str,
4753) -> Result<Vec<(String, ObjectId)>> {
4754 let git_dir = repo.git_dir();
4755 let mut results = collect_resolved_refs(repo.refs(), prefix)?;
4756 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4757 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4758 }
4759 results.sort_by(|a, b| a.0.cmp(&b.0));
4760 Ok(results)
4761}
4762
4763pub(crate) fn list_refs_in(
4774 repo: &crate::repo::Repository,
4775 prefix: &str,
4776) -> Result<Vec<(String, ObjectId)>> {
4777 if repo.refs_on_disk() {
4778 list_refs(repo.git_dir(), prefix)
4779 } else {
4780 list_refs_for_repository(repo, prefix)
4781 }
4782}
4783
4784pub(crate) fn resolve_ref_in(repo: &crate::repo::Repository, refname: &str) -> Result<ObjectId> {
4791 if repo.refs_on_disk() {
4792 resolve_ref(repo.git_dir(), refname)
4793 } else {
4794 repo.resolve_ref_oid(refname)
4795 }
4796}
4797
4798pub(crate) fn list_refs_glob_in(
4805 repo: &crate::repo::Repository,
4806 pattern: &str,
4807) -> Result<Vec<(String, ObjectId)>> {
4808 if repo.refs_on_disk() {
4809 list_refs_glob(repo.git_dir(), pattern)
4810 } else {
4811 list_refs_glob_for_repository(repo, pattern)
4812 }
4813}
4814
4815pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4835 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4836 let prefix = prefix_norm.as_str();
4837 let store = open_ref_store(git_dir)?;
4838 if store.format() != RefStorageFormat::Files {
4839 return crate::reftable::reftable_list_refs(git_dir, prefix);
4840 }
4841 let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
4842 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4843 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4844 }
4845 results.sort_by(|a, b| a.0.cmp(&b.0));
4846 Ok(results)
4847}
4848
4849pub(crate) fn list_refs_filtering_packed(
4860 git_dir: &Path,
4861 prefix: &str,
4862 keep: PackedRecordFilter<'_>,
4863) -> Result<Vec<(String, ObjectId)>> {
4864 let Some(store) = store::open_files_ref_store(git_dir)? else {
4865 return list_refs(git_dir, prefix);
4866 };
4867 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4868 let prefix = prefix_norm.as_str();
4869 let mut results = collect_resolved_ref_entries(&store, |f| {
4870 store.for_each_ref_filtered(prefix, Some(keep), f)
4871 })?;
4872 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4873 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4874 }
4875 results.sort_by(|a, b| a.0.cmp(&b.0));
4876 Ok(results)
4877}
4878
4879pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
4887 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
4888}
4889
4890pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4900 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4901 return crate::reftable::reftable_list_refs(git_dir, prefix);
4902 }
4903 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
4904 let stored_prefix = prefix.to_owned();
4905
4906 if let Some(cdir) = common_dir(git_dir) {
4907 if cdir != git_dir {
4908 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
4909 let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
4910 collect_loose_refs_into_map(
4911 &loose_root,
4912 &loose_prefix,
4913 &stored_prefix,
4914 &cdir,
4915 true,
4916 &mut by_name,
4917 )?;
4918 }
4919 }
4920
4921 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
4922 let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
4923 collect_loose_refs_into_map(
4924 &loose_root,
4925 &loose_prefix,
4926 &stored_prefix,
4927 git_dir,
4928 true,
4929 &mut by_name,
4930 )?;
4931
4932 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
4933 results.sort_by(|a, b| a.0.cmp(&b.0));
4934 Ok(results)
4935}
4936
4937pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
4950 let config = ConfigSet::load(
4951 &crate::environment::Environment::empty(),
4952 Some(receiving_git_dir),
4953 true,
4954 )?;
4955 let objects_dir = receiving_git_dir.join("objects");
4956 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
4957 let mut out = Vec::new();
4958 let mut seen = std::collections::HashSet::new();
4959 for alt_objects in alternates {
4960 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
4961 continue;
4962 };
4963 if !alt_git_dir.join("refs").is_dir() {
4964 continue;
4965 }
4966 if let Some(prefixes) = config.get("core.alternateRefsPrefixes") {
4967 for part in prefixes.split_whitespace() {
4968 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
4969 if seen.insert(oid) {
4970 out.push(oid);
4971 }
4972 continue;
4973 }
4974 for (_, oid) in list_refs(&alt_git_dir, part)? {
4975 if seen.insert(oid) {
4976 out.push(oid);
4977 }
4978 }
4979 }
4980 } else {
4981 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
4982 if seen.insert(oid) {
4983 out.push(oid);
4984 }
4985 }
4986 }
4987 }
4988 Ok(out)
4989}
4990
4991pub fn list_refs_glob_for_repository(
4993 repo: &crate::repo::Repository,
4994 pattern: &str,
4995) -> Result<Vec<(String, ObjectId)>> {
4996 let all = match pattern.find(['*', '?', '[']) {
5000 Some(pos) => list_store_refs_with_name_prefix(repo, &pattern[..pos])?,
5001 None => {
5002 let mut dir = pattern.trim_end_matches('/').to_owned();
5003 if !dir.is_empty() {
5004 dir.push('/');
5005 }
5006 list_refs_for_repository(repo, &dir)?
5007 }
5008 };
5009 Ok(all
5010 .into_iter()
5011 .filter(|(refname, _)| ref_matches_glob(refname, pattern))
5012 .collect())
5013}
5014
5015pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
5022 let glob_pos = pattern.find(['*', '?', '[']);
5023 let prefix_owned: String = match glob_pos {
5024 Some(pos) => pattern[..pos].to_owned(),
5026 None => {
5027 let mut p = pattern.trim_end_matches('/').to_owned();
5028 if !p.is_empty() {
5029 p.push('/');
5030 }
5031 p
5032 }
5033 };
5034 let prefix = prefix_owned.as_str();
5035 let all = list_refs(git_dir, prefix)?;
5036 let mut results = Vec::new();
5037 for (refname, oid) in all {
5038 if ref_matches_glob(&refname, pattern) {
5039 results.push((refname, oid));
5040 }
5041 }
5042 Ok(results)
5043}
5044
5045pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
5049 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
5051 return refname == pattern
5052 || refname.ends_with(&format!("/{pattern}"))
5053 || refname.starts_with(&format!("{pattern}/"));
5054 }
5055 glob_match(pattern, refname)
5056}
5057
5058fn glob_match(pattern: &str, text: &str) -> bool {
5059 let pat = pattern.as_bytes();
5060 let txt = text.as_bytes();
5061 let (mut pi, mut ti) = (0, 0);
5062 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
5063 while ti < txt.len() {
5064 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
5065 pi += 1;
5066 ti += 1;
5067 } else if pi < pat.len() && pat[pi] == b'*' {
5068 star_pi = pi;
5069 star_ti = ti;
5070 pi += 1;
5071 } else if star_pi != usize::MAX {
5072 pi = star_pi + 1;
5073 star_ti += 1;
5074 ti = star_ti;
5075 } else {
5076 return false;
5077 }
5078 }
5079 while pi < pat.len() && pat[pi] == b'*' {
5080 pi += 1;
5081 }
5082 pi == pat.len()
5083}
5084
5085fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
5087 let content = fs::read_to_string(path).ok()?;
5088 let content = content.trim_end_matches('\n').trim();
5089 if ObjectId::is_full_hex(content) {
5090 content.parse().ok()
5091 } else {
5092 None
5093 }
5094}
5095
5096fn collect_loose_refs_into_map(
5097 dir: &Path,
5098 prefix: &str,
5099 filter_prefix: &str,
5100 resolve_git_dir: &Path,
5101 physical_keys: bool,
5102 out: &mut HashMap<String, ObjectId>,
5103) -> Result<()> {
5104 let read = match fs::read_dir(dir) {
5105 Ok(r) => r,
5106 Err(e)
5107 if e.kind() == io::ErrorKind::NotFound
5108 || e.kind() == io::ErrorKind::NotADirectory
5109 || e.raw_os_error() == Some(libc::ENOTDIR) =>
5110 {
5111 return Ok(());
5112 }
5113 Err(e) => return Err(Error::Io(e)),
5114 };
5115
5116 for entry in read {
5117 let entry = entry?;
5118 let name = entry.file_name();
5119 let name_str = name.to_string_lossy();
5120 let refname = format!("{prefix}{name_str}");
5121 let path = entry.path();
5122 let meta = match fs::metadata(&path) {
5123 Ok(m) => m,
5124 Err(_) => continue,
5125 };
5126
5127 if meta.is_dir() {
5128 collect_loose_refs_into_map(
5129 &path,
5130 &format!("{refname}/"),
5131 filter_prefix,
5132 resolve_git_dir,
5133 physical_keys,
5134 out,
5135 )?;
5136 } else if meta.is_file() {
5137 if physical_keys {
5138 if let Some(oid) = loose_ref_file_direct_oid(&path) {
5139 out.insert(refname, oid);
5140 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
5141 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
5142 out.insert(refname, oid);
5143 }
5144 }
5145 } else {
5146 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
5147 .unwrap_or_else(|| refname.clone());
5148 if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
5149 continue;
5150 }
5151 if let Some(oid) = loose_ref_file_direct_oid(&path) {
5152 out.insert(logical, oid);
5153 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
5154 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
5155 out.insert(logical, oid);
5156 }
5157 }
5158 }
5159 }
5160 }
5161 Ok(())
5162}
5163
5164pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
5173 let inner = spec
5175 .strip_prefix("@{-")
5176 .and_then(|s| s.strip_suffix('}'))
5177 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
5178 let n: usize = inner
5179 .parse()
5180 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
5181 if n == 0 {
5182 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
5183 }
5184 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
5185 let mut count = 0usize;
5186 for entry in entries.iter().rev() {
5187 let msg = &entry.message;
5188 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
5189 count += 1;
5190 if count == n {
5191 if let Some(to_pos) = rest.find(" to ") {
5192 return Ok(rest[..to_pos].to_string());
5193 }
5194 }
5195 }
5196 }
5197 Err(Error::InvalidRef(format!(
5198 "{spec}: only {count} checkout(s) in reflog"
5199 )))
5200}
5201
5202fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
5203 let prefix = prefix.trim_end_matches('/');
5204 if prefix.is_empty() {
5205 return true;
5206 }
5207 if refname.starts_with(prefix) {
5208 return true;
5209 }
5210 if refname == prefix {
5211 return true;
5212 }
5213 false
5214}
5215
5216fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
5217 let base = prefix.trim_end_matches('/');
5218 let (head, tail) = base.rsplit_once('/')?;
5219 let mut chars: Vec<char> = tail.chars().collect();
5220 while let Some(last) = chars.pop() {
5221 if last != '\u{10ffff}' {
5222 chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
5223 return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
5224 }
5225 }
5226 None
5227}
5228
5229pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
5232 let trimmed = prefix.trim_end_matches('/');
5233 if trimmed.is_empty() {
5234 return (git_dir.join("refs"), "refs/".to_owned());
5235 }
5236 let direct = git_dir.join(trimmed);
5237 if direct.is_dir() {
5238 return (direct, format!("{trimmed}/"));
5239 }
5240 if let Some(slash) = trimmed.rfind('/') {
5241 let parent = &trimmed[..slash];
5242 return (git_dir.join(parent), format!("{parent}/"));
5243 }
5244 (git_dir.to_path_buf(), String::new())
5245}
5246
5247fn collect_packed_refs_into_map(
5248 git_dir: &Path,
5249 prefix: &str,
5250 physical_keys: bool,
5251 out: &mut HashMap<String, ObjectId>,
5252) -> Result<()> {
5253 let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
5254 for (refname, oid) in map {
5255 let key = if physical_keys {
5256 refname.clone()
5257 } else {
5258 crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
5259 };
5260 out.insert(key, oid);
5261 }
5262 Ok(())
5263}
5264
5265#[cfg(test)]
5266mod update_branch_for_commit_tests {
5267 use super::*;
5268 use crate::reflog::read_reflog;
5269 use crate::repo::init_repository;
5270 use std::thread;
5271 use tempfile::TempDir;
5272
5273 fn sample_oid(hex: &str) -> ObjectId {
5274 hex.parse().unwrap()
5275 }
5276
5277 #[test]
5278 fn update_branch_for_commit_holds_lock_until_ref_commit() {
5279 let _pause_serial = super::inject::pause_injection_test_scope();
5280 let tmp = TempDir::new().unwrap();
5281 let repo = init_repository(
5282 tmp.path(),
5283 false,
5284 "main",
5285 None,
5286 crate::RefStorageFormat::Files,
5287 )
5288 .unwrap();
5289 let git_dir = repo.git_dir().to_path_buf();
5290 let oid1 = test_commit(&git_dir, "one");
5291 let oid2 = test_commit(&git_dir, "two");
5292 let oid3 = test_commit(&git_dir, "three");
5293 write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
5294
5295 let gate = super::inject::branch_ref_pause_gate();
5296 super::inject::set_branch_ref_pause_before_commit(Some(gate.clone()));
5297
5298 let git_dir_thread = git_dir.clone();
5299 let handle = thread::spawn(move || {
5300 update_branch_for_commit(
5301 &git_dir_thread,
5302 &BranchCommitRefUpdate {
5303 branch_ref: "refs/heads/main",
5304 expected_old: Some(oid1),
5305 new_oid: oid2,
5306 identity: "T <t@e.com> 1 +0000",
5307 reflog_message: "commit: test",
5308 },
5309 )
5310 });
5311
5312 let mut wait = gate.wait.lock().unwrap();
5313 while !*wait {
5314 wait = gate.cv.wait(wait).unwrap();
5315 }
5316
5317 assert!(
5318 write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
5319 "concurrent branch update must block on the held ref lock"
5320 );
5321
5322 *wait = false;
5323 gate.cv.notify_all();
5324 drop(wait);
5325 handle.join().unwrap().unwrap();
5326 super::inject::set_branch_ref_pause_before_commit(None);
5327
5328 assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
5329 }
5330
5331 #[test]
5332 fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
5333 let tmp = TempDir::new().unwrap();
5334 let repo = init_repository(
5335 tmp.path(),
5336 false,
5337 "main",
5338 None,
5339 crate::RefStorageFormat::Reftable,
5340 )
5341 .unwrap();
5342 let git_dir = repo.git_dir().to_path_buf();
5343 let new_oid = sample_oid("3333333333333333333333333333333333333333");
5344
5345 crate::reftable::inject::set_reftable_transaction_fail(true);
5346 let err = update_branch_for_commit(
5347 &git_dir,
5348 &BranchCommitRefUpdate {
5349 branch_ref: "refs/heads/main",
5350 expected_old: None,
5351 new_oid,
5352 identity: "T <t@e.com> 1 +0000",
5353 reflog_message: "commit (initial): test",
5354 },
5355 )
5356 .unwrap_err();
5357 crate::reftable::inject::set_reftable_transaction_fail(false);
5358 assert!(!matches!(err, Error::Message(_)));
5359
5360 assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
5361 assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
5362 assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
5363 }
5364}
5365
5366#[cfg(test)]
5367mod refname_available_tests {
5368 use super::*;
5369 use std::collections::{BTreeSet, HashSet};
5370 use tempfile::tempdir;
5371
5372 #[test]
5373 fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
5374 let dir = tempdir().unwrap();
5375 let git_dir = dir.path();
5376 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5377 let items = vec![
5378 RefBatchItem {
5379 name: "refs/heads/c".to_owned(),
5380 new_oid: Some(sample_oid()),
5381 },
5382 RefBatchItem {
5383 name: "refs/heads/c/x".to_owned(),
5384 new_oid: Some(sample_oid()),
5385 },
5386 ];
5387 let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
5388 assert!(matches!(
5389 err,
5390 RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
5391 ));
5392 }
5393
5394 fn sample_oid() -> ObjectId {
5395 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5396 }
5397
5398 #[test]
5399 fn loose_parent_blocks_child_create() {
5400 let dir = tempdir().unwrap();
5401 let git_dir = dir.path();
5402 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5403 fs::write(
5404 git_dir.join("refs/1l/c"),
5405 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5406 )
5407 .unwrap();
5408 assert_eq!(
5409 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
5410 RawRefLookup::Exists
5411 );
5412 let extras = BTreeSet::from([
5413 "refs/1l/b".to_string(),
5414 "refs/1l/c/x".to_string(),
5415 "refs/1l/d".to_string(),
5416 ]);
5417 let skip = HashSet::new();
5418 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
5419 .unwrap_err();
5420 assert!(matches!(
5421 err,
5422 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5423 ));
5424 }
5425
5426 #[test]
5427 fn verify_sees_loose_ref_after_canonical_git_dir() {
5428 let dir = tempdir().unwrap();
5429 let git_dir = dir.path().join(".git");
5430 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5431 fs::write(
5432 git_dir.join("refs/1l/c"),
5433 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5434 )
5435 .unwrap();
5436 let skip = HashSet::new();
5437 let extras = BTreeSet::new();
5438 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
5439 .unwrap_err();
5440 assert!(matches!(
5441 err,
5442 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5443 ));
5444 }
5445
5446 fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
5447 if existing == candidate {
5448 return false;
5449 }
5450 existing
5451 .strip_prefix(candidate)
5452 .is_some_and(|rest| rest.starts_with('/'))
5453 || candidate
5454 .strip_prefix(existing)
5455 .is_some_and(|rest| rest.starts_with('/'))
5456 }
5457
5458 #[test]
5459 fn packed_namespace_index_matches_pairwise_scan() {
5460 let mut sorted: BTreeSet<String> = BTreeSet::new();
5461 for i in 0..500 {
5462 sorted.insert(format!("refs/heads/b{i:05}"));
5463 }
5464 sorted.insert("refs/heads/parent".into());
5465 sorted.insert("refs/heads/a/b".into());
5466 let candidates = [
5467 "refs/heads/b00001",
5468 "refs/heads/parent/child",
5469 "refs/heads/new",
5470 "refs/heads/a",
5471 ];
5472 for candidate in candidates {
5473 let indexed = packed_refname_namespace_conflict(&sorted, candidate);
5474 let linear = sorted
5475 .iter()
5476 .any(|existing| refname_namespace_conflicts(existing, candidate));
5477 assert_eq!(indexed, linear, "candidate {candidate}");
5478 }
5479 }
5480
5481 #[test]
5482 fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
5483 let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
5484 if !git_dir.is_dir() {
5485 return;
5486 }
5487 let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
5488 assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
5489 assert!(listed
5490 .iter()
5491 .all(|(name, _)| name.starts_with("refs/heads/b05")));
5492 }
5493
5494 #[test]
5495 fn list_refs_finds_sibling_under_parent_directory() {
5496 let dir = tempdir().unwrap();
5497 let git_dir = dir.path();
5498 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5499 fs::write(
5500 git_dir.join("refs/ns/p/x"),
5501 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5502 )
5503 .unwrap();
5504 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
5505 assert!(
5506 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
5507 "got {listed:?}"
5508 );
5509 }
5510
5511 #[test]
5512 fn verify_blocks_parent_when_child_ref_exists() {
5513 let dir = tempdir().unwrap();
5514 let git_dir = dir.path();
5515 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5516 fs::write(
5517 git_dir.join("refs/ns/p/x"),
5518 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5519 )
5520 .unwrap();
5521 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
5522 let skip = HashSet::new();
5523 let err =
5524 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
5525 assert!(matches!(
5526 err,
5527 RefnameUnavailable::DescendantExists { ref blocking, .. }
5528 if blocking == "refs/ns/p/x"
5529 ));
5530 }
5531
5532 #[test]
5533 fn verify_blocks_parent_when_only_a_packed_child_exists() {
5534 let dir = tempdir().unwrap();
5537 let git_dir = dir.path();
5538 fs::create_dir_all(git_dir.join("refs")).unwrap();
5539 fs::write(
5540 store::paths::packed_refs_path(git_dir),
5541 "# pack-refs with: peeled fully-peeled\n\
5542 67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
5543 )
5544 .unwrap();
5545 let err = verify_refname_available_for_create(
5546 git_dir,
5547 "refs/ns/q",
5548 &BTreeSet::new(),
5549 &HashSet::new(),
5550 )
5551 .unwrap_err();
5552 assert!(matches!(
5553 err,
5554 RefnameUnavailable::DescendantExists { ref blocking, .. }
5555 if blocking == "refs/ns/q/x"
5556 ));
5557 }
5558
5559 #[test]
5560 fn verify_allows_name_whose_siblings_share_a_prefix() {
5561 let dir = tempdir().unwrap();
5562 let git_dir = dir.path();
5563 fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
5564 for name in ["v1.0.1", "v1.0-rc1"] {
5565 fs::write(
5566 git_dir.join("refs/tags").join(name),
5567 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5568 )
5569 .unwrap();
5570 }
5571 verify_refname_available_for_create(
5572 git_dir,
5573 "refs/tags/v1.0",
5574 &BTreeSet::new(),
5575 &HashSet::new(),
5576 )
5577 .expect("siblings are not descendants");
5578 }
5579
5580 #[test]
5581 fn verify_blocks_parent_git_style_nested_path() {
5582 let dir = tempdir().unwrap();
5583 let git_dir = dir.path();
5584 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
5585 fs::write(
5586 git_dir.join("refs/3l/c/x"),
5587 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5588 )
5589 .unwrap();
5590 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
5591 let skip = HashSet::new();
5592 let err =
5593 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
5594 assert!(matches!(
5595 err,
5596 RefnameUnavailable::DescendantExists { ref blocking, .. }
5597 if blocking == "refs/3l/c/x"
5598 ));
5599 }
5600
5601 #[test]
5602 fn intermediate_directory_does_not_block_nested_create() {
5603 let dir = tempdir().unwrap();
5604 let git_dir = dir.path();
5605 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
5606 fs::write(
5607 git_dir.join("refs/ns/existing"),
5608 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5609 )
5610 .unwrap();
5611 assert_eq!(
5612 read_raw_ref(git_dir, "refs/ns").unwrap(),
5613 RawRefLookup::IsDirectory
5614 );
5615 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
5616 let skip = HashSet::new();
5617 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
5618 }
5619}
5620
5621#[cfg(test)]
5622mod ref_storage_traversal_guard_tests {
5623 use super::*;
5624 use crate::objects::ObjectId;
5625 use tempfile::tempdir;
5626
5627 const TRAVERSAL_REF: &str = "refs/heads/../../config";
5628
5629 fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
5630 let dir = tempdir().unwrap();
5631 let git_dir = dir.path().to_path_buf();
5632 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5633 fs::write(
5634 git_dir.join("config"),
5635 "[core]\nrepositoryformatversion = 0\n",
5636 )
5637 .unwrap();
5638 (dir, git_dir)
5639 }
5640
5641 fn sample_oid() -> ObjectId {
5642 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5643 }
5644
5645 fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
5646 assert!(matches!(result, Err(Error::InvalidRef(_))));
5647 assert!(
5648 git_dir.join("config").is_file(),
5649 "repository config must not be touched"
5650 );
5651 }
5652
5653 #[test]
5654 fn write_ref_rejects_traversal_refname() {
5655 let (_dir, git_dir) = bare_repo_with_config();
5656 let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
5657 assert_invalid_ref_preserves_config(&git_dir, result);
5658 }
5659
5660 #[test]
5661 fn write_ref_cached_rejects_traversal_refname() {
5662 let (_dir, git_dir) = bare_repo_with_config();
5663 let mut packed = PackedRefs::load(&git_dir).unwrap();
5664 let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
5665 assert_invalid_ref_preserves_config(&git_dir, result);
5666 }
5667
5668 #[test]
5669 fn write_symbolic_ref_rejects_traversal_refname() {
5670 let (_dir, git_dir) = bare_repo_with_config();
5671 let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
5672 assert_invalid_ref_preserves_config(&git_dir, result);
5673 }
5674
5675 #[test]
5676 fn delete_ref_rejects_traversal_refname() {
5677 let (_dir, git_dir) = bare_repo_with_config();
5678 let result = delete_ref(&git_dir, TRAVERSAL_REF);
5679 assert_invalid_ref_preserves_config(&git_dir, result);
5680 }
5681
5682 #[test]
5683 fn write_ref_rejects_illegal_branch_refname() {
5684 let (_dir, git_dir) = bare_repo_with_config();
5685 for refname in [
5686 "refs/heads/bad name",
5687 "refs/heads/x..y",
5688 "refs/heads/a~b",
5689 "refs/heads/a.lock",
5690 ] {
5691 let result = write_ref(&git_dir, refname, &sample_oid());
5692 assert_invalid_ref_preserves_config(&git_dir, result);
5693 assert!(
5694 !git_dir
5695 .join(refname.strip_prefix("refs/").unwrap())
5696 .exists(),
5697 "must not write loose ref for {refname}"
5698 );
5699 }
5700 }
5701
5702 #[test]
5703 fn write_ref_accepts_refs_heads_head() {
5704 let (_dir, git_dir) = bare_repo_with_config();
5705 let commit = test_commit(&git_dir, "head-named branch");
5706 write_ref(&git_dir, "refs/heads/HEAD", &commit)
5707 .expect("refs/heads/HEAD is valid ref syntax");
5708 assert!(git_dir.join("refs/heads/HEAD").is_file());
5709 }
5710}
5711
5712#[cfg(test)]
5713mod log_refs_config_tests {
5714 use super::*;
5715 use crate::config::{ConfigFile, ConfigScope};
5716
5717 fn set(text: &str) -> ConfigSet {
5718 let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
5719 let mut set = ConfigSet::new();
5720 set.merge(&file);
5721 set
5722 }
5723
5724 #[test]
5725 fn parse_accepts_modes_and_bool_spellings() {
5726 for (raw, mode) in [
5727 ("always", LogRefsConfig::Always),
5728 (" ALWAYS ", LogRefsConfig::Always),
5729 ("true", LogRefsConfig::Normal),
5730 ("Yes", LogRefsConfig::Normal),
5731 ("on", LogRefsConfig::Normal),
5732 ("1", LogRefsConfig::Normal),
5733 ("false", LogRefsConfig::None),
5734 ("no", LogRefsConfig::None),
5735 ("OFF", LogRefsConfig::None),
5736 ("0", LogRefsConfig::None),
5737 ("never", LogRefsConfig::None),
5738 ] {
5739 assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
5740 }
5741 assert_eq!(LogRefsConfig::parse("sometimes"), None);
5742 }
5743
5744 #[test]
5745 fn effective_mode_applies_bare_default_only_when_unset() {
5746 assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
5747 assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
5748 let bare = set("[core]\n\tbare = true\n");
5749 assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
5750 let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
5751 assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
5752 assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
5753 let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
5754 assert_eq!(
5755 bare_logged.effective_log_refs_config(),
5756 LogRefsConfig::Normal
5757 );
5758 let always = set("[core]\n\tlogAllRefUpdates = always\n");
5759 assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
5760 }
5761
5762 #[test]
5763 fn autocreate_with_config_matches_mode() {
5764 let normal = set("[core]\n\tlogAllRefUpdates = true\n");
5765 let git_dir = Path::new(".git");
5766 assert!(should_autocreate_reflog_with_config(
5767 &normal,
5768 git_dir,
5769 "refs/heads/main"
5770 ));
5771 assert!(!should_autocreate_reflog_with_config(
5772 &normal,
5773 git_dir,
5774 "refs/tags/v1"
5775 ));
5776 let none = set("[core]\n\tlogAllRefUpdates = false\n");
5777 assert!(!should_autocreate_reflog_with_config(
5778 &none, git_dir, "HEAD"
5779 ));
5780 }
5781}
5782
5783#[cfg(test)]
5784mod read_raw_ref_tests {
5785 use super::*;
5786 use tempfile::tempdir;
5787
5788 #[test]
5789 fn loose_ref_file_is_exists() {
5790 let dir = tempdir().unwrap();
5791 let git_dir = dir.path();
5792 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5793 fs::write(
5794 git_dir.join("refs/heads/side"),
5795 "0000000000000000000000000000000000000000\n",
5796 )
5797 .unwrap();
5798 assert_eq!(
5799 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
5800 RawRefLookup::Exists
5801 );
5802 }
5803
5804 #[test]
5805 fn missing_ref_is_not_found() {
5806 let dir = tempdir().unwrap();
5807 let git_dir = dir.path();
5808 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5809 assert_eq!(
5810 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
5811 RawRefLookup::NotFound
5812 );
5813 }
5814
5815 #[test]
5816 fn directory_where_ref_expected_is_is_directory() {
5817 let dir = tempdir().unwrap();
5818 let git_dir = dir.path();
5819 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5820 assert_eq!(
5821 read_raw_ref(git_dir, "refs/heads").unwrap(),
5822 RawRefLookup::IsDirectory
5823 );
5824 }
5825
5826 #[test]
5827 fn packed_ref_name_is_exists() {
5828 let dir = tempdir().unwrap();
5829 let git_dir = dir.path();
5830 fs::write(
5831 store::paths::packed_refs_path(git_dir),
5832 "# pack-refs with: peeled fully-peeled \n\
5833 0000000000000000000000000000000000000000 refs/heads/packed\n",
5834 )
5835 .unwrap();
5836 assert_eq!(
5837 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
5838 RawRefLookup::Exists
5839 );
5840 }
5841
5842 #[test]
5843 fn access_denied_on_a_directory_is_a_missing_file() {
5844 let dir = tempdir().unwrap();
5845 let ref_dir = dir.path().join("refs/heads/rel");
5846 fs::create_dir_all(&ref_dir).unwrap();
5847 let file = dir.path().join("refs/heads/main");
5848 fs::write(&file, "x\n").unwrap();
5849 let denied = || io::Error::from(io::ErrorKind::PermissionDenied);
5850
5851 assert!(io_error_is_missing_file(&denied(), &ref_dir));
5853 assert!(
5854 !io_error_is_missing_file(&denied(), &file),
5855 "an unreadable ref file is a real I/O error"
5856 );
5857 assert!(!io_error_is_missing_file(
5858 &denied(),
5859 &dir.path().join("refs/heads/none")
5860 ));
5861 for kind in [
5862 io::ErrorKind::NotFound,
5863 io::ErrorKind::IsADirectory,
5864 io::ErrorKind::NotADirectory,
5865 ] {
5866 assert!(io_error_is_missing_file(&io::Error::from(kind), &file));
5867 }
5868 assert!(io_error_is_missing_file(
5869 &io::Error::from_raw_os_error(libc::EISDIR),
5870 &file
5871 ));
5872 assert!(!io_error_is_missing_file(
5873 &io::Error::from(io::ErrorKind::InvalidData),
5874 &ref_dir
5875 ));
5876 }
5877
5878 #[test]
5879 fn reading_a_ref_directory_is_not_found() {
5880 let dir = tempdir().unwrap();
5881 let ref_dir = dir.path().join("refs/heads/rel");
5882 fs::create_dir_all(ref_dir.join("1.0")).unwrap();
5883 match read_ref_file(&ref_dir) {
5884 Err(Error::Io(e)) => assert_eq!(e.kind(), io::ErrorKind::NotFound),
5885 other => panic!("expected a missing ref, got {other:?}"),
5886 }
5887 }
5888}
5889
5890#[cfg(test)]
5891mod packed_refs_parse_tests {
5892 use super::*;
5893
5894 use tempfile::tempdir;
5895
5896 #[test]
5897 fn packed_removal_validates_the_whole_file_before_any_change() {
5898 let dir = tempdir().unwrap();
5899 let path = store::paths::packed_refs_path(dir.path());
5900 let record = "1111111111111111111111111111111111111111 refs/heads/keep\n";
5901 let malformed = [
5902 format!("{record}garbage\n").into_bytes(),
5903 format!("{record}\n").into_bytes(),
5904 format!("{record}^bad-id\n").into_bytes(),
5905 format!("{record}^{}\n^{}\n", "1".repeat(40), "2".repeat(40)).into_bytes(),
5906 format!("{record}{} refs/heads/bad..name\n", "2".repeat(40)).into_bytes(),
5907 format!("{record}# comment\r\n").into_bytes(),
5908 record.trim_end().as_bytes().to_vec(),
5909 [record.as_bytes(), b"\xff\n"].concat(),
5910 ];
5911 for content in malformed {
5912 for name in ["refs/heads/missing", "refs/heads/keep"] {
5913 fs::write(&path, &content).unwrap();
5914 assert!(
5915 remove_packed_ref(dir.path(), name).is_err(),
5916 "{name}: {content:?}"
5917 );
5918 assert_eq!(fs::read(&path).unwrap(), content);
5919 assert!(!path.with_extension("lock").exists());
5920 assert!(!dir.path().join("packed-refs.new").exists());
5921 }
5922 }
5923 }
5924
5925 #[test]
5926 fn packed_removal_without_a_match_preserves_valid_file_bytes() {
5927 let dir = tempdir().unwrap();
5928 let path = store::paths::packed_refs_path(dir.path());
5929 for hash_len in [40, 64] {
5930 let body = format!(
5931 "# pack-refs with: peeled\n# keep this comment\n{} refs/tags/tag\n^{}\n{} refs/heads/main\n",
5932 "1".repeat(hash_len), "2".repeat(hash_len), "3".repeat(hash_len)
5933 );
5934 fs::write(&path, &body).unwrap();
5935 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5936 assert_eq!(fs::read_to_string(&path).unwrap(), body);
5937 }
5938 fs::write(&path, "").unwrap();
5939 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5940 assert!(fs::read(&path).unwrap().is_empty());
5941 }
5942
5943 #[test]
5944 fn parse_rejects_garbage_line() {
5945 let dir = tempdir().unwrap();
5946 let path = store::paths::packed_refs_path(dir.path());
5947 fs::write(
5948 &path,
5949 "# pack-refs with: peeled\n\
5950 0000000000000000000000000000000000000000 refs/heads/main\n\
5951 not a ref line\n",
5952 )
5953 .unwrap();
5954 let data = fs::read(&path).unwrap();
5955 assert!(matches!(
5956 parse_packed_refs_to_map(&path, &data),
5957 Err(Error::PackedRefsUnexpectedLine { .. })
5958 ));
5959 }
5960
5961 #[test]
5962 fn parse_rejects_carriage_return_bytes() {
5963 let dir = tempdir().unwrap();
5964 let path = store::paths::packed_refs_path(dir.path());
5965 fs::write(
5966 &path,
5967 "# pack-refs with: peeled\n\
5968 0000000000000000000000000000000000000000 refs/heads/main\r\n",
5969 )
5970 .unwrap();
5971 let data = fs::read(&path).unwrap();
5972 assert!(matches!(
5973 parse_packed_refs_to_map(&path, &data),
5974 Err(Error::PackedRefsUnexpectedLine { .. })
5975 ));
5976 }
5977
5978 #[test]
5979 fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
5980 let dir = tempdir().unwrap();
5981 let git_dir = dir.path();
5982 let oid = "1111111111111111111111111111111111111111";
5983 fs::write(
5984 store::paths::packed_refs_path(git_dir),
5985 format!(
5986 "# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5987 ),
5988 )
5989 .unwrap();
5990 let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
5991 assert_eq!(listed.len(), 1);
5992 assert_eq!(listed[0].0, "refs/heads/aaa");
5993 }
5994
5995 #[test]
5996 fn parse_rejects_blank_line_and_missing_final_newline() {
5997 let dir = tempdir().unwrap();
5998 let path = store::paths::packed_refs_path(dir.path());
5999 fs::write(
6000 &path,
6001 "# pack-refs with: peeled\n\n\
6002 0000000000000000000000000000000000000000 refs/heads/main\n",
6003 )
6004 .unwrap();
6005 let data = fs::read(&path).unwrap();
6006 assert!(matches!(
6007 parse_packed_refs_to_map(&path, &data),
6008 Err(Error::PackedRefsUnexpectedLine { .. })
6009 ));
6010
6011 fs::write(
6012 &path,
6013 "# pack-refs with: peeled\n\
6014 0000000000000000000000000000000000000000 refs/heads/main",
6015 )
6016 .unwrap();
6017 let data = fs::read(&path).unwrap();
6018 assert!(matches!(
6019 parse_packed_refs_to_map(&path, &data),
6020 Err(Error::PackedRefsUnexpectedLine { .. })
6021 ));
6022 }
6023
6024 #[test]
6025 fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
6026 let dir = tempdir().unwrap();
6027 let path = store::paths::packed_refs_path(dir.path());
6028 let a = "1111111111111111111111111111111111111111";
6029 let b = "2222222222222222222222222222222222222222";
6030 let peel = "3333333333333333333333333333333333333333";
6031 let body = |header: &str| {
6032 format!(
6033 "{header}{a} refs/heads/main\n\
6034 {b} refs/heads/tagged\n\
6035 ^{peel}\n\
6036 {a} refs/tags/light\n\
6037 {b} refs/tags/v1\n\
6038 ^{peel}\n"
6039 )
6040 };
6041 let a: ObjectId = a.parse().unwrap();
6042 let b: ObjectId = b.parse().unwrap();
6043 let peel: ObjectId = peel.parse().unwrap();
6044 let cases = [
6045 (
6046 "# pack-refs with: peeled fully-peeled sorted \n",
6047 [
6048 PackedPeel::NotTag,
6049 PackedPeel::Peeled(peel),
6050 PackedPeel::NotTag,
6051 PackedPeel::Peeled(peel),
6052 ],
6053 ),
6054 (
6055 "# pack-refs with: peeled sorted \n",
6056 [
6057 PackedPeel::Unknown,
6058 PackedPeel::Unknown,
6059 PackedPeel::NotTag,
6060 PackedPeel::Peeled(peel),
6061 ],
6062 ),
6063 ("", [PackedPeel::Unknown; 4]),
6064 ];
6065 for (header, want) in cases {
6066 let data = body(header).into_bytes();
6067 let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
6068 let seen = std::cell::RefCell::new(Vec::new());
6069 let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
6070 seen.borrow_mut().push((name.to_owned(), *oid, peel));
6071 name.starts_with("refs/tags/")
6072 };
6073 let kept = view
6074 .map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
6075 .unwrap();
6076 assert_eq!(
6077 seen.into_inner(),
6078 vec![
6079 ("refs/heads/main".to_owned(), a, want[0]),
6080 ("refs/heads/tagged".to_owned(), b, want[1]),
6081 ("refs/tags/light".to_owned(), a, want[2]),
6082 ("refs/tags/v1".to_owned(), b, want[3]),
6083 ],
6084 "header {header:?}"
6085 );
6086 let mut kept: Vec<_> = kept.into_iter().collect();
6087 kept.sort();
6088 assert_eq!(
6089 kept,
6090 vec![
6091 ("refs/tags/light".to_owned(), a),
6092 ("refs/tags/v1".to_owned(), b)
6093 ]
6094 );
6095 assert_eq!(
6096 view.map_for_prefix_filtered(&path, Some("refs"), None)
6097 .unwrap(),
6098 view.map_for_prefix(&path, Some("refs")).unwrap()
6099 );
6100 }
6101
6102 let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
6104 let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
6105 assert!(matches!(
6106 view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
6107 Err(Error::PackedRefsUnexpectedLine { .. })
6108 ));
6109 }
6110
6111 #[test]
6112 fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
6113 let dir = tempdir().unwrap();
6114 let path = store::paths::packed_refs_path(dir.path());
6115 let oid = "1111111111111111111111111111111111111111";
6116 let peel = "2222222222222222222222222222222222222222";
6117 let sorted_body = format!(
6118 "# pack-refs with: peeled fully-peeled sorted\n\
6119 {oid} refs/heads/aaa\n\
6120 {oid} refs/heads/main\n\
6121 {oid} refs/heads/zzz\n\
6122 {oid} refs/tags/v1\n\
6123 ^{peel}\n"
6124 );
6125 fs::write(&path, &sorted_body).unwrap();
6126 let data = fs::read(&path).unwrap();
6127 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6128 let full = parse_packed_refs_to_map(&path, &data).unwrap();
6129 for (name, want) in &full {
6130 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
6131 }
6132 assert_eq!(
6133 lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
6134 .unwrap()
6135 .map(|(o, p)| (o, p.unwrap())),
6136 Some((oid.parse().unwrap(), peel.parse().unwrap()))
6137 );
6138
6139 let unsorted_body = format!(
6140 "# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
6141 );
6142 fs::write(&path, unsorted_body).unwrap();
6143 let data = fs::read(&path).unwrap();
6144 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6145 let full = parse_packed_refs_to_map(&path, &data).unwrap();
6146 for (name, want) in &full {
6147 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
6148 }
6149 assert_eq!(
6150 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6151 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6152 );
6153 }
6154
6155 #[test]
6156 fn sorted_prefix_without_upper_bound_filters_names() {
6157 let dir = tempdir().unwrap();
6158 let path = store::paths::packed_refs_path(dir.path());
6159 let oid = "1111111111111111111111111111111111111111";
6160 let sorted_body = format!(
6161 "# pack-refs with: peeled fully-peeled sorted\n\
6162 {oid} refs/heads/aaa\n\
6163 {oid} refs/heads/zzz\n\
6164 {oid} refs/tags/v1\n"
6165 );
6166 fs::write(&path, sorted_body).unwrap();
6167 let data = fs::read(&path).unwrap();
6168 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6169 assert_eq!(
6170 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6171 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6172 );
6173 assert!(
6174 !view
6175 .map_for_prefix(&path, Some("refs/heads/a"))
6176 .unwrap()
6177 .contains_key("refs/heads/zzz"),
6178 "prefix refs/heads/a must not include refs/heads/zzz"
6179 );
6180 assert_eq!(
6181 view.map_for_prefix(&path, Some("refs")).unwrap(),
6182 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
6183 );
6184 }
6185
6186 #[test]
6187 fn sorted_prefix_scan_validates_only_its_range() {
6188 let dir = tempdir().unwrap();
6189 let path = store::paths::packed_refs_path(dir.path());
6190 let oid = "1111111111111111111111111111111111111111";
6191 let view_of =
6192 |body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
6193
6194 let bad_peel_on_branch = view_of(format!(
6195 "# pack-refs with: peeled fully-peeled sorted\n\
6196 {oid} refs/heads/main\n\
6197 ^not-hex\n\
6198 {oid} refs/heads/topic\n\
6199 {oid} refs/tags/v1\n"
6200 ))
6201 .unwrap();
6202 assert!(matches!(
6203 bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
6204 Err(Error::PackedRefsUnexpectedLine { .. })
6205 ));
6206 let tags = bad_peel_on_branch
6207 .map_for_prefix(&path, Some("refs/tags"))
6208 .unwrap();
6209 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
6210 assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
6211
6212 let blank_among_tags = view_of(format!(
6213 "# pack-refs with: peeled fully-peeled sorted\n\
6214 {oid} refs/heads/main\n\
6215 {oid} refs/tags/v1\n\
6216 \n\
6217 {oid} refs/tags/v2\n"
6218 ))
6219 .unwrap();
6220 assert_eq!(
6221 blank_among_tags
6222 .map_for_prefix(&path, Some("refs/heads"))
6223 .unwrap()
6224 .len(),
6225 1
6226 );
6227 assert!(blank_among_tags
6228 .map_for_prefix(&path, Some("refs/tags"))
6229 .is_err());
6230
6231 let no_match = blank_among_tags
6232 .map_for_prefix(&path, Some("refs/notes"))
6233 .unwrap();
6234 assert!(no_match.is_empty());
6235 }
6236
6237 #[test]
6238 fn non_utf8_record_only_breaks_reads_that_cover_it() {
6239 let dir = tempdir().unwrap();
6240 let path = store::paths::packed_refs_path(dir.path());
6241 let oid = "1111111111111111111111111111111111111111";
6242 let mut body =
6243 format!("# pack-refs with: peeled fully-peeled sorted \n{oid} refs/heads/main\n")
6244 .into_bytes();
6245 body.extend_from_slice(format!("{oid} refs/tags/v1\n{oid} refs/tags/").as_bytes());
6246 body.extend_from_slice(b"\xff\n");
6247 let view = PackedRefsFileView::from_bytes(&path, body, HashAlgo::Sha1).unwrap();
6248 assert!(view.sorted);
6249 for name in ["refs/heads/main", "refs/tags/v1"] {
6250 assert_eq!(
6251 view.lookup_oid(&path, name).unwrap(),
6252 Some(oid.parse().unwrap())
6253 );
6254 }
6255 assert_eq!(view.lookup_oid(&path, "refs/tags/v2").unwrap(), None);
6256 let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
6257 assert_eq!(heads.keys().collect::<Vec<_>>(), ["refs/heads/main"]);
6258 assert!(matches!(
6259 view.map_for_prefix(&path, Some("refs/tags")),
6260 Err(Error::PackedRefsUnexpectedLine { .. })
6261 ));
6262 assert!(view.map_for_prefix(&path, None).is_err());
6263
6264 let mut bad_header = b"# pack-refs with: \xff sorted \n".to_vec();
6265 bad_header.extend_from_slice(format!("{oid} refs/heads/main\n").as_bytes());
6266 assert!(matches!(
6267 PackedRefsFileView::from_bytes(&path, bad_header, HashAlgo::Sha1),
6268 Err(Error::PackedRefsUnexpectedLine { .. })
6269 ));
6270 }
6271
6272 #[test]
6273 fn scan_of_file_without_ref_lines_still_validates_it() {
6274 let dir = tempdir().unwrap();
6275 let path = store::paths::packed_refs_path(dir.path());
6276 let view = PackedRefsFileView::from_bytes(
6277 &path,
6278 b"# pack-refs with: peeled fully-peeled sorted\n\
6279 ^1111111111111111111111111111111111111111\n"
6280 .to_vec(),
6281 HashAlgo::Sha1,
6282 )
6283 .unwrap();
6284 for prefix in [None, Some("refs/heads")] {
6285 assert!(matches!(
6286 view.map_for_prefix(&path, prefix),
6287 Err(Error::PackedRefsUnexpectedLine { .. })
6288 ));
6289 }
6290 let header_only = PackedRefsFileView::from_bytes(
6291 &path,
6292 b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
6293 HashAlgo::Sha1,
6294 )
6295 .unwrap();
6296 assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
6297 }
6298
6299 #[test]
6300 fn view_checks_last_line_length_against_the_repository_hash() {
6301 let dir = tempdir().unwrap();
6302 let path = store::paths::packed_refs_path(dir.path());
6303 let sha1 = "1111111111111111111111111111111111111111";
6304 let sha256 = "2".repeat(64);
6305 let view =
6306 |body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
6307 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6308 let fifty_bytes = "x".repeat(49);
6309
6310 for (body, algo) in [
6311 (
6312 format!("{header}{sha1} refs/heads/main\ngarbage\n"),
6313 HashAlgo::Sha1,
6314 ),
6315 (
6316 format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
6317 HashAlgo::Sha1,
6318 ),
6319 (
6320 format!("{header}{sha1} refs/heads/main\n#\n"),
6321 HashAlgo::Sha1,
6322 ),
6323 (
6324 format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
6325 HashAlgo::Sha256,
6326 ),
6327 (format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
6328 ] {
6329 assert!(
6330 matches!(
6331 view(body.clone(), algo),
6332 Err(Error::PackedRefsUnexpectedLine { .. })
6333 ),
6334 "{algo:?}: {body:?}"
6335 );
6336 }
6337 for (body, algo) in [
6338 (header.to_owned(), HashAlgo::Sha1),
6339 (header.to_owned(), HashAlgo::Sha256),
6340 ("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
6341 (
6342 format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
6343 HashAlgo::Sha1,
6344 ),
6345 (
6346 format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
6347 HashAlgo::Sha1,
6348 ),
6349 (
6350 format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
6351 HashAlgo::Sha256,
6352 ),
6353 ] {
6354 assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
6355 }
6356 }
6357
6358 #[test]
6359 fn sha256_short_last_record_fails_branch_listing() {
6360 let dir = tempdir().unwrap();
6361 let path = store::paths::packed_refs_path(dir.path());
6362 let sha256 = "2".repeat(64);
6363 let body = format!(
6364 "# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
6365 "x".repeat(49)
6366 );
6367 assert!(
6368 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
6369 );
6370 }
6371
6372 #[test]
6373 fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
6374 let dir = tempdir().unwrap();
6375 let path = store::paths::packed_refs_path(dir.path());
6376 let oid = "1111111111111111111111111111111111111111";
6377 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6378 let view = |body: String| {
6379 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6380 };
6381
6382 let garbage_after_first_branch = view(format!(
6383 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
6384 {oid} refs/tags/t1\n"
6385 ));
6386 assert!(matches!(
6387 garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
6388 Err(Error::PackedRefsUnexpectedLine { .. })
6389 ));
6390 let tags = garbage_after_first_branch
6391 .map_for_prefix(&path, Some("refs/tags"))
6392 .unwrap();
6393 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
6394
6395 let garbage_before_range = view(format!(
6396 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
6397 ));
6398 for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
6399 assert!(
6400 garbage_before_range
6401 .map_for_prefix(&path, Some(prefix))
6402 .is_err(),
6403 "{prefix}"
6404 );
6405 }
6406 assert_eq!(
6407 garbage_before_range
6408 .map_for_prefix(&path, Some("refs/zz"))
6409 .unwrap()
6410 .len(),
6411 0
6412 );
6413
6414 let peeled_tag_before_range = view(format!(
6415 "{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
6416 ));
6417 let orphan_peel_before_range = view(format!(
6418 "{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
6419 ));
6420 assert!(orphan_peel_before_range
6421 .map_for_prefix(&path, Some("refs/tags"))
6422 .is_err());
6423 assert_eq!(
6424 peeled_tag_before_range
6425 .map_for_prefix(&path, Some("refs/tags"))
6426 .unwrap()
6427 .len(),
6428 1
6429 );
6430 }
6431
6432 fn lying_and_unsorted_views(path: &Path) -> (PackedRefsFileView, PackedRefsFileView) {
6434 let oid = "1111111111111111111111111111111111111111";
6435 let records = format!("{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n");
6436 let view_of = |header: &str| {
6437 PackedRefsFileView::from_bytes(
6438 path,
6439 format!("{header}{records}").into_bytes(),
6440 HashAlgo::Sha1,
6441 )
6442 .unwrap()
6443 };
6444 (
6445 view_of("# pack-refs with: peeled fully-peeled sorted\n"),
6446 view_of("# pack-refs with: peeled fully-peeled\n"),
6447 )
6448 }
6449
6450 fn sorted_names(map: HashMap<String, ObjectId>) -> Vec<String> {
6451 let mut names: Vec<String> = map.into_keys().collect();
6452 names.sort();
6453 names
6454 }
6455
6456 #[test]
6457 fn full_scans_of_a_lying_sorted_file_return_every_ref() {
6458 let dir = tempdir().unwrap();
6459 let path = store::paths::packed_refs_path(dir.path());
6460 let (lying, unsorted) = lying_and_unsorted_views(&path);
6461 assert!(lying.sorted);
6462 let every = ["refs/heads/a", "refs/heads/b", "refs/tags/t"];
6463 for view in [&lying, &unsorted] {
6464 for prefix in [None, Some("refs"), Some("refs/")] {
6465 assert_eq!(
6466 sorted_names(view.map_for_prefix(&path, prefix).unwrap()),
6467 every,
6468 "{prefix:?}"
6469 );
6470 }
6471 }
6472 fs::write(&path, lying.data.as_ref() as &[u8]).unwrap();
6473 let loaded = PackedRefs::load(dir.path()).unwrap();
6474 for name in every {
6475 assert!(loaded.get(name).is_some(), "{name}");
6476 }
6477 }
6478
6479 #[test]
6480 fn garbage_between_sorted_records_rejects_listing() {
6481 let dir = tempdir().unwrap();
6482 let git_dir = dir.path();
6483 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
6484 let oid = "1111111111111111111111111111111111111111";
6485 fs::write(
6486 store::paths::packed_refs_path(git_dir),
6487 format!(
6488 "# pack-refs with: sorted\n{oid} refs/heads/a\nthis is not a valid packed-refs line\n{oid} refs/heads/b\n"
6489 ),
6490 )
6491 .unwrap();
6492 assert!(matches!(
6493 list_refs(git_dir, "refs/"),
6494 Err(Error::PackedRefsUnexpectedLine { .. })
6495 ));
6496 }
6497
6498 #[test]
6499 fn for_each_record_streaming_matches_map_and_stops_early() {
6500 let dir = tempdir().unwrap();
6501 let path = store::paths::packed_refs_path(dir.path());
6502 let oid = "1111111111111111111111111111111111111111";
6503 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6504 let body = format!("{header}{oid} refs/heads/a\n{oid} refs/heads/b\n{oid} refs/tags/t\n");
6505 let view =
6506 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6507 let mut streamed = Vec::new();
6508 let _ = view
6509 .for_each_record(&path, Some("refs/heads"), None, |name, _, _| {
6510 streamed.push(name.to_owned());
6511 ControlFlow::Continue(())
6512 })
6513 .unwrap();
6514 assert_eq!(
6515 streamed,
6516 sorted_names(view.map_for_prefix(&path, Some("refs/heads")).unwrap())
6517 );
6518 let mut count = 0;
6519 assert!(view
6520 .for_each_record(&path, Some("refs/heads"), None, |_, _, _| {
6521 count += 1;
6522 ControlFlow::Break(())
6523 })
6524 .unwrap()
6525 .is_break());
6526 assert_eq!(count, 1);
6527 }
6528
6529 #[test]
6530 fn for_each_record_break_before_corrupt_tail_ok_while_full_scan_errors() {
6531 let dir = tempdir().unwrap();
6532 let path = store::paths::packed_refs_path(dir.path());
6533 let oid = "1111111111111111111111111111111111111111";
6534 let good_only = format!(
6535 "# pack-refs with: peeled fully-peeled sorted\n{oid} refs/heads/good\n{oid} refs/heads/good2\n"
6536 );
6537 let with_corrupt_tail = format!(
6538 "# pack-refs with: peeled fully-peeled sorted\n{oid} refs/heads/good\n{oid} refs/heads/good2\n{oid} refs/heads/bad..name\n"
6539 );
6540 let view =
6541 PackedRefsFileView::from_bytes(&path, good_only.into_bytes(), HashAlgo::Sha1).unwrap();
6542 assert!(view
6543 .for_each_record(&path, None, None, |_, _, _| ControlFlow::Break(()))
6544 .unwrap()
6545 .is_break());
6546 let view =
6547 PackedRefsFileView::from_bytes(&path, with_corrupt_tail.into_bytes(), HashAlgo::Sha1)
6548 .unwrap();
6549 assert!(view
6550 .for_each_record(&path, None, None, |_, _, _| ControlFlow::Continue(()))
6551 .is_err());
6552 }
6553
6554 fn collect_records(
6555 view: &PackedRefsFileView,
6556 path: &Path,
6557 keep: Option<PackedRecordFilter<'_>>,
6558 ) -> Result<Vec<(String, ObjectId, PackedPeel)>> {
6559 let mut out = Vec::new();
6560 view.for_each_record(path, None, keep, |name, oid, peel| {
6561 out.push((name.to_owned(), oid, peel));
6562 ControlFlow::Continue(())
6563 })?;
6564 Ok(out)
6565 }
6566
6567 #[test]
6568 fn filtered_record_walk_emits_the_unfiltered_walk_minus_rejected_records() {
6569 let dir = tempdir().unwrap();
6570 let path = store::paths::packed_refs_path(dir.path());
6571 let one = "1111111111111111111111111111111111111111";
6572 let two = "2222222222222222222222222222222222222222";
6573 let tag = "3333333333333333333333333333333333333333";
6574 let body = format!(
6575 "# pack-refs with: peeled fully-peeled sorted\n\
6576 {one} refs/heads/a\n{two} refs/heads/b\n{tag} refs/tags/v1\n^{one}\n{two} refs/tags/v2\n"
6577 );
6578 let view =
6579 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6580 let target: ObjectId = one.parse().unwrap();
6581 let keep = |_: &str, oid: &ObjectId, peel: PackedPeel| match peel {
6582 PackedPeel::Peeled(peeled) => peeled == target,
6583 PackedPeel::NotTag | PackedPeel::Unknown => *oid == target,
6584 };
6585
6586 let all = collect_records(&view, &path, None).unwrap();
6587 let expected: Vec<_> = all
6588 .into_iter()
6589 .filter(|(name, oid, peel)| keep(name, oid, *peel))
6590 .collect();
6591 let kept = collect_records(&view, &path, Some(&keep)).unwrap();
6592 assert_eq!(kept, expected);
6593 let names: Vec<&str> = kept.iter().map(|(name, _, _)| name.as_str()).collect();
6594 assert_eq!(names, ["refs/heads/a", "refs/tags/v1"]);
6595 assert_eq!(kept[1].2, PackedPeel::Peeled(target));
6596 }
6597
6598 #[test]
6599 fn filtered_record_walk_emits_nothing_before_a_corrupt_line() {
6600 let dir = tempdir().unwrap();
6601 let path = store::paths::packed_refs_path(dir.path());
6602 let oid = "1111111111111111111111111111111111111111";
6603 let body = format!(
6604 "# pack-refs with: peeled fully-peeled sorted\n\
6605 {oid} refs/heads/good\n{oid} refs/heads/good2\n{oid} refs/heads/bad..name\n"
6606 );
6607 let view =
6608 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6609 let keep_all = |_: &str, _: &ObjectId, _: PackedPeel| true;
6610 let mut emitted = 0;
6611 let result = view.for_each_record(&path, None, Some(&keep_all), |_, _, _| {
6612 emitted += 1;
6613 ControlFlow::Continue(())
6614 });
6615 assert!(matches!(
6616 result,
6617 Err(Error::PackedRefsUnexpectedLine { .. })
6618 ));
6619 assert_eq!(emitted, 0);
6620 }
6621
6622 #[test]
6623 fn filtered_record_walk_stops_when_the_visitor_breaks() {
6624 let dir = tempdir().unwrap();
6625 let path = store::paths::packed_refs_path(dir.path());
6626 let oid = "1111111111111111111111111111111111111111";
6627 let body = format!(
6628 "# pack-refs with: peeled fully-peeled sorted\n\
6629 {oid} refs/heads/a\n{oid} refs/heads/b\n{oid} refs/heads/c\n"
6630 );
6631 let view =
6632 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6633 let keep_all = |_: &str, _: &ObjectId, _: PackedPeel| true;
6634 let mut seen = Vec::new();
6635 let flow = view
6636 .for_each_record(&path, None, Some(&keep_all), |name, _, _| {
6637 seen.push(name.to_owned());
6638 if seen.len() == 2 {
6639 ControlFlow::Break(())
6640 } else {
6641 ControlFlow::Continue(())
6642 }
6643 })
6644 .unwrap();
6645 assert!(flow.is_break());
6646 assert_eq!(seen, ["refs/heads/a", "refs/heads/b"]);
6647 }
6648
6649 #[test]
6650 fn filtered_record_walk_of_a_lying_sorted_file_returns_every_kept_ref() {
6651 let dir = tempdir().unwrap();
6652 let path = store::paths::packed_refs_path(dir.path());
6653 let (lying, _) = lying_and_unsorted_views(&path);
6654 let keep_all = |_: &str, _: &ObjectId, _: PackedPeel| true;
6655 let names: Vec<String> = collect_records(&lying, &path, Some(&keep_all))
6656 .unwrap()
6657 .into_iter()
6658 .map(|(name, _, _)| name)
6659 .collect();
6660 assert_eq!(names, ["refs/heads/a", "refs/heads/b", "refs/tags/t"]);
6661 }
6662
6663 #[test]
6664 fn prefix_scan_over_an_out_of_order_range_falls_back_to_the_whole_file() {
6665 let dir = tempdir().unwrap();
6666 let path = store::paths::packed_refs_path(dir.path());
6667 let (lying, _) = lying_and_unsorted_views(&path);
6668 let heads = lying.map_for_prefix(&path, Some("refs/heads")).unwrap();
6669 assert_eq!(sorted_names(heads), ["refs/heads/a", "refs/heads/b"]);
6670
6671 let oid = "1111111111111111111111111111111111111111";
6673 let view_of = |names: &[&str]| {
6674 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
6675 for name in names {
6676 body.push_str(&format!("{oid} {name}\n"));
6677 }
6678 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6679 };
6680 let interleaved = view_of(&["refs/heads/a", "refs/tags/t", "refs/heads/b", "refs/tags/u"]);
6681 let (begin, end) = interleaved.prefix_region("refs/heads").unwrap();
6682 let (_, in_order) = interleaved
6683 .parse_region::<HashMap<_, _>>(&path, begin, end, Some("refs/heads"), None)
6684 .unwrap();
6685 assert!(!in_order);
6686 assert_eq!(
6687 sorted_names(
6688 interleaved
6689 .map_for_prefix(&path, Some("refs/heads"))
6690 .unwrap()
6691 ),
6692 ["refs/heads/a", "refs/heads/b"]
6693 );
6694
6695 let bad_neighbour = view_of(&["refs/heads/x", "refs/heads/a"]);
6698 assert_eq!(bad_neighbour.prefix_region("refs/heads/a"), None);
6699 assert_eq!(
6700 sorted_names(
6701 bad_neighbour
6702 .map_for_prefix(&path, Some("refs/heads/a"))
6703 .unwrap()
6704 ),
6705 ["refs/heads/a"]
6706 );
6707
6708 let corrupt = PackedRefsFileView::from_bytes(
6710 &path,
6711 format!(
6712 "# pack-refs with: peeled fully-peeled sorted\n\
6713 {oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/bad..name\n"
6714 )
6715 .into_bytes(),
6716 HashAlgo::Sha1,
6717 )
6718 .unwrap();
6719 assert!(matches!(
6720 corrupt.map_for_prefix(&path, Some("refs/heads")),
6721 Err(Error::PackedRefsUnexpectedLine { .. })
6722 ));
6723 }
6724
6725 #[test]
6726 fn full_map_keeps_every_name_and_the_last_duplicate_value() {
6727 let dir = tempdir().unwrap();
6728 let path = store::paths::packed_refs_path(dir.path());
6729 let first = "1111111111111111111111111111111111111111";
6730 let last = "2222222222222222222222222222222222222222";
6731 for header in ["# pack-refs with: sorted\n", "# pack-refs with: peeled\n"] {
6732 for names in [
6733 ["refs/heads/a", "refs/heads/b", "refs/heads/b"],
6734 ["refs/heads/b", "refs/heads/a", "refs/heads/b"],
6735 ] {
6736 let body = format!(
6737 "{header}{first} {}\n{first} {}\n{last} {}\n",
6738 names[0], names[1], names[2]
6739 );
6740 let view = PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1)
6741 .unwrap();
6742 let full = view.full_map(&path).unwrap();
6743 assert_eq!(full.len(), 2);
6744 assert_eq!(full["refs/heads/b"], last.parse().unwrap());
6745 assert_eq!(
6746 full.into_iter().collect::<HashMap<_, _>>(),
6747 view.map_for_prefix(&path, None).unwrap()
6748 );
6749 }
6750 }
6751 }
6752
6753 #[test]
6754 fn prefix_scan_validates_both_neighbouring_records_and_their_peels() {
6755 let dir = tempdir().unwrap();
6756 let path = store::paths::packed_refs_path(dir.path());
6757 let oid = "1111111111111111111111111111111111111111";
6758 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6759 for neighbour in ["refs/heads/a", "refs/tags/z"] {
6760 for corrupt in [
6761 format!("{oid} {neighbour}..bad\n"),
6762 format!("{} {neighbour}\n", "g".repeat(40)),
6763 format!("{oid} {neighbour}\n^{}\n", "g".repeat(40)),
6764 ] {
6765 let records = if neighbour.starts_with("refs/heads/") {
6766 format!("{corrupt}{oid} refs/heads/m\n{oid} refs/tags/z\n")
6767 } else {
6768 format!("{oid} refs/heads/a\n{oid} refs/heads/m\n{corrupt}")
6769 };
6770 let view = PackedRefsFileView::from_bytes(
6771 &path,
6772 format!("{header}{records}").into_bytes(),
6773 HashAlgo::Sha1,
6774 )
6775 .unwrap();
6776 assert!(
6777 matches!(
6778 view.map_for_prefix(&path, Some("refs/heads/m")),
6779 Err(Error::PackedRefsUnexpectedLine { .. })
6780 ),
6781 "{records}"
6782 );
6783 }
6784 }
6785 }
6786
6787 #[test]
6790 fn point_lookup_in_a_lying_sorted_file_behaves_like_git() {
6791 let dir = tempdir().unwrap();
6792 let path = store::paths::packed_refs_path(dir.path());
6793 let (lying, unsorted) = lying_and_unsorted_views(&path);
6794 assert!(lying.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
6796 assert_eq!(lying.lookup_oid(&path, "refs/heads/b").unwrap(), None);
6797 assert!(lying.lookup_oid(&path, "refs/tags/t").unwrap().is_some());
6798
6799 assert!(!unsorted.sorted);
6800 for name in ["refs/heads/a", "refs/heads/b", "refs/tags/t"] {
6801 assert!(
6802 unsorted.lookup_oid(&path, name).unwrap().is_some(),
6803 "{name}"
6804 );
6805 }
6806 }
6807
6808 #[test]
6809 fn records_are_only_record_shaped_lines() {
6810 let dir = tempdir().unwrap();
6811 let path = store::paths::packed_refs_path(dir.path());
6812 let view = |data: &[u8]| {
6813 PackedRefsFileView::from_bytes(&path, data.to_vec(), HashAlgo::Sha1).unwrap()
6814 };
6815 let hex = "1".repeat(40);
6816 let data = format!("# header\n{hex} x\n^peel\n\n{hex} y\n#c\nshort\n{hex}z\n{hex} z\n");
6817 let starts: Vec<usize> = view(data.as_bytes()).records().collect();
6818 assert_eq!(starts, [9, 59, 153]);
6819 assert_eq!(view(b"").records().count(), 0);
6820 let out_of_order = format!("{hex} b\n{hex} a\n");
6821 assert_eq!(
6822 view(out_of_order.as_bytes()).records().collect::<Vec<_>>(),
6823 [0, 43]
6824 );
6825 }
6826
6827 #[test]
6828 fn parse_rejects_orphan_peel_and_double_space() {
6829 let dir = tempdir().unwrap();
6830 let path = store::paths::packed_refs_path(dir.path());
6831 fs::write(
6832 &path,
6833 "# pack-refs with: peeled\n\
6834 ^0000000000000000000000000000000000000000\n\
6835 0000000000000000000000000000000000000000 refs/heads/main\n",
6836 )
6837 .unwrap();
6838 let data = fs::read(&path).unwrap();
6839 assert!(matches!(
6840 parse_packed_refs_to_map(&path, &data),
6841 Err(Error::PackedRefsUnexpectedLine { .. })
6842 ));
6843
6844 fs::write(
6845 &path,
6846 "# pack-refs with: peeled\n\
6847 0000000000000000000000000000000000000000 refs/heads/main\n",
6848 )
6849 .unwrap();
6850 let data = fs::read(&path).unwrap();
6851 assert!(matches!(
6852 parse_packed_refs_to_map(&path, &data),
6853 Err(Error::PackedRefsUnexpectedLine { .. })
6854 ));
6855 }
6856}
6857
6858#[cfg(test)]
6859mod pack_refs_tests {
6860 use super::*;
6861 use crate::objects::{serialize_commit, CommitData, ObjectKind};
6862 use crate::repo::{init_repository, Repository};
6863 use std::fs;
6864 use std::sync::Arc;
6865
6866 fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
6867 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
6868 let raw = serialize_commit(&CommitData {
6869 tree,
6870 parents,
6871 author: "T <t@example.com> 1 +0000".into(),
6872 committer: "T <t@example.com> 1 +0000".into(),
6873 author_raw: Vec::new(),
6874 committer_raw: Vec::new(),
6875 encoding: None,
6876 message: "m\n".into(),
6877 raw_message: None,
6878 extra_headers: Vec::new(),
6879 });
6880 repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
6881 }
6882
6883 #[test]
6884 fn pack_refs_skips_concurrent_loose_update() {
6885 let dir = tempfile::tempdir().expect("tempdir");
6886 let repo = init_repository(
6887 dir.path(),
6888 false,
6889 "main",
6890 None,
6891 crate::RefStorageFormat::Files,
6892 )
6893 .expect("init");
6894 let git_dir = repo.git_dir().to_path_buf();
6895 let c1 = write_commit(&repo, vec![]);
6896 let c2 = write_commit(&repo, vec![c1]);
6897 write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
6898
6899 let git_dir_hook = git_dir.clone();
6900 let c2 = Arc::new(c2);
6901 let c2_hook = Arc::clone(&c2);
6902 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
6903 write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
6904 })));
6905
6906 let (_path, packed, pruned) =
6907 pack_refs_files(&git_dir, repo.odb(), None, true, false).expect("pack");
6908 pack_refs_test_hooks::set_before_prune(None);
6909 assert!(packed >= 1);
6910 assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
6911
6912 let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
6913 assert_eq!(resolved, *c2);
6914 let packed_body =
6915 fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
6916 assert!(
6917 !packed_body.contains("refs/heads/race"),
6918 "packed entry for concurrently updated ref must be removed"
6919 );
6920 }
6921
6922 #[test]
6923 fn clone_packing_retains_symbolic_refs_and_concurrent_tag_updates() {
6924 let dir = tempfile::tempdir().expect("tempdir");
6925 let repo = init_repository(
6926 dir.path(),
6927 false,
6928 "main",
6929 None,
6930 crate::RefStorageFormat::Files,
6931 )
6932 .expect("init");
6933 let git_dir = repo.git_dir().to_path_buf();
6934 let first = write_commit(&repo, vec![]);
6935 let second = write_commit(&repo, vec![first]);
6936 write_ref(&git_dir, "refs/tags/race", &first).expect("tag");
6937 write_ref(&git_dir, "refs/heads/main", &first).expect("branch");
6938 write_ref(&git_dir, "refs/remotes/origin/main", &first).expect("remote");
6939 write_symbolic_ref(
6940 &git_dir,
6941 "refs/remotes/origin/HEAD",
6942 "refs/remotes/origin/main",
6943 )
6944 .expect("symbolic");
6945 let tag = repo
6946 .odb()
6947 .write(
6948 ObjectKind::Tag,
6949 format!("object {first}\ntype commit\n\nremote tag\n").as_bytes(),
6950 )
6951 .expect("tag object");
6952 write_ref(&git_dir, "refs/remotes/origin/tag", &tag).expect("tagged remote");
6953 let hook_dir = git_dir.clone();
6954 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
6955 write_ref(&hook_dir, "refs/tags/race", &second).expect("concurrent update");
6956 })));
6957 pack_remote_tracking_refs_for_clone(&git_dir, "origin").expect("pack");
6958 assert_eq!(
6959 resolve_ref(&git_dir, "refs/tags/race").expect("resolve"),
6960 second
6961 );
6962 assert!(git_dir.join("refs/heads/main").exists());
6963 assert!(git_dir.join("refs/remotes/origin/HEAD").exists());
6964 assert!(!git_dir.join("refs/remotes/origin/main").exists());
6965 let body = fs::read_to_string(store::paths::packed_refs_path(&git_dir)).expect("packed");
6966 assert!(!body.contains("refs/tags/race"));
6967 assert!(!body.contains("refs/remotes/origin/HEAD"));
6968 assert!(
6969 body.contains(&format!("{tag} refs/remotes/origin/tag\n^{first}\n")),
6970 "{body}"
6971 );
6972 }
6973
6974 #[test]
6975 fn clone_packing_routes_reftable_without_creating_packed_refs() {
6976 let dir = tempfile::tempdir().expect("tempdir");
6977 let repo = init_repository(
6978 dir.path(),
6979 false,
6980 "main",
6981 None,
6982 crate::RefStorageFormat::Reftable,
6983 )
6984 .expect("init");
6985 let commit = write_commit(&repo, vec![]);
6986 write_ref(repo.git_dir(), "refs/tags/tip", &commit).expect("tag");
6987 pack_remote_tracking_refs_for_clone(repo.git_dir(), "origin").expect("pack");
6988 assert_eq!(
6989 resolve_ref(repo.git_dir(), "refs/tags/tip").expect("resolve"),
6990 commit
6991 );
6992 assert!(!store::paths::packed_refs_path(repo.git_dir()).exists());
6993 }
6994
6995 #[test]
6996 fn pack_refs_nested_tag_peel_line_points_at_commit() {
6997 let dir = tempfile::tempdir().expect("tempdir");
6998 let repo = init_repository(
6999 dir.path(),
7000 false,
7001 "main",
7002 None,
7003 crate::RefStorageFormat::Files,
7004 )
7005 .expect("init");
7006 let commit = write_commit(&repo, vec![]);
7007 let inner_tag_body = format!(
7008 "object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
7009 commit.to_hex()
7010 );
7011 let inner = repo
7012 .odb()
7013 .write(ObjectKind::Tag, inner_tag_body.as_bytes())
7014 .expect("inner tag");
7015 let outer_tag_body = format!(
7016 "object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
7017 inner.to_hex()
7018 );
7019 let outer = repo
7020 .odb()
7021 .write(ObjectKind::Tag, outer_tag_body.as_bytes())
7022 .expect("outer tag");
7023 write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
7024
7025 pack_refs_files(repo.git_dir(), repo.odb(), None, false, false).expect("pack");
7026 let body =
7027 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
7028 let mut lines = body.lines();
7029 let tag_line = lines
7030 .find(|l| l.contains("refs/tags/outer"))
7031 .expect("tag line");
7032 assert!(tag_line.starts_with(&outer.to_hex()));
7033 let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
7034 assert!(
7035 peel_line.contains(&commit.to_hex()),
7036 "peel must reach commit through nested tags, got {peel_line}"
7037 );
7038 }
7039
7040 #[test]
7041 fn pack_refs_tree_target_tag_has_peel_line() {
7042 let dir = tempfile::tempdir().expect("tempdir");
7043 let repo = init_repository(
7044 dir.path(),
7045 false,
7046 "main",
7047 None,
7048 crate::RefStorageFormat::Files,
7049 )
7050 .expect("init");
7051 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
7052 let tag_body = format!(
7053 "object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
7054 tree.to_hex()
7055 );
7056 let tag = repo
7057 .odb()
7058 .write(ObjectKind::Tag, tag_body.as_bytes())
7059 .expect("tag");
7060 write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
7061
7062 pack_refs_files(repo.git_dir(), repo.odb(), None, true, false).expect("pack");
7063 let body =
7064 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
7065 assert!(body.contains("refs/tags/tree-tag"));
7066 assert!(
7067 body.contains(&format!("^{}", tree.to_hex())),
7068 "expected peel to tree: {body}"
7069 );
7070 }
7071
7072 fn packed_entry<'a>(name: &'a str, hex: &str, peel: Option<&str>) -> PackedEntry<'a> {
7073 PackedEntry {
7074 name,
7075 oid: ObjectId::from_hex(hex).expect("oid"),
7076 peel: peel.map(|p| ObjectId::from_hex(p).expect("peel")),
7077 }
7078 }
7079
7080 const OID_A: &str = "1111111111111111111111111111111111111111";
7081 const OID_B: &str = "2222222222222222222222222222222222222222";
7082 const OID_C: &str = "3333333333333333333333333333333333333333";
7083
7084 #[test]
7085 fn merged_packed_refs_body_interleaves_loose_and_replaces_by_name() {
7086 let existing = vec![
7087 packed_entry("refs/tags/b", OID_A, Some(OID_B)),
7088 packed_entry("refs/tags/d", OID_A, None),
7089 ];
7090 let loose = vec![
7091 (
7092 "refs/tags/a".to_owned(),
7093 ObjectId::from_hex(OID_C).unwrap(),
7094 None,
7095 ),
7096 (
7097 "refs/tags/b".to_owned(),
7098 ObjectId::from_hex(OID_C).unwrap(),
7099 None,
7100 ),
7101 (
7102 "refs/tags/c".to_owned(),
7103 ObjectId::from_hex(OID_B).unwrap(),
7104 Some(ObjectId::from_hex(OID_A).unwrap()),
7105 ),
7106 (
7107 "refs/tags/e".to_owned(),
7108 ObjectId::from_hex(OID_A).unwrap(),
7109 None,
7110 ),
7111 ];
7112 let body = merged_packed_refs_body(existing, &loose);
7113 assert_eq!(
7114 body,
7115 format!(
7116 "{PACKED_REFS_HEADER}\
7117 {OID_C} refs/tags/a\n\
7118 {OID_C} refs/tags/b\n\
7119 {OID_B} refs/tags/c\n^{OID_A}\n\
7120 {OID_A} refs/tags/d\n\
7121 {OID_A} refs/tags/e\n"
7122 ),
7123 "a loose ref replaces the packed record and its old peel line"
7124 );
7125 }
7126
7127 #[test]
7128 fn merged_packed_refs_body_sorts_unsorted_input_and_keeps_last_duplicate() {
7129 let existing = vec![
7130 packed_entry("refs/tags/z", OID_A, None),
7131 packed_entry("refs/tags/m", OID_A, Some(OID_C)),
7132 packed_entry("refs/tags/m", OID_B, None),
7133 ];
7134 let loose = vec![(
7135 "refs/tags/n".to_owned(),
7136 ObjectId::from_hex(OID_C).unwrap(),
7137 None,
7138 )];
7139 let body = merged_packed_refs_body(existing, &loose);
7140 assert_eq!(
7141 body,
7142 format!(
7143 "{PACKED_REFS_HEADER}\
7144 {OID_B} refs/tags/m\n\
7145 {OID_C} refs/tags/n\n\
7146 {OID_A} refs/tags/z\n"
7147 )
7148 );
7149 }
7150
7151 #[test]
7152 fn packed_entries_with_peel_attaches_peels_and_rejects_orphans() {
7153 let path = Path::new("packed-refs");
7154 let upper = OID_A.to_ascii_uppercase();
7155 let data = format!(
7156 "# pack-refs with: peeled fully-peeled sorted \n\
7157 {upper} refs/tags/a\n^{OID_B}\n{OID_C} refs/tags/b\n"
7158 );
7159 let entries = packed_entries_with_peel(path, data.as_bytes()).expect("parse");
7160 let got: Vec<_> = entries.iter().map(|e| (e.name, e.oid, e.peel)).collect();
7161 assert_eq!(
7162 got,
7163 vec![
7164 (
7165 "refs/tags/a",
7166 ObjectId::from_hex(OID_A).unwrap(),
7167 Some(ObjectId::from_hex(OID_B).unwrap())
7168 ),
7169 ("refs/tags/b", ObjectId::from_hex(OID_C).unwrap(), None),
7170 ]
7171 );
7172 let body = merged_packed_refs_body(entries, &[]);
7173 assert!(
7174 body.contains(&format!("{OID_A} refs/tags/a\n")),
7175 "ids are rewritten in lowercase: {body}"
7176 );
7177
7178 let orphan = format!("^{OID_B}\n{OID_C} refs/tags/b\n");
7179 assert!(matches!(
7180 packed_entries_with_peel(path, orphan.as_bytes()),
7181 Err(Error::PackedRefsUnexpectedLine { .. })
7182 ));
7183 }
7184
7185 fn files_repo() -> (tempfile::TempDir, crate::repo::Repository) {
7186 let dir = tempfile::tempdir().expect("tempdir");
7187 let repo = init_repository(
7188 dir.path(),
7189 false,
7190 "main",
7191 None,
7192 crate::RefStorageFormat::Files,
7193 )
7194 .expect("init");
7195 (dir, repo)
7196 }
7197
7198 #[test]
7199 fn pack_refs_merges_loose_tags_into_existing_packed_refs() {
7200 let (_dir, repo) = files_repo();
7201 let git_dir = repo.git_dir().to_path_buf();
7202 let c1 = write_commit(&repo, vec![]);
7203 let c2 = write_commit(&repo, vec![c1]);
7204 let tag = repo
7205 .odb()
7206 .write(
7207 ObjectKind::Tag,
7208 format!("object {c1}\ntype commit\ntag v2\n\nv2\n").as_bytes(),
7209 )
7210 .expect("tag object");
7211 fs::write(
7212 store::paths::packed_refs_path(&git_dir),
7213 format!(
7214 "{PACKED_REFS_HEADER}{c1} refs/heads/old\n{c1} refs/tags/v1\n{c1} refs/tags/v3\n"
7215 ),
7216 )
7217 .expect("seed packed-refs");
7218 write_ref(&git_dir, "refs/tags/v1", &c2).expect("replace v1");
7219 write_ref(&git_dir, "refs/tags/v2", &tag).expect("v2");
7220 write_ref(&git_dir, "refs/heads/main", &c2).expect("branch");
7221
7222 let (path, packed, pruned) =
7223 pack_refs_files(&git_dir, repo.odb(), None, false, false).expect("pack");
7224 assert_eq!((packed, pruned), (2, 2));
7225 assert_eq!(
7226 fs::read_to_string(path).expect("packed-refs"),
7227 format!(
7228 "{PACKED_REFS_HEADER}{c1} refs/heads/old\n{c2} refs/tags/v1\n\
7229 {tag} refs/tags/v2\n^{c1}\n{c1} refs/tags/v3\n"
7230 )
7231 );
7232 assert!(!git_dir.join("refs/tags/v1").exists());
7233 assert!(!git_dir.join("refs/tags/v2").exists());
7234 assert!(git_dir.join("refs/heads/main").exists(), "tags-only pack");
7235 assert_eq!(resolve_ref(&git_dir, "refs/tags/v1").expect("v1"), c2);
7236 }
7237
7238 #[test]
7239 fn pack_refs_dry_run_and_noop_leave_packed_refs_untouched() {
7240 let (_dir, repo) = files_repo();
7241 let git_dir = repo.git_dir().to_path_buf();
7242 let c1 = write_commit(&repo, vec![]);
7243 let packed_path = store::paths::packed_refs_path(&git_dir);
7244 let seed = format!("# hand written\n{c1} refs/tags/v1\n");
7245 fs::write(&packed_path, &seed).expect("seed");
7246
7247 assert_eq!(
7248 pack_refs_files(&git_dir, repo.odb(), None, false, false)
7249 .expect("noop")
7250 .1,
7251 0
7252 );
7253 write_ref(&git_dir, "refs/tags/v2", &c1).expect("v2");
7254 let (_, packed, pruned) =
7255 pack_refs_files(&git_dir, repo.odb(), None, false, true).expect("dry run");
7256 assert_eq!((packed, pruned), (1, 1));
7257 assert_eq!(fs::read_to_string(&packed_path).expect("read"), seed);
7258 assert!(git_dir.join("refs/tags/v2").exists());
7259
7260 fs::write(&packed_path, format!("^{c1}\n")).expect("corrupt");
7261 assert!(matches!(
7262 pack_refs_files(&git_dir, repo.odb(), None, false, true),
7263 Err(Error::PackedRefsUnexpectedLine { .. })
7264 ));
7265 }
7266
7267 #[test]
7268 fn pack_refs_reports_held_packed_refs_lock_and_keeps_loose_refs() {
7269 let (_dir, repo) = files_repo();
7270 let git_dir = repo.git_dir().to_path_buf();
7271 let c1 = write_commit(&repo, vec![]);
7272 write_ref(&git_dir, "refs/tags/v1", &c1).expect("v1");
7273 fs::write(
7274 git_dir.join("config"),
7275 "[core]\n\trepositoryformatversion = 0\n\tpackedRefsTimeout = 0\n",
7276 )
7277 .expect("config");
7278 let lock = git_dir.join("packed-refs.lock");
7279 fs::write(&lock, b"").expect("foreign lock");
7280 let config = crate::config::ConfigSet::load(
7281 &crate::environment::Environment::empty(),
7282 Some(&git_dir),
7283 true,
7284 )
7285 .expect("config");
7286
7287 let err = pack_refs_files(&git_dir, repo.odb(), Some(&config), false, false)
7288 .expect_err("lock held");
7289 assert!(
7290 matches!(
7291 &err,
7292 Error::RefLock(RefLockError::PackedRefsLockHeld { lock_path })
7293 if lock_path.ends_with("packed-refs.lock")
7294 ),
7295 "{err:?}"
7296 );
7297 assert!(lock.exists(), "a foreign lock is never removed");
7298 assert!(git_dir.join("refs/tags/v1").exists());
7299 assert!(!store::paths::packed_refs_path(&git_dir).exists());
7300 }
7301
7302 #[test]
7303 fn write_packed_refs_file_survives_publish_failure() {
7304 let dir = tempfile::tempdir().expect("tempdir");
7305 let git_dir = dir.path();
7306 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
7307 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7308 fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
7309 fs::create_dir_all(store::paths::packed_refs_path(git_dir))
7310 .expect("block packed-refs file");
7311 let err = write_packed_refs_file(
7312 git_dir,
7313 &[PackedRefRecord {
7314 refname: "refs/heads/keep".into(),
7315 oid,
7316 peeled: None,
7317 }],
7318 )
7319 .expect_err("publish should fail");
7320 assert!(matches!(err, Error::Io(_)));
7321 assert!(git_dir.join("refs/heads/keep").is_file());
7322 }
7323
7324 #[test]
7325 fn write_packed_refs_file_replaces_loose_and_sorts() {
7326 let dir = tempfile::tempdir().expect("tempdir");
7327 let git_dir = dir.path();
7328 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
7329 let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
7330 let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
7331 fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
7332 write_packed_refs_file(
7333 git_dir,
7334 &[
7335 PackedRefRecord {
7336 refname: "refs/heads/z-last".into(),
7337 oid: oid_b,
7338 peeled: None,
7339 },
7340 PackedRefRecord {
7341 refname: "refs/heads/a-first".into(),
7342 oid: oid_a,
7343 peeled: None,
7344 },
7345 ],
7346 )
7347 .expect("write packed-refs");
7348 assert!(!git_dir.join("refs/heads/a-first").exists());
7349 let body =
7350 fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
7351 assert!(body.contains("refs/heads/a-first"));
7352 assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
7353 }
7354
7355 #[test]
7356 fn write_packed_refs_file_keeps_refs_dir_for_top_level_ref() {
7357 let dir = tempfile::tempdir().expect("tempdir");
7358 let git_dir = dir.path();
7359 fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
7360 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7361 fs::write(git_dir.join("refs/stash"), format!("{oid}\n")).expect("loose stash");
7362 write_packed_refs_file(
7363 git_dir,
7364 &[PackedRefRecord {
7365 refname: "refs/stash".into(),
7366 oid,
7367 peeled: None,
7368 }],
7369 )
7370 .expect("write packed-refs");
7371 assert!(!git_dir.join("refs/stash").exists());
7372 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
7373 assert!(git_dir.join("refs/tags").is_dir());
7374 }
7375
7376 #[test]
7377 fn write_packed_refs_file_prunes_emptied_ref_dirs_below_refs() {
7378 let dir = tempfile::tempdir().expect("tempdir");
7379 let git_dir = dir.path();
7380 fs::create_dir_all(git_dir.join("refs/heads/topic/empty")).expect("refs/heads");
7381 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7382 fs::write(git_dir.join("refs/heads/topic/a"), format!("{oid}\n")).expect("loose ref");
7383 write_packed_refs_file(
7384 git_dir,
7385 &[PackedRefRecord {
7386 refname: "refs/heads/topic/a".into(),
7387 oid,
7388 peeled: None,
7389 }],
7390 )
7391 .expect("write packed-refs");
7392 assert!(!git_dir.join("refs/heads/topic/a").exists());
7393 assert!(!git_dir.join("refs/heads/topic").exists());
7394 assert!(git_dir.join("refs/heads").is_dir(), "refs/heads/ kept");
7395 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
7396 }
7397
7398 #[test]
7399 fn pack_refs_files_keeps_refs_dir_after_packing_stash() {
7400 let dir = tempfile::tempdir().expect("tempdir");
7401 let repo = init_repository(
7402 dir.path(),
7403 false,
7404 "main",
7405 None,
7406 crate::RefStorageFormat::Files,
7407 )
7408 .expect("init");
7409 let blob = repo.odb().write(ObjectKind::Blob, b"x").expect("blob");
7410 write_ref(repo.git_dir(), "refs/stash", &blob).expect("stash");
7411 let refs = repo.git_dir().join("refs");
7412 pack_refs_files(repo.git_dir(), repo.odb(), None, true, false).expect("pack");
7413 assert!(!refs.join("stash").exists());
7414 assert!(refs.is_dir(), "refs/ must remain");
7415 }
7416}
7417
7418#[cfg(test)]
7419mod packed_ref_peels_tests {
7420 use super::*;
7421
7422 fn oid(n: u8) -> ObjectId {
7423 ObjectId::from_bytes(&[n; 20]).expect("oid")
7424 }
7425
7426 fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
7427 let dir = tempfile::tempdir().expect("tempdir");
7428 fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
7429 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7430 (dir, peels)
7431 }
7432
7433 fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
7434 let mut out = String::from(header);
7435 for (name, oid, peel) in records {
7436 out.push_str(&format!("{oid} {name}\n"));
7437 if let Some(peel) = peel {
7438 out.push_str(&format!("^{peel}\n"));
7439 }
7440 }
7441 out
7442 }
7443
7444 #[test]
7445 fn fully_peeled_file_answers_for_every_ref() {
7446 for header in [
7447 "# pack-refs with: peeled fully-peeled sorted \n",
7448 "# pack-refs with: peeled fully-peeled \n",
7449 ] {
7450 let records = [
7451 ("refs/heads/main", oid(1), None),
7452 ("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
7453 ("refs/tags/v1", oid(4), Some(oid(1))),
7454 ("refs/tags/light", oid(1), None),
7455 ];
7456 let mut ordered = records;
7458 if header.contains(" sorted") {
7459 ordered.sort_by_key(|(name, _, _)| *name);
7460 } else {
7461 ordered.reverse();
7462 }
7463 let (_dir, peels) = peels_for(&body(header, &ordered));
7464 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
7465 assert_eq!(
7466 peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
7467 Some(Some(oid(3)))
7468 );
7469 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
7470 assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
7471 assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
7473 assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
7474 }
7475 }
7476
7477 #[test]
7478 fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
7479 let records = [
7480 ("refs/heads/main", oid(1), None),
7481 ("refs/tags/v1", oid(4), Some(oid(1))),
7482 ];
7483 let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
7484 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7485 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
7486
7487 for header in [
7488 "",
7489 "# pack-refs with: sorted\n",
7490 "# some comment\n",
7491 "#pack-refs with: peeled fully-peeled\n",
7492 "# pack-refs with: peeled fully-peeled\n",
7493 ] {
7494 let (_dir, peels) = peels_for(&body(header, &records));
7495 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
7496 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
7497 }
7498 }
7499
7500 #[test]
7501 fn missing_file_and_other_backends_know_nothing() {
7502 let dir = tempfile::tempdir().expect("tempdir");
7503 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7504 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7505
7506 let records = [("refs/heads/main", oid(1), None)];
7507 fs::write(
7508 store::paths::packed_refs_path(dir.path()),
7509 body("# pack-refs with: peeled fully-peeled sorted\n", &records),
7510 )
7511 .expect("packed-refs");
7512 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
7513 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7514 }
7515
7516 #[test]
7517 fn lookups_in_any_order_agree_with_a_fresh_snapshot() {
7518 let records = [
7519 ("refs/heads/main", oid(1), None),
7520 ("refs/tags/a", oid(2), Some(oid(1))),
7521 ("refs/tags/b", oid(1), None),
7522 ("refs/tags/c", oid(3), Some(oid(4))),
7523 ("refs/tags/d", oid(5), None),
7524 ];
7525 let text = body("# pack-refs with: peeled fully-peeled sorted \n", &records);
7526 let (_dir, peels) = peels_for(&text);
7527 let fresh = |name: &str, value: &ObjectId| peels_for(&text).1.peeled(name, value);
7528 let queries = [
7529 ("refs/tags/a", oid(2)),
7530 ("refs/tags/b", oid(1)),
7531 ("refs/tags/b", oid(1)),
7532 ("refs/tags/bb", oid(1)),
7533 ("refs/tags/c", oid(3)),
7534 ("refs/tags/d", oid(9)),
7535 ("refs/tags/d", oid(5)),
7536 ("refs/heads/main", oid(1)),
7537 ("refs/tags/c", oid(3)),
7538 ("refs/tags/a", oid(2)),
7539 ("refs/tags/zzz", oid(1)),
7540 ("refs/tags/a", oid(2)),
7541 ("refs/tags/b", oid(1)),
7542 ];
7543 for (name, value) in queries {
7544 assert_eq!(
7545 peels.peeled(name, &value),
7546 fresh(name, &value),
7547 "{name} {value}"
7548 );
7549 }
7550 assert_eq!(peels.peeled("refs/tags/c", &oid(3)), Some(Some(oid(4))));
7551 assert_eq!(peels.peeled("refs/tags/d", &oid(5)), Some(None));
7552 }
7553
7554 #[test]
7555 fn store_snapshot_reuses_the_files_store_view_and_follows_rewrites() {
7556 let dir = tempfile::tempdir().expect("tempdir");
7557 let path = store::paths::packed_refs_path(dir.path());
7558 let header = "# pack-refs with: peeled fully-peeled sorted \n";
7559 let records = [
7560 ("refs/heads/main", oid(1), None),
7561 ("refs/tags/v1", oid(2), Some(oid(1))),
7562 ("refs/tags/v2", oid(3), None),
7563 ];
7564 fs::write(&path, body(header, &records)).expect("packed-refs");
7565 let files = store::FilesRefStore::from_git_dir(dir.path()).expect("files store");
7566
7567 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7568 let loaded = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7569 for (name, value, _) in records {
7570 assert_eq!(
7571 peels.peeled(name, &value),
7572 loaded.peeled(name, &value),
7573 "{name}"
7574 );
7575 }
7576 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), Some(Some(oid(1))));
7577 let cached = files
7578 .packed_refs_view()
7579 .expect("view")
7580 .expect("packed-refs");
7581 let shared = peels.view.as_ref().expect("snapshot view");
7582 assert!(std::sync::Arc::ptr_eq(&cached.data, &shared.data));
7583
7584 let rewritten = [
7585 ("refs/heads/main", oid(1), None),
7586 ("refs/tags/v1", oid(4), Some(oid(5))),
7587 ("refs/tags/v2", oid(3), None),
7588 ("refs/tags/v3", oid(6), Some(oid(1))),
7589 ];
7590 fs::write(&path, body(header, &rewritten)).expect("rewrite packed-refs");
7591 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7592 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), None);
7593 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(5))));
7594 assert_eq!(peels.peeled("refs/tags/v3", &oid(6)), Some(Some(oid(1))));
7595
7596 let memory = store::MemoryRefStore::new();
7597 let fallback = PackedRefPeels::for_store(&memory, dir.path()).expect("fallback");
7598 let direct = PackedRefPeels::load(dir.path(), memory.format()).expect("load");
7599 for (name, value, _) in rewritten {
7600 assert_eq!(
7601 fallback.peeled(name, &value),
7602 direct.peeled(name, &value),
7603 "{name}"
7604 );
7605 }
7606 }
7607
7608 fn raw_view(data: &[u8], hex_len: usize) -> PackedRefsFileView {
7610 PackedRefsFileView {
7611 data: crate::pack_map::PackData::from_owned(data.to_vec()),
7612 sorted: true,
7613 hex_len,
7614 body_start: 0,
7615 peel_trait: PackedPeelTrait::None,
7616 }
7617 }
7618
7619 fn naive_record_starts(data: &[u8], hex_len: usize) -> Vec<usize> {
7621 let mut out = Vec::new();
7622 let mut start = 0;
7623 for line in data.split(|&b| b == b'\n') {
7624 if start >= data.len() {
7625 break;
7626 }
7627 if packed_line_is_record(line, hex_len) {
7628 out.push(start);
7629 }
7630 start += line.len() + 1;
7631 }
7632 out
7633 }
7634
7635 fn assert_record_walks_match(data: &[u8], hex_len: usize) {
7636 let view = raw_view(data, hex_len);
7637 let want = naive_record_starts(data, hex_len);
7638 assert_eq!(
7639 view.records().collect::<Vec<_>>(),
7640 want,
7641 "{:?} hex_len={hex_len}",
7642 String::from_utf8_lossy(data)
7643 );
7644 let mut line_starts: Vec<usize> = memchr::memchr_iter(b'\n', data)
7645 .map(|nl| nl + 1)
7646 .filter(|&s| s < data.len())
7647 .collect();
7648 line_starts.insert(0, 0);
7649 if data.ends_with(b"\n") {
7651 line_starts.push(data.len());
7652 }
7653 for before in line_starts {
7654 let prev = want.iter().copied().filter(|&s| s < before).last();
7655 assert_eq!(view.prev_record(before), prev, "{data:?} before={before}");
7656 }
7657 }
7658
7659 fn xorshift(state: &mut u64) -> u64 {
7660 *state ^= *state << 13;
7661 *state ^= *state >> 7;
7662 *state ^= *state << 17;
7663 *state
7664 }
7665
7666 #[test]
7667 fn record_walks_match_a_line_by_line_scan() {
7668 let hex = oid(1).to_hex();
7669 let inputs = [
7670 String::new(),
7671 "\n".to_owned(),
7672 format!("# pack-refs with: peeled \n{hex} refs/heads/a\n^{hex}\n{hex} refs/tags/b\n"),
7673 format!("{hex} refs/heads/z\n{hex} refs/heads/a\n"),
7674 format!("{hex} refs/heads/a\n\n{hex} refs/heads/b"),
7675 format!("{hex}\n{hex} \n{hex} x\nshort\n#{hex} refs/heads/c\n"),
7676 format!("{hex} refs/tags/a\n^{hex}\n^{hex}\n{hex} refs/tags/b\n^{hex}"),
7677 format!("^ab\n{}\n{hex} refs/heads/a\n", "x".repeat(37)),
7678 format!("^\n{hex} refs/heads/a\n^{hex}x\n{hex} refs/heads/b\n"),
7679 format!("^{hex}\n"),
7680 format!("^{}", &hex[1..]),
7681 ];
7682 for input in &inputs {
7683 assert_record_walks_match(input.as_bytes(), 40);
7684 }
7685 }
7686
7687 #[test]
7688 fn record_walks_match_a_line_by_line_scan_on_random_bytes() {
7689 let alphabet = b"^\n #ab";
7690 let mut state = 0x2545_f491_4f6c_dd1du64;
7691 for len in 0..400 {
7692 let input: Vec<u8> = (0..len % 97)
7693 .map(|_| alphabet[(xorshift(&mut state) % alphabet.len() as u64) as usize])
7694 .collect();
7695 for hex_len in [1, 2, 3] {
7696 assert_record_walks_match(&input, hex_len);
7697 }
7698 }
7699 }
7700
7701 #[test]
7704 fn search_matches_a_linear_scan_on_sorted_records() {
7705 let hex_len = 2;
7706 let mut state = 0x9e37_79b9_7f4a_7c15u64;
7707 for _ in 0..300 {
7708 let mut names: Vec<String> = (0..xorshift(&mut state) % 12)
7709 .map(|_| format!("n{}", xorshift(&mut state) % 40))
7710 .collect();
7711 names.sort();
7712 names.dedup();
7713 let mut data = b"# h\n".to_vec();
7714 for name in &names {
7715 data.extend_from_slice(format!("ab {name}\n").as_bytes());
7716 match xorshift(&mut state) % 4 {
7717 0 => data.extend_from_slice(b"^ab\n"),
7718 1 => data.extend_from_slice(b"junk\n"),
7719 _ => {}
7720 }
7721 }
7722 let mut view = raw_view(&data, hex_len);
7723 view.body_start = 4;
7724 let records: Vec<usize> = view.records().collect();
7725 for probe in 0..42 {
7726 let target = format!("n{probe}");
7727 let want = records
7728 .iter()
7729 .copied()
7730 .find(|&s| view.record_cmp(s, &target).is_ge())
7731 .unwrap_or(data.len());
7732 assert_eq!(view.lower_bound(&target), want, "{target} in {data:?}");
7733 let hit = view.search(&target).ok();
7734 assert_eq!(
7735 hit,
7736 names.contains(&target).then_some(want),
7737 "{target} in {data:?}"
7738 );
7739 }
7740 }
7741 }
7742}