1pub mod handle;
41pub mod listing;
42pub mod query_reachability;
43pub 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, update_detached_head_for_commit_on_store,
59 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::ident::Signature;
83use crate::lockfile::LockWait;
84use crate::objects::{HashAlgo, ObjectId};
85use crate::pack;
86use store::format_reflog_identity;
87
88pub const SYMREF_MAXDEPTH: usize = 5;
90
91fn map_public_ref_store_error(err: Error, refname: &str) -> Error {
92 match err {
93 Error::RefStore(store::RefStoreError::SymrefLoop) => {
94 Error::InvalidRef(format!("ref symlink too deep: {refname}"))
95 }
96 other => other,
97 }
98}
99
100fn files_legacy_write_precheck(git_dir: &Path, refname: &str) -> Result<()> {
104 let storage_dir = ref_storage_dir(git_dir, refname);
105 let stor = crate::ref_namespace::storage_ref_name(refname);
106 let path = storage_dir.join(stor);
107 remove_empty_ref_directory(&path);
108 if fs::symlink_metadata(&path)
109 .map(|m| m.file_type().is_dir())
110 .unwrap_or(false)
111 {
112 return Err(directory_in_the_way(git_dir, refname, &path));
113 }
114 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
115 return Err(Error::InvalidRef(format!(
116 "cannot update ref '{refname}': reference broken"
117 )));
118 }
119 Ok(())
120}
121
122fn directory_in_the_way(git_dir: &Path, refname: &str, path: &Path) -> Error {
129 match verify_refname_available_for_create(git_dir, refname, &BTreeSet::new(), &HashSet::new()) {
130 Err(reason) => Error::RefStore(store::RefStoreError::NameUnavailable { reason }),
131 Ok(()) => RefLockError::DirectoryInTheWay {
132 refname: refname.to_owned(),
133 path: ref_path_for_display(path),
134 }
135 .into(),
136 }
137}
138
139fn legacy_ref_update_flags(refname: &str) -> RefUpdateFlags {
140 let mut flags = RefUpdateFlags::default();
141 if refname == "HEAD" {
143 flags.no_deref = true;
144 }
145 flags
146}
147
148pub(crate) fn commit_store_update(git_dir: &Path, update: RefUpdate) -> Result<()> {
149 commit_ref_update_on_store(&store::open_checked_ref_store(git_dir)?, update)
150}
151
152pub fn commit_ref_update_on_store(store: &dyn RefStore, update: RefUpdate) -> Result<()> {
159 commit_ref_update_on_store_with_wait(store, update, || default_ref_lock_wait(store))
160}
161
162pub(crate) fn commit_ref_update_on_store_with_wait(
165 store: &dyn RefStore,
166 update: RefUpdate,
167 wait: impl FnOnce() -> LockWait,
168) -> Result<()> {
169 if let Some(ref log) = update.reflog {
170 validate_reflog_update(log)?;
171 }
172 let refname = update.name.clone();
173 let txn = RefTransaction::new()
174 .update(update)
175 .map_err(crate::error::Error::from)?;
176 let prepared = prepare_retrying(store, txn, wait)
177 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))?;
178 prepared
179 .commit()
180 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))
181}
182
183pub fn prepare_with_lock_wait(
234 store: &dyn RefStore,
235 txn: RefTransaction,
236 wait: LockWait,
237) -> std::result::Result<Box<dyn PreparedRefTransaction + Send + '_>, RefStoreError> {
238 prepare_retrying(store, txn, || wait)
239}
240
241fn prepare_retrying(
242 store: &dyn RefStore,
243 txn: RefTransaction,
244 wait: impl FnOnce() -> LockWait,
245) -> std::result::Result<Box<dyn PreparedRefTransaction + Send + '_>, RefStoreError> {
246 use crate::lockfile::{retry_while_held, SystemLockClock, SystemLockJitter};
247 retry_while_held(
248 Path::new("refs"),
249 wait,
250 &SystemLockClock,
251 &SystemLockJitter,
252 || store.prepare(txn.clone()),
253 |err| matches!(err, RefStoreError::LockHeld { .. }),
254 )
255}
256
257fn default_ref_lock_wait(store: &dyn RefStore) -> LockWait {
259 store.ref_lock_wait()
260}
261
262pub fn update_direct_ref_on_store(
270 store: &dyn RefStore,
271 git_dir: &Path,
272 refname: &str,
273 oid: &ObjectId,
274) -> Result<()> {
275 store_write_value_on_store(
276 store,
277 git_dir,
278 refname,
279 Some(StoreRawRef::Direct(*oid)),
280 Expected::Any,
281 None,
282 )
283}
284
285pub(crate) fn store_write_value_on_store(
286 store: &dyn RefStore,
287 git_dir: &Path,
288 refname: &str,
289 new_value: Option<StoreRawRef>,
290 expected: Expected,
291 reflog: Option<ReflogUpdate>,
292) -> Result<()> {
293 ensure_refname_safe_for_storage(refname)?;
294 if new_value.is_some() && store.format() == RefStorageFormat::Files {
295 files_legacy_write_precheck(git_dir, refname)?;
296 }
297 commit_ref_update_on_store(
298 store,
299 RefUpdate {
300 name: refname.to_owned(),
301 new_value,
302 expected,
303 reflog,
304 flags: legacy_ref_update_flags(refname),
305 },
306 )
307}
308
309pub fn commit_ref_store_batch(git_dir: &Path, updates: &[RefUpdate]) -> Result<()> {
329 if updates.is_empty() {
330 return Ok(());
331 }
332 commit_ref_updates_on_store(&store::open_checked_ref_store(git_dir)?, updates, &[])
333}
334
335pub fn commit_ref_updates_non_atomic(
341 git_dir: &Path,
342 updates: Vec<RefUpdate>,
343) -> Result<Vec<RefUpdateCommitOutcome>> {
344 commit_ref_updates_non_atomic_on_store(&store::open_checked_ref_store(git_dir)?, updates)
345}
346
347pub fn commit_ref_updates_on_store(
356 store: &dyn RefStore,
357 updates: &[RefUpdate],
358 reflog_renames: &[RefReflogRename],
359) -> Result<()> {
360 commit_ref_batch_on_store(store, updates, reflog_renames, &[])
361}
362
363pub fn commit_ref_batch_on_store(
369 store: &dyn RefStore,
370 updates: &[RefUpdate],
371 reflog_renames: &[RefReflogRename],
372 case_only_renames: &[RefCaseOnlyRename],
373) -> Result<()> {
374 if updates.is_empty() && reflog_renames.is_empty() && case_only_renames.is_empty() {
375 return Ok(());
376 }
377 commit_owned_ref_batch_on_store(
378 store,
379 updates.to_vec(),
380 reflog_renames.to_vec(),
381 case_only_renames.to_vec(),
382 false,
383 || default_ref_lock_wait(store),
384 )
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
389pub struct RefUpdateCommitOutcome {
390 pub name: String,
392 pub result: std::result::Result<(), store::RefStoreError>,
394}
395
396pub fn commit_ref_updates_non_atomic_on_store(
407 store: &dyn RefStore,
408 updates: Vec<RefUpdate>,
409) -> Result<Vec<RefUpdateCommitOutcome>> {
410 if updates.is_empty() {
411 return Ok(Vec::new());
412 }
413 let order: Vec<String> = updates.iter().map(|u| u.name.clone()).collect();
414 let (deletes, rest): (Vec<_>, Vec<_>) = updates
415 .into_iter()
416 .partition(|u| u.new_value.is_none() && !u.flags.log_only);
417 let mut results: std::collections::HashMap<String, store::RefStoreError> =
418 std::collections::HashMap::new();
419 for phase in [deletes, rest] {
420 if phase.is_empty() {
421 continue;
422 }
423 for outcome in commit_ref_updates_allowing_failures_on_store(store, phase)? {
424 if let Err(err) = outcome.result {
425 results.insert(outcome.name, err);
426 }
427 }
428 }
429 Ok(order
430 .into_iter()
431 .map(|name| RefUpdateCommitOutcome {
432 result: results.remove(&name).map_or(Ok(()), Err),
433 name,
434 })
435 .collect())
436}
437
438pub fn commit_ref_updates_allowing_failures_on_store(
445 store: &dyn RefStore,
446 updates: Vec<RefUpdate>,
447) -> Result<Vec<RefUpdateCommitOutcome>> {
448 if updates.is_empty() {
449 return Ok(Vec::new());
450 }
451 let order: Vec<String> = updates.iter().map(|u| u.name.clone()).collect();
452 for update in &updates {
453 if let Some(ref log) = update.reflog {
454 validate_reflog_update(log)?;
455 }
456 }
457 let label = updates
458 .first()
459 .map(|u| u.name.clone())
460 .unwrap_or_else(|| "refs".to_owned());
461 let txn = RefTransaction::from_updates(updates).map_err(|e| {
462 let name = match &e {
463 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
464 _ => label.clone(),
465 };
466 map_public_ref_store_error(Error::from(e), &name)
467 })?;
468 let txn = txn.allowing_failures();
469 let prepared = prepare_retrying(store, txn, || default_ref_lock_wait(store))
470 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
471 let rejections: std::collections::HashMap<String, store::RefStoreError> = prepared
472 .rejected_updates()
473 .iter()
474 .map(|(name, err)| (name.clone(), err.clone()))
475 .collect();
476 prepared
477 .commit()
478 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
479
480 Ok(order
481 .into_iter()
482 .map(|name| {
483 let result = rejections.get(&name).cloned().map_or(Ok(()), Err);
484 RefUpdateCommitOutcome { name, result }
485 })
486 .collect())
487}
488
489pub(crate) fn commit_ref_updates_on_store_with_wait(
492 store: &dyn RefStore,
493 updates: &[RefUpdate],
494 wait: impl FnOnce() -> LockWait,
495) -> Result<()> {
496 if updates.is_empty() {
497 return Ok(());
498 }
499 commit_owned_ref_batch_on_store(store, updates.to_vec(), Vec::new(), Vec::new(), false, wait)
500}
501
502pub(crate) fn commit_owned_ref_updates_on_store(
508 store: &dyn RefStore,
509 updates: Vec<RefUpdate>,
510) -> Result<()> {
511 if updates.is_empty() {
512 return Ok(());
513 }
514 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new(), false, || {
515 default_ref_lock_wait(store)
516 })
517}
518
519pub(crate) fn commit_initial_ref_updates_on_store(
527 store: &dyn RefStore,
528 updates: Vec<RefUpdate>,
529) -> Result<()> {
530 commit_initial_ref_updates_on_store_with_wait(store, updates, || default_ref_lock_wait(store))
531}
532
533pub(crate) fn commit_initial_ref_updates_on_store_with_wait(
536 store: &dyn RefStore,
537 updates: Vec<RefUpdate>,
538 wait: impl FnOnce() -> LockWait,
539) -> Result<()> {
540 if updates.is_empty() {
541 return Ok(());
542 }
543 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new(), true, wait)
544}
545
546fn commit_owned_ref_batch_on_store(
547 store: &dyn RefStore,
548 updates: Vec<RefUpdate>,
549 reflog_renames: Vec<RefReflogRename>,
550 case_only_renames: Vec<RefCaseOnlyRename>,
551 initial: bool,
552 wait: impl FnOnce() -> LockWait,
553) -> Result<()> {
554 for update in &updates {
555 if let Some(ref log) = update.reflog {
556 validate_reflog_update(log)?;
557 }
558 }
559 for rename in &reflog_renames {
560 validate_reflog_update(&rename.append)?;
561 }
562 let label = updates
563 .first()
564 .map(|u| u.name.clone())
565 .or_else(|| reflog_renames.first().map(|r| r.from.clone()))
566 .or_else(|| case_only_renames.first().map(|r| r.from.clone()))
567 .unwrap_or_else(|| "refs".to_owned());
568 let mut txn = RefTransaction::from_updates(updates).map_err(|e| {
569 let name = match &e {
570 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
571 _ => label.clone(),
572 };
573 map_public_ref_store_error(Error::from(e), &name)
574 })?;
575 for rename in reflog_renames {
576 txn = txn.with_reflog_rename(rename);
577 }
578 for rename in case_only_renames {
579 txn = txn.with_case_only_rename(rename);
580 }
581 if initial {
582 txn = txn.initial();
583 }
584 let prepared = prepare_retrying(store, txn, wait)
585 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
586 prepared
587 .commit()
588 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))
589}
590
591fn store_write_value(
592 git_dir: &Path,
593 refname: &str,
594 new_value: Option<StoreRawRef>,
595 expected: Expected,
596 reflog: Option<ReflogUpdate>,
597) -> Result<()> {
598 store_write_value_on_store(
599 &store::open_checked_ref_store(git_dir)?,
600 git_dir,
601 refname,
602 new_value,
603 expected,
604 reflog,
605 )
606}
607
608pub(crate) fn collect_resolved_refs(
609 store: &dyn RefStore,
610 prefix: &str,
611) -> Result<Vec<(String, ObjectId)>> {
612 collect_resolved_ref_entries(store, |f| store.for_each_ref(prefix, f))
613}
614
615fn collect_resolved_ref_entries(
617 store: &dyn RefStore,
618 for_each: impl FnOnce(&mut dyn FnMut(&RefEntry) -> ControlFlow<()>) -> Result<()>,
619) -> Result<Vec<(String, ObjectId)>> {
620 let mut results = Vec::new();
621 for_each(&mut |entry: &RefEntry| {
622 let oid = entry
623 .peeled
624 .or_else(|| match &entry.value {
625 StoreRawRef::Direct(oid) => Some(*oid),
626 StoreRawRef::Symbolic(target) => store.resolve(target).ok(),
627 })
628 .or_else(|| store.resolve(&entry.name).ok());
629 if let Some(oid) = oid {
630 results.push((entry.name.clone(), oid));
631 }
632 ControlFlow::Continue(())
633 })?;
634 Ok(results)
635}
636
637#[derive(Debug, Clone, PartialEq, Eq)]
639pub enum Ref {
640 Direct(ObjectId),
642 Symbolic(String),
644}
645
646pub fn read_ref_file(path: &Path) -> Result<Ref> {
653 let meta = fs::symlink_metadata(path).map_err(|e| loose_ref_io_error(e, path))?;
655 if meta.file_type().is_symlink() {
656 if let Ok(target) = fs::read_link(path) {
657 return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
658 }
659 }
660 if meta.is_dir() {
661 return Err(Error::Io(io::Error::new(
662 io::ErrorKind::NotFound,
663 "loose ref path is a directory",
664 )));
665 }
666 read_regular_ref_file(path)
667}
668
669pub(crate) fn read_regular_ref_file(path: &Path) -> Result<Ref> {
671 let content = fs::read_to_string(path).map_err(|e| loose_ref_io_error(e, path))?;
672 parse_ref_content(content.trim_end_matches('\n'))
673}
674
675fn loose_ref_io_error(e: io::Error, path: &Path) -> Error {
679 if io_error_is_missing_file(&e, path) {
680 return Error::Io(io::Error::new(io::ErrorKind::NotFound, e));
681 }
682 Error::Io(e)
683}
684
685pub(crate) fn io_error_is_missing_file(err: &io::Error, path: &Path) -> bool {
693 match err.kind() {
694 io::ErrorKind::NotFound | io::ErrorKind::IsADirectory | io::ErrorKind::NotADirectory => {
695 true
696 }
697 io::ErrorKind::PermissionDenied => fs::symlink_metadata(path).is_ok_and(|m| m.is_dir()),
698 _ => matches!(err.raw_os_error(), Some(libc::EISDIR | libc::ENOTDIR)),
699 }
700}
701
702pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
704 if let Some(target) = content.strip_prefix("ref: ") {
705 Ok(Ref::Symbolic(target.trim().to_owned()))
706 } else if let Some(token) = content.split_whitespace().next() {
707 if ObjectId::is_full_hex(token) {
708 let oid: ObjectId = token.parse()?;
709 Ok(Ref::Direct(oid))
710 } else if content == "unknown-oid" {
711 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
712 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
713 Ok(Ref::Direct(oid))
714 } else {
715 Err(Error::InvalidRef(content.to_owned()))
716 }
717 } else {
718 Err(Error::InvalidRef(content.to_owned()))
719 }
720}
721
722pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
742 open_ref_store(git_dir)?
743 .resolve(refname)
744 .map_err(|err| map_public_ref_store_error(err, refname))
745}
746
747pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
752 let commondir_file = git_dir.join("commondir");
753 if !commondir_file.is_file() {
754 return None;
755 }
756 let raw = fs::read_to_string(commondir_file).ok()?;
757 let rel = raw.trim();
758 let path = if Path::new(rel).is_absolute() {
761 PathBuf::from(rel)
762 } else {
763 git_dir.join(rel)
764 };
765 path.canonicalize_simplified().ok()
766}
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub enum RawRefLookup {
775 Exists,
777 NotFound,
779 IsDirectory,
781}
782
783pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
804 let store = open_ref_store(git_dir)?;
805 match store.read_raw(refname)? {
806 Some(_) => Ok(RawRefLookup::Exists),
807 None => {
808 let (storage_dir, stor_name) =
809 crate::worktree_ref::resolve_ref_storage(git_dir, refname);
810 let path = storage_dir.join(&stor_name);
811 if fs::symlink_metadata(&path)
812 .map(|m| m.is_dir())
813 .unwrap_or(false)
814 {
815 Ok(RawRefLookup::IsDirectory)
816 } else {
817 Ok(RawRefLookup::NotFound)
818 }
819 }
820 }
821}
822
823#[must_use]
825pub fn lock_path_for_ref(path: &Path) -> PathBuf {
826 let mut name = path
827 .file_name()
828 .map(|s| s.to_os_string())
829 .unwrap_or_default();
830 name.push(".lock");
831 match path.parent() {
832 Some(parent) => parent.join(&name),
833 None => PathBuf::from(name),
834 }
835}
836
837fn first_packed_descendant_ref(store: &Path, refname: &str) -> Result<Option<String>> {
839 let leaf_dir = format!("{refname}/");
840 let map = read_packed_refs_map_for_prefix(store, refname)?;
841 let sorted: BTreeSet<String> = map.keys().cloned().collect();
842 Ok(sorted
843 .range(leaf_dir.clone()..)
844 .next()
845 .filter(|name| name.starts_with(&leaf_dir))
846 .cloned())
847}
848
849fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
850 Ok(lookup_packed_ref(git_dir, refname)?.is_some())
851}
852
853pub(crate) fn packed_refname_namespace_conflict(
858 sorted_packed: &BTreeSet<String>,
859 candidate: &str,
860) -> bool {
861 let segments: Vec<&str> = candidate.split('/').filter(|s| !s.is_empty()).collect();
862 if segments.len() > 1 {
863 let mut dirname = String::new();
864 for part in &segments[..segments.len() - 1] {
865 if !dirname.is_empty() {
866 dirname.push('/');
867 }
868 dirname.push_str(part);
869 if sorted_packed.contains(&dirname) {
870 return true;
871 }
872 }
873 }
874
875 let leaf_dir = format!("{candidate}/");
876 if let Some(existing) = sorted_packed.range(leaf_dir.clone()..).next() {
877 if existing.starts_with(&leaf_dir) {
878 return true;
879 }
880 }
881 false
882}
883
884fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
885 Ok(PackedRefs::load(git_dir)?.has_namespace_conflict(refname))
886}
887
888pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
904 if refname == "HEAD" {
905 return Ok(false);
906 }
907 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
908 return Ok(false);
909 }
910 let storage_dir = ref_storage_dir(git_dir, refname);
911 packed_ref_name_exists(&storage_dir, refname)
912}
913
914#[derive(Debug, Clone, PartialEq, Eq)]
916pub enum RefnameUnavailable {
917 AncestorExists {
919 blocking: String,
921 new_ref: String,
923 },
924 DescendantExists {
926 blocking: String,
928 new_ref: String,
930 },
931 SameBatch {
933 refname: String,
935 other: String,
937 },
938}
939
940impl RefnameUnavailable {
941 #[must_use]
943 pub fn refname(&self) -> &str {
944 match self {
945 RefnameUnavailable::AncestorExists { new_ref, .. }
946 | RefnameUnavailable::DescendantExists { new_ref, .. } => new_ref,
947 RefnameUnavailable::SameBatch { refname, .. } => refname,
948 }
949 }
950
951 #[must_use]
953 pub fn lock_message_suffix(&self) -> String {
954 match self {
955 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
956 format!("'{blocking}' exists; cannot create '{new_ref}'")
957 }
958 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
959 format!("'{blocking}' exists; cannot create '{new_ref}'")
960 }
961 RefnameUnavailable::SameBatch { refname, other } => {
962 format!("cannot process '{refname}' and '{other}' at the same time")
963 }
964 }
965 }
966}
967
968fn find_descendant_in_sorted_extras(
969 dirname_with_slash: &str,
970 extras: &BTreeSet<String>,
971) -> Option<String> {
972 let start = extras
973 .range(dirname_with_slash.to_string()..)
974 .next()
975 .cloned()?;
976 if start.starts_with(dirname_with_slash) {
977 Some(start)
978 } else {
979 None
980 }
981}
982
983#[must_use]
987pub fn is_valid_fetch_advertised_ref(name: &str) -> bool {
988 crate::check_ref_format::is_valid_fetch_advertised_ref(name)
989}
990
991#[must_use]
996pub fn is_valid_ls_refs_advertised_name(name: &str) -> bool {
997 name == "HEAD" || is_valid_fetch_advertised_ref(name)
998}
999
1000#[must_use]
1002pub fn is_valid_storable_ref_name(name: &str) -> bool {
1003 is_valid_fetch_advertised_ref(name)
1004}
1005
1006#[must_use]
1008pub fn is_valid_advertised_symref_target(name: &str) -> bool {
1009 crate::check_ref_format::is_valid_advertised_symref_target(name)
1010}
1011
1012pub fn verify_refname_available_for_create(
1025 git_dir: &Path,
1026 refname: &str,
1027 extras: &BTreeSet<String>,
1028 skip: &HashSet<String>,
1029) -> std::result::Result<(), RefnameUnavailable> {
1030 let git_dir = git_dir
1033 .canonicalize_simplified()
1034 .unwrap_or_else(|_| git_dir.to_path_buf());
1035 let mut seen_dirnames: HashSet<String> = HashSet::new();
1036 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
1037 if segments.len() <= 1 {
1038 } else {
1040 let mut dirname = String::new();
1041 for part in &segments[..segments.len() - 1] {
1042 if !dirname.is_empty() {
1043 dirname.push('/');
1044 }
1045 dirname.push_str(part);
1046
1047 if !seen_dirnames.insert(dirname.clone()) {
1048 continue;
1049 }
1050
1051 if skip.contains(&dirname) {
1052 continue;
1053 }
1054
1055 match read_raw_ref(&git_dir, &dirname) {
1056 Ok(RawRefLookup::Exists) => {
1057 return Err(RefnameUnavailable::AncestorExists {
1058 blocking: dirname.clone(),
1059 new_ref: refname.to_owned(),
1060 });
1061 }
1062 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
1065 Err(_) => {}
1066 }
1067
1068 if extras.contains(&dirname) {
1069 return Err(RefnameUnavailable::SameBatch {
1070 refname: refname.to_owned(),
1071 other: dirname.clone(),
1072 });
1073 }
1074 }
1075 }
1076
1077 let mut leaf_dir = String::with_capacity(refname.len() + 1);
1078 leaf_dir.push_str(refname);
1079 leaf_dir.push('/');
1080
1081 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1087 let may_have_loose_descendants = packed_dir.join("reftable").is_dir()
1088 || git_dir.join(refname).is_dir()
1089 || packed_dir.join(refname).is_dir();
1090 let under = if may_have_loose_descendants {
1091 list_refs(&git_dir, &leaf_dir).unwrap_or_default()
1092 } else {
1093 Vec::new()
1094 };
1095 if under.is_empty() {
1096 if let Ok(Some(name)) = first_packed_descendant_ref(&packed_dir, refname) {
1097 if !skip.contains(&name) {
1098 return Err(RefnameUnavailable::DescendantExists {
1099 blocking: name,
1100 new_ref: refname.to_owned(),
1101 });
1102 }
1103 }
1104 if packed_dir != git_dir {
1105 if let Ok(Some(name)) = first_packed_descendant_ref(&git_dir, refname) {
1106 if !skip.contains(&name) {
1107 return Err(RefnameUnavailable::DescendantExists {
1108 blocking: name,
1109 new_ref: refname.to_owned(),
1110 });
1111 }
1112 }
1113 }
1114 }
1115 if under.is_empty()
1116 && fs::symlink_metadata(git_dir.join(refname))
1117 .map(|m| m.is_dir())
1118 .unwrap_or(false)
1119 {
1120 let mut blocking: Option<String> = None;
1121 let dir_path = git_dir.join(refname);
1122 if let Ok(read) = fs::read_dir(&dir_path) {
1123 for entry in read.flatten() {
1124 let path = entry.path();
1125 let Ok(meta) = fs::metadata(&path) else {
1126 continue;
1127 };
1128 let name = entry.file_name().to_string_lossy().into_owned();
1129 if !meta.is_file() || name.ends_with(".lock") {
1130 continue;
1131 }
1132 let full = format!("{refname}/{name}");
1133 blocking = Some(full);
1134 break;
1135 }
1136 }
1137 if let Some(b) = blocking {
1138 if !skip.contains(&b) {
1139 return Err(RefnameUnavailable::DescendantExists {
1140 blocking: b,
1141 new_ref: refname.to_owned(),
1142 });
1143 }
1144 }
1145 }
1146
1147 for (existing, _) in under {
1148 if skip.contains(&existing) {
1149 continue;
1150 }
1151 return Err(RefnameUnavailable::DescendantExists {
1152 blocking: existing,
1153 new_ref: refname.to_owned(),
1154 });
1155 }
1156
1157 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
1158 if !skip.contains(&extra) {
1159 return Err(RefnameUnavailable::SameBatch {
1160 refname: refname.to_owned(),
1161 other: extra,
1162 });
1163 }
1164 }
1165
1166 Ok(())
1167}
1168
1169#[derive(Clone, Debug)]
1171pub struct RefBatchItem {
1172 pub name: String,
1174 pub new_oid: Option<ObjectId>,
1176}
1177
1178pub fn verify_ref_transaction_batch(
1189 git_dir: &Path,
1190 items: &[RefBatchItem],
1191) -> std::result::Result<(), RefnameUnavailable> {
1192 if let Some((parent, child)) =
1193 batch_refname_prefix_conflict(items.iter().map(|i| i.name.as_str()))
1194 {
1195 return Err(RefnameUnavailable::SameBatch {
1196 refname: child,
1197 other: parent,
1198 });
1199 }
1200
1201 if crate::ref_storage::detect_format(git_dir).is_ok_and(|f| f.is_reftable()) {
1202 return crate::reftable::verify_ref_transaction_batch_reftable(git_dir, items);
1203 }
1204
1205 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
1206 let empty_skip = HashSet::new();
1207 for item in items {
1208 let Some(_oid) = item.new_oid else {
1209 continue;
1210 };
1211 if resolve_ref(git_dir, &item.name).is_ok() {
1212 continue;
1213 }
1214 verify_refname_available_for_create(git_dir, &item.name, &extras, &empty_skip)?;
1215 }
1216 Ok(())
1217}
1218
1219pub(crate) fn batch_refname_prefix_conflict<'a>(
1221 names: impl Iterator<Item = &'a str>,
1222) -> Option<(String, String)> {
1223 let mut sorted: Vec<&str> = names.collect();
1224 sorted.sort_unstable();
1225 for pair in sorted.windows(2) {
1226 let (a, b) = (pair[0], pair[1]);
1227 if refname_is_strict_prefix(a, b) {
1228 return Some((a.to_owned(), b.to_owned()));
1229 }
1230 if refname_is_strict_prefix(b, a) {
1231 return Some((b.to_owned(), a.to_owned()));
1232 }
1233 }
1234 None
1235}
1236
1237fn refname_is_strict_prefix(parent: &str, child: &str) -> bool {
1238 child.len() > parent.len()
1239 && child.as_bytes().get(parent.len()) == Some(&b'/')
1240 && child.starts_with(parent)
1241}
1242
1243fn packed_refs_unexpected_line(path: &Path, line: impl Into<String>) -> Error {
1244 Error::PackedRefsUnexpectedLine {
1245 path: path.display().to_string(),
1246 line: line.into(),
1247 }
1248}
1249
1250fn packed_refs_unterminated_line(path: &Path, line: impl Into<String>) -> Error {
1251 Error::PackedRefsUnexpectedLine {
1252 path: path.display().to_string(),
1253 line: line.into(),
1254 }
1255}
1256
1257fn parse_packed_ref_peel_line(path: &Path, line: &str, expect_peel: bool) -> Result<ObjectId> {
1259 if !expect_peel {
1260 return Err(packed_refs_unexpected_line(path, line));
1261 }
1262 line.strip_prefix('^')
1263 .and_then(|inner| ObjectId::from_hex(inner).ok())
1264 .ok_or_else(|| packed_refs_unexpected_line(path, line))
1265}
1266
1267fn packed_line_oid(line: &str) -> Option<(usize, ObjectId)> {
1269 let bytes = line.as_bytes();
1270 let hex_len = [HashAlgo::Sha1.hex_len(), HashAlgo::Sha256.hex_len()]
1271 .into_iter()
1272 .find(|&len| bytes.get(len) == Some(&b' '))?;
1273 let oid = ObjectId::from_hex(&line[..hex_len]).ok()?;
1275 Some((hex_len, oid))
1276}
1277
1278fn packed_ref_data_line_refname(line: &str) -> Option<&str> {
1279 let bytes = line.as_bytes();
1280 let mut j = 0usize;
1281 while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
1282 j += 1;
1283 }
1284 if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
1285 return None;
1286 }
1287 let refname = &line[j + 1..];
1288 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
1289 return None;
1290 }
1291 Some(refname)
1292}
1293
1294fn parse_packed_ref_data_line(
1295 path: &Path,
1296 line: &str,
1297 ref_opts: &RefNameOptions,
1298) -> Result<(String, ObjectId)> {
1299 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, ref_opts)?;
1300 Ok((refname.to_owned(), oid))
1301}
1302
1303fn parse_packed_ref_data_line_borrowed<'a>(
1304 path: &Path,
1305 line: &'a str,
1306 ref_opts: &RefNameOptions,
1307) -> Result<(&'a str, ObjectId)> {
1308 let Some((hex_len, oid)) = packed_line_oid(line) else {
1309 return Err(packed_refs_unexpected_line(path, line));
1310 };
1311 let refname = &line[hex_len + 1..];
1312 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
1313 return Err(packed_refs_unexpected_line(path, line));
1314 }
1315 if check_refname_format_borrowed(refname, ref_opts).is_err() {
1316 return Err(packed_refs_unexpected_line(path, line));
1317 }
1318 Ok((refname, oid))
1319}
1320
1321#[derive(Debug, Clone)]
1340pub(crate) struct PackedRefsFileView {
1341 data: std::sync::Arc<crate::pack_map::PackData>,
1342 sorted: bool,
1345 hex_len: usize,
1347 body_start: usize,
1349 peel_trait: PackedPeelTrait,
1351}
1352
1353impl PackedRefsFileView {
1354 #[cfg(test)]
1356 fn from_bytes(path: &Path, data: Vec<u8>, hash_algo: crate::objects::HashAlgo) -> Result<Self> {
1357 Self::from_data(path, crate::pack_map::PackData::from_owned(data), hash_algo)
1358 }
1359
1360 fn from_data(
1369 path: &Path,
1370 data: std::sync::Arc<crate::pack_map::PackData>,
1371 hash_algo: crate::objects::HashAlgo,
1372 ) -> Result<Self> {
1373 let hex_len = hash_algo.hex_len();
1374 if data.is_empty() {
1375 return Ok(Self {
1376 data,
1377 sorted: false,
1378 hex_len,
1379 body_start: 0,
1380 peel_trait: PackedPeelTrait::None,
1381 });
1382 }
1383 if !data.ends_with(b"\n") {
1384 let tail = std::str::from_utf8(&data)
1385 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1386 .unwrap_or("<invalid utf-8>");
1387 return Err(packed_refs_unterminated_line(path, tail));
1388 }
1389 let text = std::str::from_utf8(&data[..packed_refs_leading_comments_end(&data)])
1392 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1393 let body_start = if data.starts_with(b"#") {
1396 memchr::memchr(b'\n', &data).map_or(data.len(), |nl| nl + 1)
1397 } else {
1398 0
1399 };
1400 let body = &data[body_start..];
1401 if let Some(rest) = body.strip_suffix(b"\n") {
1402 let last_line = memchr::memrchr(b'\n', rest).map_or(rest, |nl| &rest[nl + 1..]);
1403 if last_line.len() < hex_len + 1 {
1404 return Err(packed_refs_unexpected_line(
1405 path,
1406 String::from_utf8_lossy(last_line),
1407 ));
1408 }
1409 }
1410 Ok(Self {
1411 sorted: packed_refs_file_declares_sorted(text),
1412 hex_len,
1413 body_start,
1414 peel_trait: packed_refs_peel_trait(text),
1415 data,
1416 })
1417 }
1418
1419 fn record_name(&self, line_start: usize) -> &[u8] {
1421 let end = packed_line_end(&self.data, line_start);
1422 &self.data[line_start + self.hex_len + 1..end]
1423 }
1424
1425 fn record_cmp(&self, line_start: usize, name: &str) -> std::cmp::Ordering {
1426 self.record_name(line_start).cmp(name.as_bytes())
1427 }
1428
1429 fn next_record(&self, from: usize, limit: usize) -> Option<usize> {
1432 let hex_len = self.hex_len;
1433 let mut start = from;
1434 while start < limit {
1435 let rest = &self.data[start..];
1436 if rest[0] == b'^' && rest.get(hex_len + 1) == Some(&b'\n') {
1439 start += hex_len + 2;
1440 continue;
1441 }
1442 let end = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
1443 if packed_line_is_record(&rest[..end], hex_len) {
1444 return Some(start);
1445 }
1446 start += end + 1;
1447 }
1448 None
1449 }
1450
1451 fn prev_record(&self, before: usize) -> Option<usize> {
1453 let mut end = before;
1454 while end > 0 {
1455 let line_end = end - 1;
1456 let start = memchr::memrchr(b'\n', &self.data[..line_end]).map_or(0, |nl| nl + 1);
1457 if packed_line_is_record(&self.data[start..line_end], self.hex_len) {
1458 return Some(start);
1459 }
1460 end = start;
1461 }
1462 None
1463 }
1464
1465 fn records(&self) -> impl Iterator<Item = usize> + '_ {
1467 let len = self.data.len();
1468 std::iter::successors(self.next_record(0, len), move |&start| {
1469 self.next_record(packed_line_end(&self.data, start) + 1, len)
1470 })
1471 }
1472
1473 fn search(&self, name: &str) -> std::result::Result<usize, usize> {
1481 let len = self.data.len();
1482 let mut lo = self.body_start;
1483 let mut hi = len;
1484 while lo < hi {
1485 let mid = lo + (hi - lo) / 2;
1486 let line = memchr::memrchr(b'\n', &self.data[lo..mid]).map_or(lo, |nl| lo + nl + 1);
1487 let Some(record) = self.next_record(line, hi) else {
1488 hi = line;
1489 continue;
1490 };
1491 match self.record_cmp(record, name) {
1492 std::cmp::Ordering::Less => lo = packed_line_end(&self.data, record) + 1,
1493 std::cmp::Ordering::Greater => hi = record,
1494 std::cmp::Ordering::Equal => return Ok(record),
1495 }
1496 }
1497 Err(self.next_record(lo, len).unwrap_or(len))
1498 }
1499
1500 fn lower_bound(&self, name: &str) -> usize {
1502 let (Ok(start) | Err(start)) = self.search(name);
1503 start
1504 }
1505
1506 fn lookup_entry(
1508 &self,
1509 path: &Path,
1510 refname: &str,
1511 ) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1512 let ref_opts = RefNameOptions {
1513 allow_onelevel: false,
1514 refspec_pattern: false,
1515 normalize: false,
1516 };
1517 let line_start = if self.sorted {
1518 self.search(refname).ok()
1519 } else {
1520 self.records()
1521 .find(|&start| self.record_cmp(start, refname).is_eq())
1522 };
1523 let Some(start) = line_start else {
1524 return Ok(None);
1525 };
1526 let (name, oid) = parse_packed_line_at(path, &self.data, start, &ref_opts)?;
1527 debug_assert_eq!(name, refname);
1528 let line_end = packed_line_end(&self.data, start);
1529 if line_end + 1 < self.data.len() && self.data[line_end + 1] == b'^' {
1530 let peel_line = packed_line_as_str(&self.data, line_end + 1);
1531 parse_packed_ref_peel_line(path, peel_line, true)?;
1532 }
1533 let peel = peel_oid_following_line(&self.data, start);
1534 Ok(Some((oid, peel)))
1535 }
1536
1537 fn lookup_oid(&self, path: &Path, refname: &str) -> Result<Option<ObjectId>> {
1538 Ok(self.lookup_entry(path, refname)?.map(|(oid, _)| oid))
1539 }
1540
1541 pub(crate) fn first_descendant(
1545 &self,
1546 path: &Path,
1547 refname: &str,
1548 ) -> Result<Option<(String, ObjectId)>> {
1549 let leaf_dir = format!("{refname}/");
1550 Ok(self
1551 .map_for_prefix(path, Some(refname))?
1552 .into_iter()
1553 .filter(|(name, _)| name.starts_with(&leaf_dir))
1554 .min_by(|a, b| a.0.cmp(&b.0)))
1555 }
1556
1557 fn map_for_prefix(
1568 &self,
1569 path: &Path,
1570 prefix: Option<&str>,
1571 ) -> Result<HashMap<String, ObjectId>> {
1572 self.map_for_prefix_filtered(path, prefix, None)
1573 }
1574
1575 pub(crate) fn full_map(&self, path: &Path) -> Result<BTreeMap<String, ObjectId>> {
1578 let (mut records, in_order) =
1579 self.parse_region::<Vec<_>>(path, 0, self.data.len(), None, None)?;
1580 if !in_order {
1581 records.sort_by(|a, b| a.0.cmp(&b.0));
1582 }
1583 records.dedup_by(|later, earlier| {
1585 if later.0 != earlier.0 {
1586 return false;
1587 }
1588 earlier.1 = later.1;
1589 true
1590 });
1591 Ok(records.into_iter().collect())
1592 }
1593
1594 fn map_for_prefix_filtered(
1597 &self,
1598 path: &Path,
1599 prefix: Option<&str>,
1600 keep: Option<PackedRecordFilter<'_>>,
1601 ) -> Result<HashMap<String, ObjectId>> {
1602 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1603 let range_prefix = prefix_trim.filter(|p| self.sorted && !p.is_empty() && *p != "refs");
1604 if let Some((begin, end)) = range_prefix.and_then(|prefix| self.prefix_region(prefix)) {
1605 let (map, in_order) = self.parse_region(path, begin, end, prefix_trim, keep)?;
1606 if in_order {
1607 return Ok(map);
1608 }
1609 }
1610 let (map, _) = self.parse_region(path, 0, self.data.len(), prefix_trim, keep)?;
1612 Ok(map)
1613 }
1614
1615 fn prefix_region(&self, prefix: &str) -> Option<(usize, usize)> {
1623 let len = self.data.len();
1624 let first = self.lower_bound(prefix);
1625 let upper = list_prefix_upper_bound(prefix);
1626 let end = upper
1627 .as_deref()
1628 .map_or(len, |upper| self.lower_bound(upper));
1629 if end < first || (first < end && self.record_cmp(first, prefix).is_lt()) {
1630 return None;
1631 }
1632 if let Some(upper) = upper.as_deref() {
1633 if end < len && self.record_cmp(end, upper).is_lt() {
1634 return None;
1635 }
1636 }
1637 let begin = match self.prev_record(first) {
1638 Some(prev) => {
1639 if !self.record_cmp(prev, prefix).is_lt() {
1640 return None;
1641 }
1642 prev
1643 }
1644 None => 0,
1645 };
1646 let end = if end < len {
1647 let after = packed_line_end(&self.data, end) + 1;
1648 if self.data.get(after) == Some(&b'^') {
1649 packed_line_end(&self.data, after) + 1
1650 } else {
1651 after
1652 }
1653 } else {
1654 end
1655 };
1656 Some((begin, end))
1657 }
1658
1659 fn parse_region<C: Default + Extend<(String, ObjectId)>>(
1663 &self,
1664 path: &Path,
1665 begin: usize,
1666 end: usize,
1667 prefix: Option<&str>,
1668 keep: Option<PackedRecordFilter<'_>>,
1669 ) -> Result<(C, bool)> {
1670 let bytes = &self.data[begin..end];
1671 if memchr::memchr(b'\r', bytes).is_some() {
1672 return Err(packed_refs_unexpected_line(
1673 path,
1674 "<contains carriage return>",
1675 ));
1676 }
1677 let region = std::str::from_utf8(bytes)
1678 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1679 parse_packed_refs_region(path, region, prefix, keep, self.peel_trait)
1680 }
1681}
1682
1683#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1686pub(crate) enum PackedPeel {
1687 Peeled(ObjectId),
1689 NotTag,
1691 Unknown,
1693}
1694
1695pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
1698
1699fn parse_packed_refs_region<C: Default + Extend<(String, ObjectId)>>(
1705 path: &Path,
1706 region: &str,
1707 prefix: Option<&str>,
1708 keep: Option<PackedRecordFilter<'_>>,
1709 peel_trait: PackedPeelTrait,
1710) -> Result<(C, bool)> {
1711 let ref_opts = RefNameOptions {
1712 allow_onelevel: false,
1713 refspec_pattern: false,
1714 normalize: false,
1715 };
1716 let mut map = C::default();
1717 let mut in_order = true;
1718 let mut prev_name = "";
1719 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
1721 let flush = |map: &mut C, pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
1722 let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
1723 return;
1724 };
1725 let peel = if peel_trait.covers(refname) {
1726 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1727 } else {
1728 PackedPeel::Unknown
1729 };
1730 if keep(refname, &oid, peel) {
1731 map.extend(Some((refname.to_owned(), oid)));
1732 }
1733 };
1734 let mut expect_peel = false;
1735 let mut lines = region.split('\n').peekable();
1736 while let Some(line) = lines.next() {
1737 if line.is_empty() {
1738 if lines.peek().is_some() {
1739 return Err(packed_refs_unexpected_line(path, line));
1740 }
1741 break;
1742 }
1743 if line.starts_with('#') {
1744 flush(&mut map, &mut pending);
1745 expect_peel = false;
1746 continue;
1747 }
1748 if line.starts_with('^') {
1749 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
1750 if let Some((_, _, peeled)) = pending.as_mut() {
1751 *peeled = Some(peel);
1752 }
1753 expect_peel = false;
1754 continue;
1755 }
1756 flush(&mut map, &mut pending);
1757 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1758 in_order &= prev_name <= refname;
1759 prev_name = refname;
1760 if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
1761 if keep.is_some() {
1762 pending = Some((refname, oid, None));
1763 } else {
1764 map.extend(Some((refname.to_owned(), oid)));
1765 }
1766 }
1767 expect_peel = true;
1768 }
1769 flush(&mut map, &mut pending);
1770 Ok((map, in_order))
1771}
1772
1773fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
1774 let packed_path = store::paths::packed_refs_path(store);
1775 let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
1778 Ok(d) => d,
1779 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
1780 Err(e) => return Err(e),
1781 };
1782 let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
1783 .map(|format| format.hash_algo())
1784 .unwrap_or(crate::objects::HashAlgo::Sha1);
1785 Ok(Some(PackedRefsFileView::from_data(
1786 &packed_path,
1787 data,
1788 hash_algo,
1789 )?))
1790}
1791
1792fn packed_line_is_record(line: &[u8], hex_len: usize) -> bool {
1795 line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^')
1796}
1797
1798fn packed_line_end(data: &[u8], line_start: usize) -> usize {
1799 memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
1800}
1801
1802fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
1803 std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
1804}
1805
1806fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
1807 let line_end = packed_line_end(data, line_start);
1808 if line_end >= data.len() || data[line_end] != b'\n' {
1809 return None;
1810 }
1811 let peel_start = line_end + 1;
1812 if peel_start >= data.len() || data[peel_start] != b'^' {
1813 return None;
1814 }
1815 let peel_end = packed_line_end(data, peel_start);
1816 let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
1817 ObjectId::from_hex(inner.trim()).ok()
1818}
1819
1820fn parse_packed_line_at(
1821 path: &Path,
1822 data: &[u8],
1823 line_start: usize,
1824 ref_opts: &RefNameOptions,
1825) -> Result<(String, ObjectId)> {
1826 let line = packed_line_as_str(data, line_start);
1827 parse_packed_ref_data_line(path, line, ref_opts)
1828}
1829
1830pub(crate) fn parse_packed_refs_to_map(
1831 path: &Path,
1832 data: &[u8],
1833) -> Result<HashMap<String, ObjectId>> {
1834 parse_packed_refs_to_map_with_prefix(path, data, None)
1835}
1836
1837fn packed_refs_leading_comments_end(data: &[u8]) -> usize {
1839 let mut end = 0;
1840 while data.get(end) == Some(&b'#') {
1841 match memchr::memchr(b'\n', &data[end..]) {
1842 Some(nl) => end += nl + 1,
1843 None => return data.len(),
1844 }
1845 }
1846 end
1847}
1848
1849fn packed_refs_file_declares_sorted(text: &str) -> bool {
1851 for line in text.split('\n') {
1852 if line.is_empty() {
1853 break;
1854 }
1855 if !line.starts_with('#') {
1856 break;
1857 }
1858 let body = line[1..].trim();
1859 let Some(traits) = body.strip_prefix("pack-refs with:") else {
1860 continue;
1861 };
1862 return traits.split_whitespace().any(|t| t == "sorted");
1863 }
1864 false
1865}
1866
1867fn parse_packed_refs_to_map_with_prefix(
1868 path: &Path,
1869 data: &[u8],
1870 prefix_filter: Option<&str>,
1871) -> Result<HashMap<String, ObjectId>> {
1872 let mut map = HashMap::new();
1873 visit_packed_refs_with_prefix(path, data, prefix_filter, |refname, oid| {
1874 map.insert(refname.to_owned(), oid);
1875 })?;
1876 Ok(map)
1877}
1878
1879fn visit_packed_refs_with_prefix(
1881 path: &Path,
1882 data: &[u8],
1883 prefix_filter: Option<&str>,
1884 mut visit: impl FnMut(&str, ObjectId),
1885) -> Result<()> {
1886 if data.is_empty() {
1887 return Ok(());
1888 }
1889 if data.contains(&b'\r') {
1890 return Err(packed_refs_unexpected_line(
1891 path,
1892 "<contains carriage return>",
1893 ));
1894 }
1895 if !data.ends_with(b"\n") {
1896 let tail = std::str::from_utf8(data)
1897 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1898 .unwrap_or("<invalid utf-8>");
1899 return Err(packed_refs_unterminated_line(path, tail));
1900 }
1901 let text = std::str::from_utf8(data)
1902 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1903
1904 let ref_opts = RefNameOptions {
1905 allow_onelevel: false,
1906 refspec_pattern: false,
1907 normalize: false,
1908 };
1909
1910 let mut expect_peel = false;
1911 let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
1912 let packed_sorted = packed_refs_file_declares_sorted(text);
1913 let prefix_upper = if packed_sorted {
1914 prefix_trim.and_then(list_prefix_upper_bound)
1915 } else {
1916 None
1917 };
1918 let mut lines = text.split('\n').peekable();
1919 while let Some(line) = lines.next() {
1920 if line.is_empty() {
1921 if lines.peek().is_some() {
1922 return Err(packed_refs_unexpected_line(path, line));
1923 }
1924 break;
1925 }
1926 if line.starts_with('#') {
1927 expect_peel = false;
1928 continue;
1929 }
1930 if line.starts_with('^') {
1931 parse_packed_ref_peel_line(path, line, expect_peel)?;
1932 expect_peel = false;
1933 continue;
1934 }
1935
1936 if let Some(prefix) = prefix_trim {
1937 let Some(refname) = packed_ref_data_line_refname(line) else {
1938 return Err(packed_refs_unexpected_line(path, line));
1939 };
1940 if packed_sorted {
1941 if refname < prefix {
1942 expect_peel = true;
1943 continue;
1944 }
1945 if let Some(upper) = &prefix_upper {
1946 if refname >= upper.as_str() {
1947 break;
1948 }
1949 }
1950 }
1951 if !ref_name_matches_list_prefix(refname, prefix) {
1952 expect_peel = true;
1953 continue;
1954 }
1955 }
1956 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1957 visit(refname, oid);
1958 expect_peel = true;
1959 }
1960 Ok(())
1961}
1962
1963pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
1964 read_packed_refs_map_for_prefix(store, "")
1965}
1966
1967pub(crate) fn read_packed_refs_map_for_prefix(
1968 store: &Path,
1969 prefix: &str,
1970) -> Result<HashMap<String, ObjectId>> {
1971 read_packed_refs_map_for_prefix_filtered(store, prefix, None)
1972}
1973
1974pub(crate) fn read_packed_refs_map_for_prefix_filtered(
1976 store: &Path,
1977 prefix: &str,
1978 keep: Option<PackedRecordFilter<'_>>,
1979) -> Result<HashMap<String, ObjectId>> {
1980 let Some(view) = read_packed_refs_file_view(store)? else {
1981 return Ok(HashMap::new());
1982 };
1983 let packed_path = store::paths::packed_refs_path(store);
1984 let filter = if prefix.is_empty() {
1985 None
1986 } else {
1987 Some(prefix)
1988 };
1989 view.map_for_prefix_filtered(&packed_path, filter, keep)
1990}
1991
1992#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1994enum PackedPeelTrait {
1995 None,
1997 Tags,
1999 All,
2001}
2002
2003impl PackedPeelTrait {
2004 fn covers(self, refname: &str) -> bool {
2006 match self {
2007 Self::None => false,
2008 Self::Tags => refname.starts_with("refs/tags/"),
2009 Self::All => true,
2010 }
2011 }
2012}
2013
2014pub(crate) struct PackedRefPeels {
2024 view: Option<PackedRefsFileView>,
2025 unsorted: Option<HashMap<Box<[u8]>, usize>>,
2027 peel_trait: PackedPeelTrait,
2028 next: std::cell::Cell<usize>,
2031}
2032
2033impl PackedRefPeels {
2034 pub(crate) fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
2044 if format != RefStorageFormat::Files {
2045 return Ok(Self::from_view(None));
2046 }
2047 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2048 Ok(Self::from_view(read_packed_refs_file_view(&store)?))
2049 }
2050
2051 pub(crate) fn for_store(store: &dyn store::RefStore, git_dir: &Path) -> Result<Self> {
2060 match store.as_files() {
2061 Some(files) => Ok(Self::from_view(files.packed_refs_view()?)),
2062 None => Self::load(git_dir, store.format()),
2063 }
2064 }
2065
2066 fn from_view(view: Option<PackedRefsFileView>) -> Self {
2067 let empty = Self {
2068 view: None,
2069 unsorted: None,
2070 peel_trait: PackedPeelTrait::None,
2071 next: std::cell::Cell::new(0),
2072 };
2073 let Some(view) = view else {
2074 return empty;
2075 };
2076 let peel_trait = view.peel_trait;
2077 if peel_trait == PackedPeelTrait::None {
2078 return empty;
2079 }
2080 let unsorted = (!view.sorted).then(|| {
2081 view.records()
2082 .map(|start| (Box::<[u8]>::from(view.record_name(start)), start))
2083 .collect()
2084 });
2085 let first = view
2086 .next_record(view.body_start, view.data.len())
2087 .unwrap_or(view.data.len());
2088 Self {
2089 view: Some(view),
2090 unsorted,
2091 peel_trait,
2092 next: std::cell::Cell::new(first),
2093 }
2094 }
2095
2096 pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
2108 if !self.peel_trait.covers(refname) {
2109 return None;
2110 }
2111 let view = self.view.as_ref()?;
2112 let start = match &self.unsorted {
2113 Some(map) => *map.get(refname.as_bytes())?,
2114 None => {
2115 let len = view.data.len();
2116 let hint = self.next.get();
2117 let start = if hint < len && view.record_cmp(hint, refname).is_eq() {
2118 hint
2119 } else {
2120 view.search(refname).ok()?
2121 };
2122 let after = packed_line_end(&view.data, start) + 1;
2123 self.next.set(view.next_record(after, len).unwrap_or(len));
2124 start
2125 }
2126 };
2127 let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
2128 let packed_oid = ObjectId::from_hex(hex).ok()?;
2129 (packed_oid == *oid).then(|| peel_oid_following_line(&view.data, start))
2130 }
2131}
2132
2133fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
2135 let Some(header) = text.strip_prefix("# pack-refs with: ") else {
2136 return PackedPeelTrait::None;
2137 };
2138 let traits = header.split('\n').next().unwrap_or_default();
2139 let mut peel_trait = PackedPeelTrait::None;
2140 for t in traits.split_whitespace() {
2141 match t {
2142 "fully-peeled" => return PackedPeelTrait::All,
2143 "peeled" => peel_trait = PackedPeelTrait::Tags,
2144 _ => {}
2145 }
2146 }
2147 peel_trait
2148}
2149
2150fn acquire_packed_refs_lock(
2151 git_dir: &Path,
2152 lock: &Path,
2153 lock_display: &Path,
2154) -> Result<crate::lockfile::LockFile> {
2155 use crate::lockfile::{LockError, LockFile, LockWait};
2156 let wait = LockWait::packed_refs_for_git_dir(git_dir);
2157 LockFile::acquire(lock, wait).map_err(|err| match err {
2158 LockError::Held { .. } => Error::from(RefLockError::PackedRefsLockHeld {
2159 lock_path: lock_display.display().to_string(),
2160 }),
2161 LockError::Io { source, .. } => Error::Io(source),
2162 })
2163}
2164
2165pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
2167 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2168 return Ok(None);
2169 };
2170 let packed_path = store::paths::packed_refs_path(git_dir);
2171 view.lookup_oid(&packed_path, refname)
2172}
2173
2174#[cfg(test)]
2176pub(crate) fn lookup_packed_ref_with_peel(
2177 git_dir: &Path,
2178 refname: &str,
2179) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
2180 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2181 return Ok(None);
2182 };
2183 let packed_path = store::paths::packed_refs_path(git_dir);
2184 view.lookup_entry(&packed_path, refname)
2185}
2186
2187#[cfg(test)]
2190pub(crate) fn test_commit(git_dir: &Path, message: &str) -> ObjectId {
2191 let common = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2192 let body = format!(
2193 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
2194 author T <t@example.com> 0 +0000\n\
2195 committer T <t@example.com> 0 +0000\n\n{message}\n"
2196 );
2197 crate::odb::Odb::new(&common.join("objects"))
2198 .write(crate::objects::ObjectKind::Commit, body.as_bytes())
2199 .unwrap_or_else(|err| panic!("store test commit {message:?}: {err}"))
2200}
2201
2202pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
2217 store_write_value(
2218 git_dir,
2219 refname,
2220 Some(StoreRawRef::Symbolic(target.to_owned())),
2221 Expected::Any,
2222 None,
2223 )
2224}
2225
2226pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
2227 if let Some(rest) = refname.strip_prefix("refs/") {
2228 if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
2229 return Err(Error::InvalidRef(format!(
2230 "refusing to update ref with bad name '{refname}'"
2231 )));
2232 }
2233 return Ok(());
2234 }
2235 if crate::check_ref_format::refname_is_safe(refname) {
2236 Ok(())
2237 } else {
2238 Err(Error::InvalidRef(format!(
2239 "refusing to update ref with bad name '{refname}'"
2240 )))
2241 }
2242}
2243
2244pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
2267 store_write_value(
2268 git_dir,
2269 refname,
2270 Some(StoreRawRef::Direct(*oid)),
2271 Expected::Any,
2272 None,
2273 )
2274}
2275
2276#[must_use]
2278pub fn ref_update_flags(refname: &str) -> RefUpdateFlags {
2279 legacy_ref_update_flags(refname)
2280}
2281
2282pub(crate) fn create_ref_with_reflog_on_store(
2286 store: &dyn RefStore,
2287 git_dir: &Path,
2288 config: &ConfigSet,
2289 refname: &str,
2290 oid: &ObjectId,
2291 identity: &str,
2292 message: &str,
2293) -> Result<()> {
2294 let reflog =
2295 should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
2296 identity: identity.to_owned(),
2297 message: message.to_owned(),
2298 time: time::OffsetDateTime::now_utc(),
2299 old_oid: None,
2300 new_oid: None,
2301 });
2302 store_write_value_on_store(
2303 store,
2304 git_dir,
2305 refname,
2306 Some(StoreRawRef::Direct(*oid)),
2307 Expected::Missing,
2308 reflog,
2309 )
2310}
2311
2312fn write_ref_at_storage(
2315 git_dir: &Path,
2316 storage_dir: &Path,
2317 refname: &str,
2318 oid: &ObjectId,
2319) -> Result<()> {
2320 let stor = crate::ref_namespace::storage_ref_name(refname);
2321 let path = storage_dir.join(stor);
2322 remove_empty_ref_directory(&path);
2329 if fs::symlink_metadata(&path)
2332 .map(|m| m.file_type().is_dir())
2333 .unwrap_or(false)
2334 {
2335 return Err(directory_in_the_way(git_dir, refname, &path));
2336 }
2337 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
2338 return Err(Error::InvalidRef(format!(
2339 "cannot update ref '{refname}': reference broken"
2340 )));
2341 }
2342 if let Some(parent) = path.parent() {
2343 crate::git_dir::io_error::io_with_git_path(
2344 git_dir,
2345 parent,
2346 crate::git_dir::GitDirPathOperation::CreateDir,
2347 fs::create_dir_all(parent),
2348 )?;
2349 }
2350 let content = format!("{oid}\n");
2351 let lock_path = lock_path_for_ref(&path);
2354 use crate::lockfile::{LockError, LockFile, LockWait};
2355 use std::io::Write as _;
2356 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
2357 let mut lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
2358 held @ LockError::Held { .. } => Error::from(held),
2359 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2360 git_dir,
2361 &lock_path,
2362 crate::git_dir::GitDirPathOperation::UpdateRef,
2363 source,
2364 ),
2365 })?;
2366 crate::git_dir::io_error::io_with_git_path(
2367 git_dir,
2368 &path,
2369 crate::git_dir::GitDirPathOperation::UpdateRef,
2370 lock.write_all(content.as_bytes()),
2371 )?;
2372 lock.commit_to(&path).map_err(|err| match err {
2373 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2374 git_dir,
2375 &path,
2376 crate::git_dir::GitDirPathOperation::UpdateRef,
2377 source,
2378 ),
2379 held @ LockError::Held { .. } => Error::from(held),
2380 })?;
2381 Ok(())
2382}
2383
2384pub fn write_ref_cas(
2398 git_dir: &Path,
2399 refname: &str,
2400 oid: &ObjectId,
2401 expected_old: ObjectId,
2402) -> Result<()> {
2403 store_write_value(
2404 git_dir,
2405 refname,
2406 Some(StoreRawRef::Direct(*oid)),
2407 Expected::Oid(expected_old),
2408 None,
2409 )
2410}
2411
2412pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
2425 store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
2426}
2427
2428fn prune_loose_ref_cas(
2438 git_dir: &Path,
2439 refname: &str,
2440 expected: ObjectId,
2441 wait: crate::lockfile::LockWait,
2442) -> Result<bool> {
2443 ensure_refname_safe_for_storage(refname)?;
2444 let storage_dir = ref_storage_dir(git_dir, refname);
2445 let stor = crate::ref_namespace::storage_ref_name(refname);
2446 let path = storage_dir.join(&stor);
2447 let lock_path = lock_path_for_ref(&path);
2448 use crate::lockfile::LockFile;
2449 let _lock = LockFile::acquire(&lock_path, wait)?;
2450 if !path.is_file() {
2451 return Ok(true);
2452 }
2453 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
2454 return Ok(false);
2455 };
2456 if oid != expected {
2457 return Ok(false);
2458 }
2459 fs::remove_file(&path).map_err(Error::Io)?;
2460 Ok(true)
2461}
2462
2463pub struct PackedRefs {
2479 map: HashMap<String, ObjectId>,
2480 sorted_names: BTreeSet<String>,
2481}
2482
2483fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
2484 let sorted_names = map.keys().cloned().collect();
2485 PackedRefs { map, sorted_names }
2486}
2487
2488impl PackedRefs {
2489 pub fn load(git_dir: &Path) -> Result<Self> {
2498 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2499 let map = read_packed_refs_map(&store)?;
2500 Ok(packed_refs_from_map(map))
2501 }
2502
2503 #[must_use]
2505 pub fn get(&self, refname: &str) -> Option<ObjectId> {
2506 self.map.get(refname).copied()
2507 }
2508
2509 #[must_use]
2513 pub fn has_namespace_conflict(&self, refname: &str) -> bool {
2514 packed_refname_namespace_conflict(&self.sorted_names, refname)
2515 }
2516
2517 pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
2520 self.map.insert(refname.to_owned(), oid);
2521 self.sorted_names.insert(refname.to_owned());
2522 }
2523}
2524
2525pub fn resolve_ref_cached(
2535 git_dir: &Path,
2536 refname: &str,
2537 packed: &PackedRefs,
2538) -> Result<Option<ObjectId>> {
2539 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2540 return Ok(resolve_ref(git_dir, refname).ok());
2541 }
2542 resolve_files_ref_cached(git_dir, refname, packed)
2543}
2544
2545fn resolve_files_ref_cached(
2546 git_dir: &Path,
2547 refname: &str,
2548 packed: &PackedRefs,
2549) -> Result<Option<ObjectId>> {
2550 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
2551 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
2552 let try_names: Vec<&str> = if storage_owned != stor_name {
2553 vec![storage_owned.as_str(), stor_name.as_str()]
2554 } else {
2555 vec![stor_name.as_str()]
2556 };
2557
2558 for name in &try_names {
2559 let path = store.join(name);
2560 match read_ref_file(&path) {
2561 Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
2562 Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
2563 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
2564 Err(e) => return Err(e),
2565 }
2566 }
2567
2568 for name in &try_names {
2569 if let Some(oid) = packed.get(name) {
2570 return Ok(Some(oid));
2571 }
2572 }
2573 Ok(None)
2574}
2575
2576pub fn write_ref_cached(
2585 git_dir: &Path,
2586 refname: &str,
2587 oid: &ObjectId,
2588 packed: &mut PackedRefs,
2589) -> Result<()> {
2590 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2591 return write_ref(git_dir, refname, oid);
2592 }
2593 ensure_refname_safe_for_storage(refname)?;
2594 if packed.has_namespace_conflict(refname) {
2595 return Err(Error::InvalidRef(format!(
2596 "cannot update ref '{refname}': reference namespace conflict"
2597 )));
2598 }
2599 let storage_dir = ref_storage_dir(git_dir, refname);
2600 write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
2601 packed.record_loose_write(refname, *oid);
2602 Ok(())
2603}
2604
2605fn ref_path_for_display(path: &Path) -> String {
2610 let cwd = crate::environment::Environment::empty().cwd.clone();
2611 if let Ok(rel) = path.strip_prefix(&cwd) {
2612 return rel.to_string_lossy().into_owned();
2613 }
2614 if let (Ok(cwd_c), Ok(path_c)) = (
2618 cwd.canonicalize_simplified(),
2619 path.canonicalize_simplified(),
2620 ) {
2621 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
2622 return rel.to_string_lossy().into_owned();
2623 }
2624 }
2625 path.to_string_lossy().into_owned()
2626}
2627
2628pub(crate) fn remove_empty_ref_directory(path: &Path) {
2634 match fs::symlink_metadata(path) {
2635 Ok(meta) if meta.file_type().is_dir() => {}
2636 _ => return,
2637 }
2638 if dir_tree_has_files(path) {
2641 return;
2642 }
2643 let _ = remove_dir_tree(path);
2644}
2645
2646fn dir_tree_has_files(dir: &Path) -> bool {
2648 let Ok(entries) = fs::read_dir(dir) else {
2649 return true;
2651 };
2652 for entry in entries.flatten() {
2653 match entry.file_type() {
2654 Ok(ft) if ft.is_dir() => {
2655 if dir_tree_has_files(&entry.path()) {
2656 return true;
2657 }
2658 }
2659 Ok(_) => return true, Err(_) => return true,
2661 }
2662 }
2663 false
2664}
2665
2666pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
2668 if let Some(dir) = ref_path.parent() {
2669 prune_empty_ref_dirs(storage_dir, dir);
2670 }
2671}
2672
2673pub(crate) fn prune_empty_ref_dirs(storage_dir: &Path, dir: &Path) {
2678 let refs_root = storage_dir.join("refs");
2679 let mut current = Some(dir);
2680 while let Some(dir) = current {
2681 if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
2682 break;
2683 }
2684 if dir
2685 .parent()
2686 .is_some_and(|parent| parent == refs_root.as_path())
2687 {
2688 break;
2689 }
2690 if dir_tree_has_files(dir) {
2691 break;
2692 }
2693 if remove_dir_tree(dir).is_err() {
2694 break;
2695 }
2696 current = dir.parent();
2697 }
2698}
2699
2700fn remove_dir_tree(dir: &Path) -> io::Result<()> {
2702 for entry in fs::read_dir(dir)? {
2703 let entry = entry?;
2704 if entry.file_type()?.is_dir() {
2705 remove_dir_tree(&entry.path())?;
2706 }
2707 }
2708 fs::remove_dir(dir)
2709}
2710
2711pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
2727 if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
2728 return Err(store::RefStoreError::not_found(refname).into());
2729 }
2730 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2731 let store = open_ref_store(git_dir)?;
2732 let mut flags = legacy_ref_update_flags(refname);
2733 if refname != "HEAD" {
2734 if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
2735 flags.no_deref = true;
2736 }
2737 }
2738 return commit_store_update(
2739 git_dir,
2740 RefUpdate {
2741 name: refname.to_owned(),
2742 new_value: None,
2743 expected: Expected::Exists,
2744 reflog: None,
2745 flags,
2746 },
2747 )
2748 .map_err(|e| map_public_ref_store_error(e, refname));
2749 }
2750 ensure_refname_safe_for_storage(refname)?;
2751 let storage_dir = ref_storage_dir(git_dir, refname);
2752 let stor = crate::ref_namespace::storage_ref_name(refname);
2753 let path = storage_dir.join(&stor);
2754 let lock = lock_path_for_ref(&path);
2755 if fs::symlink_metadata(&lock).is_ok() {
2756 return Err(Error::from(crate::lockfile::LockError::Held { path: lock }));
2757 }
2758
2759 remove_packed_ref(&storage_dir, &stor)?;
2765
2766 remove_empty_ref_directory(&path);
2770 match fs::remove_file(&path) {
2771 Ok(()) => {}
2772 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
2773 Err(e)
2774 if e.kind() == io::ErrorKind::NotADirectory
2775 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
2776 Err(e)
2779 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
2780 Err(e) => return Err(Error::Io(e)),
2781 }
2782
2783 prune_empty_loose_ref_parents(&storage_dir, &path);
2784
2785 let log_path = storage_dir.join("logs").join(&stor);
2786
2787 let _ = fs::remove_file(&log_path);
2793
2794 let logs_root = storage_dir.join("logs");
2795 let mut parent = log_path.parent();
2796 while let Some(p) = parent {
2797 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
2798 break;
2799 }
2800 if fs::remove_dir(p).is_err() {
2801 break; }
2803 parent = p.parent();
2804 }
2805
2806 Ok(())
2807}
2808
2809pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
2811 remove_packed_refs_inner(git_dir, &|name| name == refname, false, false).map(drop)
2812}
2813
2814pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
2815 remove_packed_refs_inner(git_dir, &|name| name == refname, false, true).map(drop)
2816}
2817
2818pub(crate) fn remove_packed_refs_under_lock(
2828 git_dir: &Path,
2829 refnames: &HashSet<String>,
2830) -> Result<Vec<String>> {
2831 if refnames.is_empty() {
2832 return Ok(Vec::new());
2833 }
2834 remove_packed_refs_inner(git_dir, &|name| refnames.contains(name), true, true)
2835}
2836
2837pub(crate) fn packed_record_name(record: &str) -> &str {
2839 let line = record.lines().next().unwrap_or("");
2840 line.split_once(' ').map_or("", |(_, name)| name.trim())
2841}
2842
2843pub(crate) fn packed_record_oid(record: &str) -> Option<ObjectId> {
2845 let line = record.lines().next()?;
2846 ObjectId::from_hex(line.split_once(' ')?.0).ok()
2847}
2848
2849pub(crate) fn restore_packed_records_under_lock(git_dir: &Path, records: &[&str]) -> Result<()> {
2856 if records.is_empty() {
2857 return Ok(());
2858 }
2859 let packed_path = store::paths::packed_refs_path(git_dir);
2860 let content = match fs::read(&packed_path) {
2861 Ok(c) => c,
2862 Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
2863 Err(e) => return Err(Error::Io(e)),
2864 };
2865 parse_packed_refs_to_map(&packed_path, &content)?;
2866 let content = String::from_utf8(content)
2867 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
2868
2869 let mut blocks: Vec<String> = Vec::new();
2870 for line in content.lines() {
2871 if line.starts_with('#') {
2872 continue;
2873 }
2874 match blocks.last_mut() {
2875 Some(block) if line.starts_with('^') => block.push_str(line),
2876 _ => blocks.push(line.to_owned()),
2877 }
2878 if let Some(block) = blocks.last_mut() {
2879 block.push('\n');
2880 }
2881 }
2882 blocks.extend(records.iter().map(|r| (*r).to_owned()));
2883 blocks.sort_by(|a, b| packed_record_name(a).cmp(packed_record_name(b)));
2884
2885 let mut out =
2886 String::with_capacity(content.len() + records.iter().map(|r| r.len()).sum::<usize>());
2887 out.push_str(PACKED_REFS_HEADER);
2888 for block in &blocks {
2889 out.push_str(block);
2890 }
2891 replace_packed_refs_file(git_dir, &packed_path, &out, true)
2892}
2893
2894const PACKED_REFS_HEADER: &str = "# pack-refs with: peeled fully-peeled sorted\n";
2895
2896fn remove_packed_refs_inner(
2897 git_dir: &Path,
2898 remove: &dyn Fn(&str) -> bool,
2899 keep_removed: bool,
2900 lock_already_held: bool,
2901) -> Result<Vec<String>> {
2902 let mut removed: Vec<String> = Vec::new();
2903 let packed_path = store::paths::packed_refs_path(git_dir);
2904 let content = match fs::read(&packed_path) {
2905 Ok(c) => c,
2906 Err(e)
2907 if e.kind() == io::ErrorKind::NotFound
2908 || e.kind() == io::ErrorKind::NotADirectory
2909 || e.raw_os_error() == Some(libc::ENOTDIR) =>
2910 {
2911 return Ok(removed);
2912 }
2913 Err(e) => return Err(Error::Io(e)),
2914 };
2915 let mut has_removed_record = false;
2918 visit_packed_refs_with_prefix(&packed_path, &content, None, |name, _| {
2919 has_removed_record |= remove(name);
2920 })?;
2921 if !has_removed_record {
2922 return Ok(removed);
2923 }
2924
2925 let content = String::from_utf8(content)
2926 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
2927
2928 let mut out = String::with_capacity(content.len());
2929 let mut skip_peeled = false;
2930 let mut changed = false;
2931 let mut header_written = false;
2934
2935 for line in content.lines() {
2936 if skip_peeled {
2937 if line.starts_with('^') {
2938 if let Some(record) = removed.last_mut().filter(|_| keep_removed) {
2939 record.push_str(line);
2940 record.push('\n');
2941 }
2942 changed = true;
2943 continue;
2944 }
2945 skip_peeled = false;
2946 }
2947
2948 if line.starts_with('#') {
2949 continue;
2951 }
2952 if line.starts_with('^') {
2953 out.push_str(line);
2954 out.push('\n');
2955 continue;
2956 }
2957
2958 if !header_written {
2960 out.insert_str(0, PACKED_REFS_HEADER);
2961 header_written = true;
2962 }
2963
2964 let name = line.split_once(' ').map_or("", |(_, name)| name.trim());
2965 if remove(name) {
2966 if keep_removed {
2967 removed.push(format!("{line}\n"));
2968 }
2969 changed = true;
2970 skip_peeled = true;
2971 continue;
2972 }
2973
2974 out.push_str(line);
2975 out.push('\n');
2976 }
2977
2978 if changed {
2979 replace_packed_refs_file(git_dir, &packed_path, &out, lock_already_held)?;
2980 }
2981
2982 Ok(removed)
2983}
2984
2985fn replace_packed_refs_file(
2987 git_dir: &Path,
2988 packed_path: &Path,
2989 body: &str,
2990 lock_already_held: bool,
2991) -> Result<()> {
2992 let lock = lock_path_for_ref(packed_path); let _packed_lock = if !lock_already_held {
3000 let abs_lock = git_dir
3001 .canonicalize_simplified()
3002 .map(|d| d.join("packed-refs.lock"))
3003 .unwrap_or_else(|_| lock.clone());
3004 Some(acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?)
3005 } else {
3006 None
3007 };
3008
3009 let tmp = packed_path.with_extension("new");
3010 let mut created_tmp = false;
3011 let write_result = (|| -> Result<()> {
3012 let mut file = std::fs::OpenOptions::new()
3014 .write(true)
3015 .create_new(true)
3016 .open(&tmp)
3017 .map_err(Error::Io)?;
3018 created_tmp = true;
3019 use std::io::Write as _;
3020 file.write_all(body.as_bytes()).map_err(Error::Io)?;
3021 drop(file);
3022 fs::rename(&tmp, packed_path).map_err(Error::Io)?;
3023 created_tmp = false; Ok(())
3025 })();
3026
3027 if write_result.is_err() && created_tmp {
3028 let _ = fs::remove_file(&tmp);
3029 }
3030 write_result
3031}
3032
3033pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
3046 read_symbolic_ref(git_dir, "HEAD")
3047}
3048
3049pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
3061 match open_ref_store(git_dir)?.read_raw(refname)? {
3062 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3063 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3064 }
3065}
3066
3067pub fn read_symbolic_ref_for_repository(
3069 repo: &crate::repo::Repository,
3070 refname: &str,
3071) -> Result<Option<String>> {
3072 match repo.refs().read_raw(refname)? {
3073 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3074 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3075 }
3076}
3077
3078#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3080pub enum LogRefsConfig {
3081 Unset,
3083 None,
3085 Normal,
3087 Always,
3089}
3090
3091impl LogRefsConfig {
3092 pub(crate) fn parse(raw: &str) -> Option<Self> {
3094 match raw.trim().to_ascii_lowercase().as_str() {
3095 "always" => Some(Self::Always),
3096 "1" | "true" | "yes" | "on" => Some(Self::Normal),
3097 "0" | "false" | "no" | "off" | "never" => Some(Self::None),
3098 _ => None,
3099 }
3100 }
3101}
3102
3103pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3107 ConfigSet::load(
3108 &crate::environment::Environment::empty(),
3109 Some(git_dir),
3110 true,
3111 )
3112 .map(|cfg| cfg.log_refs_config())
3113 .unwrap_or(LogRefsConfig::Unset)
3114}
3115
3116pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3120 ConfigSet::load(
3121 &crate::environment::Environment::empty(),
3122 Some(git_dir),
3123 true,
3124 )
3125 .map(|cfg| cfg.effective_log_refs_config())
3126 .unwrap_or(LogRefsConfig::Normal)
3127}
3128
3129#[must_use]
3132pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
3133 match mode {
3134 LogRefsConfig::Always => true,
3135 LogRefsConfig::Normal => {
3136 refname == "HEAD"
3137 || refname.starts_with("refs/heads/")
3138 || refname.starts_with("refs/remotes/")
3139 || refname.starts_with("refs/notes/")
3140 }
3141 LogRefsConfig::None | LogRefsConfig::Unset => false,
3142 }
3143}
3144
3145#[must_use]
3147pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
3148 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
3149}
3150
3151#[must_use]
3153pub fn should_autocreate_reflog_with_config(
3154 config: &ConfigSet,
3155 _git_dir: &Path,
3156 refname: &str,
3157) -> bool {
3158 should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
3159}
3160
3161fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
3187 let Ok(rel) = target.strip_prefix(logs_root) else {
3188 return;
3189 };
3190 let mut cur = logs_root.to_path_buf();
3191 for component in rel.components() {
3192 cur.push(component);
3193 match fs::symlink_metadata(&cur) {
3194 Ok(meta) if meta.file_type().is_dir() => {}
3195 Ok(_) => {
3196 let _ = fs::remove_file(&cur);
3199 }
3200 Err(_) => break, }
3202 }
3203}
3204
3205pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
3208 let width = if !old.is_zero() {
3209 old.algo().hex_len()
3210 } else if !new.is_zero() {
3211 new.algo().hex_len()
3212 } else {
3213 old.algo().hex_len()
3214 };
3215 let render = |oid: &ObjectId| {
3216 if oid.is_zero() {
3217 "0".repeat(width)
3218 } else {
3219 oid.to_hex()
3220 }
3221 };
3222 (render(old), render(new))
3223}
3224
3225fn map_reflog_store_validation(err: store::RefStoreError) -> Error {
3226 match err {
3227 store::RefStoreError::InvalidReflog(sig) => Error::InvalidSignature(sig),
3228 other => Error::from(other),
3229 }
3230}
3231
3232fn validate_reflog_message(message: &str) -> Result<()> {
3233 store::validate_reflog_message(message).map_err(map_reflog_store_validation)
3234}
3235
3236fn validate_reflog_identity_string(identity: &str) -> Result<()> {
3237 store::validate_reflog_identity_string(identity).map_err(map_reflog_store_validation)
3238}
3239
3240fn validate_reflog_update(log: &ReflogUpdate) -> Result<()> {
3242 store::validate_reflog_update(log).map_err(map_reflog_store_validation)
3243}
3244
3245pub fn append_reflog_with_signature(
3251 git_dir: &Path,
3252 refname: &str,
3253 old_oid: &ObjectId,
3254 new_oid: &ObjectId,
3255 identity: &Signature,
3256 message: &str,
3257 force_create: bool,
3258) -> Result<()> {
3259 identity.validate().map_err(Error::InvalidSignature)?;
3260 let formatted = format_reflog_identity(&identity.reflog_name_email(), identity.time);
3261 append_reflog_with_config(
3262 git_dir,
3263 refname,
3264 old_oid,
3265 new_oid,
3266 &formatted,
3267 message,
3268 force_create,
3269 None,
3270 )
3271}
3272
3273pub fn append_reflog(
3282 git_dir: &Path,
3283 refname: &str,
3284 old_oid: &ObjectId,
3285 new_oid: &ObjectId,
3286 identity: &str,
3287 message: &str,
3288 force_create: bool,
3289) -> Result<()> {
3290 append_reflog_with_config(
3291 git_dir,
3292 refname,
3293 old_oid,
3294 new_oid,
3295 identity,
3296 message,
3297 force_create,
3298 None,
3299 )
3300}
3301
3302#[allow(clippy::too_many_arguments)]
3304pub fn append_reflog_with_config(
3305 git_dir: &Path,
3306 refname: &str,
3307 old_oid: &ObjectId,
3308 new_oid: &ObjectId,
3309 identity: &str,
3310 message: &str,
3311 force_create: bool,
3312 config: Option<&ConfigSet>,
3313) -> Result<()> {
3314 validate_reflog_identity_string(identity)?;
3315 validate_reflog_message(message)?;
3316 test_inject_reflog_fail(refname)?;
3317 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3318 return crate::reftable::reftable_append_reflog(
3319 git_dir,
3320 refname,
3321 old_oid,
3322 new_oid,
3323 identity,
3324 message,
3325 force_create,
3326 );
3327 }
3328 let storage_dir = ref_storage_dir(git_dir, refname);
3329 let stor = crate::ref_namespace::storage_ref_name(refname);
3330 let log_path = storage_dir.join("logs").join(&stor);
3331 let may_create = force_create
3332 || match config {
3333 Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
3334 None => should_autocreate_reflog(git_dir, refname),
3335 };
3336 if !may_create && !log_path.exists() {
3337 return Ok(());
3338 }
3339 if let Some(parent) = log_path.parent() {
3340 let logs_root = storage_dir.join("logs");
3345 clear_conflicting_reflog_files(&logs_root, parent);
3346 crate::git_dir::io_error::io_with_git_path(
3347 git_dir,
3348 parent,
3349 crate::git_dir::GitDirPathOperation::CreateDir,
3350 fs::create_dir_all(parent),
3351 )?;
3352 }
3353 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
3358 let line = if message.is_empty() {
3359 format!("{old_hex} {new_hex} {identity}\n")
3360 } else {
3361 format!("{old_hex} {new_hex} {identity}\t{message}\n")
3362 };
3363 let mut file = fs::OpenOptions::new()
3364 .create(true)
3365 .append(true)
3366 .open(&log_path)
3367 .map_err(|source| {
3368 crate::git_dir::io_error::map_git_dir_io(
3369 git_dir,
3370 &log_path,
3371 crate::git_dir::GitDirPathOperation::AppendReflog,
3372 source,
3373 )
3374 })?;
3375 use io::Write;
3376 crate::git_dir::io_error::io_with_git_path(
3377 git_dir,
3378 &log_path,
3379 crate::git_dir::GitDirPathOperation::AppendReflog,
3380 file.write_all(line.as_bytes()),
3381 )?;
3382 Ok(())
3383}
3384
3385fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
3386 if all {
3387 b"*"
3388 } else {
3389 b"refs/tags/*"
3390 }
3391}
3392
3393fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
3394 crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
3395}
3396
3397fn collect_loose_ref_files(
3398 dir: &Path,
3399 prefix: &str,
3400 out: &mut Vec<(String, PathBuf)>,
3401) -> Result<()> {
3402 let read = match fs::read_dir(dir) {
3403 Ok(r) => r,
3404 Err(e)
3405 if e.kind() == io::ErrorKind::NotFound
3406 || e.kind() == io::ErrorKind::NotADirectory
3407 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3408 {
3409 return Ok(());
3410 }
3411 Err(e) => return Err(Error::Io(e)),
3412 };
3413 for entry in read {
3414 let entry = entry?;
3415 let name = entry.file_name().to_string_lossy().to_string();
3416 let refname = format!("{prefix}{name}");
3417 let path = entry.path();
3418 if path.is_dir() {
3419 collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
3420 } else if path.is_file() {
3421 out.push((refname, path));
3422 }
3423 }
3424 Ok(())
3425}
3426
3427fn load_packed_refs_with_peel(
3428 packed_path: &Path,
3429 merged: &mut HashMap<String, (ObjectId, Option<ObjectId>)>,
3430) -> Result<()> {
3431 if !packed_path.is_file() {
3432 return Ok(());
3433 }
3434 let data = fs::read(packed_path)?;
3435 parse_packed_refs_to_map(packed_path, &data)?;
3436 let content = String::from_utf8(data)
3437 .map_err(|_| packed_refs_unexpected_line(packed_path, "<invalid utf-8>"))?;
3438 let mut last_name: Option<String> = None;
3439 for line in content.lines() {
3440 if line.starts_with('#') {
3441 continue;
3442 }
3443 if let Some(hex) = line.strip_prefix('^') {
3444 let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
3445 continue;
3446 };
3447 if let Some(name) = &last_name {
3448 if let Some(entry) = merged.get_mut(name) {
3449 entry.1 = Some(peel_oid);
3450 }
3451 }
3452 continue;
3453 }
3454 let mut parts = line.splitn(2, ' ');
3455 let Some(hex) = parts.next() else {
3456 continue;
3457 };
3458 let Some(name) = parts.next() else {
3459 continue;
3460 };
3461 let name = name.trim();
3462 let Ok(oid) = ObjectId::from_hex(hex) else {
3463 continue;
3464 };
3465 merged.insert(name.to_owned(), (oid, None));
3466 last_name = Some(name.to_owned());
3467 }
3468 Ok(())
3469}
3470
3471pub(crate) fn pack_refs_for_repository(
3473 repo: &crate::repo::Repository,
3474 all: bool,
3475 dry_run: bool,
3476) -> Result<(PathBuf, usize, usize)> {
3477 let git_dir = repo.git_dir();
3478 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3479 return pack_refs_reftable(git_dir, dry_run);
3480 }
3481 pack_refs_files(git_dir, repo.odb(), all, dry_run)
3482}
3483
3484fn pack_refs_reftable(git_dir: &Path, dry_run: bool) -> Result<(PathBuf, usize, usize)> {
3485 use crate::reftable::ReftableStack;
3486 let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3487 let rt_dir = store_git_dir.join("reftable");
3488 let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
3489 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3490 .unwrap_or(0);
3491 if dry_run {
3492 let tables_pruned = tables_before.saturating_sub(1);
3493 return Ok((rt_dir, 0, tables_pruned));
3494 }
3495 let mut stack = ReftableStack::open(&store_git_dir)?;
3496 stack.compact()?;
3497 let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
3498 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3499 .unwrap_or(0);
3500 Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
3501}
3502
3503fn pack_refs_files(
3504 git_dir: &Path,
3505 odb: &crate::odb::Odb,
3506 all: bool,
3507 dry_run: bool,
3508) -> Result<(PathBuf, usize, usize)> {
3509 pack_refs_files_matching(
3510 git_dir,
3511 odb,
3512 |name| ref_matches_pack_include(name, all),
3513 dry_run,
3514 false,
3515 )
3516}
3517
3518fn pack_refs_files_matching(
3519 git_dir: &Path,
3520 odb: &crate::odb::Odb,
3521 include: impl Fn(&str) -> bool,
3522 dry_run: bool,
3523 allow_missing: bool,
3524) -> Result<(PathBuf, usize, usize)> {
3525 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3526 let packed_path = store::paths::packed_refs_path(&storage_git_dir);
3527
3528 let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
3529 load_packed_refs_with_peel(&packed_path, &mut merged)?;
3530
3531 let mut loose_files = Vec::new();
3532 collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
3533
3534 let mut packed_loose = 0usize;
3535 let mut to_prune: Vec<(String, ObjectId)> = Vec::new();
3536
3537 for (refname, path) in loose_files {
3538 if crate::worktree_ref::is_per_worktree_ref(&refname) {
3539 continue;
3540 }
3541 if !include(&refname) {
3542 continue;
3543 }
3544 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
3545 continue;
3546 };
3547 let object = odb.try_read_once(&oid).ok().flatten();
3548 if object.is_none() && !allow_missing {
3549 continue;
3550 }
3551 let peel = if object.is_some_and(|object| object.kind == crate::objects::ObjectKind::Tag) {
3552 packed_ref_peel(odb, oid)
3553 } else {
3554 None
3555 };
3556 merged.insert(refname.clone(), (oid, peel));
3557 to_prune.push((refname, oid));
3558 let _ = path;
3559 packed_loose += 1;
3560 }
3561
3562 if packed_loose == 0 {
3563 return Ok((packed_path, 0, 0));
3564 }
3565
3566 if dry_run {
3567 return Ok((packed_path, packed_loose, to_prune.len()));
3568 }
3569
3570 let mut names: Vec<String> = merged.keys().cloned().collect();
3571 names.sort();
3572
3573 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
3574 for name in &names {
3575 let Some((oid, peel)) = merged.get(name) else {
3576 continue;
3577 };
3578 body.push_str(&oid.to_hex());
3579 body.push(' ');
3580 body.push_str(name);
3581 body.push('\n');
3582 if let Some(p) = peel {
3583 body.push('^');
3584 body.push_str(&p.to_hex());
3585 body.push('\n');
3586 }
3587 }
3588
3589 atomic_rewrite_packed_refs(&storage_git_dir, &body)?;
3590
3591 #[cfg(test)]
3592 pack_refs_test_hooks::run_before_prune();
3593
3594 let mut pruned = 0usize;
3595 let mut parents = HashSet::new();
3596 let wait = crate::lockfile::LockWait::files_ref_lock_for_git_dir(git_dir);
3597 for (refname, expected) in &to_prune {
3598 match prune_loose_ref_cas(git_dir, refname, *expected, wait) {
3599 Ok(true) => {
3600 pruned += 1;
3601 let path = storage_git_dir.join(crate::ref_namespace::storage_ref_name(refname));
3602 if let Some(parent) = path.parent() {
3603 parents.insert(parent.to_path_buf());
3604 }
3605 }
3606 Ok(false) => {
3607 remove_packed_ref(git_dir, refname)?;
3608 }
3609 Err(e) => return Err(e),
3610 }
3611 }
3612
3613 for parent in &parents {
3616 prune_empty_ref_dirs(&storage_git_dir, parent);
3617 }
3618
3619 Ok((packed_path, packed_loose, pruned))
3620}
3621
3622#[cfg(test)]
3624mod pack_refs_test_hooks {
3625 use std::cell::RefCell;
3626
3627 thread_local! {
3628 static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
3629 }
3630
3631 pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
3632 BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
3633 }
3634
3635 pub fn run_before_prune() {
3636 BEFORE_PRUNE.with(|c| {
3637 if let Some(f) = c.borrow_mut().take() {
3638 f();
3639 }
3640 });
3641 }
3642}
3643
3644#[cfg(test)]
3648mod reflog_fail_inject {
3649 use std::cell::RefCell;
3650
3651 thread_local! {
3653 pub static INJECT: RefCell<Option<String>> = const { RefCell::new(None) };
3654 }
3655}
3656
3657#[cfg(test)]
3659pub fn set_test_inject_reflog_fail(refname: Option<&str>) {
3660 reflog_fail_inject::INJECT.with(|c| {
3661 *c.borrow_mut() = refname.map(str::to_owned);
3662 });
3663}
3664
3665#[cfg(test)]
3666fn test_inject_reflog_fail(refname: &str) -> Result<()> {
3667 reflog_fail_inject::INJECT.with(|c| {
3668 if c.borrow().as_deref() == Some(refname) {
3669 return Err(Error::Io(std::io::Error::other("injected reflog failure")));
3670 }
3671 Ok(())
3672 })
3673}
3674
3675#[cfg(not(test))]
3676fn test_inject_reflog_fail(_refname: &str) -> Result<()> {
3677 Ok(())
3678}
3679
3680pub struct BranchCommitRefUpdate<'a> {
3693 pub branch_ref: &'a str,
3695 pub expected_old: Option<ObjectId>,
3697 pub new_oid: ObjectId,
3699 pub identity: &'a str,
3701 pub reflog_message: &'a str,
3703}
3704
3705pub fn verify_branch_commit_cas(
3715 expected: Option<ObjectId>,
3716 current: Option<ObjectId>,
3717 refname: &str,
3718) -> Result<()> {
3719 verify_branch_cas(expected, current, refname)
3720}
3721
3722fn verify_branch_cas(
3723 expected: Option<ObjectId>,
3724 current: Option<ObjectId>,
3725 refname: &str,
3726) -> Result<()> {
3727 if expected == current {
3728 return Ok(());
3729 }
3730 Err(store::RefStoreError::ExpectedMismatch {
3731 name: refname.to_owned(),
3732 expected: expected.map_or(Expected::Missing, Expected::Oid),
3733 actual: current.map(StoreRawRef::Direct),
3734 }
3735 .into())
3736}
3737
3738fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
3739 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
3740 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
3741 let try_names: Vec<&str> = if storage_owned != stor_name {
3742 vec![storage_owned.as_str(), stor_name.as_str()]
3743 } else {
3744 vec![stor_name.as_str()]
3745 };
3746 for name in try_names {
3747 let path = store.join(name);
3748 if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
3749 return Ok(Some(oid));
3750 }
3751 if let Some(oid) = lookup_packed_ref(&store, name)? {
3752 return Ok(Some(oid));
3753 }
3754 }
3755 Ok(None)
3756}
3757
3758#[cfg(test)]
3759mod test_pause_inject_serial {
3760 use std::sync::{Mutex, OnceLock};
3761
3762 static SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
3764
3765 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
3766 SERIAL
3767 .get_or_init(|| Mutex::new(()))
3768 .lock()
3769 .unwrap_or_else(|e| e.into_inner())
3770 }
3771}
3772
3773#[cfg(test)]
3775pub fn test_pause_injection_serial_guard() -> std::sync::MutexGuard<'static, ()> {
3776 test_pause_inject_serial::lock()
3777}
3778
3779#[cfg(test)]
3780mod branch_ref_pause_inject {
3781 use std::sync::{Condvar, Mutex, OnceLock};
3782
3783 pub struct PauseGate {
3784 pub wait: Mutex<bool>,
3785 pub cv: Condvar,
3786 }
3787
3788 static PAUSE_BEFORE_COMMIT: OnceLock<Mutex<Option<std::sync::Arc<PauseGate>>>> =
3790 OnceLock::new();
3791
3792 pub fn pause_slot() -> &'static Mutex<Option<std::sync::Arc<PauseGate>>> {
3793 PAUSE_BEFORE_COMMIT.get_or_init(|| Mutex::new(None))
3794 }
3795}
3796
3797#[cfg(test)]
3799pub type BranchRefPauseGate = std::sync::Arc<branch_ref_pause_inject::PauseGate>;
3800
3801#[cfg(test)]
3803pub fn set_test_inject_branch_ref_pause_before_commit(gate: Option<BranchRefPauseGate>) {
3804 *branch_ref_pause_inject::pause_slot()
3805 .lock()
3806 .unwrap_or_else(|e| e.into_inner()) = gate;
3807}
3808
3809#[cfg(test)]
3810fn test_branch_ref_pause_before_commit() {
3811 let gate = branch_ref_pause_inject::pause_slot()
3812 .lock()
3813 .unwrap_or_else(|e| e.into_inner())
3814 .clone();
3815 if let Some(gate) = gate {
3816 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
3817 *wait = true;
3818 gate.cv.notify_all();
3819 while *wait {
3820 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
3821 }
3822 }
3823}
3824
3825#[cfg(not(test))]
3826fn test_branch_ref_pause_before_commit() {}
3827
3828#[cfg(test)]
3829mod ref_store_batch_pause_inject {
3830 use std::sync::{Condvar, Mutex, OnceLock};
3831
3832 pub struct PauseGate {
3833 pub wait: Mutex<bool>,
3834 pub cv: Condvar,
3835 }
3836
3837 pub type PauseSlot = Option<(usize, std::path::PathBuf, std::sync::Arc<PauseGate>)>;
3840
3841 static PAUSE_AFTER_REF_WRITE: OnceLock<Mutex<PauseSlot>> = OnceLock::new();
3842
3843 pub fn pause_slot() -> &'static Mutex<PauseSlot> {
3844 PAUSE_AFTER_REF_WRITE.get_or_init(|| Mutex::new(None))
3845 }
3846}
3847
3848#[cfg(test)]
3850pub type RefStoreBatchPauseGate = std::sync::Arc<ref_store_batch_pause_inject::PauseGate>;
3851
3852#[cfg(test)]
3854pub fn new_ref_store_batch_pause_gate() -> RefStoreBatchPauseGate {
3855 std::sync::Arc::new(ref_store_batch_pause_inject::PauseGate {
3856 wait: std::sync::Mutex::new(false),
3857 cv: std::sync::Condvar::new(),
3858 })
3859}
3860
3861#[cfg(test)]
3866pub fn set_test_inject_ref_store_batch_pause_after_apply(
3867 apply_index: usize,
3868 git_dir: &Path,
3869 gate: Option<RefStoreBatchPauseGate>,
3870) {
3871 *ref_store_batch_pause_inject::pause_slot()
3872 .lock()
3873 .unwrap_or_else(|e| e.into_inner()) = gate.map(|g| (apply_index, git_dir.to_path_buf(), g));
3874}
3875
3876#[cfg(test)]
3877struct RefStorePauseTestSerial {
3878 holder: Option<std::thread::ThreadId>,
3880}
3881
3882#[cfg(test)]
3883static REF_STORE_PAUSE_TEST_SERIAL: std::sync::Mutex<RefStorePauseTestSerial> =
3884 std::sync::Mutex::new(RefStorePauseTestSerial { holder: None });
3885
3886#[cfg(test)]
3888pub(crate) struct RefStoreBatchPauseTestScope {
3889 _serial: std::sync::MutexGuard<'static, RefStorePauseTestSerial>,
3890}
3891
3892#[cfg(test)]
3893pub(crate) fn ref_store_batch_pause_test_scope() -> RefStoreBatchPauseTestScope {
3894 let mut serial = REF_STORE_PAUSE_TEST_SERIAL
3895 .lock()
3896 .unwrap_or_else(|e| e.into_inner());
3897 assert!(
3898 serial.holder.is_none(),
3899 "ref_store_batch_pause_test_scope must not nest"
3900 );
3901 serial.holder = Some(std::thread::current().id());
3902 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
3903 RefStoreBatchPauseTestScope { _serial: serial }
3904}
3905
3906#[cfg(test)]
3907impl Drop for RefStoreBatchPauseTestScope {
3908 fn drop(&mut self) {
3909 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
3910 self._serial.holder = None;
3911 }
3912}
3913
3914#[cfg(test)]
3915pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
3916 let slot = ref_store_batch_pause_inject::pause_slot()
3917 .lock()
3918 .unwrap_or_else(|e| e.into_inner())
3919 .clone();
3920 if let Some((target, target_dir, gate)) = slot {
3921 let same_repo = |a: &Path, b: &Path| {
3922 a == b
3923 || matches!((a.canonicalize_simplified(), b.canonicalize_simplified()), (Ok(a), Ok(b)) if a == b)
3924 };
3925 if idx != target || !same_repo(git_dir, &target_dir) {
3926 return;
3927 }
3928 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
3929 *wait = true;
3930 gate.cv.notify_all();
3931 while *wait {
3932 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
3933 }
3934 }
3935}
3936
3937#[cfg(not(test))]
3938pub(crate) fn test_ref_store_batch_pause_after_ref_write(_idx: usize, _git_dir: &Path) {}
3939
3940pub struct BranchCommitRefLock {
3944 lock: Option<crate::lockfile::LockFile>,
3945 disarmed: bool,
3946}
3947
3948impl BranchCommitRefLock {
3949 pub fn acquire(
3955 git_dir: &Path,
3956 branch_ref: &str,
3957 expected_old: Option<ObjectId>,
3958 ) -> Result<Self> {
3959 ensure_refname_safe_for_storage(branch_ref)?;
3960 let storage_dir = ref_storage_dir(git_dir, branch_ref);
3961 if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
3962 return Err(Error::InvalidRef(format!(
3963 "cannot update ref '{}': reference namespace conflict",
3964 branch_ref
3965 )));
3966 }
3967 let stor = crate::ref_namespace::storage_ref_name(branch_ref);
3968 let path = storage_dir.join(&stor);
3969 remove_empty_ref_directory(&path);
3970 if fs::symlink_metadata(&path)
3971 .map(|m| m.file_type().is_dir())
3972 .unwrap_or(false)
3973 {
3974 return Err(directory_in_the_way(git_dir, branch_ref, &path));
3975 }
3976 if let Some(parent) = path.parent() {
3977 crate::git_dir::io_error::io_with_git_path(
3978 git_dir,
3979 parent,
3980 crate::git_dir::GitDirPathOperation::CreateDir,
3981 fs::create_dir_all(parent),
3982 )?;
3983 }
3984
3985 let lock_path = lock_path_for_ref(&path);
3986 use crate::lockfile::{LockError, LockFile, LockWait};
3987 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
3988 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
3989 held @ LockError::Held { .. } => Error::from(held),
3990 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
3991 git_dir,
3992 path.as_path(),
3993 crate::git_dir::GitDirPathOperation::UpdateRef,
3994 source,
3995 ),
3996 })?;
3997
3998 let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
3999 verify_branch_cas(expected_old, current, branch_ref)?;
4000
4001 Ok(Self {
4002 lock: Some(lock),
4003 disarmed: false,
4004 })
4005 }
4006
4007 pub fn write_reflogs(
4017 &mut self,
4018 git_dir: &Path,
4019 update: &BranchCommitRefUpdate<'_>,
4020 reflog_old: &ObjectId,
4021 config: &ConfigSet,
4022 ) -> Result<()> {
4023 write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
4024 }
4025
4026 pub fn finish_ref_commit(
4034 mut self,
4035 git_dir: &Path,
4036 update: &BranchCommitRefUpdate<'_>,
4037 ) -> Result<()> {
4038 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4039 self.disarmed = true;
4040 Ok(())
4041 }
4042
4043 pub fn commit(
4049 mut self,
4050 git_dir: &Path,
4051 update: &BranchCommitRefUpdate<'_>,
4052 reflog_old: &ObjectId,
4053 config: &ConfigSet,
4054 ) -> Result<()> {
4055 write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
4056 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4057 self.disarmed = true;
4058 Ok(())
4059 }
4060}
4061
4062impl Drop for BranchCommitRefLock {
4063 fn drop(&mut self) {
4064 drop(self.lock.take());
4065 }
4066}
4067
4068fn write_branch_commit_reflogs(
4069 git_dir: &Path,
4070 lock: &mut BranchCommitRefLock,
4071 update: &BranchCommitRefUpdate<'_>,
4072 reflog_old: &ObjectId,
4073 config: &ConfigSet,
4074) -> Result<()> {
4075 let mut branch_reflog_written = false;
4076 if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
4077 match append_reflog_with_config(
4078 git_dir,
4079 update.branch_ref,
4080 reflog_old,
4081 &update.new_oid,
4082 update.identity,
4083 update.reflog_message,
4084 false,
4085 Some(config),
4086 ) {
4087 Ok(()) => branch_reflog_written = true,
4088 Err(err) => {
4089 drop(lock.lock.take());
4090 return Err(err);
4091 }
4092 }
4093 }
4094
4095 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4096 if let Err(err) = append_reflog_with_config(
4097 git_dir,
4098 "HEAD",
4099 reflog_old,
4100 &update.new_oid,
4101 update.identity,
4102 update.reflog_message,
4103 false,
4104 Some(config),
4105 ) {
4106 if branch_reflog_written {
4107 let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
4108 }
4109 drop(lock.lock.take());
4110 return Err(err);
4111 }
4112 }
4113
4114 Ok(())
4115}
4116
4117fn finish_branch_commit_ref_lock_files(
4118 git_dir: &Path,
4119 lock: &mut BranchCommitRefLock,
4120 update: &BranchCommitRefUpdate<'_>,
4121) -> Result<()> {
4122 test_branch_ref_pause_before_commit();
4123
4124 let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
4125 let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
4126 let path = storage_dir.join(&stor);
4127 let content = format!("{}\n", update.new_oid);
4128 use crate::lockfile::LockError;
4129 use std::io::Write as _;
4130 let mut file_lock = lock
4131 .lock
4132 .take()
4133 .ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
4134 crate::git_dir::io_error::io_with_git_path(
4135 git_dir,
4136 &path,
4137 crate::git_dir::GitDirPathOperation::UpdateRef,
4138 file_lock.write_all(content.as_bytes()),
4139 )?;
4140 file_lock.commit_to(&path).map_err(|err| match err {
4141 LockError::Io {
4142 source,
4143 path: lock_path,
4144 } => crate::git_dir::io_error::map_git_dir_io(
4145 git_dir,
4146 lock_path.as_path(),
4147 crate::git_dir::GitDirPathOperation::UpdateRef,
4148 source,
4149 ),
4150 held @ LockError::Held { .. } => Error::from(held),
4151 })?;
4152 Ok(())
4153}
4154
4155pub(crate) fn update_branch_for_commit_files(
4156 git_dir: &Path,
4157 update: &BranchCommitRefUpdate<'_>,
4158 reflog_old: &ObjectId,
4159 config: &ConfigSet,
4160) -> Result<()> {
4161 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4162 lock.commit(git_dir, update, reflog_old, config)
4163}
4164
4165pub(crate) fn update_branch_for_commit_files_with_reference_transaction_hooks(
4168 git_dir: &Path,
4169 update: &BranchCommitRefUpdate<'_>,
4170 reflog_old: &ObjectId,
4171 config: &ConfigSet,
4172 ctx: &crate::hooks::RefHookContext,
4173 lines: &[store::reference_transaction::ReferenceTransactionLine],
4174) -> Result<()> {
4175 use crate::hooks::ReferenceTransactionPhase;
4176 use store::reference_transaction::stdin_from_lines;
4177
4178 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4179 if lines.is_empty() {
4180 return lock.commit(git_dir, update, reflog_old, config);
4181 }
4182 let stdin = stdin_from_lines(lines);
4183 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4184 .map_err(Error::Hook)?;
4185 match lock.commit(git_dir, update, reflog_old, config) {
4186 Ok(()) => {
4187 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4188 Ok(())
4189 }
4190 Err(err) => {
4191 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4192 Err(err)
4193 }
4194 }
4195}
4196
4197pub(crate) struct DetachedHeadRefLock {
4199 lock: Option<crate::lockfile::LockFile>,
4200 disarmed: bool,
4201}
4202
4203impl DetachedHeadRefLock {
4204 fn acquire(git_dir: &Path, expected_old: Option<ObjectId>) -> Result<Self> {
4205 let current = resolve_ref(git_dir, "HEAD").ok();
4206 verify_branch_cas(expected_old, current, "HEAD")?;
4207 let path = git_dir.join("HEAD");
4208 let lock_path = lock_path_for_ref(&path);
4209 use crate::lockfile::{LockError, LockFile, LockWait};
4210 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4211 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4212 held @ LockError::Held { .. } => Error::from(held),
4213 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4214 git_dir,
4215 path.as_path(),
4216 crate::git_dir::GitDirPathOperation::UpdateRef,
4217 source,
4218 ),
4219 })?;
4220 Ok(Self {
4221 lock: Some(lock),
4222 disarmed: false,
4223 })
4224 }
4225
4226 fn commit(
4227 mut self,
4228 git_dir: &Path,
4229 new_oid: ObjectId,
4230 reflog_old: &ObjectId,
4231 identity: &str,
4232 reflog_message: &str,
4233 config: &ConfigSet,
4234 ) -> Result<()> {
4235 let path = git_dir.join("HEAD");
4236 let content = format!("{new_oid}\n");
4237 use crate::lockfile::LockError;
4238 use std::io::Write as _;
4239 let mut file_lock = self
4240 .lock
4241 .take()
4242 .ok_or_else(|| Error::Message("HEAD ref lock already released".into()))?;
4243 crate::git_dir::io_error::io_with_git_path(
4244 git_dir,
4245 &path,
4246 crate::git_dir::GitDirPathOperation::UpdateRef,
4247 file_lock.write_all(content.as_bytes()),
4248 )?;
4249 file_lock.commit_to(&path).map_err(|err| match err {
4250 LockError::Io {
4251 source,
4252 path: lock_path,
4253 } => crate::git_dir::io_error::map_git_dir_io(
4254 git_dir,
4255 lock_path.as_path(),
4256 crate::git_dir::GitDirPathOperation::UpdateRef,
4257 source,
4258 ),
4259 held @ LockError::Held { .. } => Error::from(held),
4260 })?;
4261 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4262 if let Err(err) = append_reflog_with_config(
4263 git_dir,
4264 "HEAD",
4265 reflog_old,
4266 &new_oid,
4267 identity,
4268 reflog_message,
4269 false,
4270 Some(config),
4271 ) {
4272 let _ = write_ref_cas(git_dir, "HEAD", reflog_old, new_oid);
4273 return Err(err);
4274 }
4275 }
4276 self.disarmed = true;
4277 Ok(())
4278 }
4279}
4280
4281impl Drop for DetachedHeadRefLock {
4282 fn drop(&mut self) {
4283 drop(self.lock.take());
4284 }
4285}
4286
4287#[allow(clippy::too_many_arguments)]
4288pub(crate) fn update_detached_head_for_commit_files_with_reference_transaction_hooks(
4289 git_dir: &Path,
4290 expected_old: Option<ObjectId>,
4291 new_oid: ObjectId,
4292 identity: &str,
4293 reflog_message: &str,
4294 config: &ConfigSet,
4295 ctx: &crate::hooks::RefHookContext,
4296 lines: &[store::reference_transaction::ReferenceTransactionLine],
4297) -> Result<()> {
4298 use crate::hooks::ReferenceTransactionPhase;
4299 use store::reference_transaction::stdin_from_lines;
4300
4301 let lock = DetachedHeadRefLock::acquire(git_dir, expected_old)?;
4302 if lines.is_empty() {
4303 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4304 return lock.commit(
4305 git_dir,
4306 new_oid,
4307 &reflog_old,
4308 identity,
4309 reflog_message,
4310 config,
4311 );
4312 }
4313 let stdin = stdin_from_lines(lines);
4314 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4315 .map_err(Error::Hook)?;
4316 let reflog_old = expected_old.unwrap_or_else(|| ctx.null_oid());
4317 match lock.commit(
4318 git_dir,
4319 new_oid,
4320 &reflog_old,
4321 identity,
4322 reflog_message,
4323 config,
4324 ) {
4325 Ok(()) => {
4326 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4327 Ok(())
4328 }
4329 Err(err) => {
4330 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4331 Err(err)
4332 }
4333 }
4334}
4335
4336pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
4344 let config = ConfigSet::load(
4345 &crate::environment::Environment::empty(),
4346 Some(git_dir),
4347 true,
4348 )
4349 .unwrap_or_default();
4350 update_branch_for_commit_with_config(git_dir, update, &config)
4351}
4352
4353pub fn update_branch_for_commit_with_config(
4362 git_dir: &Path,
4363 update: &BranchCommitRefUpdate<'_>,
4364 config: &ConfigSet,
4365) -> Result<()> {
4366 update_branch_for_commit_on_store(&store::open_checked_ref_store(git_dir)?, update, config)
4367}
4368
4369pub(crate) fn update_detached_head_for_commit_files(
4372 git_dir: &Path,
4373 expected_old: Option<ObjectId>,
4374 new_oid: ObjectId,
4375 identity: &str,
4376 reflog_message: &str,
4377 config: &ConfigSet,
4378) -> Result<()> {
4379 let current = resolve_ref(git_dir, "HEAD").ok();
4380 verify_branch_cas(expected_old, current, "HEAD")?;
4381 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4382
4383 write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
4384 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4385 if let Err(err) = append_reflog_with_config(
4386 git_dir,
4387 "HEAD",
4388 &reflog_old,
4389 &new_oid,
4390 identity,
4391 reflog_message,
4392 false,
4393 Some(config),
4394 ) {
4395 let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
4396 return Err(err);
4397 }
4398 }
4399 Ok(())
4400}
4401
4402#[must_use]
4408pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
4409 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
4410 store.join("logs").join(stor_name)
4411}
4412
4413fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
4414 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
4415}
4416
4417fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
4426 if prefix.is_empty() {
4427 return String::new();
4428 }
4429 if prefix.ends_with('/') {
4430 return prefix.to_string();
4431 }
4432 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
4433 if candidate.is_file() {
4434 prefix.to_string()
4435 } else {
4436 format!("{prefix}/")
4437 }
4438}
4439
4440pub(crate) fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
4443 odb.peel_stored_tag_target(oid).ok().flatten()
4444}
4445
4446pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
4457 pack_imported_refs_for_clone(git_dir, Some(remote))
4458}
4459
4460pub(crate) fn pack_imported_refs_for_clone(git_dir: &Path, remote: Option<&str>) -> Result<()> {
4462 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4463 return Ok(());
4464 }
4465 let prefix = remote.map(|name| format!("refs/remotes/{name}/"));
4466 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
4467 let odb = crate::odb::Odb::new(&storage_git_dir.join("objects"))
4468 .with_config_git_dir(git_dir.to_path_buf());
4469 odb.with_pack_read_context(|| {
4470 pack_refs_files_matching(
4471 git_dir,
4472 &odb,
4473 |name| {
4474 name.starts_with("refs/tags/")
4475 || prefix
4476 .as_ref()
4477 .is_some_and(|prefix| name.starts_with(prefix))
4478 },
4479 false,
4480 true,
4482 )
4483 })?;
4484 Ok(())
4485}
4486
4487fn atomic_rewrite_packed_refs(git_dir: &Path, body: &str) -> Result<()> {
4489 let packed_path = store::paths::packed_refs_path(git_dir);
4490 let lock = lock_path_for_ref(&packed_path);
4491 let abs_lock = git_dir
4492 .canonicalize_simplified()
4493 .map(|d| d.join("packed-refs.lock"))
4494 .unwrap_or_else(|_| lock.clone());
4495 let _packed_lock = acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?;
4496
4497 let tmp = packed_path.with_extension("new");
4498 let mut created_tmp = false;
4499 let write_result = (|| -> Result<()> {
4500 let mut file = std::fs::OpenOptions::new()
4502 .write(true)
4503 .create_new(true)
4504 .open(&tmp)
4505 .map_err(Error::Io)?;
4506 created_tmp = true;
4507 use std::io::Write as _;
4508 file.write_all(body.as_bytes()).map_err(Error::Io)?;
4509 drop(file);
4510 fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
4511 created_tmp = false;
4512 Ok(())
4513 })();
4514 if write_result.is_err() && created_tmp {
4515 let _ = fs::remove_file(&tmp);
4516 }
4517 write_result
4518}
4519
4520#[derive(Debug, Clone, PartialEq, Eq)]
4522pub struct PackedRefRecord {
4523 pub refname: String,
4525 pub oid: ObjectId,
4527 pub peeled: Option<ObjectId>,
4529}
4530
4531pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
4541 let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
4542 sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
4543 for rec in &sorted {
4544 ensure_refname_safe_for_storage(&rec.refname)?;
4545 }
4546 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
4547 for rec in &sorted {
4548 body.push_str(&rec.oid.to_hex());
4549 body.push(' ');
4550 body.push_str(&rec.refname);
4551 body.push('\n');
4552 if let Some(peeled) = rec.peeled {
4553 body.push('^');
4554 body.push_str(&peeled.to_hex());
4555 body.push('\n');
4556 }
4557 }
4558 atomic_rewrite_packed_refs(git_dir, &body)?;
4559 for rec in &sorted {
4560 let storage_dir = ref_storage_dir(git_dir, &rec.refname);
4561 let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
4562 let loose = storage_dir.join(stor);
4563 if loose.is_file() {
4564 fs::remove_file(&loose).map_err(Error::Io)?;
4565 }
4566 if let Some(parent) = loose.parent() {
4567 prune_empty_ref_dirs(&storage_dir, parent);
4568 }
4569 }
4570 Ok(())
4571}
4572
4573pub fn list_refs_for_repository(
4584 repo: &crate::repo::Repository,
4585 prefix: &str,
4586) -> Result<Vec<(String, ObjectId)>> {
4587 let prefix_norm = normalize_list_refs_prefix(repo.git_dir(), prefix);
4588 list_store_refs_with_name_prefix(repo, &prefix_norm)
4589}
4590
4591fn list_store_refs_with_name_prefix(
4593 repo: &crate::repo::Repository,
4594 prefix: &str,
4595) -> Result<Vec<(String, ObjectId)>> {
4596 let git_dir = repo.git_dir();
4597 let mut results = collect_resolved_refs(repo.refs(), prefix)?;
4598 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4599 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4600 }
4601 results.sort_by(|a, b| a.0.cmp(&b.0));
4602 Ok(results)
4603}
4604
4605pub(crate) fn list_refs_in(
4616 repo: &crate::repo::Repository,
4617 prefix: &str,
4618) -> Result<Vec<(String, ObjectId)>> {
4619 if repo.refs_on_disk() {
4620 list_refs(repo.git_dir(), prefix)
4621 } else {
4622 list_refs_for_repository(repo, prefix)
4623 }
4624}
4625
4626pub(crate) fn resolve_ref_in(repo: &crate::repo::Repository, refname: &str) -> Result<ObjectId> {
4633 if repo.refs_on_disk() {
4634 resolve_ref(repo.git_dir(), refname)
4635 } else {
4636 repo.resolve_ref_oid(refname)
4637 }
4638}
4639
4640pub(crate) fn list_refs_glob_in(
4647 repo: &crate::repo::Repository,
4648 pattern: &str,
4649) -> Result<Vec<(String, ObjectId)>> {
4650 if repo.refs_on_disk() {
4651 list_refs_glob(repo.git_dir(), pattern)
4652 } else {
4653 list_refs_glob_for_repository(repo, pattern)
4654 }
4655}
4656
4657pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4672 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4673 let prefix = prefix_norm.as_str();
4674 let store = open_ref_store(git_dir)?;
4675 if store.format() != RefStorageFormat::Files {
4676 return crate::reftable::reftable_list_refs(git_dir, prefix);
4677 }
4678 let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
4679 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4680 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4681 }
4682 results.sort_by(|a, b| a.0.cmp(&b.0));
4683 Ok(results)
4684}
4685
4686pub(crate) fn list_refs_filtering_packed(
4697 git_dir: &Path,
4698 prefix: &str,
4699 keep: PackedRecordFilter<'_>,
4700) -> Result<Vec<(String, ObjectId)>> {
4701 let Some(store) = store::open_files_ref_store(git_dir)? else {
4702 return list_refs(git_dir, prefix);
4703 };
4704 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4705 let prefix = prefix_norm.as_str();
4706 let mut results = collect_resolved_ref_entries(&store, |f| {
4707 store.for_each_ref_filtered(prefix, Some(keep), f)
4708 })?;
4709 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4710 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4711 }
4712 results.sort_by(|a, b| a.0.cmp(&b.0));
4713 Ok(results)
4714}
4715
4716pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
4724 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
4725}
4726
4727pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4737 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4738 return crate::reftable::reftable_list_refs(git_dir, prefix);
4739 }
4740 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
4741 let stored_prefix = prefix.to_owned();
4742
4743 if let Some(cdir) = common_dir(git_dir) {
4744 if cdir != git_dir {
4745 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
4746 let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
4747 collect_loose_refs_into_map(
4748 &loose_root,
4749 &loose_prefix,
4750 &stored_prefix,
4751 &cdir,
4752 true,
4753 &mut by_name,
4754 )?;
4755 }
4756 }
4757
4758 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
4759 let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
4760 collect_loose_refs_into_map(
4761 &loose_root,
4762 &loose_prefix,
4763 &stored_prefix,
4764 git_dir,
4765 true,
4766 &mut by_name,
4767 )?;
4768
4769 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
4770 results.sort_by(|a, b| a.0.cmp(&b.0));
4771 Ok(results)
4772}
4773
4774pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
4787 let config = ConfigSet::load(
4788 &crate::environment::Environment::empty(),
4789 Some(receiving_git_dir),
4790 true,
4791 )?;
4792 let objects_dir = receiving_git_dir.join("objects");
4793 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
4794 let mut out = Vec::new();
4795 let mut seen = std::collections::HashSet::new();
4796 for alt_objects in alternates {
4797 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
4798 continue;
4799 };
4800 if !alt_git_dir.join("refs").is_dir() {
4801 continue;
4802 }
4803 if let Some(prefixes) = config
4804 .get("core.alternaterefsprefixes")
4805 .or_else(|| config.get("core.alternateRefsPrefixes"))
4806 {
4807 for part in prefixes.split_whitespace() {
4808 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
4809 if seen.insert(oid) {
4810 out.push(oid);
4811 }
4812 continue;
4813 }
4814 for (_, oid) in list_refs(&alt_git_dir, part)? {
4815 if seen.insert(oid) {
4816 out.push(oid);
4817 }
4818 }
4819 }
4820 } else {
4821 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
4822 if seen.insert(oid) {
4823 out.push(oid);
4824 }
4825 }
4826 }
4827 }
4828 Ok(out)
4829}
4830
4831pub fn list_refs_glob_for_repository(
4833 repo: &crate::repo::Repository,
4834 pattern: &str,
4835) -> Result<Vec<(String, ObjectId)>> {
4836 let all = match pattern.find(['*', '?', '[']) {
4840 Some(pos) => list_store_refs_with_name_prefix(repo, &pattern[..pos])?,
4841 None => {
4842 let mut dir = pattern.trim_end_matches('/').to_owned();
4843 if !dir.is_empty() {
4844 dir.push('/');
4845 }
4846 list_refs_for_repository(repo, &dir)?
4847 }
4848 };
4849 Ok(all
4850 .into_iter()
4851 .filter(|(refname, _)| ref_matches_glob(refname, pattern))
4852 .collect())
4853}
4854
4855pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
4862 let glob_pos = pattern.find(['*', '?', '[']);
4863 let prefix_owned: String = match glob_pos {
4864 Some(pos) => pattern[..pos].to_owned(),
4866 None => {
4867 let mut p = pattern.trim_end_matches('/').to_owned();
4868 if !p.is_empty() {
4869 p.push('/');
4870 }
4871 p
4872 }
4873 };
4874 let prefix = prefix_owned.as_str();
4875 let all = list_refs(git_dir, prefix)?;
4876 let mut results = Vec::new();
4877 for (refname, oid) in all {
4878 if ref_matches_glob(&refname, pattern) {
4879 results.push((refname, oid));
4880 }
4881 }
4882 Ok(results)
4883}
4884
4885pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
4889 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
4891 return refname == pattern
4892 || refname.ends_with(&format!("/{pattern}"))
4893 || refname.starts_with(&format!("{pattern}/"));
4894 }
4895 glob_match(pattern, refname)
4896}
4897
4898fn glob_match(pattern: &str, text: &str) -> bool {
4899 let pat = pattern.as_bytes();
4900 let txt = text.as_bytes();
4901 let (mut pi, mut ti) = (0, 0);
4902 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
4903 while ti < txt.len() {
4904 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
4905 pi += 1;
4906 ti += 1;
4907 } else if pi < pat.len() && pat[pi] == b'*' {
4908 star_pi = pi;
4909 star_ti = ti;
4910 pi += 1;
4911 } else if star_pi != usize::MAX {
4912 pi = star_pi + 1;
4913 star_ti += 1;
4914 ti = star_ti;
4915 } else {
4916 return false;
4917 }
4918 }
4919 while pi < pat.len() && pat[pi] == b'*' {
4920 pi += 1;
4921 }
4922 pi == pat.len()
4923}
4924
4925fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
4927 let content = fs::read_to_string(path).ok()?;
4928 let content = content.trim_end_matches('\n').trim();
4929 if ObjectId::is_full_hex(content) {
4930 content.parse().ok()
4931 } else {
4932 None
4933 }
4934}
4935
4936fn collect_loose_refs_into_map(
4937 dir: &Path,
4938 prefix: &str,
4939 filter_prefix: &str,
4940 resolve_git_dir: &Path,
4941 physical_keys: bool,
4942 out: &mut HashMap<String, ObjectId>,
4943) -> Result<()> {
4944 let read = match fs::read_dir(dir) {
4945 Ok(r) => r,
4946 Err(e)
4947 if e.kind() == io::ErrorKind::NotFound
4948 || e.kind() == io::ErrorKind::NotADirectory
4949 || e.raw_os_error() == Some(libc::ENOTDIR) =>
4950 {
4951 return Ok(());
4952 }
4953 Err(e) => return Err(Error::Io(e)),
4954 };
4955
4956 for entry in read {
4957 let entry = entry?;
4958 let name = entry.file_name();
4959 let name_str = name.to_string_lossy();
4960 let refname = format!("{prefix}{name_str}");
4961 let path = entry.path();
4962 let meta = match fs::metadata(&path) {
4963 Ok(m) => m,
4964 Err(_) => continue,
4965 };
4966
4967 if meta.is_dir() {
4968 collect_loose_refs_into_map(
4969 &path,
4970 &format!("{refname}/"),
4971 filter_prefix,
4972 resolve_git_dir,
4973 physical_keys,
4974 out,
4975 )?;
4976 } else if meta.is_file() {
4977 if physical_keys {
4978 if let Some(oid) = loose_ref_file_direct_oid(&path) {
4979 out.insert(refname, oid);
4980 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
4981 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
4982 out.insert(refname, oid);
4983 }
4984 }
4985 } else {
4986 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
4987 .unwrap_or_else(|| refname.clone());
4988 if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
4989 continue;
4990 }
4991 if let Some(oid) = loose_ref_file_direct_oid(&path) {
4992 out.insert(logical, oid);
4993 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
4994 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
4995 out.insert(logical, oid);
4996 }
4997 }
4998 }
4999 }
5000 }
5001 Ok(())
5002}
5003
5004pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
5013 let inner = spec
5015 .strip_prefix("@{-")
5016 .and_then(|s| s.strip_suffix('}'))
5017 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
5018 let n: usize = inner
5019 .parse()
5020 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
5021 if n == 0 {
5022 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
5023 }
5024 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
5025 let mut count = 0usize;
5026 for entry in entries.iter().rev() {
5027 let msg = &entry.message;
5028 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
5029 count += 1;
5030 if count == n {
5031 if let Some(to_pos) = rest.find(" to ") {
5032 return Ok(rest[..to_pos].to_string());
5033 }
5034 }
5035 }
5036 }
5037 Err(Error::InvalidRef(format!(
5038 "{spec}: only {count} checkout(s) in reflog"
5039 )))
5040}
5041
5042fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
5043 let prefix = prefix.trim_end_matches('/');
5044 if prefix.is_empty() {
5045 return true;
5046 }
5047 if refname.starts_with(prefix) {
5048 return true;
5049 }
5050 if refname == prefix {
5051 return true;
5052 }
5053 false
5054}
5055
5056fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
5057 let base = prefix.trim_end_matches('/');
5058 let (head, tail) = base.rsplit_once('/')?;
5059 let mut chars: Vec<char> = tail.chars().collect();
5060 while let Some(last) = chars.pop() {
5061 if last != '\u{10ffff}' {
5062 chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
5063 return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
5064 }
5065 }
5066 None
5067}
5068
5069pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
5072 let trimmed = prefix.trim_end_matches('/');
5073 if trimmed.is_empty() {
5074 return (git_dir.join("refs"), "refs/".to_owned());
5075 }
5076 let direct = git_dir.join(trimmed);
5077 if direct.is_dir() {
5078 return (direct, format!("{trimmed}/"));
5079 }
5080 if let Some(slash) = trimmed.rfind('/') {
5081 let parent = &trimmed[..slash];
5082 return (git_dir.join(parent), format!("{parent}/"));
5083 }
5084 (git_dir.to_path_buf(), String::new())
5085}
5086
5087fn collect_packed_refs_into_map(
5088 git_dir: &Path,
5089 prefix: &str,
5090 physical_keys: bool,
5091 out: &mut HashMap<String, ObjectId>,
5092) -> Result<()> {
5093 let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
5094 for (refname, oid) in map {
5095 let key = if physical_keys {
5096 refname.clone()
5097 } else {
5098 crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
5099 };
5100 out.insert(key, oid);
5101 }
5102 Ok(())
5103}
5104
5105#[cfg(test)]
5106mod update_branch_for_commit_tests {
5107 use super::*;
5108 use crate::reflog::read_reflog;
5109 use crate::repo::init_repository;
5110 use std::sync::{Arc, Condvar, Mutex};
5111 use std::thread;
5112 use tempfile::TempDir;
5113
5114 fn sample_oid(hex: &str) -> ObjectId {
5115 hex.parse().unwrap()
5116 }
5117
5118 #[test]
5119 fn update_branch_for_commit_holds_lock_until_ref_commit() {
5120 let _pause_serial = super::test_pause_injection_serial_guard();
5121 let tmp = TempDir::new().unwrap();
5122 let repo = init_repository(
5123 tmp.path(),
5124 false,
5125 "main",
5126 None,
5127 crate::RefStorageFormat::Files,
5128 )
5129 .unwrap();
5130 let git_dir = repo.git_dir().to_path_buf();
5131 let oid1 = test_commit(&git_dir, "one");
5132 let oid2 = test_commit(&git_dir, "two");
5133 let oid3 = test_commit(&git_dir, "three");
5134 write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
5135
5136 let gate = Arc::new(branch_ref_pause_inject::PauseGate {
5137 wait: Mutex::new(false),
5138 cv: Condvar::new(),
5139 });
5140 set_test_inject_branch_ref_pause_before_commit(Some(gate.clone()));
5141
5142 let git_dir_thread = git_dir.clone();
5143 let handle = thread::spawn(move || {
5144 update_branch_for_commit(
5145 &git_dir_thread,
5146 &BranchCommitRefUpdate {
5147 branch_ref: "refs/heads/main",
5148 expected_old: Some(oid1),
5149 new_oid: oid2,
5150 identity: "T <t@e.com> 1 +0000",
5151 reflog_message: "commit: test",
5152 },
5153 )
5154 });
5155
5156 let mut wait = gate.wait.lock().unwrap();
5157 while !*wait {
5158 wait = gate.cv.wait(wait).unwrap();
5159 }
5160
5161 assert!(
5162 write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
5163 "concurrent branch update must block on the held ref lock"
5164 );
5165
5166 *wait = false;
5167 gate.cv.notify_all();
5168 drop(wait);
5169 handle.join().unwrap().unwrap();
5170 set_test_inject_branch_ref_pause_before_commit(None);
5171
5172 assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
5173 }
5174
5175 #[test]
5176 fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
5177 let tmp = TempDir::new().unwrap();
5178 let repo = init_repository(
5179 tmp.path(),
5180 false,
5181 "main",
5182 None,
5183 crate::RefStorageFormat::Reftable,
5184 )
5185 .unwrap();
5186 let git_dir = repo.git_dir().to_path_buf();
5187 let new_oid = sample_oid("3333333333333333333333333333333333333333");
5188
5189 crate::reftable::set_test_inject_reftable_transaction_fail(true);
5190 let err = update_branch_for_commit(
5191 &git_dir,
5192 &BranchCommitRefUpdate {
5193 branch_ref: "refs/heads/main",
5194 expected_old: None,
5195 new_oid,
5196 identity: "T <t@e.com> 1 +0000",
5197 reflog_message: "commit (initial): test",
5198 },
5199 )
5200 .unwrap_err();
5201 crate::reftable::set_test_inject_reftable_transaction_fail(false);
5202 assert!(!matches!(err, Error::Message(_)));
5203
5204 assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
5205 assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
5206 assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
5207 }
5208}
5209
5210#[cfg(test)]
5211mod refname_available_tests {
5212 use super::*;
5213 use std::collections::{BTreeSet, HashSet};
5214 use tempfile::tempdir;
5215
5216 #[test]
5217 fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
5218 let dir = tempdir().unwrap();
5219 let git_dir = dir.path();
5220 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5221 let items = vec![
5222 RefBatchItem {
5223 name: "refs/heads/c".to_owned(),
5224 new_oid: Some(sample_oid()),
5225 },
5226 RefBatchItem {
5227 name: "refs/heads/c/x".to_owned(),
5228 new_oid: Some(sample_oid()),
5229 },
5230 ];
5231 let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
5232 assert!(matches!(
5233 err,
5234 RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
5235 ));
5236 }
5237
5238 fn sample_oid() -> ObjectId {
5239 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5240 }
5241
5242 #[test]
5243 fn loose_parent_blocks_child_create() {
5244 let dir = tempdir().unwrap();
5245 let git_dir = dir.path();
5246 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5247 fs::write(
5248 git_dir.join("refs/1l/c"),
5249 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5250 )
5251 .unwrap();
5252 assert_eq!(
5253 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
5254 RawRefLookup::Exists
5255 );
5256 let extras = BTreeSet::from([
5257 "refs/1l/b".to_string(),
5258 "refs/1l/c/x".to_string(),
5259 "refs/1l/d".to_string(),
5260 ]);
5261 let skip = HashSet::new();
5262 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
5263 .unwrap_err();
5264 assert!(matches!(
5265 err,
5266 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5267 ));
5268 }
5269
5270 #[test]
5271 fn verify_sees_loose_ref_after_canonical_git_dir() {
5272 let dir = tempdir().unwrap();
5273 let git_dir = dir.path().join(".git");
5274 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5275 fs::write(
5276 git_dir.join("refs/1l/c"),
5277 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5278 )
5279 .unwrap();
5280 let skip = HashSet::new();
5281 let extras = BTreeSet::new();
5282 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
5283 .unwrap_err();
5284 assert!(matches!(
5285 err,
5286 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5287 ));
5288 }
5289
5290 fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
5291 if existing == candidate {
5292 return false;
5293 }
5294 existing
5295 .strip_prefix(candidate)
5296 .is_some_and(|rest| rest.starts_with('/'))
5297 || candidate
5298 .strip_prefix(existing)
5299 .is_some_and(|rest| rest.starts_with('/'))
5300 }
5301
5302 #[test]
5303 fn packed_namespace_index_matches_pairwise_scan() {
5304 let mut sorted: BTreeSet<String> = BTreeSet::new();
5305 for i in 0..500 {
5306 sorted.insert(format!("refs/heads/b{i:05}"));
5307 }
5308 sorted.insert("refs/heads/parent".into());
5309 sorted.insert("refs/heads/a/b".into());
5310 let candidates = [
5311 "refs/heads/b00001",
5312 "refs/heads/parent/child",
5313 "refs/heads/new",
5314 "refs/heads/a",
5315 ];
5316 for candidate in candidates {
5317 let indexed = packed_refname_namespace_conflict(&sorted, candidate);
5318 let linear = sorted
5319 .iter()
5320 .any(|existing| refname_namespace_conflicts(existing, candidate));
5321 assert_eq!(indexed, linear, "candidate {candidate}");
5322 }
5323 }
5324
5325 #[test]
5326 fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
5327 let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
5328 if !git_dir.is_dir() {
5329 return;
5330 }
5331 let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
5332 assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
5333 assert!(listed
5334 .iter()
5335 .all(|(name, _)| name.starts_with("refs/heads/b05")));
5336 }
5337
5338 #[test]
5339 fn list_refs_finds_sibling_under_parent_directory() {
5340 let dir = tempdir().unwrap();
5341 let git_dir = dir.path();
5342 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5343 fs::write(
5344 git_dir.join("refs/ns/p/x"),
5345 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5346 )
5347 .unwrap();
5348 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
5349 assert!(
5350 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
5351 "got {listed:?}"
5352 );
5353 }
5354
5355 #[test]
5356 fn verify_blocks_parent_when_child_ref_exists() {
5357 let dir = tempdir().unwrap();
5358 let git_dir = dir.path();
5359 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5360 fs::write(
5361 git_dir.join("refs/ns/p/x"),
5362 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5363 )
5364 .unwrap();
5365 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
5366 let skip = HashSet::new();
5367 let err =
5368 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
5369 assert!(matches!(
5370 err,
5371 RefnameUnavailable::DescendantExists { ref blocking, .. }
5372 if blocking == "refs/ns/p/x"
5373 ));
5374 }
5375
5376 #[test]
5377 fn verify_blocks_parent_when_only_a_packed_child_exists() {
5378 let dir = tempdir().unwrap();
5381 let git_dir = dir.path();
5382 fs::create_dir_all(git_dir.join("refs")).unwrap();
5383 fs::write(
5384 store::paths::packed_refs_path(git_dir),
5385 "# pack-refs with: peeled fully-peeled\n\
5386 67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
5387 )
5388 .unwrap();
5389 let err = verify_refname_available_for_create(
5390 git_dir,
5391 "refs/ns/q",
5392 &BTreeSet::new(),
5393 &HashSet::new(),
5394 )
5395 .unwrap_err();
5396 assert!(matches!(
5397 err,
5398 RefnameUnavailable::DescendantExists { ref blocking, .. }
5399 if blocking == "refs/ns/q/x"
5400 ));
5401 }
5402
5403 #[test]
5404 fn verify_allows_name_whose_siblings_share_a_prefix() {
5405 let dir = tempdir().unwrap();
5406 let git_dir = dir.path();
5407 fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
5408 for name in ["v1.0.1", "v1.0-rc1"] {
5409 fs::write(
5410 git_dir.join("refs/tags").join(name),
5411 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5412 )
5413 .unwrap();
5414 }
5415 verify_refname_available_for_create(
5416 git_dir,
5417 "refs/tags/v1.0",
5418 &BTreeSet::new(),
5419 &HashSet::new(),
5420 )
5421 .expect("siblings are not descendants");
5422 }
5423
5424 #[test]
5425 fn verify_blocks_parent_git_style_nested_path() {
5426 let dir = tempdir().unwrap();
5427 let git_dir = dir.path();
5428 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
5429 fs::write(
5430 git_dir.join("refs/3l/c/x"),
5431 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5432 )
5433 .unwrap();
5434 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
5435 let skip = HashSet::new();
5436 let err =
5437 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
5438 assert!(matches!(
5439 err,
5440 RefnameUnavailable::DescendantExists { ref blocking, .. }
5441 if blocking == "refs/3l/c/x"
5442 ));
5443 }
5444
5445 #[test]
5446 fn intermediate_directory_does_not_block_nested_create() {
5447 let dir = tempdir().unwrap();
5448 let git_dir = dir.path();
5449 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
5450 fs::write(
5451 git_dir.join("refs/ns/existing"),
5452 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5453 )
5454 .unwrap();
5455 assert_eq!(
5456 read_raw_ref(git_dir, "refs/ns").unwrap(),
5457 RawRefLookup::IsDirectory
5458 );
5459 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
5460 let skip = HashSet::new();
5461 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
5462 }
5463}
5464
5465#[cfg(test)]
5466mod ref_storage_traversal_guard_tests {
5467 use super::*;
5468 use crate::objects::ObjectId;
5469 use tempfile::tempdir;
5470
5471 const TRAVERSAL_REF: &str = "refs/heads/../../config";
5472
5473 fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
5474 let dir = tempdir().unwrap();
5475 let git_dir = dir.path().to_path_buf();
5476 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5477 fs::write(
5478 git_dir.join("config"),
5479 "[core]\nrepositoryformatversion = 0\n",
5480 )
5481 .unwrap();
5482 (dir, git_dir)
5483 }
5484
5485 fn sample_oid() -> ObjectId {
5486 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5487 }
5488
5489 fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
5490 assert!(matches!(result, Err(Error::InvalidRef(_))));
5491 assert!(
5492 git_dir.join("config").is_file(),
5493 "repository config must not be touched"
5494 );
5495 }
5496
5497 #[test]
5498 fn write_ref_rejects_traversal_refname() {
5499 let (_dir, git_dir) = bare_repo_with_config();
5500 let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
5501 assert_invalid_ref_preserves_config(&git_dir, result);
5502 }
5503
5504 #[test]
5505 fn write_ref_cached_rejects_traversal_refname() {
5506 let (_dir, git_dir) = bare_repo_with_config();
5507 let mut packed = PackedRefs::load(&git_dir).unwrap();
5508 let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
5509 assert_invalid_ref_preserves_config(&git_dir, result);
5510 }
5511
5512 #[test]
5513 fn write_symbolic_ref_rejects_traversal_refname() {
5514 let (_dir, git_dir) = bare_repo_with_config();
5515 let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
5516 assert_invalid_ref_preserves_config(&git_dir, result);
5517 }
5518
5519 #[test]
5520 fn delete_ref_rejects_traversal_refname() {
5521 let (_dir, git_dir) = bare_repo_with_config();
5522 let result = delete_ref(&git_dir, TRAVERSAL_REF);
5523 assert_invalid_ref_preserves_config(&git_dir, result);
5524 }
5525
5526 #[test]
5527 fn write_ref_rejects_illegal_branch_refname() {
5528 let (_dir, git_dir) = bare_repo_with_config();
5529 for refname in [
5530 "refs/heads/bad name",
5531 "refs/heads/x..y",
5532 "refs/heads/a~b",
5533 "refs/heads/a.lock",
5534 ] {
5535 let result = write_ref(&git_dir, refname, &sample_oid());
5536 assert_invalid_ref_preserves_config(&git_dir, result);
5537 assert!(
5538 !git_dir
5539 .join(refname.strip_prefix("refs/").unwrap())
5540 .exists(),
5541 "must not write loose ref for {refname}"
5542 );
5543 }
5544 }
5545
5546 #[test]
5547 fn write_ref_accepts_refs_heads_head() {
5548 let (_dir, git_dir) = bare_repo_with_config();
5549 let commit = test_commit(&git_dir, "head-named branch");
5550 write_ref(&git_dir, "refs/heads/HEAD", &commit)
5551 .expect("refs/heads/HEAD is valid ref syntax");
5552 assert!(git_dir.join("refs/heads/HEAD").is_file());
5553 }
5554}
5555
5556#[cfg(test)]
5557mod log_refs_config_tests {
5558 use super::*;
5559 use crate::config::{ConfigFile, ConfigScope};
5560
5561 fn set(text: &str) -> ConfigSet {
5562 let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
5563 let mut set = ConfigSet::new();
5564 set.merge(&file);
5565 set
5566 }
5567
5568 #[test]
5569 fn parse_accepts_modes_and_bool_spellings() {
5570 for (raw, mode) in [
5571 ("always", LogRefsConfig::Always),
5572 (" ALWAYS ", LogRefsConfig::Always),
5573 ("true", LogRefsConfig::Normal),
5574 ("Yes", LogRefsConfig::Normal),
5575 ("on", LogRefsConfig::Normal),
5576 ("1", LogRefsConfig::Normal),
5577 ("false", LogRefsConfig::None),
5578 ("no", LogRefsConfig::None),
5579 ("OFF", LogRefsConfig::None),
5580 ("0", LogRefsConfig::None),
5581 ("never", LogRefsConfig::None),
5582 ] {
5583 assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
5584 }
5585 assert_eq!(LogRefsConfig::parse("sometimes"), None);
5586 }
5587
5588 #[test]
5589 fn effective_mode_applies_bare_default_only_when_unset() {
5590 assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
5591 assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
5592 let bare = set("[core]\n\tbare = true\n");
5593 assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
5594 let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
5595 assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
5596 assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
5597 let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
5598 assert_eq!(
5599 bare_logged.effective_log_refs_config(),
5600 LogRefsConfig::Normal
5601 );
5602 let always = set("[core]\n\tlogAllRefUpdates = always\n");
5603 assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
5604 }
5605
5606 #[test]
5607 fn autocreate_with_config_matches_mode() {
5608 let normal = set("[core]\n\tlogAllRefUpdates = true\n");
5609 let git_dir = Path::new(".git");
5610 assert!(should_autocreate_reflog_with_config(
5611 &normal,
5612 git_dir,
5613 "refs/heads/main"
5614 ));
5615 assert!(!should_autocreate_reflog_with_config(
5616 &normal,
5617 git_dir,
5618 "refs/tags/v1"
5619 ));
5620 let none = set("[core]\n\tlogAllRefUpdates = false\n");
5621 assert!(!should_autocreate_reflog_with_config(
5622 &none, git_dir, "HEAD"
5623 ));
5624 }
5625}
5626
5627#[cfg(test)]
5628mod read_raw_ref_tests {
5629 use super::*;
5630 use tempfile::tempdir;
5631
5632 #[test]
5633 fn loose_ref_file_is_exists() {
5634 let dir = tempdir().unwrap();
5635 let git_dir = dir.path();
5636 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5637 fs::write(
5638 git_dir.join("refs/heads/side"),
5639 "0000000000000000000000000000000000000000\n",
5640 )
5641 .unwrap();
5642 assert_eq!(
5643 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
5644 RawRefLookup::Exists
5645 );
5646 }
5647
5648 #[test]
5649 fn missing_ref_is_not_found() {
5650 let dir = tempdir().unwrap();
5651 let git_dir = dir.path();
5652 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5653 assert_eq!(
5654 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
5655 RawRefLookup::NotFound
5656 );
5657 }
5658
5659 #[test]
5660 fn directory_where_ref_expected_is_is_directory() {
5661 let dir = tempdir().unwrap();
5662 let git_dir = dir.path();
5663 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5664 assert_eq!(
5665 read_raw_ref(git_dir, "refs/heads").unwrap(),
5666 RawRefLookup::IsDirectory
5667 );
5668 }
5669
5670 #[test]
5671 fn packed_ref_name_is_exists() {
5672 let dir = tempdir().unwrap();
5673 let git_dir = dir.path();
5674 fs::write(
5675 store::paths::packed_refs_path(git_dir),
5676 "# pack-refs with: peeled fully-peeled \n\
5677 0000000000000000000000000000000000000000 refs/heads/packed\n",
5678 )
5679 .unwrap();
5680 assert_eq!(
5681 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
5682 RawRefLookup::Exists
5683 );
5684 }
5685
5686 #[test]
5687 fn access_denied_on_a_directory_is_a_missing_file() {
5688 let dir = tempdir().unwrap();
5689 let ref_dir = dir.path().join("refs/heads/rel");
5690 fs::create_dir_all(&ref_dir).unwrap();
5691 let file = dir.path().join("refs/heads/main");
5692 fs::write(&file, "x\n").unwrap();
5693 let denied = || io::Error::from(io::ErrorKind::PermissionDenied);
5694
5695 assert!(io_error_is_missing_file(&denied(), &ref_dir));
5697 assert!(
5698 !io_error_is_missing_file(&denied(), &file),
5699 "an unreadable ref file is a real I/O error"
5700 );
5701 assert!(!io_error_is_missing_file(
5702 &denied(),
5703 &dir.path().join("refs/heads/none")
5704 ));
5705 for kind in [
5706 io::ErrorKind::NotFound,
5707 io::ErrorKind::IsADirectory,
5708 io::ErrorKind::NotADirectory,
5709 ] {
5710 assert!(io_error_is_missing_file(&io::Error::from(kind), &file));
5711 }
5712 assert!(io_error_is_missing_file(
5713 &io::Error::from_raw_os_error(libc::EISDIR),
5714 &file
5715 ));
5716 assert!(!io_error_is_missing_file(
5717 &io::Error::from(io::ErrorKind::InvalidData),
5718 &ref_dir
5719 ));
5720 }
5721
5722 #[test]
5723 fn reading_a_ref_directory_is_not_found() {
5724 let dir = tempdir().unwrap();
5725 let ref_dir = dir.path().join("refs/heads/rel");
5726 fs::create_dir_all(ref_dir.join("1.0")).unwrap();
5727 match read_ref_file(&ref_dir) {
5728 Err(Error::Io(e)) => assert_eq!(e.kind(), io::ErrorKind::NotFound),
5729 other => panic!("expected a missing ref, got {other:?}"),
5730 }
5731 }
5732}
5733
5734#[cfg(test)]
5735mod packed_refs_parse_tests {
5736 use super::*;
5737
5738 use tempfile::tempdir;
5739
5740 #[test]
5741 fn packed_removal_validates_the_whole_file_before_any_change() {
5742 let dir = tempdir().unwrap();
5743 let path = store::paths::packed_refs_path(dir.path());
5744 let record = "1111111111111111111111111111111111111111 refs/heads/keep\n";
5745 let malformed = [
5746 format!("{record}garbage\n").into_bytes(),
5747 format!("{record}\n").into_bytes(),
5748 format!("{record}^bad-id\n").into_bytes(),
5749 format!("{record}^{}\n^{}\n", "1".repeat(40), "2".repeat(40)).into_bytes(),
5750 format!("{record}{} refs/heads/bad..name\n", "2".repeat(40)).into_bytes(),
5751 format!("{record}# comment\r\n").into_bytes(),
5752 record.trim_end().as_bytes().to_vec(),
5753 [record.as_bytes(), b"\xff\n"].concat(),
5754 ];
5755 for content in malformed {
5756 for name in ["refs/heads/missing", "refs/heads/keep"] {
5757 fs::write(&path, &content).unwrap();
5758 assert!(
5759 remove_packed_ref(dir.path(), name).is_err(),
5760 "{name}: {content:?}"
5761 );
5762 assert_eq!(fs::read(&path).unwrap(), content);
5763 assert!(!path.with_extension("lock").exists());
5764 assert!(!dir.path().join("packed-refs.new").exists());
5765 }
5766 }
5767 }
5768
5769 #[test]
5770 fn packed_removal_without_a_match_preserves_valid_file_bytes() {
5771 let dir = tempdir().unwrap();
5772 let path = store::paths::packed_refs_path(dir.path());
5773 for hash_len in [40, 64] {
5774 let body = format!(
5775 "# pack-refs with: peeled\n# keep this comment\n{} refs/tags/tag\n^{}\n{} refs/heads/main\n",
5776 "1".repeat(hash_len), "2".repeat(hash_len), "3".repeat(hash_len)
5777 );
5778 fs::write(&path, &body).unwrap();
5779 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5780 assert_eq!(fs::read_to_string(&path).unwrap(), body);
5781 }
5782 fs::write(&path, "").unwrap();
5783 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5784 assert!(fs::read(&path).unwrap().is_empty());
5785 }
5786
5787 #[test]
5788 fn parse_rejects_garbage_line() {
5789 let dir = tempdir().unwrap();
5790 let path = store::paths::packed_refs_path(dir.path());
5791 fs::write(
5792 &path,
5793 "# pack-refs with: peeled\n\
5794 0000000000000000000000000000000000000000 refs/heads/main\n\
5795 not a ref line\n",
5796 )
5797 .unwrap();
5798 let data = fs::read(&path).unwrap();
5799 assert!(matches!(
5800 parse_packed_refs_to_map(&path, &data),
5801 Err(Error::PackedRefsUnexpectedLine { .. })
5802 ));
5803 }
5804
5805 #[test]
5806 fn parse_rejects_carriage_return_bytes() {
5807 let dir = tempdir().unwrap();
5808 let path = store::paths::packed_refs_path(dir.path());
5809 fs::write(
5810 &path,
5811 "# pack-refs with: peeled\n\
5812 0000000000000000000000000000000000000000 refs/heads/main\r\n",
5813 )
5814 .unwrap();
5815 let data = fs::read(&path).unwrap();
5816 assert!(matches!(
5817 parse_packed_refs_to_map(&path, &data),
5818 Err(Error::PackedRefsUnexpectedLine { .. })
5819 ));
5820 }
5821
5822 #[test]
5823 fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
5824 let dir = tempdir().unwrap();
5825 let git_dir = dir.path();
5826 let oid = "1111111111111111111111111111111111111111";
5827 fs::write(
5828 store::paths::packed_refs_path(git_dir),
5829 format!(
5830 "# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5831 ),
5832 )
5833 .unwrap();
5834 let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
5835 assert_eq!(listed.len(), 1);
5836 assert_eq!(listed[0].0, "refs/heads/aaa");
5837 }
5838
5839 #[test]
5840 fn parse_rejects_blank_line_and_missing_final_newline() {
5841 let dir = tempdir().unwrap();
5842 let path = store::paths::packed_refs_path(dir.path());
5843 fs::write(
5844 &path,
5845 "# pack-refs with: peeled\n\n\
5846 0000000000000000000000000000000000000000 refs/heads/main\n",
5847 )
5848 .unwrap();
5849 let data = fs::read(&path).unwrap();
5850 assert!(matches!(
5851 parse_packed_refs_to_map(&path, &data),
5852 Err(Error::PackedRefsUnexpectedLine { .. })
5853 ));
5854
5855 fs::write(
5856 &path,
5857 "# pack-refs with: peeled\n\
5858 0000000000000000000000000000000000000000 refs/heads/main",
5859 )
5860 .unwrap();
5861 let data = fs::read(&path).unwrap();
5862 assert!(matches!(
5863 parse_packed_refs_to_map(&path, &data),
5864 Err(Error::PackedRefsUnexpectedLine { .. })
5865 ));
5866 }
5867
5868 #[test]
5869 fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
5870 let dir = tempdir().unwrap();
5871 let path = store::paths::packed_refs_path(dir.path());
5872 let a = "1111111111111111111111111111111111111111";
5873 let b = "2222222222222222222222222222222222222222";
5874 let peel = "3333333333333333333333333333333333333333";
5875 let body = |header: &str| {
5876 format!(
5877 "{header}{a} refs/heads/main\n\
5878 {b} refs/heads/tagged\n\
5879 ^{peel}\n\
5880 {a} refs/tags/light\n\
5881 {b} refs/tags/v1\n\
5882 ^{peel}\n"
5883 )
5884 };
5885 let a: ObjectId = a.parse().unwrap();
5886 let b: ObjectId = b.parse().unwrap();
5887 let peel: ObjectId = peel.parse().unwrap();
5888 let cases = [
5889 (
5890 "# pack-refs with: peeled fully-peeled sorted \n",
5891 [
5892 PackedPeel::NotTag,
5893 PackedPeel::Peeled(peel),
5894 PackedPeel::NotTag,
5895 PackedPeel::Peeled(peel),
5896 ],
5897 ),
5898 (
5899 "# pack-refs with: peeled sorted \n",
5900 [
5901 PackedPeel::Unknown,
5902 PackedPeel::Unknown,
5903 PackedPeel::NotTag,
5904 PackedPeel::Peeled(peel),
5905 ],
5906 ),
5907 ("", [PackedPeel::Unknown; 4]),
5908 ];
5909 for (header, want) in cases {
5910 let data = body(header).into_bytes();
5911 let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
5912 let seen = std::cell::RefCell::new(Vec::new());
5913 let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
5914 seen.borrow_mut().push((name.to_owned(), *oid, peel));
5915 name.starts_with("refs/tags/")
5916 };
5917 let kept = view
5918 .map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
5919 .unwrap();
5920 assert_eq!(
5921 seen.into_inner(),
5922 vec![
5923 ("refs/heads/main".to_owned(), a, want[0]),
5924 ("refs/heads/tagged".to_owned(), b, want[1]),
5925 ("refs/tags/light".to_owned(), a, want[2]),
5926 ("refs/tags/v1".to_owned(), b, want[3]),
5927 ],
5928 "header {header:?}"
5929 );
5930 let mut kept: Vec<_> = kept.into_iter().collect();
5931 kept.sort();
5932 assert_eq!(
5933 kept,
5934 vec![
5935 ("refs/tags/light".to_owned(), a),
5936 ("refs/tags/v1".to_owned(), b)
5937 ]
5938 );
5939 assert_eq!(
5940 view.map_for_prefix_filtered(&path, Some("refs"), None)
5941 .unwrap(),
5942 view.map_for_prefix(&path, Some("refs")).unwrap()
5943 );
5944 }
5945
5946 let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
5948 let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
5949 assert!(matches!(
5950 view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
5951 Err(Error::PackedRefsUnexpectedLine { .. })
5952 ));
5953 }
5954
5955 #[test]
5956 fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
5957 let dir = tempdir().unwrap();
5958 let path = store::paths::packed_refs_path(dir.path());
5959 let oid = "1111111111111111111111111111111111111111";
5960 let peel = "2222222222222222222222222222222222222222";
5961 let sorted_body = format!(
5962 "# pack-refs with: peeled fully-peeled sorted\n\
5963 {oid} refs/heads/aaa\n\
5964 {oid} refs/heads/main\n\
5965 {oid} refs/heads/zzz\n\
5966 {oid} refs/tags/v1\n\
5967 ^{peel}\n"
5968 );
5969 fs::write(&path, &sorted_body).unwrap();
5970 let data = fs::read(&path).unwrap();
5971 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5972 let full = parse_packed_refs_to_map(&path, &data).unwrap();
5973 for (name, want) in &full {
5974 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
5975 }
5976 assert_eq!(
5977 lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
5978 .unwrap()
5979 .map(|(o, p)| (o, p.unwrap())),
5980 Some((oid.parse().unwrap(), peel.parse().unwrap()))
5981 );
5982
5983 let unsorted_body = format!(
5984 "# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5985 );
5986 fs::write(&path, unsorted_body).unwrap();
5987 let data = fs::read(&path).unwrap();
5988 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5989 let full = parse_packed_refs_to_map(&path, &data).unwrap();
5990 for (name, want) in &full {
5991 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
5992 }
5993 assert_eq!(
5994 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
5995 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
5996 );
5997 }
5998
5999 #[test]
6000 fn sorted_prefix_without_upper_bound_filters_names() {
6001 let dir = tempdir().unwrap();
6002 let path = store::paths::packed_refs_path(dir.path());
6003 let oid = "1111111111111111111111111111111111111111";
6004 let sorted_body = format!(
6005 "# pack-refs with: peeled fully-peeled sorted\n\
6006 {oid} refs/heads/aaa\n\
6007 {oid} refs/heads/zzz\n\
6008 {oid} refs/tags/v1\n"
6009 );
6010 fs::write(&path, sorted_body).unwrap();
6011 let data = fs::read(&path).unwrap();
6012 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6013 assert_eq!(
6014 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6015 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6016 );
6017 assert!(
6018 !view
6019 .map_for_prefix(&path, Some("refs/heads/a"))
6020 .unwrap()
6021 .contains_key("refs/heads/zzz"),
6022 "prefix refs/heads/a must not include refs/heads/zzz"
6023 );
6024 assert_eq!(
6025 view.map_for_prefix(&path, Some("refs")).unwrap(),
6026 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
6027 );
6028 }
6029
6030 #[test]
6031 fn sorted_prefix_scan_validates_only_its_range() {
6032 let dir = tempdir().unwrap();
6033 let path = store::paths::packed_refs_path(dir.path());
6034 let oid = "1111111111111111111111111111111111111111";
6035 let view_of =
6036 |body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
6037
6038 let bad_peel_on_branch = view_of(format!(
6039 "# pack-refs with: peeled fully-peeled sorted\n\
6040 {oid} refs/heads/main\n\
6041 ^not-hex\n\
6042 {oid} refs/heads/topic\n\
6043 {oid} refs/tags/v1\n"
6044 ))
6045 .unwrap();
6046 assert!(matches!(
6047 bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
6048 Err(Error::PackedRefsUnexpectedLine { .. })
6049 ));
6050 let tags = bad_peel_on_branch
6051 .map_for_prefix(&path, Some("refs/tags"))
6052 .unwrap();
6053 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
6054 assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
6055
6056 let blank_among_tags = view_of(format!(
6057 "# pack-refs with: peeled fully-peeled sorted\n\
6058 {oid} refs/heads/main\n\
6059 {oid} refs/tags/v1\n\
6060 \n\
6061 {oid} refs/tags/v2\n"
6062 ))
6063 .unwrap();
6064 assert_eq!(
6065 blank_among_tags
6066 .map_for_prefix(&path, Some("refs/heads"))
6067 .unwrap()
6068 .len(),
6069 1
6070 );
6071 assert!(blank_among_tags
6072 .map_for_prefix(&path, Some("refs/tags"))
6073 .is_err());
6074
6075 let no_match = blank_among_tags
6076 .map_for_prefix(&path, Some("refs/notes"))
6077 .unwrap();
6078 assert!(no_match.is_empty());
6079 }
6080
6081 #[test]
6082 fn non_utf8_record_only_breaks_reads_that_cover_it() {
6083 let dir = tempdir().unwrap();
6084 let path = store::paths::packed_refs_path(dir.path());
6085 let oid = "1111111111111111111111111111111111111111";
6086 let mut body =
6087 format!("# pack-refs with: peeled fully-peeled sorted \n{oid} refs/heads/main\n")
6088 .into_bytes();
6089 body.extend_from_slice(format!("{oid} refs/tags/v1\n{oid} refs/tags/").as_bytes());
6090 body.extend_from_slice(b"\xff\n");
6091 let view = PackedRefsFileView::from_bytes(&path, body, HashAlgo::Sha1).unwrap();
6092 assert!(view.sorted);
6093 for name in ["refs/heads/main", "refs/tags/v1"] {
6094 assert_eq!(
6095 view.lookup_oid(&path, name).unwrap(),
6096 Some(oid.parse().unwrap())
6097 );
6098 }
6099 assert_eq!(view.lookup_oid(&path, "refs/tags/v2").unwrap(), None);
6100 let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
6101 assert_eq!(heads.keys().collect::<Vec<_>>(), ["refs/heads/main"]);
6102 assert!(matches!(
6103 view.map_for_prefix(&path, Some("refs/tags")),
6104 Err(Error::PackedRefsUnexpectedLine { .. })
6105 ));
6106 assert!(view.map_for_prefix(&path, None).is_err());
6107
6108 let mut bad_header = b"# pack-refs with: \xff sorted \n".to_vec();
6109 bad_header.extend_from_slice(format!("{oid} refs/heads/main\n").as_bytes());
6110 assert!(matches!(
6111 PackedRefsFileView::from_bytes(&path, bad_header, HashAlgo::Sha1),
6112 Err(Error::PackedRefsUnexpectedLine { .. })
6113 ));
6114 }
6115
6116 #[test]
6117 fn scan_of_file_without_ref_lines_still_validates_it() {
6118 let dir = tempdir().unwrap();
6119 let path = store::paths::packed_refs_path(dir.path());
6120 let view = PackedRefsFileView::from_bytes(
6121 &path,
6122 b"# pack-refs with: peeled fully-peeled sorted\n\
6123 ^1111111111111111111111111111111111111111\n"
6124 .to_vec(),
6125 HashAlgo::Sha1,
6126 )
6127 .unwrap();
6128 for prefix in [None, Some("refs/heads")] {
6129 assert!(matches!(
6130 view.map_for_prefix(&path, prefix),
6131 Err(Error::PackedRefsUnexpectedLine { .. })
6132 ));
6133 }
6134 let header_only = PackedRefsFileView::from_bytes(
6135 &path,
6136 b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
6137 HashAlgo::Sha1,
6138 )
6139 .unwrap();
6140 assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
6141 }
6142
6143 #[test]
6144 fn view_checks_last_line_length_against_the_repository_hash() {
6145 let dir = tempdir().unwrap();
6146 let path = store::paths::packed_refs_path(dir.path());
6147 let sha1 = "1111111111111111111111111111111111111111";
6148 let sha256 = "2".repeat(64);
6149 let view =
6150 |body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
6151 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6152 let fifty_bytes = "x".repeat(49);
6153
6154 for (body, algo) in [
6155 (
6156 format!("{header}{sha1} refs/heads/main\ngarbage\n"),
6157 HashAlgo::Sha1,
6158 ),
6159 (
6160 format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
6161 HashAlgo::Sha1,
6162 ),
6163 (
6164 format!("{header}{sha1} refs/heads/main\n#\n"),
6165 HashAlgo::Sha1,
6166 ),
6167 (
6168 format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
6169 HashAlgo::Sha256,
6170 ),
6171 (format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
6172 ] {
6173 assert!(
6174 matches!(
6175 view(body.clone(), algo),
6176 Err(Error::PackedRefsUnexpectedLine { .. })
6177 ),
6178 "{algo:?}: {body:?}"
6179 );
6180 }
6181 for (body, algo) in [
6182 (header.to_owned(), HashAlgo::Sha1),
6183 (header.to_owned(), HashAlgo::Sha256),
6184 ("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
6185 (
6186 format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
6187 HashAlgo::Sha1,
6188 ),
6189 (
6190 format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
6191 HashAlgo::Sha1,
6192 ),
6193 (
6194 format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
6195 HashAlgo::Sha256,
6196 ),
6197 ] {
6198 assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
6199 }
6200 }
6201
6202 #[test]
6203 fn sha256_short_last_record_fails_branch_listing() {
6204 let dir = tempdir().unwrap();
6205 let path = store::paths::packed_refs_path(dir.path());
6206 let sha256 = "2".repeat(64);
6207 let body = format!(
6208 "# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
6209 "x".repeat(49)
6210 );
6211 assert!(
6212 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
6213 );
6214 }
6215
6216 #[test]
6217 fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
6218 let dir = tempdir().unwrap();
6219 let path = store::paths::packed_refs_path(dir.path());
6220 let oid = "1111111111111111111111111111111111111111";
6221 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6222 let view = |body: String| {
6223 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6224 };
6225
6226 let garbage_after_first_branch = view(format!(
6227 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
6228 {oid} refs/tags/t1\n"
6229 ));
6230 assert!(matches!(
6231 garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
6232 Err(Error::PackedRefsUnexpectedLine { .. })
6233 ));
6234 let tags = garbage_after_first_branch
6235 .map_for_prefix(&path, Some("refs/tags"))
6236 .unwrap();
6237 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
6238
6239 let garbage_before_range = view(format!(
6240 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
6241 ));
6242 for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
6243 assert!(
6244 garbage_before_range
6245 .map_for_prefix(&path, Some(prefix))
6246 .is_err(),
6247 "{prefix}"
6248 );
6249 }
6250 assert_eq!(
6251 garbage_before_range
6252 .map_for_prefix(&path, Some("refs/zz"))
6253 .unwrap()
6254 .len(),
6255 0
6256 );
6257
6258 let peeled_tag_before_range = view(format!(
6259 "{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
6260 ));
6261 let orphan_peel_before_range = view(format!(
6262 "{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
6263 ));
6264 assert!(orphan_peel_before_range
6265 .map_for_prefix(&path, Some("refs/tags"))
6266 .is_err());
6267 assert_eq!(
6268 peeled_tag_before_range
6269 .map_for_prefix(&path, Some("refs/tags"))
6270 .unwrap()
6271 .len(),
6272 1
6273 );
6274 }
6275
6276 fn lying_and_unsorted_views(path: &Path) -> (PackedRefsFileView, PackedRefsFileView) {
6278 let oid = "1111111111111111111111111111111111111111";
6279 let records = format!("{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n");
6280 let view_of = |header: &str| {
6281 PackedRefsFileView::from_bytes(
6282 path,
6283 format!("{header}{records}").into_bytes(),
6284 HashAlgo::Sha1,
6285 )
6286 .unwrap()
6287 };
6288 (
6289 view_of("# pack-refs with: peeled fully-peeled sorted\n"),
6290 view_of("# pack-refs with: peeled fully-peeled\n"),
6291 )
6292 }
6293
6294 fn sorted_names(map: HashMap<String, ObjectId>) -> Vec<String> {
6295 let mut names: Vec<String> = map.into_keys().collect();
6296 names.sort();
6297 names
6298 }
6299
6300 #[test]
6301 fn full_scans_of_a_lying_sorted_file_return_every_ref() {
6302 let dir = tempdir().unwrap();
6303 let path = store::paths::packed_refs_path(dir.path());
6304 let (lying, unsorted) = lying_and_unsorted_views(&path);
6305 assert!(lying.sorted);
6306 let every = ["refs/heads/a", "refs/heads/b", "refs/tags/t"];
6307 for view in [&lying, &unsorted] {
6308 for prefix in [None, Some("refs"), Some("refs/")] {
6309 assert_eq!(
6310 sorted_names(view.map_for_prefix(&path, prefix).unwrap()),
6311 every,
6312 "{prefix:?}"
6313 );
6314 }
6315 }
6316 fs::write(&path, lying.data.as_ref() as &[u8]).unwrap();
6317 let loaded = PackedRefs::load(dir.path()).unwrap();
6318 for name in every {
6319 assert!(loaded.get(name).is_some(), "{name}");
6320 }
6321 }
6322
6323 #[test]
6324 fn prefix_scan_over_an_out_of_order_range_falls_back_to_the_whole_file() {
6325 let dir = tempdir().unwrap();
6326 let path = store::paths::packed_refs_path(dir.path());
6327 let (lying, _) = lying_and_unsorted_views(&path);
6328 let heads = lying.map_for_prefix(&path, Some("refs/heads")).unwrap();
6329 assert_eq!(sorted_names(heads), ["refs/heads/a", "refs/heads/b"]);
6330
6331 let oid = "1111111111111111111111111111111111111111";
6333 let view_of = |names: &[&str]| {
6334 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
6335 for name in names {
6336 body.push_str(&format!("{oid} {name}\n"));
6337 }
6338 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6339 };
6340 let interleaved = view_of(&["refs/heads/a", "refs/tags/t", "refs/heads/b", "refs/tags/u"]);
6341 let (begin, end) = interleaved.prefix_region("refs/heads").unwrap();
6342 let (_, in_order) = interleaved
6343 .parse_region::<HashMap<_, _>>(&path, begin, end, Some("refs/heads"), None)
6344 .unwrap();
6345 assert!(!in_order);
6346 assert_eq!(
6347 sorted_names(
6348 interleaved
6349 .map_for_prefix(&path, Some("refs/heads"))
6350 .unwrap()
6351 ),
6352 ["refs/heads/a", "refs/heads/b"]
6353 );
6354
6355 let bad_neighbour = view_of(&["refs/heads/x", "refs/heads/a"]);
6358 assert_eq!(bad_neighbour.prefix_region("refs/heads/a"), None);
6359 assert_eq!(
6360 sorted_names(
6361 bad_neighbour
6362 .map_for_prefix(&path, Some("refs/heads/a"))
6363 .unwrap()
6364 ),
6365 ["refs/heads/a"]
6366 );
6367
6368 let corrupt = PackedRefsFileView::from_bytes(
6370 &path,
6371 format!(
6372 "# pack-refs with: peeled fully-peeled sorted\n\
6373 {oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/bad..name\n"
6374 )
6375 .into_bytes(),
6376 HashAlgo::Sha1,
6377 )
6378 .unwrap();
6379 assert!(matches!(
6380 corrupt.map_for_prefix(&path, Some("refs/heads")),
6381 Err(Error::PackedRefsUnexpectedLine { .. })
6382 ));
6383 }
6384
6385 #[test]
6386 fn full_map_keeps_every_name_and_the_last_duplicate_value() {
6387 let dir = tempdir().unwrap();
6388 let path = store::paths::packed_refs_path(dir.path());
6389 let first = "1111111111111111111111111111111111111111";
6390 let last = "2222222222222222222222222222222222222222";
6391 for header in ["# pack-refs with: sorted\n", "# pack-refs with: peeled\n"] {
6392 for names in [
6393 ["refs/heads/a", "refs/heads/b", "refs/heads/b"],
6394 ["refs/heads/b", "refs/heads/a", "refs/heads/b"],
6395 ] {
6396 let body = format!(
6397 "{header}{first} {}\n{first} {}\n{last} {}\n",
6398 names[0], names[1], names[2]
6399 );
6400 let view = PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1)
6401 .unwrap();
6402 let full = view.full_map(&path).unwrap();
6403 assert_eq!(full.len(), 2);
6404 assert_eq!(full["refs/heads/b"], last.parse().unwrap());
6405 assert_eq!(
6406 full.into_iter().collect::<HashMap<_, _>>(),
6407 view.map_for_prefix(&path, None).unwrap()
6408 );
6409 }
6410 }
6411 }
6412
6413 #[test]
6414 fn prefix_scan_validates_both_neighbouring_records_and_their_peels() {
6415 let dir = tempdir().unwrap();
6416 let path = store::paths::packed_refs_path(dir.path());
6417 let oid = "1111111111111111111111111111111111111111";
6418 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6419 for neighbour in ["refs/heads/a", "refs/tags/z"] {
6420 for corrupt in [
6421 format!("{oid} {neighbour}..bad\n"),
6422 format!("{} {neighbour}\n", "g".repeat(40)),
6423 format!("{oid} {neighbour}\n^{}\n", "g".repeat(40)),
6424 ] {
6425 let records = if neighbour.starts_with("refs/heads/") {
6426 format!("{corrupt}{oid} refs/heads/m\n{oid} refs/tags/z\n")
6427 } else {
6428 format!("{oid} refs/heads/a\n{oid} refs/heads/m\n{corrupt}")
6429 };
6430 let view = PackedRefsFileView::from_bytes(
6431 &path,
6432 format!("{header}{records}").into_bytes(),
6433 HashAlgo::Sha1,
6434 )
6435 .unwrap();
6436 assert!(
6437 matches!(
6438 view.map_for_prefix(&path, Some("refs/heads/m")),
6439 Err(Error::PackedRefsUnexpectedLine { .. })
6440 ),
6441 "{records}"
6442 );
6443 }
6444 }
6445 }
6446
6447 #[test]
6450 fn point_lookup_in_a_lying_sorted_file_behaves_like_git() {
6451 let dir = tempdir().unwrap();
6452 let path = store::paths::packed_refs_path(dir.path());
6453 let (lying, unsorted) = lying_and_unsorted_views(&path);
6454 assert!(lying.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
6456 assert_eq!(lying.lookup_oid(&path, "refs/heads/b").unwrap(), None);
6457 assert!(lying.lookup_oid(&path, "refs/tags/t").unwrap().is_some());
6458
6459 assert!(!unsorted.sorted);
6460 for name in ["refs/heads/a", "refs/heads/b", "refs/tags/t"] {
6461 assert!(
6462 unsorted.lookup_oid(&path, name).unwrap().is_some(),
6463 "{name}"
6464 );
6465 }
6466 }
6467
6468 #[test]
6469 fn records_are_only_record_shaped_lines() {
6470 let dir = tempdir().unwrap();
6471 let path = store::paths::packed_refs_path(dir.path());
6472 let view = |data: &[u8]| {
6473 PackedRefsFileView::from_bytes(&path, data.to_vec(), HashAlgo::Sha1).unwrap()
6474 };
6475 let hex = "1".repeat(40);
6476 let data = format!("# header\n{hex} x\n^peel\n\n{hex} y\n#c\nshort\n{hex}z\n{hex} z\n");
6477 let starts: Vec<usize> = view(data.as_bytes()).records().collect();
6478 assert_eq!(starts, [9, 59, 153]);
6479 assert_eq!(view(b"").records().count(), 0);
6480 let out_of_order = format!("{hex} b\n{hex} a\n");
6481 assert_eq!(
6482 view(out_of_order.as_bytes()).records().collect::<Vec<_>>(),
6483 [0, 43]
6484 );
6485 }
6486
6487 #[test]
6488 fn parse_rejects_orphan_peel_and_double_space() {
6489 let dir = tempdir().unwrap();
6490 let path = store::paths::packed_refs_path(dir.path());
6491 fs::write(
6492 &path,
6493 "# pack-refs with: peeled\n\
6494 ^0000000000000000000000000000000000000000\n\
6495 0000000000000000000000000000000000000000 refs/heads/main\n",
6496 )
6497 .unwrap();
6498 let data = fs::read(&path).unwrap();
6499 assert!(matches!(
6500 parse_packed_refs_to_map(&path, &data),
6501 Err(Error::PackedRefsUnexpectedLine { .. })
6502 ));
6503
6504 fs::write(
6505 &path,
6506 "# pack-refs with: peeled\n\
6507 0000000000000000000000000000000000000000 refs/heads/main\n",
6508 )
6509 .unwrap();
6510 let data = fs::read(&path).unwrap();
6511 assert!(matches!(
6512 parse_packed_refs_to_map(&path, &data),
6513 Err(Error::PackedRefsUnexpectedLine { .. })
6514 ));
6515 }
6516}
6517
6518#[cfg(test)]
6519mod pack_refs_tests {
6520 use super::*;
6521 use crate::objects::{serialize_commit, CommitData, ObjectKind};
6522 use crate::repo::{init_repository, Repository};
6523 use std::fs;
6524 use std::sync::Arc;
6525
6526 fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
6527 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
6528 let raw = serialize_commit(&CommitData {
6529 tree,
6530 parents,
6531 author: "T <t@example.com> 1 +0000".into(),
6532 committer: "T <t@example.com> 1 +0000".into(),
6533 author_raw: Vec::new(),
6534 committer_raw: Vec::new(),
6535 encoding: None,
6536 message: "m\n".into(),
6537 raw_message: None,
6538 extra_headers: Vec::new(),
6539 });
6540 repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
6541 }
6542
6543 #[test]
6544 fn pack_refs_skips_concurrent_loose_update() {
6545 let dir = tempfile::tempdir().expect("tempdir");
6546 let repo = init_repository(
6547 dir.path(),
6548 false,
6549 "main",
6550 None,
6551 crate::RefStorageFormat::Files,
6552 )
6553 .expect("init");
6554 let git_dir = repo.git_dir().to_path_buf();
6555 let c1 = write_commit(&repo, vec![]);
6556 let c2 = write_commit(&repo, vec![c1]);
6557 write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
6558
6559 let git_dir_hook = git_dir.clone();
6560 let c2 = Arc::new(c2);
6561 let c2_hook = Arc::clone(&c2);
6562 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
6563 write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
6564 })));
6565
6566 let (_path, packed, pruned) =
6567 pack_refs_files(&git_dir, repo.odb(), true, false).expect("pack");
6568 pack_refs_test_hooks::set_before_prune(None);
6569 assert!(packed >= 1);
6570 assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
6571
6572 let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
6573 assert_eq!(resolved, *c2);
6574 let packed_body =
6575 fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
6576 assert!(
6577 !packed_body.contains("refs/heads/race"),
6578 "packed entry for concurrently updated ref must be removed"
6579 );
6580 }
6581
6582 #[test]
6583 fn clone_packing_retains_symbolic_refs_and_concurrent_tag_updates() {
6584 let dir = tempfile::tempdir().expect("tempdir");
6585 let repo = init_repository(
6586 dir.path(),
6587 false,
6588 "main",
6589 None,
6590 crate::RefStorageFormat::Files,
6591 )
6592 .expect("init");
6593 let git_dir = repo.git_dir().to_path_buf();
6594 let first = write_commit(&repo, vec![]);
6595 let second = write_commit(&repo, vec![first]);
6596 write_ref(&git_dir, "refs/tags/race", &first).expect("tag");
6597 write_ref(&git_dir, "refs/heads/main", &first).expect("branch");
6598 write_ref(&git_dir, "refs/remotes/origin/main", &first).expect("remote");
6599 write_symbolic_ref(
6600 &git_dir,
6601 "refs/remotes/origin/HEAD",
6602 "refs/remotes/origin/main",
6603 )
6604 .expect("symbolic");
6605 let tag = repo
6606 .odb()
6607 .write(
6608 ObjectKind::Tag,
6609 format!("object {first}\ntype commit\n\nremote tag\n").as_bytes(),
6610 )
6611 .expect("tag object");
6612 write_ref(&git_dir, "refs/remotes/origin/tag", &tag).expect("tagged remote");
6613 let hook_dir = git_dir.clone();
6614 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
6615 write_ref(&hook_dir, "refs/tags/race", &second).expect("concurrent update");
6616 })));
6617 pack_remote_tracking_refs_for_clone(&git_dir, "origin").expect("pack");
6618 assert_eq!(
6619 resolve_ref(&git_dir, "refs/tags/race").expect("resolve"),
6620 second
6621 );
6622 assert!(git_dir.join("refs/heads/main").exists());
6623 assert!(git_dir.join("refs/remotes/origin/HEAD").exists());
6624 assert!(!git_dir.join("refs/remotes/origin/main").exists());
6625 let body = fs::read_to_string(store::paths::packed_refs_path(&git_dir)).expect("packed");
6626 assert!(!body.contains("refs/tags/race"));
6627 assert!(!body.contains("refs/remotes/origin/HEAD"));
6628 assert!(
6629 body.contains(&format!("{tag} refs/remotes/origin/tag\n^{first}\n")),
6630 "{body}"
6631 );
6632 }
6633
6634 #[test]
6635 fn clone_packing_routes_reftable_without_creating_packed_refs() {
6636 let dir = tempfile::tempdir().expect("tempdir");
6637 let repo = init_repository(
6638 dir.path(),
6639 false,
6640 "main",
6641 None,
6642 crate::RefStorageFormat::Reftable,
6643 )
6644 .expect("init");
6645 let commit = write_commit(&repo, vec![]);
6646 write_ref(repo.git_dir(), "refs/tags/tip", &commit).expect("tag");
6647 pack_remote_tracking_refs_for_clone(repo.git_dir(), "origin").expect("pack");
6648 assert_eq!(
6649 resolve_ref(repo.git_dir(), "refs/tags/tip").expect("resolve"),
6650 commit
6651 );
6652 assert!(!store::paths::packed_refs_path(repo.git_dir()).exists());
6653 }
6654
6655 #[test]
6656 fn pack_refs_nested_tag_peel_line_points_at_commit() {
6657 let dir = tempfile::tempdir().expect("tempdir");
6658 let repo = init_repository(
6659 dir.path(),
6660 false,
6661 "main",
6662 None,
6663 crate::RefStorageFormat::Files,
6664 )
6665 .expect("init");
6666 let commit = write_commit(&repo, vec![]);
6667 let inner_tag_body = format!(
6668 "object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
6669 commit.to_hex()
6670 );
6671 let inner = repo
6672 .odb()
6673 .write(ObjectKind::Tag, inner_tag_body.as_bytes())
6674 .expect("inner tag");
6675 let outer_tag_body = format!(
6676 "object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
6677 inner.to_hex()
6678 );
6679 let outer = repo
6680 .odb()
6681 .write(ObjectKind::Tag, outer_tag_body.as_bytes())
6682 .expect("outer tag");
6683 write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
6684
6685 pack_refs_files(repo.git_dir(), repo.odb(), false, false).expect("pack");
6686 let body =
6687 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
6688 let mut lines = body.lines();
6689 let tag_line = lines
6690 .find(|l| l.contains("refs/tags/outer"))
6691 .expect("tag line");
6692 assert!(tag_line.starts_with(&outer.to_hex()));
6693 let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
6694 assert!(
6695 peel_line.contains(&commit.to_hex()),
6696 "peel must reach commit through nested tags, got {peel_line}"
6697 );
6698 }
6699
6700 #[test]
6701 fn pack_refs_tree_target_tag_has_peel_line() {
6702 let dir = tempfile::tempdir().expect("tempdir");
6703 let repo = init_repository(
6704 dir.path(),
6705 false,
6706 "main",
6707 None,
6708 crate::RefStorageFormat::Files,
6709 )
6710 .expect("init");
6711 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
6712 let tag_body = format!(
6713 "object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
6714 tree.to_hex()
6715 );
6716 let tag = repo
6717 .odb()
6718 .write(ObjectKind::Tag, tag_body.as_bytes())
6719 .expect("tag");
6720 write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
6721
6722 pack_refs_files(repo.git_dir(), repo.odb(), true, false).expect("pack");
6723 let body =
6724 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
6725 assert!(body.contains("refs/tags/tree-tag"));
6726 assert!(
6727 body.contains(&format!("^{}", tree.to_hex())),
6728 "expected peel to tree: {body}"
6729 );
6730 }
6731
6732 #[test]
6733 fn write_packed_refs_file_survives_publish_failure() {
6734 let dir = tempfile::tempdir().expect("tempdir");
6735 let git_dir = dir.path();
6736 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
6737 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
6738 fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
6739 fs::create_dir_all(store::paths::packed_refs_path(git_dir))
6740 .expect("block packed-refs file");
6741 let err = write_packed_refs_file(
6742 git_dir,
6743 &[PackedRefRecord {
6744 refname: "refs/heads/keep".into(),
6745 oid,
6746 peeled: None,
6747 }],
6748 )
6749 .expect_err("publish should fail");
6750 assert!(matches!(err, Error::Io(_)));
6751 assert!(git_dir.join("refs/heads/keep").is_file());
6752 }
6753
6754 #[test]
6755 fn write_packed_refs_file_replaces_loose_and_sorts() {
6756 let dir = tempfile::tempdir().expect("tempdir");
6757 let git_dir = dir.path();
6758 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
6759 let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
6760 let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
6761 fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
6762 write_packed_refs_file(
6763 git_dir,
6764 &[
6765 PackedRefRecord {
6766 refname: "refs/heads/z-last".into(),
6767 oid: oid_b,
6768 peeled: None,
6769 },
6770 PackedRefRecord {
6771 refname: "refs/heads/a-first".into(),
6772 oid: oid_a,
6773 peeled: None,
6774 },
6775 ],
6776 )
6777 .expect("write packed-refs");
6778 assert!(!git_dir.join("refs/heads/a-first").exists());
6779 let body =
6780 fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
6781 assert!(body.contains("refs/heads/a-first"));
6782 assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
6783 }
6784
6785 #[test]
6786 fn write_packed_refs_file_keeps_refs_dir_for_top_level_ref() {
6787 let dir = tempfile::tempdir().expect("tempdir");
6788 let git_dir = dir.path();
6789 fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
6790 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
6791 fs::write(git_dir.join("refs/stash"), format!("{oid}\n")).expect("loose stash");
6792 write_packed_refs_file(
6793 git_dir,
6794 &[PackedRefRecord {
6795 refname: "refs/stash".into(),
6796 oid,
6797 peeled: None,
6798 }],
6799 )
6800 .expect("write packed-refs");
6801 assert!(!git_dir.join("refs/stash").exists());
6802 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
6803 assert!(git_dir.join("refs/tags").is_dir());
6804 }
6805
6806 #[test]
6807 fn write_packed_refs_file_prunes_emptied_ref_dirs_below_refs() {
6808 let dir = tempfile::tempdir().expect("tempdir");
6809 let git_dir = dir.path();
6810 fs::create_dir_all(git_dir.join("refs/heads/topic/empty")).expect("refs/heads");
6811 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
6812 fs::write(git_dir.join("refs/heads/topic/a"), format!("{oid}\n")).expect("loose ref");
6813 write_packed_refs_file(
6814 git_dir,
6815 &[PackedRefRecord {
6816 refname: "refs/heads/topic/a".into(),
6817 oid,
6818 peeled: None,
6819 }],
6820 )
6821 .expect("write packed-refs");
6822 assert!(!git_dir.join("refs/heads/topic/a").exists());
6823 assert!(!git_dir.join("refs/heads/topic").exists());
6824 assert!(git_dir.join("refs/heads").is_dir(), "refs/heads/ kept");
6825 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
6826 }
6827
6828 #[test]
6829 fn pack_refs_files_keeps_refs_dir_after_packing_stash() {
6830 let dir = tempfile::tempdir().expect("tempdir");
6831 let repo = init_repository(
6832 dir.path(),
6833 false,
6834 "main",
6835 None,
6836 crate::RefStorageFormat::Files,
6837 )
6838 .expect("init");
6839 let blob = repo.odb().write(ObjectKind::Blob, b"x").expect("blob");
6840 write_ref(repo.git_dir(), "refs/stash", &blob).expect("stash");
6841 let refs = repo.git_dir().join("refs");
6842 pack_refs_files(repo.git_dir(), repo.odb(), true, false).expect("pack");
6843 assert!(!refs.join("stash").exists());
6844 assert!(refs.is_dir(), "refs/ must remain");
6845 }
6846}
6847
6848#[cfg(test)]
6849mod packed_ref_peels_tests {
6850 use super::*;
6851
6852 fn oid(n: u8) -> ObjectId {
6853 ObjectId::from_bytes(&[n; 20]).expect("oid")
6854 }
6855
6856 fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
6857 let dir = tempfile::tempdir().expect("tempdir");
6858 fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
6859 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
6860 (dir, peels)
6861 }
6862
6863 fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
6864 let mut out = String::from(header);
6865 for (name, oid, peel) in records {
6866 out.push_str(&format!("{oid} {name}\n"));
6867 if let Some(peel) = peel {
6868 out.push_str(&format!("^{peel}\n"));
6869 }
6870 }
6871 out
6872 }
6873
6874 #[test]
6875 fn fully_peeled_file_answers_for_every_ref() {
6876 for header in [
6877 "# pack-refs with: peeled fully-peeled sorted \n",
6878 "# pack-refs with: peeled fully-peeled \n",
6879 ] {
6880 let records = [
6881 ("refs/heads/main", oid(1), None),
6882 ("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
6883 ("refs/tags/v1", oid(4), Some(oid(1))),
6884 ("refs/tags/light", oid(1), None),
6885 ];
6886 let mut ordered = records;
6888 if header.contains(" sorted") {
6889 ordered.sort_by_key(|(name, _, _)| *name);
6890 } else {
6891 ordered.reverse();
6892 }
6893 let (_dir, peels) = peels_for(&body(header, &ordered));
6894 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
6895 assert_eq!(
6896 peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
6897 Some(Some(oid(3)))
6898 );
6899 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
6900 assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
6901 assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
6903 assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
6904 }
6905 }
6906
6907 #[test]
6908 fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
6909 let records = [
6910 ("refs/heads/main", oid(1), None),
6911 ("refs/tags/v1", oid(4), Some(oid(1))),
6912 ];
6913 let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
6914 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
6915 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
6916
6917 for header in [
6918 "",
6919 "# pack-refs with: sorted\n",
6920 "# some comment\n",
6921 "#pack-refs with: peeled fully-peeled\n",
6922 "# pack-refs with: peeled fully-peeled\n",
6923 ] {
6924 let (_dir, peels) = peels_for(&body(header, &records));
6925 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
6926 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
6927 }
6928 }
6929
6930 #[test]
6931 fn missing_file_and_other_backends_know_nothing() {
6932 let dir = tempfile::tempdir().expect("tempdir");
6933 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
6934 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
6935
6936 let records = [("refs/heads/main", oid(1), None)];
6937 fs::write(
6938 store::paths::packed_refs_path(dir.path()),
6939 body("# pack-refs with: peeled fully-peeled sorted\n", &records),
6940 )
6941 .expect("packed-refs");
6942 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
6943 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
6944 }
6945
6946 #[test]
6947 fn lookups_in_any_order_agree_with_a_fresh_snapshot() {
6948 let records = [
6949 ("refs/heads/main", oid(1), None),
6950 ("refs/tags/a", oid(2), Some(oid(1))),
6951 ("refs/tags/b", oid(1), None),
6952 ("refs/tags/c", oid(3), Some(oid(4))),
6953 ("refs/tags/d", oid(5), None),
6954 ];
6955 let text = body("# pack-refs with: peeled fully-peeled sorted \n", &records);
6956 let (_dir, peels) = peels_for(&text);
6957 let fresh = |name: &str, value: &ObjectId| peels_for(&text).1.peeled(name, value);
6958 let queries = [
6959 ("refs/tags/a", oid(2)),
6960 ("refs/tags/b", oid(1)),
6961 ("refs/tags/b", oid(1)),
6962 ("refs/tags/bb", oid(1)),
6963 ("refs/tags/c", oid(3)),
6964 ("refs/tags/d", oid(9)),
6965 ("refs/tags/d", oid(5)),
6966 ("refs/heads/main", oid(1)),
6967 ("refs/tags/c", oid(3)),
6968 ("refs/tags/a", oid(2)),
6969 ("refs/tags/zzz", oid(1)),
6970 ("refs/tags/a", oid(2)),
6971 ("refs/tags/b", oid(1)),
6972 ];
6973 for (name, value) in queries {
6974 assert_eq!(
6975 peels.peeled(name, &value),
6976 fresh(name, &value),
6977 "{name} {value}"
6978 );
6979 }
6980 assert_eq!(peels.peeled("refs/tags/c", &oid(3)), Some(Some(oid(4))));
6981 assert_eq!(peels.peeled("refs/tags/d", &oid(5)), Some(None));
6982 }
6983
6984 #[test]
6985 fn store_snapshot_reuses_the_files_store_view_and_follows_rewrites() {
6986 let dir = tempfile::tempdir().expect("tempdir");
6987 let path = store::paths::packed_refs_path(dir.path());
6988 let header = "# pack-refs with: peeled fully-peeled sorted \n";
6989 let records = [
6990 ("refs/heads/main", oid(1), None),
6991 ("refs/tags/v1", oid(2), Some(oid(1))),
6992 ("refs/tags/v2", oid(3), None),
6993 ];
6994 fs::write(&path, body(header, &records)).expect("packed-refs");
6995 let files = store::FilesRefStore::from_git_dir(dir.path()).expect("files store");
6996
6997 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
6998 let loaded = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
6999 for (name, value, _) in records {
7000 assert_eq!(
7001 peels.peeled(name, &value),
7002 loaded.peeled(name, &value),
7003 "{name}"
7004 );
7005 }
7006 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), Some(Some(oid(1))));
7007 let cached = files
7008 .packed_refs_view()
7009 .expect("view")
7010 .expect("packed-refs");
7011 let shared = peels.view.as_ref().expect("snapshot view");
7012 assert!(std::sync::Arc::ptr_eq(&cached.data, &shared.data));
7013
7014 let rewritten = [
7015 ("refs/heads/main", oid(1), None),
7016 ("refs/tags/v1", oid(4), Some(oid(5))),
7017 ("refs/tags/v2", oid(3), None),
7018 ("refs/tags/v3", oid(6), Some(oid(1))),
7019 ];
7020 fs::write(&path, body(header, &rewritten)).expect("rewrite packed-refs");
7021 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7022 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), None);
7023 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(5))));
7024 assert_eq!(peels.peeled("refs/tags/v3", &oid(6)), Some(Some(oid(1))));
7025
7026 let memory = store::MemoryRefStore::new();
7027 let fallback = PackedRefPeels::for_store(&memory, dir.path()).expect("fallback");
7028 let direct = PackedRefPeels::load(dir.path(), memory.format()).expect("load");
7029 for (name, value, _) in rewritten {
7030 assert_eq!(
7031 fallback.peeled(name, &value),
7032 direct.peeled(name, &value),
7033 "{name}"
7034 );
7035 }
7036 }
7037
7038 fn raw_view(data: &[u8], hex_len: usize) -> PackedRefsFileView {
7040 PackedRefsFileView {
7041 data: crate::pack_map::PackData::from_owned(data.to_vec()),
7042 sorted: true,
7043 hex_len,
7044 body_start: 0,
7045 peel_trait: PackedPeelTrait::None,
7046 }
7047 }
7048
7049 fn naive_record_starts(data: &[u8], hex_len: usize) -> Vec<usize> {
7051 let mut out = Vec::new();
7052 let mut start = 0;
7053 for line in data.split(|&b| b == b'\n') {
7054 if start >= data.len() {
7055 break;
7056 }
7057 if packed_line_is_record(line, hex_len) {
7058 out.push(start);
7059 }
7060 start += line.len() + 1;
7061 }
7062 out
7063 }
7064
7065 fn assert_record_walks_match(data: &[u8], hex_len: usize) {
7066 let view = raw_view(data, hex_len);
7067 let want = naive_record_starts(data, hex_len);
7068 assert_eq!(
7069 view.records().collect::<Vec<_>>(),
7070 want,
7071 "{:?} hex_len={hex_len}",
7072 String::from_utf8_lossy(data)
7073 );
7074 let mut line_starts: Vec<usize> = memchr::memchr_iter(b'\n', data)
7075 .map(|nl| nl + 1)
7076 .filter(|&s| s < data.len())
7077 .collect();
7078 line_starts.insert(0, 0);
7079 if data.ends_with(b"\n") {
7081 line_starts.push(data.len());
7082 }
7083 for before in line_starts {
7084 let prev = want.iter().copied().filter(|&s| s < before).last();
7085 assert_eq!(view.prev_record(before), prev, "{data:?} before={before}");
7086 }
7087 }
7088
7089 fn xorshift(state: &mut u64) -> u64 {
7090 *state ^= *state << 13;
7091 *state ^= *state >> 7;
7092 *state ^= *state << 17;
7093 *state
7094 }
7095
7096 #[test]
7097 fn record_walks_match_a_line_by_line_scan() {
7098 let hex = oid(1).to_hex();
7099 let inputs = [
7100 String::new(),
7101 "\n".to_owned(),
7102 format!("# pack-refs with: peeled \n{hex} refs/heads/a\n^{hex}\n{hex} refs/tags/b\n"),
7103 format!("{hex} refs/heads/z\n{hex} refs/heads/a\n"),
7104 format!("{hex} refs/heads/a\n\n{hex} refs/heads/b"),
7105 format!("{hex}\n{hex} \n{hex} x\nshort\n#{hex} refs/heads/c\n"),
7106 format!("{hex} refs/tags/a\n^{hex}\n^{hex}\n{hex} refs/tags/b\n^{hex}"),
7107 format!("^ab\n{}\n{hex} refs/heads/a\n", "x".repeat(37)),
7108 format!("^\n{hex} refs/heads/a\n^{hex}x\n{hex} refs/heads/b\n"),
7109 format!("^{hex}\n"),
7110 format!("^{}", &hex[1..]),
7111 ];
7112 for input in &inputs {
7113 assert_record_walks_match(input.as_bytes(), 40);
7114 }
7115 }
7116
7117 #[test]
7118 fn record_walks_match_a_line_by_line_scan_on_random_bytes() {
7119 let alphabet = b"^\n #ab";
7120 let mut state = 0x2545_f491_4f6c_dd1du64;
7121 for len in 0..400 {
7122 let input: Vec<u8> = (0..len % 97)
7123 .map(|_| alphabet[(xorshift(&mut state) % alphabet.len() as u64) as usize])
7124 .collect();
7125 for hex_len in [1, 2, 3] {
7126 assert_record_walks_match(&input, hex_len);
7127 }
7128 }
7129 }
7130
7131 #[test]
7134 fn search_matches_a_linear_scan_on_sorted_records() {
7135 let hex_len = 2;
7136 let mut state = 0x9e37_79b9_7f4a_7c15u64;
7137 for _ in 0..300 {
7138 let mut names: Vec<String> = (0..xorshift(&mut state) % 12)
7139 .map(|_| format!("n{}", xorshift(&mut state) % 40))
7140 .collect();
7141 names.sort();
7142 names.dedup();
7143 let mut data = b"# h\n".to_vec();
7144 for name in &names {
7145 data.extend_from_slice(format!("ab {name}\n").as_bytes());
7146 match xorshift(&mut state) % 4 {
7147 0 => data.extend_from_slice(b"^ab\n"),
7148 1 => data.extend_from_slice(b"junk\n"),
7149 _ => {}
7150 }
7151 }
7152 let mut view = raw_view(&data, hex_len);
7153 view.body_start = 4;
7154 let records: Vec<usize> = view.records().collect();
7155 for probe in 0..42 {
7156 let target = format!("n{probe}");
7157 let want = records
7158 .iter()
7159 .copied()
7160 .find(|&s| view.record_cmp(s, &target).is_ge())
7161 .unwrap_or(data.len());
7162 assert_eq!(view.lower_bound(&target), want, "{target} in {data:?}");
7163 let hit = view.search(&target).ok();
7164 assert_eq!(
7165 hit,
7166 names.contains(&target).then_some(want),
7167 "{target} in {data:?}"
7168 );
7169 }
7170 }
7171 }
7172}