1pub mod handle;
30pub mod listing;
31pub mod query_reachability;
32pub mod shorten;
33pub mod store;
34pub mod version_sort;
35
36pub use shorten::{
37 expand_ref_dwim_full_name, peel_symref_to_terminal_ref, shorten_unambiguous,
38 warn_ambiguous_refs_from_config,
39};
40
41mod domain_reexports;
42mod on_store;
43pub use domain_reexports::*;
44pub use handle::References;
45pub use on_store::update_branch_for_commit_on_store;
46pub(crate) use on_store::{
47 append_reflog_on_store, delete_ref_on_store, update_detached_head_for_commit_on_store,
48 write_symbolic_ref_on_store,
49};
50
51pub mod gc {
53 pub use crate::gc::*;
54}
55
56use crate::git_path::CanonicalizeExt;
57use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
58use std::fs;
59use std::io;
60use std::ops::ControlFlow;
61use std::path::{Path, PathBuf};
62use store::{
63 open_ref_store, Expected, RawRef as StoreRawRef, RefCaseOnlyRename, RefEntry, RefReflogRename,
64 RefStorageFormat, RefStore, RefTransaction, RefUpdate, RefUpdateFlags, ReflogUpdate,
65};
66
67use crate::check_ref_format::{check_refname_format_borrowed, RefNameOptions};
68use crate::config::ConfigSet;
69use crate::error::{Error, RefLockError, Result};
70use crate::ident::Signature;
71use crate::objects::{HashAlgo, ObjectId};
72use crate::pack;
73use store::format_reflog_identity;
74
75pub const SYMREF_MAXDEPTH: usize = 5;
77
78fn map_public_ref_store_error(err: Error, refname: &str) -> Error {
79 match err {
80 Error::RefStore(store::RefStoreError::SymrefLoop) => {
81 Error::InvalidRef(format!("ref symlink too deep: {refname}"))
82 }
83 other => other,
84 }
85}
86
87fn files_legacy_write_precheck(git_dir: &Path, refname: &str) -> Result<()> {
91 let storage_dir = ref_storage_dir(git_dir, refname);
92 let stor = crate::ref_namespace::storage_ref_name(refname);
93 let path = storage_dir.join(stor);
94 remove_empty_ref_directory(&path);
95 if fs::symlink_metadata(&path)
96 .map(|m| m.file_type().is_dir())
97 .unwrap_or(false)
98 {
99 let display = ref_path_for_display(&path);
100 return Err(crate::error::RefLockError::DirectoryInTheWay {
101 refname: refname.to_owned(),
102 path: display,
103 }
104 .into());
105 }
106 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
107 return Err(Error::InvalidRef(format!(
108 "cannot update ref '{refname}': reference broken"
109 )));
110 }
111 Ok(())
112}
113
114fn legacy_ref_update_flags(refname: &str) -> RefUpdateFlags {
115 let mut flags = RefUpdateFlags::default();
116 if refname == "HEAD" {
118 flags.no_deref = true;
119 }
120 flags
121}
122
123pub(crate) fn commit_store_update(git_dir: &Path, update: RefUpdate) -> Result<()> {
124 let store = open_ref_store(git_dir)?;
125 commit_ref_update_on_store(store.as_ref(), update)
126}
127
128pub fn commit_ref_update_on_store(store: &dyn RefStore, update: RefUpdate) -> Result<()> {
135 if let Some(ref log) = update.reflog {
136 validate_reflog_update(log)?;
137 }
138 let refname = update.name.clone();
139 let txn = RefTransaction::new()
140 .update(update)
141 .map_err(crate::error::Error::from)?;
142 let prepared = store
143 .prepare(txn)
144 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))?;
145 prepared
146 .commit()
147 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))
148}
149
150pub fn update_direct_ref_on_store(
158 store: &dyn RefStore,
159 git_dir: &Path,
160 refname: &str,
161 oid: &ObjectId,
162) -> Result<()> {
163 store_write_value_on_store(
164 store,
165 git_dir,
166 refname,
167 Some(StoreRawRef::Direct(*oid)),
168 Expected::Any,
169 None,
170 )
171}
172
173pub(crate) fn store_write_value_on_store(
174 store: &dyn RefStore,
175 git_dir: &Path,
176 refname: &str,
177 new_value: Option<StoreRawRef>,
178 expected: Expected,
179 reflog: Option<ReflogUpdate>,
180) -> Result<()> {
181 ensure_refname_safe_for_storage(refname)?;
182 if new_value.is_some() && store.format() == RefStorageFormat::Files {
183 files_legacy_write_precheck(git_dir, refname)?;
184 }
185 commit_ref_update_on_store(
186 store,
187 RefUpdate {
188 name: refname.to_owned(),
189 new_value,
190 expected,
191 reflog,
192 flags: legacy_ref_update_flags(refname),
193 },
194 )
195}
196
197pub fn commit_ref_store_batch(git_dir: &Path, updates: &[RefUpdate]) -> Result<()> {
206 if updates.is_empty() {
207 return Ok(());
208 }
209 let store = open_ref_store(git_dir)?;
210 commit_ref_updates_on_store(store.as_ref(), updates, &[])
211}
212
213pub fn commit_ref_updates_on_store(
219 store: &dyn RefStore,
220 updates: &[RefUpdate],
221 reflog_renames: &[RefReflogRename],
222) -> Result<()> {
223 commit_ref_batch_on_store(store, updates, reflog_renames, &[])
224}
225
226pub fn commit_ref_batch_on_store(
232 store: &dyn RefStore,
233 updates: &[RefUpdate],
234 reflog_renames: &[RefReflogRename],
235 case_only_renames: &[RefCaseOnlyRename],
236) -> Result<()> {
237 if updates.is_empty() && reflog_renames.is_empty() && case_only_renames.is_empty() {
238 return Ok(());
239 }
240 commit_owned_ref_batch_on_store(
241 store,
242 updates.to_vec(),
243 reflog_renames.to_vec(),
244 case_only_renames.to_vec(),
245 )
246}
247
248pub(crate) fn commit_owned_ref_updates_on_store(
254 store: &dyn RefStore,
255 updates: Vec<RefUpdate>,
256) -> Result<()> {
257 if updates.is_empty() {
258 return Ok(());
259 }
260 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new())
261}
262
263fn commit_owned_ref_batch_on_store(
264 store: &dyn RefStore,
265 updates: Vec<RefUpdate>,
266 reflog_renames: Vec<RefReflogRename>,
267 case_only_renames: Vec<RefCaseOnlyRename>,
268) -> Result<()> {
269 for update in &updates {
270 if let Some(ref log) = update.reflog {
271 validate_reflog_update(log)?;
272 }
273 }
274 for rename in &reflog_renames {
275 validate_reflog_update(&rename.append)?;
276 }
277 let label = updates
278 .first()
279 .map(|u| u.name.clone())
280 .or_else(|| reflog_renames.first().map(|r| r.from.clone()))
281 .or_else(|| case_only_renames.first().map(|r| r.from.clone()))
282 .unwrap_or_else(|| "refs".to_owned());
283 let mut txn = RefTransaction::from_updates(updates).map_err(|e| {
284 let name = match &e {
285 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
286 _ => label.clone(),
287 };
288 map_public_ref_store_error(Error::from(e), &name)
289 })?;
290 for rename in reflog_renames {
291 txn = txn.with_reflog_rename(rename);
292 }
293 for rename in case_only_renames {
294 txn = txn.with_case_only_rename(rename);
295 }
296 let prepared = store
297 .prepare(txn)
298 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
299 prepared
300 .commit()
301 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))
302}
303
304fn store_write_value(
305 git_dir: &Path,
306 refname: &str,
307 new_value: Option<StoreRawRef>,
308 expected: Expected,
309 reflog: Option<ReflogUpdate>,
310) -> Result<()> {
311 let store = open_ref_store(git_dir)?;
312 store_write_value_on_store(
313 store.as_ref(),
314 git_dir,
315 refname,
316 new_value,
317 expected,
318 reflog,
319 )
320}
321
322pub(crate) fn collect_resolved_refs(
323 store: &dyn RefStore,
324 prefix: &str,
325) -> Result<Vec<(String, ObjectId)>> {
326 collect_resolved_ref_entries(store, |f| store.for_each_ref(prefix, f))
327}
328
329fn collect_resolved_ref_entries(
331 store: &dyn RefStore,
332 for_each: impl FnOnce(&mut dyn FnMut(&RefEntry) -> ControlFlow<()>) -> Result<()>,
333) -> Result<Vec<(String, ObjectId)>> {
334 let mut results = Vec::new();
335 for_each(&mut |entry: &RefEntry| {
336 let oid = entry
337 .peeled
338 .or_else(|| match &entry.value {
339 StoreRawRef::Direct(oid) => Some(*oid),
340 StoreRawRef::Symbolic(target) => store.resolve(target).ok(),
341 })
342 .or_else(|| store.resolve(&entry.name).ok());
343 if let Some(oid) = oid {
344 results.push((entry.name.clone(), oid));
345 }
346 ControlFlow::Continue(())
347 })?;
348 Ok(results)
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
353pub enum Ref {
354 Direct(ObjectId),
356 Symbolic(String),
358}
359
360pub fn read_ref_file(path: &Path) -> Result<Ref> {
367 if let Ok(target) = fs::read_link(path) {
368 return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
369 }
370
371 let content = match fs::read_to_string(path) {
372 Ok(c) => c,
373 Err(e)
377 if e.kind() == io::ErrorKind::IsADirectory
378 || e.kind() == io::ErrorKind::NotADirectory
379 || e.raw_os_error() == Some(libc::EISDIR)
380 || e.raw_os_error() == Some(libc::ENOTDIR) =>
381 {
382 return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, e)));
383 }
384 Err(e) => return Err(Error::Io(e)),
385 };
386 let content = content.trim_end_matches('\n');
387 parse_ref_content(content)
388}
389
390pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
392 if let Some(target) = content.strip_prefix("ref: ") {
393 Ok(Ref::Symbolic(target.trim().to_owned()))
394 } else if let Some(token) = content.split_whitespace().next() {
395 if ObjectId::is_full_hex(token) {
396 let oid: ObjectId = token.parse()?;
397 Ok(Ref::Direct(oid))
398 } else if content == "unknown-oid" {
399 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
400 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
401 Ok(Ref::Direct(oid))
402 } else {
403 Err(Error::InvalidRef(content.to_owned()))
404 }
405 } else {
406 Err(Error::InvalidRef(content.to_owned()))
407 }
408}
409
410pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
425 open_ref_store(git_dir)?
426 .resolve(refname)
427 .map_err(|err| map_public_ref_store_error(err, refname))
428}
429
430pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
435 let commondir_file = git_dir.join("commondir");
436 if !commondir_file.is_file() {
437 return None;
438 }
439 let raw = fs::read_to_string(commondir_file).ok()?;
440 let rel = raw.trim();
441 let path = if Path::new(rel).is_absolute() {
444 PathBuf::from(rel)
445 } else {
446 git_dir.join(rel)
447 };
448 path.canonicalize_simplified().ok()
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub enum RawRefLookup {
458 Exists,
460 NotFound,
462 IsDirectory,
464}
465
466pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
481 let store = open_ref_store(git_dir)?;
482 match store.read_raw(refname)? {
483 Some(_) => Ok(RawRefLookup::Exists),
484 None => {
485 let (storage_dir, stor_name) =
486 crate::worktree_ref::resolve_ref_storage(git_dir, refname);
487 let path = storage_dir.join(&stor_name);
488 if fs::symlink_metadata(&path)
489 .map(|m| m.is_dir())
490 .unwrap_or(false)
491 {
492 Ok(RawRefLookup::IsDirectory)
493 } else {
494 Ok(RawRefLookup::NotFound)
495 }
496 }
497 }
498}
499
500#[must_use]
502pub fn lock_path_for_ref(path: &Path) -> PathBuf {
503 let mut s = path.as_os_str().to_owned();
504 s.push(".lock");
505 PathBuf::from(s)
506}
507
508fn first_packed_descendant_ref(store: &Path, refname: &str) -> Result<Option<String>> {
510 let leaf_dir = format!("{refname}/");
511 let map = read_packed_refs_map_for_prefix(store, refname)?;
512 let sorted: BTreeSet<String> = map.keys().cloned().collect();
513 Ok(sorted
514 .range(leaf_dir.clone()..)
515 .next()
516 .filter(|name| name.starts_with(&leaf_dir))
517 .cloned())
518}
519
520fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
521 Ok(lookup_packed_ref(git_dir, refname)?.is_some())
522}
523
524pub(crate) fn packed_refname_namespace_conflict(
529 sorted_packed: &BTreeSet<String>,
530 candidate: &str,
531) -> bool {
532 let segments: Vec<&str> = candidate.split('/').filter(|s| !s.is_empty()).collect();
533 if segments.len() > 1 {
534 let mut dirname = String::new();
535 for part in &segments[..segments.len() - 1] {
536 if !dirname.is_empty() {
537 dirname.push('/');
538 }
539 dirname.push_str(part);
540 if sorted_packed.contains(&dirname) {
541 return true;
542 }
543 }
544 }
545
546 let leaf_dir = format!("{candidate}/");
547 if let Some(existing) = sorted_packed.range(leaf_dir.clone()..).next() {
548 if existing.starts_with(&leaf_dir) {
549 return true;
550 }
551 }
552 false
553}
554
555fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
556 Ok(PackedRefs::load(git_dir)?.has_namespace_conflict(refname))
557}
558
559pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
571 if refname == "HEAD" {
572 return Ok(false);
573 }
574 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
575 return Ok(false);
576 }
577 let storage_dir = ref_storage_dir(git_dir, refname);
578 packed_ref_name_exists(&storage_dir, refname)
579}
580
581#[derive(Debug, Clone, PartialEq, Eq)]
583pub enum RefnameUnavailable {
584 AncestorExists {
586 blocking: String,
588 new_ref: String,
590 },
591 DescendantExists {
593 blocking: String,
595 new_ref: String,
597 },
598 SameBatch {
600 refname: String,
602 other: String,
604 },
605}
606
607impl RefnameUnavailable {
608 #[must_use]
610 pub fn lock_message_suffix(&self) -> String {
611 match self {
612 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
613 format!("'{blocking}' exists; cannot create '{new_ref}'")
614 }
615 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
616 format!("'{blocking}' exists; cannot create '{new_ref}'")
617 }
618 RefnameUnavailable::SameBatch { refname, other } => {
619 format!("cannot process '{refname}' and '{other}' at the same time")
620 }
621 }
622 }
623}
624
625fn find_descendant_in_sorted_extras(
626 dirname_with_slash: &str,
627 extras: &BTreeSet<String>,
628) -> Option<String> {
629 let start = extras
630 .range(dirname_with_slash.to_string()..)
631 .next()
632 .cloned()?;
633 if start.starts_with(dirname_with_slash) {
634 Some(start)
635 } else {
636 None
637 }
638}
639
640#[must_use]
644pub fn is_valid_fetch_advertised_ref(name: &str) -> bool {
645 crate::check_ref_format::is_valid_fetch_advertised_ref(name)
646}
647
648#[must_use]
653pub fn is_valid_ls_refs_advertised_name(name: &str) -> bool {
654 name == "HEAD" || is_valid_fetch_advertised_ref(name)
655}
656
657#[must_use]
659pub fn is_valid_storable_ref_name(name: &str) -> bool {
660 is_valid_fetch_advertised_ref(name)
661}
662
663#[must_use]
665pub fn is_valid_advertised_symref_target(name: &str) -> bool {
666 crate::check_ref_format::is_valid_advertised_symref_target(name)
667}
668
669pub fn verify_refname_available_for_create(
682 git_dir: &Path,
683 refname: &str,
684 extras: &BTreeSet<String>,
685 skip: &HashSet<String>,
686) -> std::result::Result<(), RefnameUnavailable> {
687 let git_dir = git_dir
690 .canonicalize_simplified()
691 .unwrap_or_else(|_| git_dir.to_path_buf());
692 let mut seen_dirnames: HashSet<String> = HashSet::new();
693 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
694 if segments.len() <= 1 {
695 } else {
697 let mut dirname = String::new();
698 for part in &segments[..segments.len() - 1] {
699 if !dirname.is_empty() {
700 dirname.push('/');
701 }
702 dirname.push_str(part);
703
704 if !seen_dirnames.insert(dirname.clone()) {
705 continue;
706 }
707
708 if skip.contains(&dirname) {
709 continue;
710 }
711
712 match read_raw_ref(&git_dir, &dirname) {
713 Ok(RawRefLookup::Exists) => {
714 return Err(RefnameUnavailable::AncestorExists {
715 blocking: dirname.clone(),
716 new_ref: refname.to_owned(),
717 });
718 }
719 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
722 Err(_) => {}
723 }
724
725 if extras.contains(&dirname) {
726 return Err(RefnameUnavailable::SameBatch {
727 refname: refname.to_owned(),
728 other: dirname.clone(),
729 });
730 }
731 }
732 }
733
734 let mut leaf_dir = String::with_capacity(refname.len() + 1);
735 leaf_dir.push_str(refname);
736 leaf_dir.push('/');
737
738 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
744 let may_have_loose_descendants = packed_dir.join("reftable").is_dir()
745 || git_dir.join(refname).is_dir()
746 || packed_dir.join(refname).is_dir();
747 let under = if may_have_loose_descendants {
748 list_refs(&git_dir, &leaf_dir).unwrap_or_default()
749 } else {
750 Vec::new()
751 };
752 if under.is_empty() {
753 if let Ok(Some(name)) = first_packed_descendant_ref(&packed_dir, refname) {
754 if !skip.contains(&name) {
755 return Err(RefnameUnavailable::DescendantExists {
756 blocking: name,
757 new_ref: refname.to_owned(),
758 });
759 }
760 }
761 if packed_dir != git_dir {
762 if let Ok(Some(name)) = first_packed_descendant_ref(&git_dir, refname) {
763 if !skip.contains(&name) {
764 return Err(RefnameUnavailable::DescendantExists {
765 blocking: name,
766 new_ref: refname.to_owned(),
767 });
768 }
769 }
770 }
771 }
772 if under.is_empty()
773 && fs::symlink_metadata(git_dir.join(refname))
774 .map(|m| m.is_dir())
775 .unwrap_or(false)
776 {
777 let mut blocking: Option<String> = None;
778 let dir_path = git_dir.join(refname);
779 if let Ok(read) = fs::read_dir(&dir_path) {
780 for entry in read.flatten() {
781 let path = entry.path();
782 let Ok(meta) = fs::metadata(&path) else {
783 continue;
784 };
785 if !meta.is_file() {
786 continue;
787 }
788 let name = entry.file_name().to_string_lossy().into_owned();
789 let full = format!("{refname}/{name}");
790 blocking = Some(full);
791 break;
792 }
793 }
794 if let Some(b) = blocking {
795 if !skip.contains(&b) {
796 return Err(RefnameUnavailable::DescendantExists {
797 blocking: b,
798 new_ref: refname.to_owned(),
799 });
800 }
801 }
802 }
803
804 for (existing, _) in under {
805 if skip.contains(&existing) {
806 continue;
807 }
808 return Err(RefnameUnavailable::DescendantExists {
809 blocking: existing,
810 new_ref: refname.to_owned(),
811 });
812 }
813
814 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
815 if !skip.contains(&extra) {
816 return Err(RefnameUnavailable::SameBatch {
817 refname: refname.to_owned(),
818 other: extra,
819 });
820 }
821 }
822
823 Ok(())
824}
825
826#[derive(Clone, Debug)]
828pub struct RefBatchItem {
829 pub name: String,
831 pub new_oid: Option<ObjectId>,
833}
834
835pub fn verify_ref_transaction_batch(
846 git_dir: &Path,
847 items: &[RefBatchItem],
848) -> std::result::Result<(), RefnameUnavailable> {
849 if let Some((parent, child)) =
850 batch_refname_prefix_conflict(items.iter().map(|i| i.name.as_str()))
851 {
852 return Err(RefnameUnavailable::SameBatch {
853 refname: child,
854 other: parent,
855 });
856 }
857
858 if crate::ref_storage::detect_format(git_dir).is_ok_and(|f| f.is_reftable()) {
859 return crate::reftable::verify_ref_transaction_batch_reftable(git_dir, items);
860 }
861
862 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
863 let empty_skip = HashSet::new();
864 for item in items {
865 let Some(_oid) = item.new_oid else {
866 continue;
867 };
868 if resolve_ref(git_dir, &item.name).is_ok() {
869 continue;
870 }
871 verify_refname_available_for_create(git_dir, &item.name, &extras, &empty_skip)?;
872 }
873 Ok(())
874}
875
876pub(crate) fn batch_refname_prefix_conflict<'a>(
878 names: impl Iterator<Item = &'a str>,
879) -> Option<(String, String)> {
880 let mut sorted: Vec<&str> = names.collect();
881 sorted.sort_unstable();
882 for pair in sorted.windows(2) {
883 let (a, b) = (pair[0], pair[1]);
884 if refname_is_strict_prefix(a, b) {
885 return Some((a.to_owned(), b.to_owned()));
886 }
887 if refname_is_strict_prefix(b, a) {
888 return Some((b.to_owned(), a.to_owned()));
889 }
890 }
891 None
892}
893
894fn refname_is_strict_prefix(parent: &str, child: &str) -> bool {
895 child.len() > parent.len()
896 && child.as_bytes().get(parent.len()) == Some(&b'/')
897 && child.starts_with(parent)
898}
899
900fn packed_refs_unexpected_line(path: &Path, line: impl Into<String>) -> Error {
901 Error::PackedRefsUnexpectedLine {
902 path: path.display().to_string(),
903 line: line.into(),
904 }
905}
906
907fn packed_refs_unterminated_line(path: &Path, line: impl Into<String>) -> Error {
908 Error::PackedRefsUnexpectedLine {
909 path: path.display().to_string(),
910 line: line.into(),
911 }
912}
913
914fn parse_packed_ref_peel_line(path: &Path, line: &str, expect_peel: bool) -> Result<ObjectId> {
916 if !expect_peel {
917 return Err(packed_refs_unexpected_line(path, line));
918 }
919 line.strip_prefix('^')
920 .and_then(|inner| ObjectId::from_hex(inner).ok())
921 .ok_or_else(|| packed_refs_unexpected_line(path, line))
922}
923
924fn packed_line_oid(line: &str) -> Option<(usize, ObjectId)> {
926 let bytes = line.as_bytes();
927 let hex_len = [HashAlgo::Sha1.hex_len(), HashAlgo::Sha256.hex_len()]
928 .into_iter()
929 .find(|&len| bytes.get(len) == Some(&b' '))?;
930 let oid = ObjectId::from_hex(&line[..hex_len]).ok()?;
932 Some((hex_len, oid))
933}
934
935fn packed_ref_data_line_refname(line: &str) -> Option<&str> {
936 let bytes = line.as_bytes();
937 let mut j = 0usize;
938 while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
939 j += 1;
940 }
941 if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
942 return None;
943 }
944 let refname = &line[j + 1..];
945 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
946 return None;
947 }
948 Some(refname)
949}
950
951fn parse_packed_ref_data_line(
952 path: &Path,
953 line: &str,
954 ref_opts: &RefNameOptions,
955) -> Result<(String, ObjectId)> {
956 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, ref_opts)?;
957 Ok((refname.to_owned(), oid))
958}
959
960fn parse_packed_ref_data_line_borrowed<'a>(
961 path: &Path,
962 line: &'a str,
963 ref_opts: &RefNameOptions,
964) -> Result<(&'a str, ObjectId)> {
965 let Some((hex_len, oid)) = packed_line_oid(line) else {
966 return Err(packed_refs_unexpected_line(path, line));
967 };
968 let refname = &line[hex_len + 1..];
969 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
970 return Err(packed_refs_unexpected_line(path, line));
971 }
972 if check_refname_format_borrowed(refname, ref_opts).is_err() {
973 return Err(packed_refs_unexpected_line(path, line));
974 }
975 Ok((refname, oid))
976}
977
978#[derive(Debug, Clone)]
997pub(crate) struct PackedRefsFileView {
998 data: std::sync::Arc<crate::pack_map::PackData>,
999 sorted: bool,
1002 hex_len: usize,
1004 body_start: usize,
1006 peel_trait: PackedPeelTrait,
1008}
1009
1010impl PackedRefsFileView {
1011 #[cfg(test)]
1013 fn from_bytes(path: &Path, data: Vec<u8>, hash_algo: crate::objects::HashAlgo) -> Result<Self> {
1014 Self::from_data(path, crate::pack_map::PackData::from_owned(data), hash_algo)
1015 }
1016
1017 fn from_data(
1026 path: &Path,
1027 data: std::sync::Arc<crate::pack_map::PackData>,
1028 hash_algo: crate::objects::HashAlgo,
1029 ) -> Result<Self> {
1030 let hex_len = hash_algo.hex_len();
1031 if data.is_empty() {
1032 return Ok(Self {
1033 data,
1034 sorted: false,
1035 hex_len,
1036 body_start: 0,
1037 peel_trait: PackedPeelTrait::None,
1038 });
1039 }
1040 if !data.ends_with(b"\n") {
1041 let tail = std::str::from_utf8(&data)
1042 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1043 .unwrap_or("<invalid utf-8>");
1044 return Err(packed_refs_unterminated_line(path, tail));
1045 }
1046 let text = std::str::from_utf8(&data[..packed_refs_leading_comments_end(&data)])
1049 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1050 let body_start = if data.starts_with(b"#") {
1053 memchr::memchr(b'\n', &data).map_or(data.len(), |nl| nl + 1)
1054 } else {
1055 0
1056 };
1057 let body = &data[body_start..];
1058 if let Some(rest) = body.strip_suffix(b"\n") {
1059 let last_line = memchr::memrchr(b'\n', rest).map_or(rest, |nl| &rest[nl + 1..]);
1060 if last_line.len() < hex_len + 1 {
1061 return Err(packed_refs_unexpected_line(
1062 path,
1063 String::from_utf8_lossy(last_line),
1064 ));
1065 }
1066 }
1067 Ok(Self {
1068 sorted: packed_refs_file_declares_sorted(text),
1069 hex_len,
1070 body_start,
1071 peel_trait: packed_refs_peel_trait(text),
1072 data,
1073 })
1074 }
1075
1076 fn record_name(&self, line_start: usize) -> &[u8] {
1078 let end = packed_line_end(&self.data, line_start);
1079 &self.data[line_start + self.hex_len + 1..end]
1080 }
1081
1082 fn record_cmp(&self, line_start: usize, name: &str) -> std::cmp::Ordering {
1083 self.record_name(line_start).cmp(name.as_bytes())
1084 }
1085
1086 fn next_record(&self, from: usize, limit: usize) -> Option<usize> {
1089 let hex_len = self.hex_len;
1090 let mut start = from;
1091 while start < limit {
1092 let rest = &self.data[start..];
1093 if rest[0] == b'^' && rest.get(hex_len + 1) == Some(&b'\n') {
1096 start += hex_len + 2;
1097 continue;
1098 }
1099 let end = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
1100 if packed_line_is_record(&rest[..end], hex_len) {
1101 return Some(start);
1102 }
1103 start += end + 1;
1104 }
1105 None
1106 }
1107
1108 fn prev_record(&self, before: usize) -> Option<usize> {
1110 let mut end = before;
1111 while end > 0 {
1112 let line_end = end - 1;
1113 let start = memchr::memrchr(b'\n', &self.data[..line_end]).map_or(0, |nl| nl + 1);
1114 if packed_line_is_record(&self.data[start..line_end], self.hex_len) {
1115 return Some(start);
1116 }
1117 end = start;
1118 }
1119 None
1120 }
1121
1122 fn records(&self) -> impl Iterator<Item = usize> + '_ {
1124 let len = self.data.len();
1125 std::iter::successors(self.next_record(0, len), move |&start| {
1126 self.next_record(packed_line_end(&self.data, start) + 1, len)
1127 })
1128 }
1129
1130 fn search(&self, name: &str) -> std::result::Result<usize, usize> {
1138 let len = self.data.len();
1139 let mut lo = self.body_start;
1140 let mut hi = len;
1141 while lo < hi {
1142 let mid = lo + (hi - lo) / 2;
1143 let line = memchr::memrchr(b'\n', &self.data[lo..mid]).map_or(lo, |nl| lo + nl + 1);
1144 let Some(record) = self.next_record(line, hi) else {
1145 hi = line;
1146 continue;
1147 };
1148 match self.record_cmp(record, name) {
1149 std::cmp::Ordering::Less => lo = packed_line_end(&self.data, record) + 1,
1150 std::cmp::Ordering::Greater => hi = record,
1151 std::cmp::Ordering::Equal => return Ok(record),
1152 }
1153 }
1154 Err(self.next_record(lo, len).unwrap_or(len))
1155 }
1156
1157 fn lower_bound(&self, name: &str) -> usize {
1159 let (Ok(start) | Err(start)) = self.search(name);
1160 start
1161 }
1162
1163 fn lookup_entry(
1165 &self,
1166 path: &Path,
1167 refname: &str,
1168 ) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1169 let ref_opts = RefNameOptions {
1170 allow_onelevel: false,
1171 refspec_pattern: false,
1172 normalize: false,
1173 };
1174 let line_start = if self.sorted {
1175 self.search(refname).ok()
1176 } else {
1177 self.records()
1178 .find(|&start| self.record_cmp(start, refname).is_eq())
1179 };
1180 let Some(start) = line_start else {
1181 return Ok(None);
1182 };
1183 let (name, oid) = parse_packed_line_at(path, &self.data, start, &ref_opts)?;
1184 debug_assert_eq!(name, refname);
1185 let line_end = packed_line_end(&self.data, start);
1186 if line_end + 1 < self.data.len() && self.data[line_end + 1] == b'^' {
1187 let peel_line = packed_line_as_str(&self.data, line_end + 1);
1188 parse_packed_ref_peel_line(path, peel_line, true)?;
1189 }
1190 let peel = peel_oid_following_line(&self.data, start);
1191 Ok(Some((oid, peel)))
1192 }
1193
1194 fn lookup_oid(&self, path: &Path, refname: &str) -> Result<Option<ObjectId>> {
1195 Ok(self.lookup_entry(path, refname)?.map(|(oid, _)| oid))
1196 }
1197
1198 pub(crate) fn first_descendant(
1202 &self,
1203 path: &Path,
1204 refname: &str,
1205 ) -> Result<Option<(String, ObjectId)>> {
1206 let leaf_dir = format!("{refname}/");
1207 Ok(self
1208 .map_for_prefix(path, Some(refname))?
1209 .into_iter()
1210 .filter(|(name, _)| name.starts_with(&leaf_dir))
1211 .min_by(|a, b| a.0.cmp(&b.0)))
1212 }
1213
1214 fn map_for_prefix(
1225 &self,
1226 path: &Path,
1227 prefix: Option<&str>,
1228 ) -> Result<HashMap<String, ObjectId>> {
1229 self.map_for_prefix_filtered(path, prefix, None)
1230 }
1231
1232 pub(crate) fn full_map(&self, path: &Path) -> Result<BTreeMap<String, ObjectId>> {
1235 let (mut records, in_order) =
1236 self.parse_region::<Vec<_>>(path, 0, self.data.len(), None, None)?;
1237 if !in_order {
1238 records.sort_by(|a, b| a.0.cmp(&b.0));
1239 }
1240 records.dedup_by(|later, earlier| {
1242 if later.0 != earlier.0 {
1243 return false;
1244 }
1245 earlier.1 = later.1;
1246 true
1247 });
1248 Ok(records.into_iter().collect())
1249 }
1250
1251 fn map_for_prefix_filtered(
1254 &self,
1255 path: &Path,
1256 prefix: Option<&str>,
1257 keep: Option<PackedRecordFilter<'_>>,
1258 ) -> Result<HashMap<String, ObjectId>> {
1259 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1260 let range_prefix = prefix_trim.filter(|p| self.sorted && !p.is_empty() && *p != "refs");
1261 if let Some((begin, end)) = range_prefix.and_then(|prefix| self.prefix_region(prefix)) {
1262 let (map, in_order) = self.parse_region(path, begin, end, prefix_trim, keep)?;
1263 if in_order {
1264 return Ok(map);
1265 }
1266 }
1267 let (map, _) = self.parse_region(path, 0, self.data.len(), prefix_trim, keep)?;
1269 Ok(map)
1270 }
1271
1272 fn prefix_region(&self, prefix: &str) -> Option<(usize, usize)> {
1280 let len = self.data.len();
1281 let first = self.lower_bound(prefix);
1282 let upper = list_prefix_upper_bound(prefix);
1283 let end = upper
1284 .as_deref()
1285 .map_or(len, |upper| self.lower_bound(upper));
1286 if end < first || (first < end && self.record_cmp(first, prefix).is_lt()) {
1287 return None;
1288 }
1289 if let Some(upper) = upper.as_deref() {
1290 if end < len && self.record_cmp(end, upper).is_lt() {
1291 return None;
1292 }
1293 }
1294 let begin = match self.prev_record(first) {
1295 Some(prev) => {
1296 if !self.record_cmp(prev, prefix).is_lt() {
1297 return None;
1298 }
1299 prev
1300 }
1301 None => 0,
1302 };
1303 let end = if end < len {
1304 let after = packed_line_end(&self.data, end) + 1;
1305 if self.data.get(after) == Some(&b'^') {
1306 packed_line_end(&self.data, after) + 1
1307 } else {
1308 after
1309 }
1310 } else {
1311 end
1312 };
1313 Some((begin, end))
1314 }
1315
1316 fn parse_region<C: Default + Extend<(String, ObjectId)>>(
1320 &self,
1321 path: &Path,
1322 begin: usize,
1323 end: usize,
1324 prefix: Option<&str>,
1325 keep: Option<PackedRecordFilter<'_>>,
1326 ) -> Result<(C, bool)> {
1327 let bytes = &self.data[begin..end];
1328 if memchr::memchr(b'\r', bytes).is_some() {
1329 return Err(packed_refs_unexpected_line(
1330 path,
1331 "<contains carriage return>",
1332 ));
1333 }
1334 let region = std::str::from_utf8(bytes)
1335 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1336 parse_packed_refs_region(path, region, prefix, keep, self.peel_trait)
1337 }
1338}
1339
1340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1343pub(crate) enum PackedPeel {
1344 Peeled(ObjectId),
1346 NotTag,
1348 Unknown,
1350}
1351
1352pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
1355
1356fn parse_packed_refs_region<C: Default + Extend<(String, ObjectId)>>(
1362 path: &Path,
1363 region: &str,
1364 prefix: Option<&str>,
1365 keep: Option<PackedRecordFilter<'_>>,
1366 peel_trait: PackedPeelTrait,
1367) -> Result<(C, bool)> {
1368 let ref_opts = RefNameOptions {
1369 allow_onelevel: false,
1370 refspec_pattern: false,
1371 normalize: false,
1372 };
1373 let mut map = C::default();
1374 let mut in_order = true;
1375 let mut prev_name = "";
1376 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
1378 let flush = |map: &mut C, pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
1379 let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
1380 return;
1381 };
1382 let peel = if peel_trait.covers(refname) {
1383 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1384 } else {
1385 PackedPeel::Unknown
1386 };
1387 if keep(refname, &oid, peel) {
1388 map.extend(Some((refname.to_owned(), oid)));
1389 }
1390 };
1391 let mut expect_peel = false;
1392 let mut lines = region.split('\n').peekable();
1393 while let Some(line) = lines.next() {
1394 if line.is_empty() {
1395 if lines.peek().is_some() {
1396 return Err(packed_refs_unexpected_line(path, line));
1397 }
1398 break;
1399 }
1400 if line.starts_with('#') {
1401 flush(&mut map, &mut pending);
1402 expect_peel = false;
1403 continue;
1404 }
1405 if line.starts_with('^') {
1406 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
1407 if let Some((_, _, peeled)) = pending.as_mut() {
1408 *peeled = Some(peel);
1409 }
1410 expect_peel = false;
1411 continue;
1412 }
1413 flush(&mut map, &mut pending);
1414 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1415 in_order &= prev_name <= refname;
1416 prev_name = refname;
1417 if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
1418 if keep.is_some() {
1419 pending = Some((refname, oid, None));
1420 } else {
1421 map.extend(Some((refname.to_owned(), oid)));
1422 }
1423 }
1424 expect_peel = true;
1425 }
1426 flush(&mut map, &mut pending);
1427 Ok((map, in_order))
1428}
1429
1430fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
1431 let packed_path = store::paths::packed_refs_path(store);
1432 let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
1435 Ok(d) => d,
1436 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
1437 Err(e) => return Err(e),
1438 };
1439 let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
1440 .map(|format| format.hash_algo())
1441 .unwrap_or(crate::objects::HashAlgo::Sha1);
1442 Ok(Some(PackedRefsFileView::from_data(
1443 &packed_path,
1444 data,
1445 hash_algo,
1446 )?))
1447}
1448
1449fn packed_line_is_record(line: &[u8], hex_len: usize) -> bool {
1452 line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^')
1453}
1454
1455fn packed_line_end(data: &[u8], line_start: usize) -> usize {
1456 memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
1457}
1458
1459fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
1460 std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
1461}
1462
1463fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
1464 let line_end = packed_line_end(data, line_start);
1465 if line_end >= data.len() || data[line_end] != b'\n' {
1466 return None;
1467 }
1468 let peel_start = line_end + 1;
1469 if peel_start >= data.len() || data[peel_start] != b'^' {
1470 return None;
1471 }
1472 let peel_end = packed_line_end(data, peel_start);
1473 let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
1474 ObjectId::from_hex(inner.trim()).ok()
1475}
1476
1477fn parse_packed_line_at(
1478 path: &Path,
1479 data: &[u8],
1480 line_start: usize,
1481 ref_opts: &RefNameOptions,
1482) -> Result<(String, ObjectId)> {
1483 let line = packed_line_as_str(data, line_start);
1484 parse_packed_ref_data_line(path, line, ref_opts)
1485}
1486
1487pub(crate) fn parse_packed_refs_to_map(
1488 path: &Path,
1489 data: &[u8],
1490) -> Result<HashMap<String, ObjectId>> {
1491 parse_packed_refs_to_map_with_prefix(path, data, None)
1492}
1493
1494fn packed_refs_leading_comments_end(data: &[u8]) -> usize {
1496 let mut end = 0;
1497 while data.get(end) == Some(&b'#') {
1498 match memchr::memchr(b'\n', &data[end..]) {
1499 Some(nl) => end += nl + 1,
1500 None => return data.len(),
1501 }
1502 }
1503 end
1504}
1505
1506fn packed_refs_file_declares_sorted(text: &str) -> bool {
1508 for line in text.split('\n') {
1509 if line.is_empty() {
1510 break;
1511 }
1512 if !line.starts_with('#') {
1513 break;
1514 }
1515 let body = line[1..].trim();
1516 let Some(traits) = body.strip_prefix("pack-refs with:") else {
1517 continue;
1518 };
1519 return traits.split_whitespace().any(|t| t == "sorted");
1520 }
1521 false
1522}
1523
1524fn parse_packed_refs_to_map_with_prefix(
1525 path: &Path,
1526 data: &[u8],
1527 prefix_filter: Option<&str>,
1528) -> Result<HashMap<String, ObjectId>> {
1529 let mut map = HashMap::new();
1530 visit_packed_refs_with_prefix(path, data, prefix_filter, |refname, oid| {
1531 map.insert(refname.to_owned(), oid);
1532 })?;
1533 Ok(map)
1534}
1535
1536fn visit_packed_refs_with_prefix(
1538 path: &Path,
1539 data: &[u8],
1540 prefix_filter: Option<&str>,
1541 mut visit: impl FnMut(&str, ObjectId),
1542) -> Result<()> {
1543 if data.is_empty() {
1544 return Ok(());
1545 }
1546 if data.contains(&b'\r') {
1547 return Err(packed_refs_unexpected_line(
1548 path,
1549 "<contains carriage return>",
1550 ));
1551 }
1552 if !data.ends_with(b"\n") {
1553 let tail = std::str::from_utf8(data)
1554 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1555 .unwrap_or("<invalid utf-8>");
1556 return Err(packed_refs_unterminated_line(path, tail));
1557 }
1558 let text = std::str::from_utf8(data)
1559 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1560
1561 let ref_opts = RefNameOptions {
1562 allow_onelevel: false,
1563 refspec_pattern: false,
1564 normalize: false,
1565 };
1566
1567 let mut expect_peel = false;
1568 let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
1569 let packed_sorted = packed_refs_file_declares_sorted(text);
1570 let prefix_upper = if packed_sorted {
1571 prefix_trim.and_then(list_prefix_upper_bound)
1572 } else {
1573 None
1574 };
1575 let mut lines = text.split('\n').peekable();
1576 while let Some(line) = lines.next() {
1577 if line.is_empty() {
1578 if lines.peek().is_some() {
1579 return Err(packed_refs_unexpected_line(path, line));
1580 }
1581 break;
1582 }
1583 if line.starts_with('#') {
1584 expect_peel = false;
1585 continue;
1586 }
1587 if line.starts_with('^') {
1588 parse_packed_ref_peel_line(path, line, expect_peel)?;
1589 expect_peel = false;
1590 continue;
1591 }
1592
1593 if let Some(prefix) = prefix_trim {
1594 let Some(refname) = packed_ref_data_line_refname(line) else {
1595 return Err(packed_refs_unexpected_line(path, line));
1596 };
1597 if packed_sorted {
1598 if refname < prefix {
1599 expect_peel = true;
1600 continue;
1601 }
1602 if let Some(upper) = &prefix_upper {
1603 if refname >= upper.as_str() {
1604 break;
1605 }
1606 }
1607 }
1608 if !ref_name_matches_list_prefix(refname, prefix) {
1609 expect_peel = true;
1610 continue;
1611 }
1612 }
1613 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1614 visit(refname, oid);
1615 expect_peel = true;
1616 }
1617 Ok(())
1618}
1619
1620pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
1621 read_packed_refs_map_for_prefix(store, "")
1622}
1623
1624pub(crate) fn read_packed_refs_map_for_prefix(
1625 store: &Path,
1626 prefix: &str,
1627) -> Result<HashMap<String, ObjectId>> {
1628 read_packed_refs_map_for_prefix_filtered(store, prefix, None)
1629}
1630
1631pub(crate) fn read_packed_refs_map_for_prefix_filtered(
1633 store: &Path,
1634 prefix: &str,
1635 keep: Option<PackedRecordFilter<'_>>,
1636) -> Result<HashMap<String, ObjectId>> {
1637 let Some(view) = read_packed_refs_file_view(store)? else {
1638 return Ok(HashMap::new());
1639 };
1640 let packed_path = store::paths::packed_refs_path(store);
1641 let filter = if prefix.is_empty() {
1642 None
1643 } else {
1644 Some(prefix)
1645 };
1646 view.map_for_prefix_filtered(&packed_path, filter, keep)
1647}
1648
1649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1651enum PackedPeelTrait {
1652 None,
1654 Tags,
1656 All,
1658}
1659
1660impl PackedPeelTrait {
1661 fn covers(self, refname: &str) -> bool {
1663 match self {
1664 Self::None => false,
1665 Self::Tags => refname.starts_with("refs/tags/"),
1666 Self::All => true,
1667 }
1668 }
1669}
1670
1671pub(crate) struct PackedRefPeels {
1681 view: Option<PackedRefsFileView>,
1682 unsorted: Option<HashMap<Box<[u8]>, usize>>,
1684 peel_trait: PackedPeelTrait,
1685 next: std::cell::Cell<usize>,
1688}
1689
1690impl PackedRefPeels {
1691 pub(crate) fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
1701 if format != RefStorageFormat::Files {
1702 return Ok(Self::from_view(None));
1703 }
1704 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1705 Ok(Self::from_view(read_packed_refs_file_view(&store)?))
1706 }
1707
1708 pub(crate) fn for_store(store: &dyn store::RefStore, git_dir: &Path) -> Result<Self> {
1717 match store.as_files() {
1718 Some(files) => Ok(Self::from_view(files.packed_refs_view()?)),
1719 None => Self::load(git_dir, store.format()),
1720 }
1721 }
1722
1723 fn from_view(view: Option<PackedRefsFileView>) -> Self {
1724 let empty = Self {
1725 view: None,
1726 unsorted: None,
1727 peel_trait: PackedPeelTrait::None,
1728 next: std::cell::Cell::new(0),
1729 };
1730 let Some(view) = view else {
1731 return empty;
1732 };
1733 let peel_trait = view.peel_trait;
1734 if peel_trait == PackedPeelTrait::None {
1735 return empty;
1736 }
1737 let unsorted = (!view.sorted).then(|| {
1738 view.records()
1739 .map(|start| (Box::<[u8]>::from(view.record_name(start)), start))
1740 .collect()
1741 });
1742 let first = view
1743 .next_record(view.body_start, view.data.len())
1744 .unwrap_or(view.data.len());
1745 Self {
1746 view: Some(view),
1747 unsorted,
1748 peel_trait,
1749 next: std::cell::Cell::new(first),
1750 }
1751 }
1752
1753 pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
1765 if !self.peel_trait.covers(refname) {
1766 return None;
1767 }
1768 let view = self.view.as_ref()?;
1769 let start = match &self.unsorted {
1770 Some(map) => *map.get(refname.as_bytes())?,
1771 None => {
1772 let len = view.data.len();
1773 let hint = self.next.get();
1774 let start = if hint < len && view.record_cmp(hint, refname).is_eq() {
1775 hint
1776 } else {
1777 view.search(refname).ok()?
1778 };
1779 let after = packed_line_end(&view.data, start) + 1;
1780 self.next.set(view.next_record(after, len).unwrap_or(len));
1781 start
1782 }
1783 };
1784 let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
1785 let packed_oid = ObjectId::from_hex(hex).ok()?;
1786 (packed_oid == *oid).then(|| peel_oid_following_line(&view.data, start))
1787 }
1788}
1789
1790fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
1792 let Some(header) = text.strip_prefix("# pack-refs with: ") else {
1793 return PackedPeelTrait::None;
1794 };
1795 let traits = header.split('\n').next().unwrap_or_default();
1796 let mut peel_trait = PackedPeelTrait::None;
1797 for t in traits.split_whitespace() {
1798 match t {
1799 "fully-peeled" => return PackedPeelTrait::All,
1800 "peeled" => peel_trait = PackedPeelTrait::Tags,
1801 _ => {}
1802 }
1803 }
1804 peel_trait
1805}
1806
1807fn acquire_packed_refs_lock(
1808 git_dir: &Path,
1809 lock: &Path,
1810 lock_display: &Path,
1811) -> Result<crate::lockfile::LockFile> {
1812 use crate::lockfile::{LockError, LockFile, LockWait};
1813 let wait = LockWait::packed_refs_for_git_dir(git_dir);
1814 LockFile::acquire(lock, wait).map_err(|err| match err {
1815 LockError::Held { .. } => Error::from(RefLockError::PackedRefsLockHeld {
1816 lock_path: lock_display.display().to_string(),
1817 }),
1818 LockError::Io { source, .. } => Error::Io(source),
1819 })
1820}
1821
1822pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
1824 let Some(view) = read_packed_refs_file_view(git_dir)? else {
1825 return Ok(None);
1826 };
1827 let packed_path = store::paths::packed_refs_path(git_dir);
1828 view.lookup_oid(&packed_path, refname)
1829}
1830
1831#[cfg(test)]
1833pub(crate) fn lookup_packed_ref_with_peel(
1834 git_dir: &Path,
1835 refname: &str,
1836) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1837 let Some(view) = read_packed_refs_file_view(git_dir)? else {
1838 return Ok(None);
1839 };
1840 let packed_path = store::paths::packed_refs_path(git_dir);
1841 view.lookup_entry(&packed_path, refname)
1842}
1843
1844pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
1853 store_write_value(
1854 git_dir,
1855 refname,
1856 Some(StoreRawRef::Symbolic(target.to_owned())),
1857 Expected::Any,
1858 None,
1859 )
1860}
1861
1862pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
1863 if let Some(rest) = refname.strip_prefix("refs/") {
1864 if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
1865 return Err(Error::InvalidRef(format!(
1866 "refusing to update ref with bad name '{refname}'"
1867 )));
1868 }
1869 return Ok(());
1870 }
1871 if crate::check_ref_format::refname_is_safe(refname) {
1872 Ok(())
1873 } else {
1874 Err(Error::InvalidRef(format!(
1875 "refusing to update ref with bad name '{refname}'"
1876 )))
1877 }
1878}
1879
1880pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
1895 store_write_value(
1896 git_dir,
1897 refname,
1898 Some(StoreRawRef::Direct(*oid)),
1899 Expected::Any,
1900 None,
1901 )
1902}
1903
1904#[must_use]
1906pub fn ref_update_flags(refname: &str) -> RefUpdateFlags {
1907 legacy_ref_update_flags(refname)
1908}
1909
1910pub fn create_ref_with_reflog(
1916 git_dir: &Path,
1917 refname: &str,
1918 oid: &ObjectId,
1919 identity: &str,
1920 message: &str,
1921) -> Result<()> {
1922 let config = ConfigSet::load(
1923 &crate::environment::Environment::empty(),
1924 Some(git_dir),
1925 true,
1926 )
1927 .unwrap_or_default();
1928 let store = open_ref_store(git_dir)?;
1929 create_ref_with_reflog_on_store(
1930 store.as_ref(),
1931 git_dir,
1932 &config,
1933 refname,
1934 oid,
1935 identity,
1936 message,
1937 )
1938}
1939
1940pub(crate) fn create_ref_with_reflog_on_store(
1943 store: &dyn RefStore,
1944 git_dir: &Path,
1945 config: &ConfigSet,
1946 refname: &str,
1947 oid: &ObjectId,
1948 identity: &str,
1949 message: &str,
1950) -> Result<()> {
1951 let reflog =
1952 should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
1953 identity: identity.to_owned(),
1954 message: message.to_owned(),
1955 time: time::OffsetDateTime::now_utc(),
1956 old_oid: None,
1957 new_oid: None,
1958 });
1959 store_write_value_on_store(
1960 store,
1961 git_dir,
1962 refname,
1963 Some(StoreRawRef::Direct(*oid)),
1964 Expected::Missing,
1965 reflog,
1966 )
1967}
1968
1969fn write_ref_at_storage(
1972 git_dir: &Path,
1973 storage_dir: &Path,
1974 refname: &str,
1975 oid: &ObjectId,
1976) -> Result<()> {
1977 let stor = crate::ref_namespace::storage_ref_name(refname);
1978 let path = storage_dir.join(stor);
1979 remove_empty_ref_directory(&path);
1986 if fs::symlink_metadata(&path)
1993 .map(|m| m.file_type().is_dir())
1994 .unwrap_or(false)
1995 {
1996 let display = ref_path_for_display(&path);
1997 return Err(crate::error::RefLockError::DirectoryInTheWay {
1998 refname: refname.to_owned(),
1999 path: display,
2000 }
2001 .into());
2002 }
2003 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
2004 return Err(Error::InvalidRef(format!(
2005 "cannot update ref '{refname}': reference broken"
2006 )));
2007 }
2008 if let Some(parent) = path.parent() {
2009 crate::git_dir::io_error::io_with_git_path(
2010 git_dir,
2011 parent,
2012 crate::git_dir::GitDirPathOperation::CreateDir,
2013 fs::create_dir_all(parent),
2014 )?;
2015 }
2016 let content = format!("{oid}\n");
2017 let lock_path = lock_path_for_ref(&path);
2020 use crate::lockfile::{LockError, LockFile, LockWait};
2021 use std::io::Write as _;
2022 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
2023 let mut lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
2024 held @ LockError::Held { .. } => Error::from(held),
2025 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2026 git_dir,
2027 &lock_path,
2028 crate::git_dir::GitDirPathOperation::UpdateRef,
2029 source,
2030 ),
2031 })?;
2032 crate::git_dir::io_error::io_with_git_path(
2033 git_dir,
2034 &path,
2035 crate::git_dir::GitDirPathOperation::UpdateRef,
2036 lock.write_all(content.as_bytes()),
2037 )?;
2038 lock.commit_to(&path).map_err(|err| match err {
2039 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2040 git_dir,
2041 &path,
2042 crate::git_dir::GitDirPathOperation::UpdateRef,
2043 source,
2044 ),
2045 held @ LockError::Held { .. } => Error::from(held),
2046 })?;
2047 Ok(())
2048}
2049
2050pub fn write_ref_cas(
2058 git_dir: &Path,
2059 refname: &str,
2060 oid: &ObjectId,
2061 expected_old: ObjectId,
2062) -> Result<()> {
2063 store_write_value(
2064 git_dir,
2065 refname,
2066 Some(StoreRawRef::Direct(*oid)),
2067 Expected::Oid(expected_old),
2068 None,
2069 )
2070}
2071
2072pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
2079 store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
2080}
2081
2082fn prune_loose_ref_cas(
2092 git_dir: &Path,
2093 refname: &str,
2094 expected: ObjectId,
2095 wait: crate::lockfile::LockWait,
2096) -> Result<bool> {
2097 ensure_refname_safe_for_storage(refname)?;
2098 let storage_dir = ref_storage_dir(git_dir, refname);
2099 let stor = crate::ref_namespace::storage_ref_name(refname);
2100 let path = storage_dir.join(&stor);
2101 let lock_path = lock_path_for_ref(&path);
2102 use crate::lockfile::LockFile;
2103 let _lock = LockFile::acquire(&lock_path, wait)?;
2104 if !path.is_file() {
2105 return Ok(true);
2106 }
2107 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
2108 return Ok(false);
2109 };
2110 if oid != expected {
2111 return Ok(false);
2112 }
2113 fs::remove_file(&path).map_err(Error::Io)?;
2114 Ok(true)
2115}
2116
2117pub struct PackedRefs {
2133 map: HashMap<String, ObjectId>,
2134 sorted_names: BTreeSet<String>,
2135}
2136
2137fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
2138 let sorted_names = map.keys().cloned().collect();
2139 PackedRefs { map, sorted_names }
2140}
2141
2142impl PackedRefs {
2143 pub fn load(git_dir: &Path) -> Result<Self> {
2152 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2153 let map = read_packed_refs_map(&store)?;
2154 Ok(packed_refs_from_map(map))
2155 }
2156
2157 #[must_use]
2159 pub fn get(&self, refname: &str) -> Option<ObjectId> {
2160 self.map.get(refname).copied()
2161 }
2162
2163 #[must_use]
2167 pub fn has_namespace_conflict(&self, refname: &str) -> bool {
2168 packed_refname_namespace_conflict(&self.sorted_names, refname)
2169 }
2170
2171 pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
2174 self.map.insert(refname.to_owned(), oid);
2175 self.sorted_names.insert(refname.to_owned());
2176 }
2177}
2178
2179pub fn resolve_ref_cached(
2189 git_dir: &Path,
2190 refname: &str,
2191 packed: &PackedRefs,
2192) -> Result<Option<ObjectId>> {
2193 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2194 return Ok(resolve_ref(git_dir, refname).ok());
2195 }
2196 resolve_files_ref_cached(git_dir, refname, packed)
2197}
2198
2199fn resolve_files_ref_cached(
2200 git_dir: &Path,
2201 refname: &str,
2202 packed: &PackedRefs,
2203) -> Result<Option<ObjectId>> {
2204 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
2205 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
2206 let try_names: Vec<&str> = if storage_owned != stor_name {
2207 vec![storage_owned.as_str(), stor_name.as_str()]
2208 } else {
2209 vec![stor_name.as_str()]
2210 };
2211
2212 for name in &try_names {
2213 let path = store.join(name);
2214 match read_ref_file(&path) {
2215 Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
2216 Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
2217 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
2218 Err(e) => return Err(e),
2219 }
2220 }
2221
2222 for name in &try_names {
2223 if let Some(oid) = packed.get(name) {
2224 return Ok(Some(oid));
2225 }
2226 }
2227 Ok(None)
2228}
2229
2230pub fn write_ref_cached(
2239 git_dir: &Path,
2240 refname: &str,
2241 oid: &ObjectId,
2242 packed: &mut PackedRefs,
2243) -> Result<()> {
2244 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2245 return write_ref(git_dir, refname, oid);
2246 }
2247 ensure_refname_safe_for_storage(refname)?;
2248 if packed.has_namespace_conflict(refname) {
2249 return Err(Error::InvalidRef(format!(
2250 "cannot update ref '{refname}': reference namespace conflict"
2251 )));
2252 }
2253 let storage_dir = ref_storage_dir(git_dir, refname);
2254 write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
2255 packed.record_loose_write(refname, *oid);
2256 Ok(())
2257}
2258
2259fn ref_path_for_display(path: &Path) -> String {
2264 let cwd = crate::environment::Environment::empty().cwd.clone();
2265 if let Ok(rel) = path.strip_prefix(&cwd) {
2266 return rel.to_string_lossy().into_owned();
2267 }
2268 if let (Ok(cwd_c), Ok(path_c)) = (
2272 cwd.canonicalize_simplified(),
2273 path.canonicalize_simplified(),
2274 ) {
2275 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
2276 return rel.to_string_lossy().into_owned();
2277 }
2278 }
2279 path.to_string_lossy().into_owned()
2280}
2281
2282pub(crate) fn remove_empty_ref_directory(path: &Path) {
2288 match fs::symlink_metadata(path) {
2289 Ok(meta) if meta.file_type().is_dir() => {}
2290 _ => return,
2291 }
2292 if dir_tree_has_files(path) {
2295 return;
2296 }
2297 let _ = remove_dir_tree(path);
2298}
2299
2300fn dir_tree_has_files(dir: &Path) -> bool {
2302 let Ok(entries) = fs::read_dir(dir) else {
2303 return true;
2305 };
2306 for entry in entries.flatten() {
2307 match entry.file_type() {
2308 Ok(ft) if ft.is_dir() => {
2309 if dir_tree_has_files(&entry.path()) {
2310 return true;
2311 }
2312 }
2313 Ok(_) => return true, Err(_) => return true,
2315 }
2316 }
2317 false
2318}
2319
2320pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
2322 if let Some(dir) = ref_path.parent() {
2323 prune_empty_ref_dirs(storage_dir, dir);
2324 }
2325}
2326
2327pub(crate) fn prune_empty_ref_dirs(storage_dir: &Path, dir: &Path) {
2329 let refs_root = storage_dir.join("refs");
2330 let mut current = Some(dir);
2331 while let Some(dir) = current {
2332 if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
2333 break;
2334 }
2335 if dir_tree_has_files(dir) {
2336 break;
2337 }
2338 if fs::remove_dir(dir).is_err() {
2339 break;
2340 }
2341 current = dir.parent();
2342 }
2343}
2344
2345fn remove_dir_tree(dir: &Path) -> io::Result<()> {
2347 for entry in fs::read_dir(dir)? {
2348 let entry = entry?;
2349 if entry.file_type()?.is_dir() {
2350 remove_dir_tree(&entry.path())?;
2351 }
2352 }
2353 fs::remove_dir(dir)
2354}
2355
2356pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
2366 if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
2367 return Err(store::RefStoreError::not_found(refname).into());
2368 }
2369 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2370 let store = open_ref_store(git_dir)?;
2371 let mut flags = legacy_ref_update_flags(refname);
2372 if refname != "HEAD" {
2373 if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
2374 flags.no_deref = true;
2375 }
2376 }
2377 return commit_store_update(
2378 git_dir,
2379 RefUpdate {
2380 name: refname.to_owned(),
2381 new_value: None,
2382 expected: Expected::Exists,
2383 reflog: None,
2384 flags,
2385 },
2386 )
2387 .map_err(|e| map_public_ref_store_error(e, refname));
2388 }
2389 ensure_refname_safe_for_storage(refname)?;
2390 let storage_dir = ref_storage_dir(git_dir, refname);
2391 let stor = crate::ref_namespace::storage_ref_name(refname);
2392 let path = storage_dir.join(&stor);
2393 let lock = lock_path_for_ref(&path);
2394 if fs::symlink_metadata(&lock).is_ok() {
2395 return Err(Error::from(crate::lockfile::LockError::Held { path: lock }));
2396 }
2397
2398 remove_packed_ref(&storage_dir, &stor)?;
2404
2405 remove_empty_ref_directory(&path);
2409 match fs::remove_file(&path) {
2410 Ok(()) => {}
2411 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
2412 Err(e)
2413 if e.kind() == io::ErrorKind::NotADirectory
2414 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
2415 Err(e)
2418 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
2419 Err(e) => return Err(Error::Io(e)),
2420 }
2421
2422 prune_empty_loose_ref_parents(&storage_dir, &path);
2423
2424 let log_path = storage_dir.join("logs").join(&stor);
2425
2426 let _ = fs::remove_file(&log_path);
2432
2433 let logs_root = storage_dir.join("logs");
2434 let mut parent = log_path.parent();
2435 while let Some(p) = parent {
2436 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
2437 break;
2438 }
2439 if fs::remove_dir(p).is_err() {
2440 break; }
2442 parent = p.parent();
2443 }
2444
2445 Ok(())
2446}
2447
2448pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
2450 remove_packed_refs_inner(git_dir, &|name| name == refname, false, false).map(drop)
2451}
2452
2453pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
2454 remove_packed_refs_inner(git_dir, &|name| name == refname, false, true).map(drop)
2455}
2456
2457pub(crate) fn remove_packed_refs_under_lock(
2467 git_dir: &Path,
2468 refnames: &HashSet<String>,
2469) -> Result<Vec<String>> {
2470 if refnames.is_empty() {
2471 return Ok(Vec::new());
2472 }
2473 remove_packed_refs_inner(git_dir, &|name| refnames.contains(name), true, true)
2474}
2475
2476pub(crate) fn packed_record_name(record: &str) -> &str {
2478 let line = record.lines().next().unwrap_or("");
2479 line.split_once(' ').map_or("", |(_, name)| name.trim())
2480}
2481
2482pub(crate) fn packed_record_oid(record: &str) -> Option<ObjectId> {
2484 let line = record.lines().next()?;
2485 ObjectId::from_hex(line.split_once(' ')?.0).ok()
2486}
2487
2488pub(crate) fn restore_packed_records_under_lock(git_dir: &Path, records: &[&str]) -> Result<()> {
2495 if records.is_empty() {
2496 return Ok(());
2497 }
2498 let packed_path = store::paths::packed_refs_path(git_dir);
2499 let content = match fs::read(&packed_path) {
2500 Ok(c) => c,
2501 Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
2502 Err(e) => return Err(Error::Io(e)),
2503 };
2504 parse_packed_refs_to_map(&packed_path, &content)?;
2505 let content = String::from_utf8(content)
2506 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
2507
2508 let mut blocks: Vec<String> = Vec::new();
2509 for line in content.lines() {
2510 if line.starts_with('#') {
2511 continue;
2512 }
2513 match blocks.last_mut() {
2514 Some(block) if line.starts_with('^') => block.push_str(line),
2515 _ => blocks.push(line.to_owned()),
2516 }
2517 if let Some(block) = blocks.last_mut() {
2518 block.push('\n');
2519 }
2520 }
2521 blocks.extend(records.iter().map(|r| (*r).to_owned()));
2522 blocks.sort_by(|a, b| packed_record_name(a).cmp(packed_record_name(b)));
2523
2524 let mut out =
2525 String::with_capacity(content.len() + records.iter().map(|r| r.len()).sum::<usize>());
2526 out.push_str(PACKED_REFS_HEADER);
2527 for block in &blocks {
2528 out.push_str(block);
2529 }
2530 replace_packed_refs_file(git_dir, &packed_path, &out, true)
2531}
2532
2533const PACKED_REFS_HEADER: &str = "# pack-refs with: peeled fully-peeled sorted\n";
2534
2535fn remove_packed_refs_inner(
2536 git_dir: &Path,
2537 remove: &dyn Fn(&str) -> bool,
2538 keep_removed: bool,
2539 lock_already_held: bool,
2540) -> Result<Vec<String>> {
2541 let mut removed: Vec<String> = Vec::new();
2542 let packed_path = store::paths::packed_refs_path(git_dir);
2543 let content = match fs::read(&packed_path) {
2544 Ok(c) => c,
2545 Err(e)
2546 if e.kind() == io::ErrorKind::NotFound
2547 || e.kind() == io::ErrorKind::NotADirectory
2548 || e.raw_os_error() == Some(libc::ENOTDIR) =>
2549 {
2550 return Ok(removed);
2551 }
2552 Err(e) => return Err(Error::Io(e)),
2553 };
2554 let mut has_removed_record = false;
2557 visit_packed_refs_with_prefix(&packed_path, &content, None, |name, _| {
2558 has_removed_record |= remove(name);
2559 })?;
2560 if !has_removed_record {
2561 return Ok(removed);
2562 }
2563
2564 let content = String::from_utf8(content)
2565 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
2566
2567 let mut out = String::with_capacity(content.len());
2568 let mut skip_peeled = false;
2569 let mut changed = false;
2570 let mut header_written = false;
2573
2574 for line in content.lines() {
2575 if skip_peeled {
2576 if line.starts_with('^') {
2577 if let Some(record) = removed.last_mut().filter(|_| keep_removed) {
2578 record.push_str(line);
2579 record.push('\n');
2580 }
2581 changed = true;
2582 continue;
2583 }
2584 skip_peeled = false;
2585 }
2586
2587 if line.starts_with('#') {
2588 continue;
2590 }
2591 if line.starts_with('^') {
2592 out.push_str(line);
2593 out.push('\n');
2594 continue;
2595 }
2596
2597 if !header_written {
2599 out.insert_str(0, PACKED_REFS_HEADER);
2600 header_written = true;
2601 }
2602
2603 let name = line.split_once(' ').map_or("", |(_, name)| name.trim());
2604 if remove(name) {
2605 if keep_removed {
2606 removed.push(format!("{line}\n"));
2607 }
2608 changed = true;
2609 skip_peeled = true;
2610 continue;
2611 }
2612
2613 out.push_str(line);
2614 out.push('\n');
2615 }
2616
2617 if changed {
2618 replace_packed_refs_file(git_dir, &packed_path, &out, lock_already_held)?;
2619 }
2620
2621 Ok(removed)
2622}
2623
2624fn replace_packed_refs_file(
2626 git_dir: &Path,
2627 packed_path: &Path,
2628 body: &str,
2629 lock_already_held: bool,
2630) -> Result<()> {
2631 let lock = lock_path_for_ref(packed_path); let _packed_lock = if !lock_already_held {
2639 let abs_lock = git_dir
2640 .canonicalize_simplified()
2641 .map(|d| d.join("packed-refs.lock"))
2642 .unwrap_or_else(|_| lock.clone());
2643 Some(acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?)
2644 } else {
2645 None
2646 };
2647
2648 let tmp = packed_path.with_extension("new");
2649 let mut created_tmp = false;
2650 let write_result = (|| -> Result<()> {
2651 let mut file = std::fs::OpenOptions::new()
2653 .write(true)
2654 .create_new(true)
2655 .open(&tmp)
2656 .map_err(Error::Io)?;
2657 created_tmp = true;
2658 use std::io::Write as _;
2659 file.write_all(body.as_bytes()).map_err(Error::Io)?;
2660 drop(file);
2661 fs::rename(&tmp, packed_path).map_err(Error::Io)?;
2662 created_tmp = false; Ok(())
2664 })();
2665
2666 if write_result.is_err() && created_tmp {
2667 let _ = fs::remove_file(&tmp);
2668 }
2669 write_result
2670}
2671
2672pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
2680 read_symbolic_ref(git_dir, "HEAD")
2681}
2682
2683pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
2690 match open_ref_store(git_dir)?.read_raw(refname)? {
2691 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
2692 Some(StoreRawRef::Direct(_)) | None => Ok(None),
2693 }
2694}
2695
2696pub fn read_symbolic_ref_for_repository(
2698 repo: &crate::repo::Repository,
2699 refname: &str,
2700) -> Result<Option<String>> {
2701 match repo.refs().read_raw(refname)? {
2702 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
2703 Some(StoreRawRef::Direct(_)) | None => Ok(None),
2704 }
2705}
2706
2707#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2709pub enum LogRefsConfig {
2710 Unset,
2712 None,
2714 Normal,
2716 Always,
2718}
2719
2720impl LogRefsConfig {
2721 pub(crate) fn parse(raw: &str) -> Option<Self> {
2723 match raw.trim().to_ascii_lowercase().as_str() {
2724 "always" => Some(Self::Always),
2725 "1" | "true" | "yes" | "on" => Some(Self::Normal),
2726 "0" | "false" | "no" | "off" | "never" => Some(Self::None),
2727 _ => None,
2728 }
2729 }
2730}
2731
2732pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
2736 ConfigSet::load(
2737 &crate::environment::Environment::empty(),
2738 Some(git_dir),
2739 true,
2740 )
2741 .map(|cfg| cfg.log_refs_config())
2742 .unwrap_or(LogRefsConfig::Unset)
2743}
2744
2745pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
2749 ConfigSet::load(
2750 &crate::environment::Environment::empty(),
2751 Some(git_dir),
2752 true,
2753 )
2754 .map(|cfg| cfg.effective_log_refs_config())
2755 .unwrap_or(LogRefsConfig::Normal)
2756}
2757
2758#[must_use]
2761pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
2762 match mode {
2763 LogRefsConfig::Always => true,
2764 LogRefsConfig::Normal => {
2765 refname == "HEAD"
2766 || refname.starts_with("refs/heads/")
2767 || refname.starts_with("refs/remotes/")
2768 || refname.starts_with("refs/notes/")
2769 }
2770 LogRefsConfig::None | LogRefsConfig::Unset => false,
2771 }
2772}
2773
2774#[must_use]
2776pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
2777 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
2778}
2779
2780#[must_use]
2782pub fn should_autocreate_reflog_with_config(
2783 config: &ConfigSet,
2784 _git_dir: &Path,
2785 refname: &str,
2786) -> bool {
2787 should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
2788}
2789
2790fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
2816 let Ok(rel) = target.strip_prefix(logs_root) else {
2817 return;
2818 };
2819 let mut cur = logs_root.to_path_buf();
2820 for component in rel.components() {
2821 cur.push(component);
2822 match fs::symlink_metadata(&cur) {
2823 Ok(meta) if meta.file_type().is_dir() => {}
2824 Ok(_) => {
2825 let _ = fs::remove_file(&cur);
2828 }
2829 Err(_) => break, }
2831 }
2832}
2833
2834pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
2837 let width = if !old.is_zero() {
2838 old.algo().hex_len()
2839 } else if !new.is_zero() {
2840 new.algo().hex_len()
2841 } else {
2842 old.algo().hex_len()
2843 };
2844 let render = |oid: &ObjectId| {
2845 if oid.is_zero() {
2846 "0".repeat(width)
2847 } else {
2848 oid.to_hex()
2849 }
2850 };
2851 (render(old), render(new))
2852}
2853
2854fn map_reflog_store_validation(err: store::RefStoreError) -> Error {
2855 match err {
2856 store::RefStoreError::InvalidReflog(sig) => Error::InvalidSignature(sig),
2857 other => Error::from(other),
2858 }
2859}
2860
2861fn validate_reflog_message(message: &str) -> Result<()> {
2862 store::validate_reflog_message(message).map_err(map_reflog_store_validation)
2863}
2864
2865fn validate_reflog_identity_string(identity: &str) -> Result<()> {
2866 store::validate_reflog_identity_string(identity).map_err(map_reflog_store_validation)
2867}
2868
2869fn validate_reflog_update(log: &ReflogUpdate) -> Result<()> {
2871 store::validate_reflog_update(log).map_err(map_reflog_store_validation)
2872}
2873
2874pub fn append_reflog_with_signature(
2880 git_dir: &Path,
2881 refname: &str,
2882 old_oid: &ObjectId,
2883 new_oid: &ObjectId,
2884 identity: &Signature,
2885 message: &str,
2886 force_create: bool,
2887) -> Result<()> {
2888 identity.validate().map_err(Error::InvalidSignature)?;
2889 let formatted = format_reflog_identity(&identity.reflog_name_email(), identity.time);
2890 append_reflog_with_config(
2891 git_dir,
2892 refname,
2893 old_oid,
2894 new_oid,
2895 &formatted,
2896 message,
2897 force_create,
2898 None,
2899 )
2900}
2901
2902pub fn append_reflog(
2911 git_dir: &Path,
2912 refname: &str,
2913 old_oid: &ObjectId,
2914 new_oid: &ObjectId,
2915 identity: &str,
2916 message: &str,
2917 force_create: bool,
2918) -> Result<()> {
2919 append_reflog_with_config(
2920 git_dir,
2921 refname,
2922 old_oid,
2923 new_oid,
2924 identity,
2925 message,
2926 force_create,
2927 None,
2928 )
2929}
2930
2931#[allow(clippy::too_many_arguments)]
2933pub fn append_reflog_with_config(
2934 git_dir: &Path,
2935 refname: &str,
2936 old_oid: &ObjectId,
2937 new_oid: &ObjectId,
2938 identity: &str,
2939 message: &str,
2940 force_create: bool,
2941 config: Option<&ConfigSet>,
2942) -> Result<()> {
2943 validate_reflog_identity_string(identity)?;
2944 validate_reflog_message(message)?;
2945 test_inject_reflog_fail(refname)?;
2946 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2947 return crate::reftable::reftable_append_reflog(
2948 git_dir,
2949 refname,
2950 old_oid,
2951 new_oid,
2952 identity,
2953 message,
2954 force_create,
2955 );
2956 }
2957 let storage_dir = ref_storage_dir(git_dir, refname);
2958 let stor = crate::ref_namespace::storage_ref_name(refname);
2959 let log_path = storage_dir.join("logs").join(&stor);
2960 let may_create = force_create
2961 || match config {
2962 Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
2963 None => should_autocreate_reflog(git_dir, refname),
2964 };
2965 if !may_create && !log_path.exists() {
2966 return Ok(());
2967 }
2968 if let Some(parent) = log_path.parent() {
2969 let logs_root = storage_dir.join("logs");
2974 clear_conflicting_reflog_files(&logs_root, parent);
2975 crate::git_dir::io_error::io_with_git_path(
2976 git_dir,
2977 parent,
2978 crate::git_dir::GitDirPathOperation::CreateDir,
2979 fs::create_dir_all(parent),
2980 )?;
2981 }
2982 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
2987 let line = if message.is_empty() {
2988 format!("{old_hex} {new_hex} {identity}\n")
2989 } else {
2990 format!("{old_hex} {new_hex} {identity}\t{message}\n")
2991 };
2992 let mut file = fs::OpenOptions::new()
2993 .create(true)
2994 .append(true)
2995 .open(&log_path)
2996 .map_err(|source| {
2997 crate::git_dir::io_error::map_git_dir_io(
2998 git_dir,
2999 &log_path,
3000 crate::git_dir::GitDirPathOperation::AppendReflog,
3001 source,
3002 )
3003 })?;
3004 use io::Write;
3005 crate::git_dir::io_error::io_with_git_path(
3006 git_dir,
3007 &log_path,
3008 crate::git_dir::GitDirPathOperation::AppendReflog,
3009 file.write_all(line.as_bytes()),
3010 )?;
3011 Ok(())
3012}
3013
3014fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
3015 if all {
3016 b"*"
3017 } else {
3018 b"refs/tags/*"
3019 }
3020}
3021
3022fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
3023 crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
3024}
3025
3026fn collect_loose_ref_files(
3027 dir: &Path,
3028 prefix: &str,
3029 out: &mut Vec<(String, PathBuf)>,
3030) -> Result<()> {
3031 let read = match fs::read_dir(dir) {
3032 Ok(r) => r,
3033 Err(e)
3034 if e.kind() == io::ErrorKind::NotFound
3035 || e.kind() == io::ErrorKind::NotADirectory
3036 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3037 {
3038 return Ok(());
3039 }
3040 Err(e) => return Err(Error::Io(e)),
3041 };
3042 for entry in read {
3043 let entry = entry?;
3044 let name = entry.file_name().to_string_lossy().to_string();
3045 let refname = format!("{prefix}{name}");
3046 let path = entry.path();
3047 if path.is_dir() {
3048 collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
3049 } else if path.is_file() {
3050 out.push((refname, path));
3051 }
3052 }
3053 Ok(())
3054}
3055
3056fn load_packed_refs_with_peel(
3057 packed_path: &Path,
3058 merged: &mut HashMap<String, (ObjectId, Option<ObjectId>)>,
3059) -> Result<()> {
3060 if !packed_path.is_file() {
3061 return Ok(());
3062 }
3063 let data = fs::read(packed_path)?;
3064 parse_packed_refs_to_map(packed_path, &data)?;
3065 let content = String::from_utf8(data)
3066 .map_err(|_| packed_refs_unexpected_line(packed_path, "<invalid utf-8>"))?;
3067 let mut last_name: Option<String> = None;
3068 for line in content.lines() {
3069 if line.starts_with('#') {
3070 continue;
3071 }
3072 if let Some(hex) = line.strip_prefix('^') {
3073 let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
3074 continue;
3075 };
3076 if let Some(name) = &last_name {
3077 if let Some(entry) = merged.get_mut(name) {
3078 entry.1 = Some(peel_oid);
3079 }
3080 }
3081 continue;
3082 }
3083 let mut parts = line.splitn(2, ' ');
3084 let Some(hex) = parts.next() else {
3085 continue;
3086 };
3087 let Some(name) = parts.next() else {
3088 continue;
3089 };
3090 let name = name.trim();
3091 let Ok(oid) = ObjectId::from_hex(hex) else {
3092 continue;
3093 };
3094 merged.insert(name.to_owned(), (oid, None));
3095 last_name = Some(name.to_owned());
3096 }
3097 Ok(())
3098}
3099
3100pub(crate) fn pack_refs_for_repository(
3102 repo: &crate::repo::Repository,
3103 all: bool,
3104 dry_run: bool,
3105) -> Result<(PathBuf, usize, usize)> {
3106 let git_dir = repo.git_dir();
3107 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3108 return pack_refs_reftable(git_dir, dry_run);
3109 }
3110 pack_refs_files(git_dir, repo.odb(), all, dry_run)
3111}
3112
3113fn pack_refs_reftable(git_dir: &Path, dry_run: bool) -> Result<(PathBuf, usize, usize)> {
3114 use crate::reftable::ReftableStack;
3115 let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3116 let rt_dir = store_git_dir.join("reftable");
3117 let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
3118 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3119 .unwrap_or(0);
3120 if dry_run {
3121 let tables_pruned = tables_before.saturating_sub(1);
3122 return Ok((rt_dir, 0, tables_pruned));
3123 }
3124 let mut stack = ReftableStack::open(&store_git_dir)?;
3125 stack.compact()?;
3126 let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
3127 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3128 .unwrap_or(0);
3129 Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
3130}
3131
3132fn pack_refs_files(
3133 git_dir: &Path,
3134 odb: &crate::odb::Odb,
3135 all: bool,
3136 dry_run: bool,
3137) -> Result<(PathBuf, usize, usize)> {
3138 pack_refs_files_matching(
3139 git_dir,
3140 odb,
3141 |name| ref_matches_pack_include(name, all),
3142 dry_run,
3143 false,
3144 )
3145}
3146
3147fn pack_refs_files_matching(
3148 git_dir: &Path,
3149 odb: &crate::odb::Odb,
3150 include: impl Fn(&str) -> bool,
3151 dry_run: bool,
3152 allow_missing: bool,
3153) -> Result<(PathBuf, usize, usize)> {
3154 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3155 let packed_path = store::paths::packed_refs_path(&storage_git_dir);
3156
3157 let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
3158 load_packed_refs_with_peel(&packed_path, &mut merged)?;
3159
3160 let mut loose_files = Vec::new();
3161 collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
3162
3163 let mut packed_loose = 0usize;
3164 let mut to_prune: Vec<(String, ObjectId)> = Vec::new();
3165
3166 for (refname, path) in loose_files {
3167 if crate::worktree_ref::is_per_worktree_ref(&refname) {
3168 continue;
3169 }
3170 if !include(&refname) {
3171 continue;
3172 }
3173 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
3174 continue;
3175 };
3176 let object = odb.try_read_once(&oid).ok().flatten();
3177 if object.is_none() && !allow_missing {
3178 continue;
3179 }
3180 let peel = if object.is_some_and(|object| object.kind == crate::objects::ObjectKind::Tag) {
3181 packed_ref_peel(odb, oid)
3182 } else {
3183 None
3184 };
3185 merged.insert(refname.clone(), (oid, peel));
3186 to_prune.push((refname, oid));
3187 let _ = path;
3188 packed_loose += 1;
3189 }
3190
3191 if packed_loose == 0 && merged.is_empty() {
3192 return Ok((packed_path, 0, 0));
3193 }
3194
3195 if dry_run {
3196 return Ok((packed_path, packed_loose, to_prune.len()));
3197 }
3198
3199 let mut names: Vec<String> = merged.keys().cloned().collect();
3200 names.sort();
3201
3202 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
3203 for name in &names {
3204 let Some((oid, peel)) = merged.get(name) else {
3205 continue;
3206 };
3207 body.push_str(&oid.to_hex());
3208 body.push(' ');
3209 body.push_str(name);
3210 body.push('\n');
3211 if let Some(p) = peel {
3212 body.push('^');
3213 body.push_str(&p.to_hex());
3214 body.push('\n');
3215 }
3216 }
3217
3218 atomic_rewrite_packed_refs(&storage_git_dir, &body)?;
3219
3220 #[cfg(test)]
3221 pack_refs_test_hooks::run_before_prune();
3222
3223 let mut pruned = 0usize;
3224 let mut parents = HashSet::new();
3225 let wait = crate::lockfile::LockWait::files_ref_lock_for_git_dir(git_dir);
3226 for (refname, expected) in &to_prune {
3227 match prune_loose_ref_cas(git_dir, refname, *expected, wait) {
3228 Ok(true) => {
3229 pruned += 1;
3230 let path = storage_git_dir.join(crate::ref_namespace::storage_ref_name(refname));
3231 if let Some(parent) = path.parent() {
3232 parents.insert(parent.to_path_buf());
3233 }
3234 }
3235 Ok(false) => {
3236 remove_packed_ref(git_dir, refname)?;
3237 }
3238 Err(e) => return Err(e),
3239 }
3240 }
3241
3242 for parent in parents {
3245 remove_empty_ref_directory(&parent);
3246 }
3247
3248 Ok((packed_path, packed_loose, pruned))
3249}
3250
3251#[cfg(test)]
3253mod pack_refs_test_hooks {
3254 use std::cell::RefCell;
3255
3256 thread_local! {
3257 static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
3258 }
3259
3260 pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
3261 BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
3262 }
3263
3264 pub fn run_before_prune() {
3265 BEFORE_PRUNE.with(|c| {
3266 if let Some(f) = c.borrow_mut().take() {
3267 f();
3268 }
3269 });
3270 }
3271}
3272
3273#[cfg(test)]
3277mod reflog_fail_inject {
3278 use std::cell::RefCell;
3279
3280 thread_local! {
3282 pub static INJECT: RefCell<Option<String>> = const { RefCell::new(None) };
3283 }
3284}
3285
3286#[cfg(test)]
3288pub fn set_test_inject_reflog_fail(refname: Option<&str>) {
3289 reflog_fail_inject::INJECT.with(|c| {
3290 *c.borrow_mut() = refname.map(str::to_owned);
3291 });
3292}
3293
3294#[cfg(test)]
3295fn test_inject_reflog_fail(refname: &str) -> Result<()> {
3296 reflog_fail_inject::INJECT.with(|c| {
3297 if c.borrow().as_deref() == Some(refname) {
3298 return Err(Error::Io(std::io::Error::other("injected reflog failure")));
3299 }
3300 Ok(())
3301 })
3302}
3303
3304#[cfg(not(test))]
3305fn test_inject_reflog_fail(_refname: &str) -> Result<()> {
3306 Ok(())
3307}
3308
3309pub struct BranchCommitRefUpdate<'a> {
3322 pub branch_ref: &'a str,
3324 pub expected_old: Option<ObjectId>,
3326 pub new_oid: ObjectId,
3328 pub identity: &'a str,
3330 pub reflog_message: &'a str,
3332}
3333
3334pub fn verify_branch_commit_cas(
3344 expected: Option<ObjectId>,
3345 current: Option<ObjectId>,
3346 refname: &str,
3347) -> Result<()> {
3348 verify_branch_cas(expected, current, refname)
3349}
3350
3351fn verify_branch_cas(
3352 expected: Option<ObjectId>,
3353 current: Option<ObjectId>,
3354 refname: &str,
3355) -> Result<()> {
3356 if expected == current {
3357 return Ok(());
3358 }
3359 Err(store::RefStoreError::ExpectedMismatch {
3360 name: refname.to_owned(),
3361 expected: expected.map_or(Expected::Missing, Expected::Oid),
3362 actual: current.map(StoreRawRef::Direct),
3363 }
3364 .into())
3365}
3366
3367fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
3368 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
3369 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
3370 let try_names: Vec<&str> = if storage_owned != stor_name {
3371 vec![storage_owned.as_str(), stor_name.as_str()]
3372 } else {
3373 vec![stor_name.as_str()]
3374 };
3375 for name in try_names {
3376 let path = store.join(name);
3377 if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
3378 return Ok(Some(oid));
3379 }
3380 if let Some(oid) = lookup_packed_ref(&store, name)? {
3381 return Ok(Some(oid));
3382 }
3383 }
3384 Ok(None)
3385}
3386
3387#[cfg(test)]
3388mod test_pause_inject_serial {
3389 use std::sync::{Mutex, OnceLock};
3390
3391 static SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
3393
3394 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
3395 SERIAL
3396 .get_or_init(|| Mutex::new(()))
3397 .lock()
3398 .unwrap_or_else(|e| e.into_inner())
3399 }
3400}
3401
3402#[cfg(test)]
3404pub fn test_pause_injection_serial_guard() -> std::sync::MutexGuard<'static, ()> {
3405 test_pause_inject_serial::lock()
3406}
3407
3408#[cfg(test)]
3409mod branch_ref_pause_inject {
3410 use std::sync::{Condvar, Mutex, OnceLock};
3411
3412 pub struct PauseGate {
3413 pub wait: Mutex<bool>,
3414 pub cv: Condvar,
3415 }
3416
3417 static PAUSE_BEFORE_COMMIT: OnceLock<Mutex<Option<std::sync::Arc<PauseGate>>>> =
3419 OnceLock::new();
3420
3421 pub fn pause_slot() -> &'static Mutex<Option<std::sync::Arc<PauseGate>>> {
3422 PAUSE_BEFORE_COMMIT.get_or_init(|| Mutex::new(None))
3423 }
3424}
3425
3426#[cfg(test)]
3428pub type BranchRefPauseGate = std::sync::Arc<branch_ref_pause_inject::PauseGate>;
3429
3430#[cfg(test)]
3432pub fn set_test_inject_branch_ref_pause_before_commit(gate: Option<BranchRefPauseGate>) {
3433 *branch_ref_pause_inject::pause_slot()
3434 .lock()
3435 .unwrap_or_else(|e| e.into_inner()) = gate;
3436}
3437
3438#[cfg(test)]
3439fn test_branch_ref_pause_before_commit() {
3440 let gate = branch_ref_pause_inject::pause_slot()
3441 .lock()
3442 .unwrap_or_else(|e| e.into_inner())
3443 .clone();
3444 if let Some(gate) = gate {
3445 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
3446 *wait = true;
3447 gate.cv.notify_all();
3448 while *wait {
3449 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
3450 }
3451 }
3452}
3453
3454#[cfg(not(test))]
3455fn test_branch_ref_pause_before_commit() {}
3456
3457#[cfg(test)]
3458mod ref_store_batch_pause_inject {
3459 use std::sync::{Condvar, Mutex, OnceLock};
3460
3461 pub struct PauseGate {
3462 pub wait: Mutex<bool>,
3463 pub cv: Condvar,
3464 }
3465
3466 pub type PauseSlot = Option<(usize, std::path::PathBuf, std::sync::Arc<PauseGate>)>;
3469
3470 static PAUSE_AFTER_REF_WRITE: OnceLock<Mutex<PauseSlot>> = OnceLock::new();
3471
3472 pub fn pause_slot() -> &'static Mutex<PauseSlot> {
3473 PAUSE_AFTER_REF_WRITE.get_or_init(|| Mutex::new(None))
3474 }
3475}
3476
3477#[cfg(test)]
3479pub type RefStoreBatchPauseGate = std::sync::Arc<ref_store_batch_pause_inject::PauseGate>;
3480
3481#[cfg(test)]
3483pub fn new_ref_store_batch_pause_gate() -> RefStoreBatchPauseGate {
3484 std::sync::Arc::new(ref_store_batch_pause_inject::PauseGate {
3485 wait: std::sync::Mutex::new(false),
3486 cv: std::sync::Condvar::new(),
3487 })
3488}
3489
3490#[cfg(test)]
3495pub fn set_test_inject_ref_store_batch_pause_after_apply(
3496 apply_index: usize,
3497 git_dir: &Path,
3498 gate: Option<RefStoreBatchPauseGate>,
3499) {
3500 *ref_store_batch_pause_inject::pause_slot()
3501 .lock()
3502 .unwrap_or_else(|e| e.into_inner()) = gate.map(|g| (apply_index, git_dir.to_path_buf(), g));
3503}
3504
3505#[cfg(test)]
3506struct RefStorePauseTestSerial {
3507 holder: Option<std::thread::ThreadId>,
3509}
3510
3511#[cfg(test)]
3512static REF_STORE_PAUSE_TEST_SERIAL: std::sync::Mutex<RefStorePauseTestSerial> =
3513 std::sync::Mutex::new(RefStorePauseTestSerial { holder: None });
3514
3515#[cfg(test)]
3517pub(crate) struct RefStoreBatchPauseTestScope {
3518 _serial: std::sync::MutexGuard<'static, RefStorePauseTestSerial>,
3519}
3520
3521#[cfg(test)]
3522pub(crate) fn ref_store_batch_pause_test_scope() -> RefStoreBatchPauseTestScope {
3523 let mut serial = REF_STORE_PAUSE_TEST_SERIAL
3524 .lock()
3525 .unwrap_or_else(|e| e.into_inner());
3526 assert!(
3527 serial.holder.is_none(),
3528 "ref_store_batch_pause_test_scope must not nest"
3529 );
3530 serial.holder = Some(std::thread::current().id());
3531 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
3532 RefStoreBatchPauseTestScope { _serial: serial }
3533}
3534
3535#[cfg(test)]
3536impl Drop for RefStoreBatchPauseTestScope {
3537 fn drop(&mut self) {
3538 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
3539 self._serial.holder = None;
3540 }
3541}
3542
3543#[cfg(test)]
3544pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
3545 let slot = ref_store_batch_pause_inject::pause_slot()
3546 .lock()
3547 .unwrap_or_else(|e| e.into_inner())
3548 .clone();
3549 if let Some((target, target_dir, gate)) = slot {
3550 let same_repo = |a: &Path, b: &Path| {
3551 a == b
3552 || matches!((a.canonicalize_simplified(), b.canonicalize_simplified()), (Ok(a), Ok(b)) if a == b)
3553 };
3554 if idx != target || !same_repo(git_dir, &target_dir) {
3555 return;
3556 }
3557 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
3558 *wait = true;
3559 gate.cv.notify_all();
3560 while *wait {
3561 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
3562 }
3563 }
3564}
3565
3566#[cfg(not(test))]
3567pub(crate) fn test_ref_store_batch_pause_after_ref_write(_idx: usize, _git_dir: &Path) {}
3568
3569pub struct BranchCommitRefLock {
3573 lock: Option<crate::lockfile::LockFile>,
3574 disarmed: bool,
3575}
3576
3577impl BranchCommitRefLock {
3578 pub fn acquire(
3584 git_dir: &Path,
3585 branch_ref: &str,
3586 expected_old: Option<ObjectId>,
3587 ) -> Result<Self> {
3588 ensure_refname_safe_for_storage(branch_ref)?;
3589 let storage_dir = ref_storage_dir(git_dir, branch_ref);
3590 if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
3591 return Err(Error::InvalidRef(format!(
3592 "cannot update ref '{}': reference namespace conflict",
3593 branch_ref
3594 )));
3595 }
3596 let stor = crate::ref_namespace::storage_ref_name(branch_ref);
3597 let path = storage_dir.join(&stor);
3598 remove_empty_ref_directory(&path);
3599 if fs::symlink_metadata(&path)
3600 .map(|m| m.file_type().is_dir())
3601 .unwrap_or(false)
3602 {
3603 let display = ref_path_for_display(&path);
3604 return Err(crate::error::RefLockError::DirectoryInTheWay {
3605 refname: branch_ref.to_owned(),
3606 path: display,
3607 }
3608 .into());
3609 }
3610 if let Some(parent) = path.parent() {
3611 crate::git_dir::io_error::io_with_git_path(
3612 git_dir,
3613 parent,
3614 crate::git_dir::GitDirPathOperation::CreateDir,
3615 fs::create_dir_all(parent),
3616 )?;
3617 }
3618
3619 let lock_path = lock_path_for_ref(&path);
3620 use crate::lockfile::{LockError, LockFile, LockWait};
3621 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
3622 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
3623 held @ LockError::Held { .. } => Error::from(held),
3624 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
3625 git_dir,
3626 path.as_path(),
3627 crate::git_dir::GitDirPathOperation::UpdateRef,
3628 source,
3629 ),
3630 })?;
3631
3632 let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
3633 verify_branch_cas(expected_old, current, branch_ref)?;
3634
3635 Ok(Self {
3636 lock: Some(lock),
3637 disarmed: false,
3638 })
3639 }
3640
3641 pub fn write_reflogs(
3651 &mut self,
3652 git_dir: &Path,
3653 update: &BranchCommitRefUpdate<'_>,
3654 reflog_old: &ObjectId,
3655 config: &ConfigSet,
3656 ) -> Result<()> {
3657 write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
3658 }
3659
3660 pub fn finish_ref_commit(
3668 mut self,
3669 git_dir: &Path,
3670 update: &BranchCommitRefUpdate<'_>,
3671 ) -> Result<()> {
3672 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
3673 self.disarmed = true;
3674 Ok(())
3675 }
3676
3677 pub fn commit(
3683 mut self,
3684 git_dir: &Path,
3685 update: &BranchCommitRefUpdate<'_>,
3686 reflog_old: &ObjectId,
3687 config: &ConfigSet,
3688 ) -> Result<()> {
3689 write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
3690 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
3691 self.disarmed = true;
3692 Ok(())
3693 }
3694}
3695
3696impl Drop for BranchCommitRefLock {
3697 fn drop(&mut self) {
3698 drop(self.lock.take());
3699 }
3700}
3701
3702fn write_branch_commit_reflogs(
3703 git_dir: &Path,
3704 lock: &mut BranchCommitRefLock,
3705 update: &BranchCommitRefUpdate<'_>,
3706 reflog_old: &ObjectId,
3707 config: &ConfigSet,
3708) -> Result<()> {
3709 let mut branch_reflog_written = false;
3710 if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
3711 match append_reflog_with_config(
3712 git_dir,
3713 update.branch_ref,
3714 reflog_old,
3715 &update.new_oid,
3716 update.identity,
3717 update.reflog_message,
3718 false,
3719 Some(config),
3720 ) {
3721 Ok(()) => branch_reflog_written = true,
3722 Err(err) => {
3723 drop(lock.lock.take());
3724 return Err(err);
3725 }
3726 }
3727 }
3728
3729 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
3730 if let Err(err) = append_reflog_with_config(
3731 git_dir,
3732 "HEAD",
3733 reflog_old,
3734 &update.new_oid,
3735 update.identity,
3736 update.reflog_message,
3737 false,
3738 Some(config),
3739 ) {
3740 if branch_reflog_written {
3741 let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
3742 }
3743 drop(lock.lock.take());
3744 return Err(err);
3745 }
3746 }
3747
3748 Ok(())
3749}
3750
3751fn finish_branch_commit_ref_lock_files(
3752 git_dir: &Path,
3753 lock: &mut BranchCommitRefLock,
3754 update: &BranchCommitRefUpdate<'_>,
3755) -> Result<()> {
3756 test_branch_ref_pause_before_commit();
3757
3758 let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
3759 let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
3760 let path = storage_dir.join(&stor);
3761 let content = format!("{}\n", update.new_oid);
3762 use crate::lockfile::LockError;
3763 use std::io::Write as _;
3764 let mut file_lock = lock
3765 .lock
3766 .take()
3767 .ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
3768 crate::git_dir::io_error::io_with_git_path(
3769 git_dir,
3770 &path,
3771 crate::git_dir::GitDirPathOperation::UpdateRef,
3772 file_lock.write_all(content.as_bytes()),
3773 )?;
3774 file_lock.commit_to(&path).map_err(|err| match err {
3775 LockError::Io {
3776 source,
3777 path: lock_path,
3778 } => crate::git_dir::io_error::map_git_dir_io(
3779 git_dir,
3780 lock_path.as_path(),
3781 crate::git_dir::GitDirPathOperation::UpdateRef,
3782 source,
3783 ),
3784 held @ LockError::Held { .. } => Error::from(held),
3785 })?;
3786 Ok(())
3787}
3788
3789pub(crate) fn update_branch_for_commit_files(
3790 git_dir: &Path,
3791 update: &BranchCommitRefUpdate<'_>,
3792 reflog_old: &ObjectId,
3793 config: &ConfigSet,
3794) -> Result<()> {
3795 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
3796 lock.commit(git_dir, update, reflog_old, config)
3797}
3798
3799pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
3804 let config = ConfigSet::load(
3805 &crate::environment::Environment::empty(),
3806 Some(git_dir),
3807 true,
3808 )
3809 .unwrap_or_default();
3810 update_branch_for_commit_with_config(git_dir, update, &config)
3811}
3812
3813pub fn update_branch_for_commit_with_config(
3817 git_dir: &Path,
3818 update: &BranchCommitRefUpdate<'_>,
3819 config: &ConfigSet,
3820) -> Result<()> {
3821 update_branch_for_commit_on_store(open_ref_store(git_dir)?.as_ref(), update, config)
3822}
3823
3824pub(crate) fn update_detached_head_for_commit_files(
3827 git_dir: &Path,
3828 expected_old: Option<ObjectId>,
3829 new_oid: ObjectId,
3830 identity: &str,
3831 reflog_message: &str,
3832 config: &ConfigSet,
3833) -> Result<()> {
3834 let current = resolve_ref(git_dir, "HEAD").ok();
3835 verify_branch_cas(expected_old, current, "HEAD")?;
3836 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
3837
3838 write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
3839 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
3840 if let Err(err) = append_reflog_with_config(
3841 git_dir,
3842 "HEAD",
3843 &reflog_old,
3844 &new_oid,
3845 identity,
3846 reflog_message,
3847 false,
3848 Some(config),
3849 ) {
3850 let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
3851 return Err(err);
3852 }
3853 }
3854 Ok(())
3855}
3856
3857#[must_use]
3863pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
3864 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
3865 store.join("logs").join(stor_name)
3866}
3867
3868fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
3869 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
3870}
3871
3872fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
3881 if prefix.is_empty() {
3882 return String::new();
3883 }
3884 if prefix.ends_with('/') {
3885 return prefix.to_string();
3886 }
3887 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
3888 if candidate.is_file() {
3889 prefix.to_string()
3890 } else {
3891 format!("{prefix}/")
3892 }
3893}
3894
3895fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
3898 odb.peel_stored_tag_target(oid).ok().flatten()
3899}
3900
3901pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
3912 pack_imported_refs_for_clone(git_dir, Some(remote))
3913}
3914
3915pub(crate) fn pack_imported_refs_for_clone(git_dir: &Path, remote: Option<&str>) -> Result<()> {
3917 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3918 return Ok(());
3919 }
3920 let prefix = remote.map(|name| format!("refs/remotes/{name}/"));
3921 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3922 let odb = crate::odb::Odb::new(&storage_git_dir.join("objects"))
3923 .with_config_git_dir(git_dir.to_path_buf());
3924 odb.with_pack_read_context(|| {
3925 pack_refs_files_matching(
3926 git_dir,
3927 &odb,
3928 |name| {
3929 name.starts_with("refs/tags/")
3930 || prefix
3931 .as_ref()
3932 .is_some_and(|prefix| name.starts_with(prefix))
3933 },
3934 false,
3935 true,
3937 )
3938 })?;
3939 Ok(())
3940}
3941
3942fn atomic_rewrite_packed_refs(git_dir: &Path, body: &str) -> Result<()> {
3944 let packed_path = store::paths::packed_refs_path(git_dir);
3945 let lock = lock_path_for_ref(&packed_path);
3946 let abs_lock = git_dir
3947 .canonicalize_simplified()
3948 .map(|d| d.join("packed-refs.lock"))
3949 .unwrap_or_else(|_| lock.clone());
3950 let _packed_lock = acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?;
3951
3952 let tmp = packed_path.with_extension("new");
3953 let mut created_tmp = false;
3954 let write_result = (|| -> Result<()> {
3955 let mut file = std::fs::OpenOptions::new()
3957 .write(true)
3958 .create_new(true)
3959 .open(&tmp)
3960 .map_err(Error::Io)?;
3961 created_tmp = true;
3962 use std::io::Write as _;
3963 file.write_all(body.as_bytes()).map_err(Error::Io)?;
3964 drop(file);
3965 fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
3966 created_tmp = false;
3967 Ok(())
3968 })();
3969 if write_result.is_err() && created_tmp {
3970 let _ = fs::remove_file(&tmp);
3971 }
3972 write_result
3973}
3974
3975#[derive(Debug, Clone, PartialEq, Eq)]
3977pub struct PackedRefRecord {
3978 pub refname: String,
3980 pub oid: ObjectId,
3982 pub peeled: Option<ObjectId>,
3984}
3985
3986pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
3995 let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
3996 sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
3997 for rec in &sorted {
3998 ensure_refname_safe_for_storage(&rec.refname)?;
3999 }
4000 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
4001 for rec in &sorted {
4002 body.push_str(&rec.oid.to_hex());
4003 body.push(' ');
4004 body.push_str(&rec.refname);
4005 body.push('\n');
4006 if let Some(peeled) = rec.peeled {
4007 body.push('^');
4008 body.push_str(&peeled.to_hex());
4009 body.push('\n');
4010 }
4011 }
4012 atomic_rewrite_packed_refs(git_dir, &body)?;
4013 for rec in &sorted {
4014 let storage_dir = ref_storage_dir(git_dir, &rec.refname);
4015 let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
4016 let loose = storage_dir.join(stor);
4017 if loose.is_file() {
4018 fs::remove_file(&loose).map_err(Error::Io)?;
4019 }
4020 if let Some(parent) = loose.parent() {
4021 remove_empty_ref_directory(parent);
4022 }
4023 }
4024 Ok(())
4025}
4026
4027pub fn list_refs_for_repository(
4038 repo: &crate::repo::Repository,
4039 prefix: &str,
4040) -> Result<Vec<(String, ObjectId)>> {
4041 let prefix_norm = normalize_list_refs_prefix(repo.git_dir(), prefix);
4042 list_store_refs_with_name_prefix(repo, &prefix_norm)
4043}
4044
4045fn list_store_refs_with_name_prefix(
4047 repo: &crate::repo::Repository,
4048 prefix: &str,
4049) -> Result<Vec<(String, ObjectId)>> {
4050 let git_dir = repo.git_dir();
4051 let mut results = collect_resolved_refs(repo.refs(), prefix)?;
4052 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4053 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4054 }
4055 results.sort_by(|a, b| a.0.cmp(&b.0));
4056 Ok(results)
4057}
4058
4059pub(crate) fn list_refs_in(
4070 repo: &crate::repo::Repository,
4071 prefix: &str,
4072) -> Result<Vec<(String, ObjectId)>> {
4073 if repo.refs_on_disk() {
4074 list_refs(repo.git_dir(), prefix)
4075 } else {
4076 list_refs_for_repository(repo, prefix)
4077 }
4078}
4079
4080pub(crate) fn resolve_ref_in(repo: &crate::repo::Repository, refname: &str) -> Result<ObjectId> {
4087 if repo.refs_on_disk() {
4088 resolve_ref(repo.git_dir(), refname)
4089 } else {
4090 repo.resolve_ref_oid(refname)
4091 }
4092}
4093
4094pub(crate) fn list_refs_glob_in(
4101 repo: &crate::repo::Repository,
4102 pattern: &str,
4103) -> Result<Vec<(String, ObjectId)>> {
4104 if repo.refs_on_disk() {
4105 list_refs_glob(repo.git_dir(), pattern)
4106 } else {
4107 list_refs_glob_for_repository(repo, pattern)
4108 }
4109}
4110
4111pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4120 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4121 let prefix = prefix_norm.as_str();
4122 let store = open_ref_store(git_dir)?;
4123 if store.format() != RefStorageFormat::Files {
4124 return crate::reftable::reftable_list_refs(git_dir, prefix);
4125 }
4126 let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
4127 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4128 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4129 }
4130 results.sort_by(|a, b| a.0.cmp(&b.0));
4131 Ok(results)
4132}
4133
4134pub(crate) fn list_refs_filtering_packed(
4145 git_dir: &Path,
4146 prefix: &str,
4147 keep: PackedRecordFilter<'_>,
4148) -> Result<Vec<(String, ObjectId)>> {
4149 let Some(store) = store::open_files_ref_store(git_dir)? else {
4150 return list_refs(git_dir, prefix);
4151 };
4152 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4153 let prefix = prefix_norm.as_str();
4154 let mut results = collect_resolved_ref_entries(&store, |f| {
4155 store.for_each_ref_filtered(prefix, Some(keep), f)
4156 })?;
4157 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4158 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4159 }
4160 results.sort_by(|a, b| a.0.cmp(&b.0));
4161 Ok(results)
4162}
4163
4164pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
4166 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
4167}
4168
4169pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4174 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4175 return crate::reftable::reftable_list_refs(git_dir, prefix);
4176 }
4177 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
4178 let stored_prefix = prefix.to_owned();
4179
4180 if let Some(cdir) = common_dir(git_dir) {
4181 if cdir != git_dir {
4182 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
4183 let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
4184 collect_loose_refs_into_map(
4185 &loose_root,
4186 &loose_prefix,
4187 &stored_prefix,
4188 &cdir,
4189 true,
4190 &mut by_name,
4191 )?;
4192 }
4193 }
4194
4195 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
4196 let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
4197 collect_loose_refs_into_map(
4198 &loose_root,
4199 &loose_prefix,
4200 &stored_prefix,
4201 git_dir,
4202 true,
4203 &mut by_name,
4204 )?;
4205
4206 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
4207 results.sort_by(|a, b| a.0.cmp(&b.0));
4208 Ok(results)
4209}
4210
4211pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
4220 let config = ConfigSet::load(
4221 &crate::environment::Environment::empty(),
4222 Some(receiving_git_dir),
4223 true,
4224 )?;
4225 let objects_dir = receiving_git_dir.join("objects");
4226 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
4227 let mut out = Vec::new();
4228 let mut seen = std::collections::HashSet::new();
4229 for alt_objects in alternates {
4230 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
4231 continue;
4232 };
4233 if !alt_git_dir.join("refs").is_dir() {
4234 continue;
4235 }
4236 if let Some(prefixes) = config
4237 .get("core.alternaterefsprefixes")
4238 .or_else(|| config.get("core.alternateRefsPrefixes"))
4239 {
4240 for part in prefixes.split_whitespace() {
4241 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
4242 if seen.insert(oid) {
4243 out.push(oid);
4244 }
4245 continue;
4246 }
4247 for (_, oid) in list_refs(&alt_git_dir, part)? {
4248 if seen.insert(oid) {
4249 out.push(oid);
4250 }
4251 }
4252 }
4253 } else {
4254 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
4255 if seen.insert(oid) {
4256 out.push(oid);
4257 }
4258 }
4259 }
4260 }
4261 Ok(out)
4262}
4263
4264pub fn list_refs_glob_for_repository(
4266 repo: &crate::repo::Repository,
4267 pattern: &str,
4268) -> Result<Vec<(String, ObjectId)>> {
4269 let all = match pattern.find(['*', '?', '[']) {
4273 Some(pos) => list_store_refs_with_name_prefix(repo, &pattern[..pos])?,
4274 None => {
4275 let mut dir = pattern.trim_end_matches('/').to_owned();
4276 if !dir.is_empty() {
4277 dir.push('/');
4278 }
4279 list_refs_for_repository(repo, &dir)?
4280 }
4281 };
4282 Ok(all
4283 .into_iter()
4284 .filter(|(refname, _)| ref_matches_glob(refname, pattern))
4285 .collect())
4286}
4287
4288pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
4290 let glob_pos = pattern.find(['*', '?', '[']);
4291 let prefix_owned: String = match glob_pos {
4292 Some(pos) => pattern[..pos].to_owned(),
4294 None => {
4295 let mut p = pattern.trim_end_matches('/').to_owned();
4296 if !p.is_empty() {
4297 p.push('/');
4298 }
4299 p
4300 }
4301 };
4302 let prefix = prefix_owned.as_str();
4303 let all = list_refs(git_dir, prefix)?;
4304 let mut results = Vec::new();
4305 for (refname, oid) in all {
4306 if ref_matches_glob(&refname, pattern) {
4307 results.push((refname, oid));
4308 }
4309 }
4310 Ok(results)
4311}
4312
4313pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
4317 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
4319 return refname == pattern
4320 || refname.ends_with(&format!("/{pattern}"))
4321 || refname.starts_with(&format!("{pattern}/"));
4322 }
4323 glob_match(pattern, refname)
4324}
4325
4326fn glob_match(pattern: &str, text: &str) -> bool {
4327 let pat = pattern.as_bytes();
4328 let txt = text.as_bytes();
4329 let (mut pi, mut ti) = (0, 0);
4330 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
4331 while ti < txt.len() {
4332 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
4333 pi += 1;
4334 ti += 1;
4335 } else if pi < pat.len() && pat[pi] == b'*' {
4336 star_pi = pi;
4337 star_ti = ti;
4338 pi += 1;
4339 } else if star_pi != usize::MAX {
4340 pi = star_pi + 1;
4341 star_ti += 1;
4342 ti = star_ti;
4343 } else {
4344 return false;
4345 }
4346 }
4347 while pi < pat.len() && pat[pi] == b'*' {
4348 pi += 1;
4349 }
4350 pi == pat.len()
4351}
4352
4353fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
4355 let content = fs::read_to_string(path).ok()?;
4356 let content = content.trim_end_matches('\n').trim();
4357 if ObjectId::is_full_hex(content) {
4358 content.parse().ok()
4359 } else {
4360 None
4361 }
4362}
4363
4364fn collect_loose_refs_into_map(
4365 dir: &Path,
4366 prefix: &str,
4367 filter_prefix: &str,
4368 resolve_git_dir: &Path,
4369 physical_keys: bool,
4370 out: &mut HashMap<String, ObjectId>,
4371) -> Result<()> {
4372 let read = match fs::read_dir(dir) {
4373 Ok(r) => r,
4374 Err(e)
4375 if e.kind() == io::ErrorKind::NotFound
4376 || e.kind() == io::ErrorKind::NotADirectory
4377 || e.raw_os_error() == Some(libc::ENOTDIR) =>
4378 {
4379 return Ok(());
4380 }
4381 Err(e) => return Err(Error::Io(e)),
4382 };
4383
4384 for entry in read {
4385 let entry = entry?;
4386 let name = entry.file_name();
4387 let name_str = name.to_string_lossy();
4388 let refname = format!("{prefix}{name_str}");
4389 let path = entry.path();
4390 let meta = match fs::metadata(&path) {
4391 Ok(m) => m,
4392 Err(_) => continue,
4393 };
4394
4395 if meta.is_dir() {
4396 collect_loose_refs_into_map(
4397 &path,
4398 &format!("{refname}/"),
4399 filter_prefix,
4400 resolve_git_dir,
4401 physical_keys,
4402 out,
4403 )?;
4404 } else if meta.is_file() {
4405 if physical_keys {
4406 if let Some(oid) = loose_ref_file_direct_oid(&path) {
4407 out.insert(refname, oid);
4408 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
4409 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
4410 out.insert(refname, oid);
4411 }
4412 }
4413 } else {
4414 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
4415 .unwrap_or_else(|| refname.clone());
4416 if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
4417 continue;
4418 }
4419 if let Some(oid) = loose_ref_file_direct_oid(&path) {
4420 out.insert(logical, oid);
4421 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
4422 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
4423 out.insert(logical, oid);
4424 }
4425 }
4426 }
4427 }
4428 }
4429 Ok(())
4430}
4431
4432pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
4435 let inner = spec
4437 .strip_prefix("@{-")
4438 .and_then(|s| s.strip_suffix('}'))
4439 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
4440 let n: usize = inner
4441 .parse()
4442 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
4443 if n == 0 {
4444 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
4445 }
4446 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
4447 let mut count = 0usize;
4448 for entry in entries.iter().rev() {
4449 let msg = &entry.message;
4450 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
4451 count += 1;
4452 if count == n {
4453 if let Some(to_pos) = rest.find(" to ") {
4454 return Ok(rest[..to_pos].to_string());
4455 }
4456 }
4457 }
4458 }
4459 Err(Error::InvalidRef(format!(
4460 "{spec}: only {count} checkout(s) in reflog"
4461 )))
4462}
4463
4464fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
4465 let prefix = prefix.trim_end_matches('/');
4466 if prefix.is_empty() {
4467 return true;
4468 }
4469 if refname.starts_with(prefix) {
4470 return true;
4471 }
4472 if refname == prefix {
4473 return true;
4474 }
4475 false
4476}
4477
4478fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
4479 let base = prefix.trim_end_matches('/');
4480 let (head, tail) = base.rsplit_once('/')?;
4481 let mut chars: Vec<char> = tail.chars().collect();
4482 while let Some(last) = chars.pop() {
4483 if last != '\u{10ffff}' {
4484 chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
4485 return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
4486 }
4487 }
4488 None
4489}
4490
4491pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
4494 let trimmed = prefix.trim_end_matches('/');
4495 if trimmed.is_empty() {
4496 return (git_dir.join("refs"), "refs/".to_owned());
4497 }
4498 let direct = git_dir.join(trimmed);
4499 if direct.is_dir() {
4500 return (direct, format!("{trimmed}/"));
4501 }
4502 if let Some(slash) = trimmed.rfind('/') {
4503 let parent = &trimmed[..slash];
4504 return (git_dir.join(parent), format!("{parent}/"));
4505 }
4506 (git_dir.to_path_buf(), String::new())
4507}
4508
4509fn collect_packed_refs_into_map(
4510 git_dir: &Path,
4511 prefix: &str,
4512 physical_keys: bool,
4513 out: &mut HashMap<String, ObjectId>,
4514) -> Result<()> {
4515 let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
4516 for (refname, oid) in map {
4517 let key = if physical_keys {
4518 refname.clone()
4519 } else {
4520 crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
4521 };
4522 out.insert(key, oid);
4523 }
4524 Ok(())
4525}
4526
4527#[cfg(test)]
4528mod update_branch_for_commit_tests {
4529 use super::*;
4530 use crate::reflog::read_reflog;
4531 use crate::repo::init_repository;
4532 use std::sync::{Arc, Condvar, Mutex};
4533 use std::thread;
4534 use tempfile::TempDir;
4535
4536 fn sample_oid(hex: &str) -> ObjectId {
4537 hex.parse().unwrap()
4538 }
4539
4540 #[test]
4541 fn update_branch_for_commit_holds_lock_until_ref_commit() {
4542 let _pause_serial = super::test_pause_injection_serial_guard();
4543 let tmp = TempDir::new().unwrap();
4544 let repo = init_repository(
4545 tmp.path(),
4546 false,
4547 "main",
4548 None,
4549 crate::RefStorageFormat::Files,
4550 )
4551 .unwrap();
4552 let git_dir = repo.git_dir().to_path_buf();
4553 let oid1 = sample_oid("67bf698f3ab735e92fb011a99cff3497c44d30c1");
4554 let oid2 = sample_oid("1111111111111111111111111111111111111111");
4555 let oid3 = sample_oid("2222222222222222222222222222222222222222");
4556 write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
4557
4558 let gate = Arc::new(branch_ref_pause_inject::PauseGate {
4559 wait: Mutex::new(false),
4560 cv: Condvar::new(),
4561 });
4562 set_test_inject_branch_ref_pause_before_commit(Some(gate.clone()));
4563
4564 let git_dir_thread = git_dir.clone();
4565 let handle = thread::spawn(move || {
4566 update_branch_for_commit(
4567 &git_dir_thread,
4568 &BranchCommitRefUpdate {
4569 branch_ref: "refs/heads/main",
4570 expected_old: Some(oid1),
4571 new_oid: oid2,
4572 identity: "T <t@e.com> 1 +0000",
4573 reflog_message: "commit: test",
4574 },
4575 )
4576 });
4577
4578 let mut wait = gate.wait.lock().unwrap();
4579 while !*wait {
4580 wait = gate.cv.wait(wait).unwrap();
4581 }
4582
4583 assert!(
4584 write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
4585 "concurrent branch update must block on the held ref lock"
4586 );
4587
4588 *wait = false;
4589 gate.cv.notify_all();
4590 drop(wait);
4591 handle.join().unwrap().unwrap();
4592 set_test_inject_branch_ref_pause_before_commit(None);
4593
4594 assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
4595 }
4596
4597 #[test]
4598 fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
4599 let tmp = TempDir::new().unwrap();
4600 let repo = init_repository(
4601 tmp.path(),
4602 false,
4603 "main",
4604 None,
4605 crate::RefStorageFormat::Reftable,
4606 )
4607 .unwrap();
4608 let git_dir = repo.git_dir().to_path_buf();
4609 let new_oid = sample_oid("3333333333333333333333333333333333333333");
4610
4611 crate::reftable::set_test_inject_reftable_transaction_fail(true);
4612 let err = update_branch_for_commit(
4613 &git_dir,
4614 &BranchCommitRefUpdate {
4615 branch_ref: "refs/heads/main",
4616 expected_old: None,
4617 new_oid,
4618 identity: "T <t@e.com> 1 +0000",
4619 reflog_message: "commit (initial): test",
4620 },
4621 )
4622 .unwrap_err();
4623 crate::reftable::set_test_inject_reftable_transaction_fail(false);
4624 assert!(!matches!(err, Error::Message(_)));
4625
4626 assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
4627 assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
4628 assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
4629 }
4630}
4631
4632#[cfg(test)]
4633mod refname_available_tests {
4634 use super::*;
4635 use std::collections::{BTreeSet, HashSet};
4636 use tempfile::tempdir;
4637
4638 #[test]
4639 fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
4640 let dir = tempdir().unwrap();
4641 let git_dir = dir.path();
4642 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4643 let items = vec![
4644 RefBatchItem {
4645 name: "refs/heads/c".to_owned(),
4646 new_oid: Some(sample_oid()),
4647 },
4648 RefBatchItem {
4649 name: "refs/heads/c/x".to_owned(),
4650 new_oid: Some(sample_oid()),
4651 },
4652 ];
4653 let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
4654 assert!(matches!(
4655 err,
4656 RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
4657 ));
4658 }
4659
4660 fn sample_oid() -> ObjectId {
4661 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
4662 }
4663
4664 #[test]
4665 fn loose_parent_blocks_child_create() {
4666 let dir = tempdir().unwrap();
4667 let git_dir = dir.path();
4668 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
4669 fs::write(
4670 git_dir.join("refs/1l/c"),
4671 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4672 )
4673 .unwrap();
4674 assert_eq!(
4675 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
4676 RawRefLookup::Exists
4677 );
4678 let extras = BTreeSet::from([
4679 "refs/1l/b".to_string(),
4680 "refs/1l/c/x".to_string(),
4681 "refs/1l/d".to_string(),
4682 ]);
4683 let skip = HashSet::new();
4684 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
4685 .unwrap_err();
4686 assert!(matches!(
4687 err,
4688 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
4689 ));
4690 }
4691
4692 #[test]
4693 fn verify_sees_loose_ref_after_canonical_git_dir() {
4694 let dir = tempdir().unwrap();
4695 let git_dir = dir.path().join(".git");
4696 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
4697 fs::write(
4698 git_dir.join("refs/1l/c"),
4699 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4700 )
4701 .unwrap();
4702 let skip = HashSet::new();
4703 let extras = BTreeSet::new();
4704 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
4705 .unwrap_err();
4706 assert!(matches!(
4707 err,
4708 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
4709 ));
4710 }
4711
4712 fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
4713 if existing == candidate {
4714 return false;
4715 }
4716 existing
4717 .strip_prefix(candidate)
4718 .is_some_and(|rest| rest.starts_with('/'))
4719 || candidate
4720 .strip_prefix(existing)
4721 .is_some_and(|rest| rest.starts_with('/'))
4722 }
4723
4724 #[test]
4725 fn packed_namespace_index_matches_pairwise_scan() {
4726 let mut sorted: BTreeSet<String> = BTreeSet::new();
4727 for i in 0..500 {
4728 sorted.insert(format!("refs/heads/b{i:05}"));
4729 }
4730 sorted.insert("refs/heads/parent".into());
4731 sorted.insert("refs/heads/a/b".into());
4732 let candidates = [
4733 "refs/heads/b00001",
4734 "refs/heads/parent/child",
4735 "refs/heads/new",
4736 "refs/heads/a",
4737 ];
4738 for candidate in candidates {
4739 let indexed = packed_refname_namespace_conflict(&sorted, candidate);
4740 let linear = sorted
4741 .iter()
4742 .any(|existing| refname_namespace_conflicts(existing, candidate));
4743 assert_eq!(indexed, linear, "candidate {candidate}");
4744 }
4745 }
4746
4747 #[test]
4748 fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
4749 let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
4750 if !git_dir.is_dir() {
4751 return;
4752 }
4753 let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
4754 assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
4755 assert!(listed
4756 .iter()
4757 .all(|(name, _)| name.starts_with("refs/heads/b05")));
4758 }
4759
4760 #[test]
4761 fn list_refs_finds_sibling_under_parent_directory() {
4762 let dir = tempdir().unwrap();
4763 let git_dir = dir.path();
4764 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
4765 fs::write(
4766 git_dir.join("refs/ns/p/x"),
4767 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4768 )
4769 .unwrap();
4770 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
4771 assert!(
4772 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
4773 "got {listed:?}"
4774 );
4775 }
4776
4777 #[test]
4778 fn verify_blocks_parent_when_child_ref_exists() {
4779 let dir = tempdir().unwrap();
4780 let git_dir = dir.path();
4781 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
4782 fs::write(
4783 git_dir.join("refs/ns/p/x"),
4784 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4785 )
4786 .unwrap();
4787 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
4788 let skip = HashSet::new();
4789 let err =
4790 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
4791 assert!(matches!(
4792 err,
4793 RefnameUnavailable::DescendantExists { ref blocking, .. }
4794 if blocking == "refs/ns/p/x"
4795 ));
4796 }
4797
4798 #[test]
4799 fn verify_blocks_parent_when_only_a_packed_child_exists() {
4800 let dir = tempdir().unwrap();
4803 let git_dir = dir.path();
4804 fs::create_dir_all(git_dir.join("refs")).unwrap();
4805 fs::write(
4806 store::paths::packed_refs_path(git_dir),
4807 "# pack-refs with: peeled fully-peeled\n\
4808 67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
4809 )
4810 .unwrap();
4811 let err = verify_refname_available_for_create(
4812 git_dir,
4813 "refs/ns/q",
4814 &BTreeSet::new(),
4815 &HashSet::new(),
4816 )
4817 .unwrap_err();
4818 assert!(matches!(
4819 err,
4820 RefnameUnavailable::DescendantExists { ref blocking, .. }
4821 if blocking == "refs/ns/q/x"
4822 ));
4823 }
4824
4825 #[test]
4826 fn verify_allows_name_whose_siblings_share_a_prefix() {
4827 let dir = tempdir().unwrap();
4828 let git_dir = dir.path();
4829 fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
4830 for name in ["v1.0.1", "v1.0-rc1"] {
4831 fs::write(
4832 git_dir.join("refs/tags").join(name),
4833 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4834 )
4835 .unwrap();
4836 }
4837 verify_refname_available_for_create(
4838 git_dir,
4839 "refs/tags/v1.0",
4840 &BTreeSet::new(),
4841 &HashSet::new(),
4842 )
4843 .expect("siblings are not descendants");
4844 }
4845
4846 #[test]
4847 fn verify_blocks_parent_git_style_nested_path() {
4848 let dir = tempdir().unwrap();
4849 let git_dir = dir.path();
4850 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
4851 fs::write(
4852 git_dir.join("refs/3l/c/x"),
4853 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4854 )
4855 .unwrap();
4856 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
4857 let skip = HashSet::new();
4858 let err =
4859 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
4860 assert!(matches!(
4861 err,
4862 RefnameUnavailable::DescendantExists { ref blocking, .. }
4863 if blocking == "refs/3l/c/x"
4864 ));
4865 }
4866
4867 #[test]
4868 fn intermediate_directory_does_not_block_nested_create() {
4869 let dir = tempdir().unwrap();
4870 let git_dir = dir.path();
4871 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
4872 fs::write(
4873 git_dir.join("refs/ns/existing"),
4874 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4875 )
4876 .unwrap();
4877 assert_eq!(
4878 read_raw_ref(git_dir, "refs/ns").unwrap(),
4879 RawRefLookup::IsDirectory
4880 );
4881 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
4882 let skip = HashSet::new();
4883 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
4884 }
4885}
4886
4887#[cfg(test)]
4888mod ref_storage_traversal_guard_tests {
4889 use super::*;
4890 use crate::objects::ObjectId;
4891 use tempfile::tempdir;
4892
4893 const TRAVERSAL_REF: &str = "refs/heads/../../config";
4894
4895 fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
4896 let dir = tempdir().unwrap();
4897 let git_dir = dir.path().to_path_buf();
4898 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4899 fs::write(
4900 git_dir.join("config"),
4901 "[core]\nrepositoryformatversion = 0\n",
4902 )
4903 .unwrap();
4904 (dir, git_dir)
4905 }
4906
4907 fn sample_oid() -> ObjectId {
4908 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
4909 }
4910
4911 fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
4912 assert!(matches!(result, Err(Error::InvalidRef(_))));
4913 assert!(
4914 git_dir.join("config").is_file(),
4915 "repository config must not be touched"
4916 );
4917 }
4918
4919 #[test]
4920 fn write_ref_rejects_traversal_refname() {
4921 let (_dir, git_dir) = bare_repo_with_config();
4922 let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
4923 assert_invalid_ref_preserves_config(&git_dir, result);
4924 }
4925
4926 #[test]
4927 fn write_ref_cached_rejects_traversal_refname() {
4928 let (_dir, git_dir) = bare_repo_with_config();
4929 let mut packed = PackedRefs::load(&git_dir).unwrap();
4930 let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
4931 assert_invalid_ref_preserves_config(&git_dir, result);
4932 }
4933
4934 #[test]
4935 fn write_symbolic_ref_rejects_traversal_refname() {
4936 let (_dir, git_dir) = bare_repo_with_config();
4937 let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
4938 assert_invalid_ref_preserves_config(&git_dir, result);
4939 }
4940
4941 #[test]
4942 fn delete_ref_rejects_traversal_refname() {
4943 let (_dir, git_dir) = bare_repo_with_config();
4944 let result = delete_ref(&git_dir, TRAVERSAL_REF);
4945 assert_invalid_ref_preserves_config(&git_dir, result);
4946 }
4947
4948 #[test]
4949 fn write_ref_rejects_illegal_branch_refname() {
4950 let (_dir, git_dir) = bare_repo_with_config();
4951 for refname in [
4952 "refs/heads/bad name",
4953 "refs/heads/x..y",
4954 "refs/heads/a~b",
4955 "refs/heads/a.lock",
4956 ] {
4957 let result = write_ref(&git_dir, refname, &sample_oid());
4958 assert_invalid_ref_preserves_config(&git_dir, result);
4959 assert!(
4960 !git_dir
4961 .join(refname.strip_prefix("refs/").unwrap())
4962 .exists(),
4963 "must not write loose ref for {refname}"
4964 );
4965 }
4966 }
4967
4968 #[test]
4969 fn write_ref_accepts_refs_heads_head() {
4970 let (_dir, git_dir) = bare_repo_with_config();
4971 write_ref(&git_dir, "refs/heads/HEAD", &sample_oid())
4972 .expect("refs/heads/HEAD is valid ref syntax");
4973 assert!(git_dir.join("refs/heads/HEAD").is_file());
4974 }
4975}
4976
4977#[cfg(test)]
4978mod log_refs_config_tests {
4979 use super::*;
4980 use crate::config::{ConfigFile, ConfigScope};
4981
4982 fn set(text: &str) -> ConfigSet {
4983 let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
4984 let mut set = ConfigSet::new();
4985 set.merge(&file);
4986 set
4987 }
4988
4989 #[test]
4990 fn parse_accepts_modes_and_bool_spellings() {
4991 for (raw, mode) in [
4992 ("always", LogRefsConfig::Always),
4993 (" ALWAYS ", LogRefsConfig::Always),
4994 ("true", LogRefsConfig::Normal),
4995 ("Yes", LogRefsConfig::Normal),
4996 ("on", LogRefsConfig::Normal),
4997 ("1", LogRefsConfig::Normal),
4998 ("false", LogRefsConfig::None),
4999 ("no", LogRefsConfig::None),
5000 ("OFF", LogRefsConfig::None),
5001 ("0", LogRefsConfig::None),
5002 ("never", LogRefsConfig::None),
5003 ] {
5004 assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
5005 }
5006 assert_eq!(LogRefsConfig::parse("sometimes"), None);
5007 }
5008
5009 #[test]
5010 fn effective_mode_applies_bare_default_only_when_unset() {
5011 assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
5012 assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
5013 let bare = set("[core]\n\tbare = true\n");
5014 assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
5015 let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
5016 assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
5017 assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
5018 let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
5019 assert_eq!(
5020 bare_logged.effective_log_refs_config(),
5021 LogRefsConfig::Normal
5022 );
5023 let always = set("[core]\n\tlogAllRefUpdates = always\n");
5024 assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
5025 }
5026
5027 #[test]
5028 fn autocreate_with_config_matches_mode() {
5029 let normal = set("[core]\n\tlogAllRefUpdates = true\n");
5030 let git_dir = Path::new(".git");
5031 assert!(should_autocreate_reflog_with_config(
5032 &normal,
5033 git_dir,
5034 "refs/heads/main"
5035 ));
5036 assert!(!should_autocreate_reflog_with_config(
5037 &normal,
5038 git_dir,
5039 "refs/tags/v1"
5040 ));
5041 let none = set("[core]\n\tlogAllRefUpdates = false\n");
5042 assert!(!should_autocreate_reflog_with_config(
5043 &none, git_dir, "HEAD"
5044 ));
5045 }
5046}
5047
5048#[cfg(test)]
5049mod read_raw_ref_tests {
5050 use super::*;
5051 use tempfile::tempdir;
5052
5053 #[test]
5054 fn loose_ref_file_is_exists() {
5055 let dir = tempdir().unwrap();
5056 let git_dir = dir.path();
5057 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5058 fs::write(
5059 git_dir.join("refs/heads/side"),
5060 "0000000000000000000000000000000000000000\n",
5061 )
5062 .unwrap();
5063 assert_eq!(
5064 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
5065 RawRefLookup::Exists
5066 );
5067 }
5068
5069 #[test]
5070 fn missing_ref_is_not_found() {
5071 let dir = tempdir().unwrap();
5072 let git_dir = dir.path();
5073 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5074 assert_eq!(
5075 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
5076 RawRefLookup::NotFound
5077 );
5078 }
5079
5080 #[test]
5081 fn directory_where_ref_expected_is_is_directory() {
5082 let dir = tempdir().unwrap();
5083 let git_dir = dir.path();
5084 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5085 assert_eq!(
5086 read_raw_ref(git_dir, "refs/heads").unwrap(),
5087 RawRefLookup::IsDirectory
5088 );
5089 }
5090
5091 #[test]
5092 fn packed_ref_name_is_exists() {
5093 let dir = tempdir().unwrap();
5094 let git_dir = dir.path();
5095 fs::write(
5096 store::paths::packed_refs_path(git_dir),
5097 "# pack-refs with: peeled fully-peeled \n\
5098 0000000000000000000000000000000000000000 refs/heads/packed\n",
5099 )
5100 .unwrap();
5101 assert_eq!(
5102 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
5103 RawRefLookup::Exists
5104 );
5105 }
5106}
5107
5108#[cfg(test)]
5109mod packed_refs_parse_tests {
5110 use super::*;
5111
5112 use tempfile::tempdir;
5113
5114 #[test]
5115 fn packed_removal_validates_the_whole_file_before_any_change() {
5116 let dir = tempdir().unwrap();
5117 let path = store::paths::packed_refs_path(dir.path());
5118 let record = "1111111111111111111111111111111111111111 refs/heads/keep\n";
5119 let malformed = [
5120 format!("{record}garbage\n").into_bytes(),
5121 format!("{record}\n").into_bytes(),
5122 format!("{record}^bad-id\n").into_bytes(),
5123 format!("{record}^{}\n^{}\n", "1".repeat(40), "2".repeat(40)).into_bytes(),
5124 format!("{record}{} refs/heads/bad..name\n", "2".repeat(40)).into_bytes(),
5125 format!("{record}# comment\r\n").into_bytes(),
5126 record.trim_end().as_bytes().to_vec(),
5127 [record.as_bytes(), b"\xff\n"].concat(),
5128 ];
5129 for content in malformed {
5130 for name in ["refs/heads/missing", "refs/heads/keep"] {
5131 fs::write(&path, &content).unwrap();
5132 assert!(
5133 remove_packed_ref(dir.path(), name).is_err(),
5134 "{name}: {content:?}"
5135 );
5136 assert_eq!(fs::read(&path).unwrap(), content);
5137 assert!(!path.with_extension("lock").exists());
5138 assert!(!dir.path().join("packed-refs.new").exists());
5139 }
5140 }
5141 }
5142
5143 #[test]
5144 fn packed_removal_without_a_match_preserves_valid_file_bytes() {
5145 let dir = tempdir().unwrap();
5146 let path = store::paths::packed_refs_path(dir.path());
5147 for hash_len in [40, 64] {
5148 let body = format!(
5149 "# pack-refs with: peeled\n# keep this comment\n{} refs/tags/tag\n^{}\n{} refs/heads/main\n",
5150 "1".repeat(hash_len), "2".repeat(hash_len), "3".repeat(hash_len)
5151 );
5152 fs::write(&path, &body).unwrap();
5153 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5154 assert_eq!(fs::read_to_string(&path).unwrap(), body);
5155 }
5156 fs::write(&path, "").unwrap();
5157 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5158 assert!(fs::read(&path).unwrap().is_empty());
5159 }
5160
5161 #[test]
5162 fn parse_rejects_garbage_line() {
5163 let dir = tempdir().unwrap();
5164 let path = store::paths::packed_refs_path(dir.path());
5165 fs::write(
5166 &path,
5167 "# pack-refs with: peeled\n\
5168 0000000000000000000000000000000000000000 refs/heads/main\n\
5169 not a ref line\n",
5170 )
5171 .unwrap();
5172 let data = fs::read(&path).unwrap();
5173 assert!(matches!(
5174 parse_packed_refs_to_map(&path, &data),
5175 Err(Error::PackedRefsUnexpectedLine { .. })
5176 ));
5177 }
5178
5179 #[test]
5180 fn parse_rejects_carriage_return_bytes() {
5181 let dir = tempdir().unwrap();
5182 let path = store::paths::packed_refs_path(dir.path());
5183 fs::write(
5184 &path,
5185 "# pack-refs with: peeled\n\
5186 0000000000000000000000000000000000000000 refs/heads/main\r\n",
5187 )
5188 .unwrap();
5189 let data = fs::read(&path).unwrap();
5190 assert!(matches!(
5191 parse_packed_refs_to_map(&path, &data),
5192 Err(Error::PackedRefsUnexpectedLine { .. })
5193 ));
5194 }
5195
5196 #[test]
5197 fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
5198 let dir = tempdir().unwrap();
5199 let git_dir = dir.path();
5200 let oid = "1111111111111111111111111111111111111111";
5201 fs::write(
5202 store::paths::packed_refs_path(git_dir),
5203 format!(
5204 "# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5205 ),
5206 )
5207 .unwrap();
5208 let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
5209 assert_eq!(listed.len(), 1);
5210 assert_eq!(listed[0].0, "refs/heads/aaa");
5211 }
5212
5213 #[test]
5214 fn parse_rejects_blank_line_and_missing_final_newline() {
5215 let dir = tempdir().unwrap();
5216 let path = store::paths::packed_refs_path(dir.path());
5217 fs::write(
5218 &path,
5219 "# pack-refs with: peeled\n\n\
5220 0000000000000000000000000000000000000000 refs/heads/main\n",
5221 )
5222 .unwrap();
5223 let data = fs::read(&path).unwrap();
5224 assert!(matches!(
5225 parse_packed_refs_to_map(&path, &data),
5226 Err(Error::PackedRefsUnexpectedLine { .. })
5227 ));
5228
5229 fs::write(
5230 &path,
5231 "# pack-refs with: peeled\n\
5232 0000000000000000000000000000000000000000 refs/heads/main",
5233 )
5234 .unwrap();
5235 let data = fs::read(&path).unwrap();
5236 assert!(matches!(
5237 parse_packed_refs_to_map(&path, &data),
5238 Err(Error::PackedRefsUnexpectedLine { .. })
5239 ));
5240 }
5241
5242 #[test]
5243 fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
5244 let dir = tempdir().unwrap();
5245 let path = store::paths::packed_refs_path(dir.path());
5246 let a = "1111111111111111111111111111111111111111";
5247 let b = "2222222222222222222222222222222222222222";
5248 let peel = "3333333333333333333333333333333333333333";
5249 let body = |header: &str| {
5250 format!(
5251 "{header}{a} refs/heads/main\n\
5252 {b} refs/heads/tagged\n\
5253 ^{peel}\n\
5254 {a} refs/tags/light\n\
5255 {b} refs/tags/v1\n\
5256 ^{peel}\n"
5257 )
5258 };
5259 let a: ObjectId = a.parse().unwrap();
5260 let b: ObjectId = b.parse().unwrap();
5261 let peel: ObjectId = peel.parse().unwrap();
5262 let cases = [
5263 (
5264 "# pack-refs with: peeled fully-peeled sorted \n",
5265 [
5266 PackedPeel::NotTag,
5267 PackedPeel::Peeled(peel),
5268 PackedPeel::NotTag,
5269 PackedPeel::Peeled(peel),
5270 ],
5271 ),
5272 (
5273 "# pack-refs with: peeled sorted \n",
5274 [
5275 PackedPeel::Unknown,
5276 PackedPeel::Unknown,
5277 PackedPeel::NotTag,
5278 PackedPeel::Peeled(peel),
5279 ],
5280 ),
5281 ("", [PackedPeel::Unknown; 4]),
5282 ];
5283 for (header, want) in cases {
5284 let data = body(header).into_bytes();
5285 let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
5286 let seen = std::cell::RefCell::new(Vec::new());
5287 let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
5288 seen.borrow_mut().push((name.to_owned(), *oid, peel));
5289 name.starts_with("refs/tags/")
5290 };
5291 let kept = view
5292 .map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
5293 .unwrap();
5294 assert_eq!(
5295 seen.into_inner(),
5296 vec![
5297 ("refs/heads/main".to_owned(), a, want[0]),
5298 ("refs/heads/tagged".to_owned(), b, want[1]),
5299 ("refs/tags/light".to_owned(), a, want[2]),
5300 ("refs/tags/v1".to_owned(), b, want[3]),
5301 ],
5302 "header {header:?}"
5303 );
5304 let mut kept: Vec<_> = kept.into_iter().collect();
5305 kept.sort();
5306 assert_eq!(
5307 kept,
5308 vec![
5309 ("refs/tags/light".to_owned(), a),
5310 ("refs/tags/v1".to_owned(), b)
5311 ]
5312 );
5313 assert_eq!(
5314 view.map_for_prefix_filtered(&path, Some("refs"), None)
5315 .unwrap(),
5316 view.map_for_prefix(&path, Some("refs")).unwrap()
5317 );
5318 }
5319
5320 let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
5322 let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
5323 assert!(matches!(
5324 view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
5325 Err(Error::PackedRefsUnexpectedLine { .. })
5326 ));
5327 }
5328
5329 #[test]
5330 fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
5331 let dir = tempdir().unwrap();
5332 let path = store::paths::packed_refs_path(dir.path());
5333 let oid = "1111111111111111111111111111111111111111";
5334 let peel = "2222222222222222222222222222222222222222";
5335 let sorted_body = format!(
5336 "# pack-refs with: peeled fully-peeled sorted\n\
5337 {oid} refs/heads/aaa\n\
5338 {oid} refs/heads/main\n\
5339 {oid} refs/heads/zzz\n\
5340 {oid} refs/tags/v1\n\
5341 ^{peel}\n"
5342 );
5343 fs::write(&path, &sorted_body).unwrap();
5344 let data = fs::read(&path).unwrap();
5345 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5346 let full = parse_packed_refs_to_map(&path, &data).unwrap();
5347 for (name, want) in &full {
5348 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
5349 }
5350 assert_eq!(
5351 lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
5352 .unwrap()
5353 .map(|(o, p)| (o, p.unwrap())),
5354 Some((oid.parse().unwrap(), peel.parse().unwrap()))
5355 );
5356
5357 let unsorted_body = format!(
5358 "# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5359 );
5360 fs::write(&path, unsorted_body).unwrap();
5361 let data = fs::read(&path).unwrap();
5362 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5363 let full = parse_packed_refs_to_map(&path, &data).unwrap();
5364 for (name, want) in &full {
5365 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
5366 }
5367 assert_eq!(
5368 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
5369 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
5370 );
5371 }
5372
5373 #[test]
5374 fn sorted_prefix_without_upper_bound_filters_names() {
5375 let dir = tempdir().unwrap();
5376 let path = store::paths::packed_refs_path(dir.path());
5377 let oid = "1111111111111111111111111111111111111111";
5378 let sorted_body = format!(
5379 "# pack-refs with: peeled fully-peeled sorted\n\
5380 {oid} refs/heads/aaa\n\
5381 {oid} refs/heads/zzz\n\
5382 {oid} refs/tags/v1\n"
5383 );
5384 fs::write(&path, sorted_body).unwrap();
5385 let data = fs::read(&path).unwrap();
5386 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5387 assert_eq!(
5388 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
5389 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
5390 );
5391 assert!(
5392 !view
5393 .map_for_prefix(&path, Some("refs/heads/a"))
5394 .unwrap()
5395 .contains_key("refs/heads/zzz"),
5396 "prefix refs/heads/a must not include refs/heads/zzz"
5397 );
5398 assert_eq!(
5399 view.map_for_prefix(&path, Some("refs")).unwrap(),
5400 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
5401 );
5402 }
5403
5404 #[test]
5405 fn sorted_prefix_scan_validates_only_its_range() {
5406 let dir = tempdir().unwrap();
5407 let path = store::paths::packed_refs_path(dir.path());
5408 let oid = "1111111111111111111111111111111111111111";
5409 let view_of =
5410 |body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
5411
5412 let bad_peel_on_branch = view_of(format!(
5413 "# pack-refs with: peeled fully-peeled sorted\n\
5414 {oid} refs/heads/main\n\
5415 ^not-hex\n\
5416 {oid} refs/heads/topic\n\
5417 {oid} refs/tags/v1\n"
5418 ))
5419 .unwrap();
5420 assert!(matches!(
5421 bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
5422 Err(Error::PackedRefsUnexpectedLine { .. })
5423 ));
5424 let tags = bad_peel_on_branch
5425 .map_for_prefix(&path, Some("refs/tags"))
5426 .unwrap();
5427 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
5428 assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
5429
5430 let blank_among_tags = view_of(format!(
5431 "# pack-refs with: peeled fully-peeled sorted\n\
5432 {oid} refs/heads/main\n\
5433 {oid} refs/tags/v1\n\
5434 \n\
5435 {oid} refs/tags/v2\n"
5436 ))
5437 .unwrap();
5438 assert_eq!(
5439 blank_among_tags
5440 .map_for_prefix(&path, Some("refs/heads"))
5441 .unwrap()
5442 .len(),
5443 1
5444 );
5445 assert!(blank_among_tags
5446 .map_for_prefix(&path, Some("refs/tags"))
5447 .is_err());
5448
5449 let no_match = blank_among_tags
5450 .map_for_prefix(&path, Some("refs/notes"))
5451 .unwrap();
5452 assert!(no_match.is_empty());
5453 }
5454
5455 #[test]
5456 fn non_utf8_record_only_breaks_reads_that_cover_it() {
5457 let dir = tempdir().unwrap();
5458 let path = store::paths::packed_refs_path(dir.path());
5459 let oid = "1111111111111111111111111111111111111111";
5460 let mut body =
5461 format!("# pack-refs with: peeled fully-peeled sorted \n{oid} refs/heads/main\n")
5462 .into_bytes();
5463 body.extend_from_slice(format!("{oid} refs/tags/v1\n{oid} refs/tags/").as_bytes());
5464 body.extend_from_slice(b"\xff\n");
5465 let view = PackedRefsFileView::from_bytes(&path, body, HashAlgo::Sha1).unwrap();
5466 assert!(view.sorted);
5467 for name in ["refs/heads/main", "refs/tags/v1"] {
5468 assert_eq!(
5469 view.lookup_oid(&path, name).unwrap(),
5470 Some(oid.parse().unwrap())
5471 );
5472 }
5473 assert_eq!(view.lookup_oid(&path, "refs/tags/v2").unwrap(), None);
5474 let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
5475 assert_eq!(heads.keys().collect::<Vec<_>>(), ["refs/heads/main"]);
5476 assert!(matches!(
5477 view.map_for_prefix(&path, Some("refs/tags")),
5478 Err(Error::PackedRefsUnexpectedLine { .. })
5479 ));
5480 assert!(view.map_for_prefix(&path, None).is_err());
5481
5482 let mut bad_header = b"# pack-refs with: \xff sorted \n".to_vec();
5483 bad_header.extend_from_slice(format!("{oid} refs/heads/main\n").as_bytes());
5484 assert!(matches!(
5485 PackedRefsFileView::from_bytes(&path, bad_header, HashAlgo::Sha1),
5486 Err(Error::PackedRefsUnexpectedLine { .. })
5487 ));
5488 }
5489
5490 #[test]
5491 fn scan_of_file_without_ref_lines_still_validates_it() {
5492 let dir = tempdir().unwrap();
5493 let path = store::paths::packed_refs_path(dir.path());
5494 let view = PackedRefsFileView::from_bytes(
5495 &path,
5496 b"# pack-refs with: peeled fully-peeled sorted\n\
5497 ^1111111111111111111111111111111111111111\n"
5498 .to_vec(),
5499 HashAlgo::Sha1,
5500 )
5501 .unwrap();
5502 for prefix in [None, Some("refs/heads")] {
5503 assert!(matches!(
5504 view.map_for_prefix(&path, prefix),
5505 Err(Error::PackedRefsUnexpectedLine { .. })
5506 ));
5507 }
5508 let header_only = PackedRefsFileView::from_bytes(
5509 &path,
5510 b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
5511 HashAlgo::Sha1,
5512 )
5513 .unwrap();
5514 assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
5515 }
5516
5517 #[test]
5518 fn view_checks_last_line_length_against_the_repository_hash() {
5519 let dir = tempdir().unwrap();
5520 let path = store::paths::packed_refs_path(dir.path());
5521 let sha1 = "1111111111111111111111111111111111111111";
5522 let sha256 = "2".repeat(64);
5523 let view =
5524 |body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
5525 let header = "# pack-refs with: peeled fully-peeled sorted\n";
5526 let fifty_bytes = "x".repeat(49);
5527
5528 for (body, algo) in [
5529 (
5530 format!("{header}{sha1} refs/heads/main\ngarbage\n"),
5531 HashAlgo::Sha1,
5532 ),
5533 (
5534 format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
5535 HashAlgo::Sha1,
5536 ),
5537 (
5538 format!("{header}{sha1} refs/heads/main\n#\n"),
5539 HashAlgo::Sha1,
5540 ),
5541 (
5542 format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
5543 HashAlgo::Sha256,
5544 ),
5545 (format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
5546 ] {
5547 assert!(
5548 matches!(
5549 view(body.clone(), algo),
5550 Err(Error::PackedRefsUnexpectedLine { .. })
5551 ),
5552 "{algo:?}: {body:?}"
5553 );
5554 }
5555 for (body, algo) in [
5556 (header.to_owned(), HashAlgo::Sha1),
5557 (header.to_owned(), HashAlgo::Sha256),
5558 ("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
5559 (
5560 format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
5561 HashAlgo::Sha1,
5562 ),
5563 (
5564 format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
5565 HashAlgo::Sha1,
5566 ),
5567 (
5568 format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
5569 HashAlgo::Sha256,
5570 ),
5571 ] {
5572 assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
5573 }
5574 }
5575
5576 #[test]
5577 fn sha256_short_last_record_fails_branch_listing() {
5578 let dir = tempdir().unwrap();
5579 let path = store::paths::packed_refs_path(dir.path());
5580 let sha256 = "2".repeat(64);
5581 let body = format!(
5582 "# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
5583 "x".repeat(49)
5584 );
5585 assert!(
5586 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
5587 );
5588 }
5589
5590 #[test]
5591 fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
5592 let dir = tempdir().unwrap();
5593 let path = store::paths::packed_refs_path(dir.path());
5594 let oid = "1111111111111111111111111111111111111111";
5595 let header = "# pack-refs with: peeled fully-peeled sorted\n";
5596 let view = |body: String| {
5597 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
5598 };
5599
5600 let garbage_after_first_branch = view(format!(
5601 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
5602 {oid} refs/tags/t1\n"
5603 ));
5604 assert!(matches!(
5605 garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
5606 Err(Error::PackedRefsUnexpectedLine { .. })
5607 ));
5608 let tags = garbage_after_first_branch
5609 .map_for_prefix(&path, Some("refs/tags"))
5610 .unwrap();
5611 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
5612
5613 let garbage_before_range = view(format!(
5614 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
5615 ));
5616 for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
5617 assert!(
5618 garbage_before_range
5619 .map_for_prefix(&path, Some(prefix))
5620 .is_err(),
5621 "{prefix}"
5622 );
5623 }
5624 assert_eq!(
5625 garbage_before_range
5626 .map_for_prefix(&path, Some("refs/zz"))
5627 .unwrap()
5628 .len(),
5629 0
5630 );
5631
5632 let peeled_tag_before_range = view(format!(
5633 "{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
5634 ));
5635 let orphan_peel_before_range = view(format!(
5636 "{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
5637 ));
5638 assert!(orphan_peel_before_range
5639 .map_for_prefix(&path, Some("refs/tags"))
5640 .is_err());
5641 assert_eq!(
5642 peeled_tag_before_range
5643 .map_for_prefix(&path, Some("refs/tags"))
5644 .unwrap()
5645 .len(),
5646 1
5647 );
5648 }
5649
5650 fn lying_and_unsorted_views(path: &Path) -> (PackedRefsFileView, PackedRefsFileView) {
5652 let oid = "1111111111111111111111111111111111111111";
5653 let records = format!("{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n");
5654 let view_of = |header: &str| {
5655 PackedRefsFileView::from_bytes(
5656 path,
5657 format!("{header}{records}").into_bytes(),
5658 HashAlgo::Sha1,
5659 )
5660 .unwrap()
5661 };
5662 (
5663 view_of("# pack-refs with: peeled fully-peeled sorted\n"),
5664 view_of("# pack-refs with: peeled fully-peeled\n"),
5665 )
5666 }
5667
5668 fn sorted_names(map: HashMap<String, ObjectId>) -> Vec<String> {
5669 let mut names: Vec<String> = map.into_keys().collect();
5670 names.sort();
5671 names
5672 }
5673
5674 #[test]
5675 fn full_scans_of_a_lying_sorted_file_return_every_ref() {
5676 let dir = tempdir().unwrap();
5677 let path = store::paths::packed_refs_path(dir.path());
5678 let (lying, unsorted) = lying_and_unsorted_views(&path);
5679 assert!(lying.sorted);
5680 let every = ["refs/heads/a", "refs/heads/b", "refs/tags/t"];
5681 for view in [&lying, &unsorted] {
5682 for prefix in [None, Some("refs"), Some("refs/")] {
5683 assert_eq!(
5684 sorted_names(view.map_for_prefix(&path, prefix).unwrap()),
5685 every,
5686 "{prefix:?}"
5687 );
5688 }
5689 }
5690 fs::write(&path, lying.data.as_ref() as &[u8]).unwrap();
5691 let loaded = PackedRefs::load(dir.path()).unwrap();
5692 for name in every {
5693 assert!(loaded.get(name).is_some(), "{name}");
5694 }
5695 }
5696
5697 #[test]
5698 fn prefix_scan_over_an_out_of_order_range_falls_back_to_the_whole_file() {
5699 let dir = tempdir().unwrap();
5700 let path = store::paths::packed_refs_path(dir.path());
5701 let (lying, _) = lying_and_unsorted_views(&path);
5702 let heads = lying.map_for_prefix(&path, Some("refs/heads")).unwrap();
5703 assert_eq!(sorted_names(heads), ["refs/heads/a", "refs/heads/b"]);
5704
5705 let oid = "1111111111111111111111111111111111111111";
5707 let view_of = |names: &[&str]| {
5708 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
5709 for name in names {
5710 body.push_str(&format!("{oid} {name}\n"));
5711 }
5712 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
5713 };
5714 let interleaved = view_of(&["refs/heads/a", "refs/tags/t", "refs/heads/b", "refs/tags/u"]);
5715 let (begin, end) = interleaved.prefix_region("refs/heads").unwrap();
5716 let (_, in_order) = interleaved
5717 .parse_region::<HashMap<_, _>>(&path, begin, end, Some("refs/heads"), None)
5718 .unwrap();
5719 assert!(!in_order);
5720 assert_eq!(
5721 sorted_names(
5722 interleaved
5723 .map_for_prefix(&path, Some("refs/heads"))
5724 .unwrap()
5725 ),
5726 ["refs/heads/a", "refs/heads/b"]
5727 );
5728
5729 let bad_neighbour = view_of(&["refs/heads/x", "refs/heads/a"]);
5732 assert_eq!(bad_neighbour.prefix_region("refs/heads/a"), None);
5733 assert_eq!(
5734 sorted_names(
5735 bad_neighbour
5736 .map_for_prefix(&path, Some("refs/heads/a"))
5737 .unwrap()
5738 ),
5739 ["refs/heads/a"]
5740 );
5741
5742 let corrupt = PackedRefsFileView::from_bytes(
5744 &path,
5745 format!(
5746 "# pack-refs with: peeled fully-peeled sorted\n\
5747 {oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/bad..name\n"
5748 )
5749 .into_bytes(),
5750 HashAlgo::Sha1,
5751 )
5752 .unwrap();
5753 assert!(matches!(
5754 corrupt.map_for_prefix(&path, Some("refs/heads")),
5755 Err(Error::PackedRefsUnexpectedLine { .. })
5756 ));
5757 }
5758
5759 #[test]
5760 fn full_map_keeps_every_name_and_the_last_duplicate_value() {
5761 let dir = tempdir().unwrap();
5762 let path = store::paths::packed_refs_path(dir.path());
5763 let first = "1111111111111111111111111111111111111111";
5764 let last = "2222222222222222222222222222222222222222";
5765 for header in ["# pack-refs with: sorted\n", "# pack-refs with: peeled\n"] {
5766 for names in [
5767 ["refs/heads/a", "refs/heads/b", "refs/heads/b"],
5768 ["refs/heads/b", "refs/heads/a", "refs/heads/b"],
5769 ] {
5770 let body = format!(
5771 "{header}{first} {}\n{first} {}\n{last} {}\n",
5772 names[0], names[1], names[2]
5773 );
5774 let view = PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1)
5775 .unwrap();
5776 let full = view.full_map(&path).unwrap();
5777 assert_eq!(full.len(), 2);
5778 assert_eq!(full["refs/heads/b"], last.parse().unwrap());
5779 assert_eq!(
5780 full.into_iter().collect::<HashMap<_, _>>(),
5781 view.map_for_prefix(&path, None).unwrap()
5782 );
5783 }
5784 }
5785 }
5786
5787 #[test]
5788 fn prefix_scan_validates_both_neighbouring_records_and_their_peels() {
5789 let dir = tempdir().unwrap();
5790 let path = store::paths::packed_refs_path(dir.path());
5791 let oid = "1111111111111111111111111111111111111111";
5792 let header = "# pack-refs with: peeled fully-peeled sorted\n";
5793 for neighbour in ["refs/heads/a", "refs/tags/z"] {
5794 for corrupt in [
5795 format!("{oid} {neighbour}..bad\n"),
5796 format!("{} {neighbour}\n", "g".repeat(40)),
5797 format!("{oid} {neighbour}\n^{}\n", "g".repeat(40)),
5798 ] {
5799 let records = if neighbour.starts_with("refs/heads/") {
5800 format!("{corrupt}{oid} refs/heads/m\n{oid} refs/tags/z\n")
5801 } else {
5802 format!("{oid} refs/heads/a\n{oid} refs/heads/m\n{corrupt}")
5803 };
5804 let view = PackedRefsFileView::from_bytes(
5805 &path,
5806 format!("{header}{records}").into_bytes(),
5807 HashAlgo::Sha1,
5808 )
5809 .unwrap();
5810 assert!(
5811 matches!(
5812 view.map_for_prefix(&path, Some("refs/heads/m")),
5813 Err(Error::PackedRefsUnexpectedLine { .. })
5814 ),
5815 "{records}"
5816 );
5817 }
5818 }
5819 }
5820
5821 #[test]
5824 fn point_lookup_in_a_lying_sorted_file_behaves_like_git() {
5825 let dir = tempdir().unwrap();
5826 let path = store::paths::packed_refs_path(dir.path());
5827 let (lying, unsorted) = lying_and_unsorted_views(&path);
5828 assert!(lying.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
5830 assert_eq!(lying.lookup_oid(&path, "refs/heads/b").unwrap(), None);
5831 assert!(lying.lookup_oid(&path, "refs/tags/t").unwrap().is_some());
5832
5833 assert!(!unsorted.sorted);
5834 for name in ["refs/heads/a", "refs/heads/b", "refs/tags/t"] {
5835 assert!(
5836 unsorted.lookup_oid(&path, name).unwrap().is_some(),
5837 "{name}"
5838 );
5839 }
5840 }
5841
5842 #[test]
5843 fn records_are_only_record_shaped_lines() {
5844 let dir = tempdir().unwrap();
5845 let path = store::paths::packed_refs_path(dir.path());
5846 let view = |data: &[u8]| {
5847 PackedRefsFileView::from_bytes(&path, data.to_vec(), HashAlgo::Sha1).unwrap()
5848 };
5849 let hex = "1".repeat(40);
5850 let data = format!("# header\n{hex} x\n^peel\n\n{hex} y\n#c\nshort\n{hex}z\n{hex} z\n");
5851 let starts: Vec<usize> = view(data.as_bytes()).records().collect();
5852 assert_eq!(starts, [9, 59, 153]);
5853 assert_eq!(view(b"").records().count(), 0);
5854 let out_of_order = format!("{hex} b\n{hex} a\n");
5855 assert_eq!(
5856 view(out_of_order.as_bytes()).records().collect::<Vec<_>>(),
5857 [0, 43]
5858 );
5859 }
5860
5861 #[test]
5862 fn parse_rejects_orphan_peel_and_double_space() {
5863 let dir = tempdir().unwrap();
5864 let path = store::paths::packed_refs_path(dir.path());
5865 fs::write(
5866 &path,
5867 "# pack-refs with: peeled\n\
5868 ^0000000000000000000000000000000000000000\n\
5869 0000000000000000000000000000000000000000 refs/heads/main\n",
5870 )
5871 .unwrap();
5872 let data = fs::read(&path).unwrap();
5873 assert!(matches!(
5874 parse_packed_refs_to_map(&path, &data),
5875 Err(Error::PackedRefsUnexpectedLine { .. })
5876 ));
5877
5878 fs::write(
5879 &path,
5880 "# pack-refs with: peeled\n\
5881 0000000000000000000000000000000000000000 refs/heads/main\n",
5882 )
5883 .unwrap();
5884 let data = fs::read(&path).unwrap();
5885 assert!(matches!(
5886 parse_packed_refs_to_map(&path, &data),
5887 Err(Error::PackedRefsUnexpectedLine { .. })
5888 ));
5889 }
5890}
5891
5892#[cfg(test)]
5893mod pack_refs_tests {
5894 use super::*;
5895 use crate::objects::{serialize_commit, CommitData, ObjectKind};
5896 use crate::repo::{init_repository, Repository};
5897 use std::fs;
5898 use std::sync::Arc;
5899
5900 fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
5901 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
5902 let raw = serialize_commit(&CommitData {
5903 tree,
5904 parents,
5905 author: "T <t@example.com> 1 +0000".into(),
5906 committer: "T <t@example.com> 1 +0000".into(),
5907 author_raw: Vec::new(),
5908 committer_raw: Vec::new(),
5909 encoding: None,
5910 message: "m\n".into(),
5911 raw_message: None,
5912 extra_headers: Vec::new(),
5913 });
5914 repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
5915 }
5916
5917 #[test]
5918 fn pack_refs_skips_concurrent_loose_update() {
5919 let dir = tempfile::tempdir().expect("tempdir");
5920 let repo = init_repository(
5921 dir.path(),
5922 false,
5923 "main",
5924 None,
5925 crate::RefStorageFormat::Files,
5926 )
5927 .expect("init");
5928 let git_dir = repo.git_dir().to_path_buf();
5929 let c1 = write_commit(&repo, vec![]);
5930 let c2 = write_commit(&repo, vec![c1]);
5931 write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
5932
5933 let git_dir_hook = git_dir.clone();
5934 let c2 = Arc::new(c2);
5935 let c2_hook = Arc::clone(&c2);
5936 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
5937 write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
5938 })));
5939
5940 let (_path, packed, pruned) =
5941 pack_refs_files(&git_dir, repo.odb(), true, false).expect("pack");
5942 pack_refs_test_hooks::set_before_prune(None);
5943 assert!(packed >= 1);
5944 assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
5945
5946 let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
5947 assert_eq!(resolved, *c2);
5948 let packed_body =
5949 fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
5950 assert!(
5951 !packed_body.contains("refs/heads/race"),
5952 "packed entry for concurrently updated ref must be removed"
5953 );
5954 }
5955
5956 #[test]
5957 fn clone_packing_retains_symbolic_refs_and_concurrent_tag_updates() {
5958 let dir = tempfile::tempdir().expect("tempdir");
5959 let repo = init_repository(
5960 dir.path(),
5961 false,
5962 "main",
5963 None,
5964 crate::RefStorageFormat::Files,
5965 )
5966 .expect("init");
5967 let git_dir = repo.git_dir().to_path_buf();
5968 let first = write_commit(&repo, vec![]);
5969 let second = write_commit(&repo, vec![first]);
5970 write_ref(&git_dir, "refs/tags/race", &first).expect("tag");
5971 write_ref(&git_dir, "refs/heads/main", &first).expect("branch");
5972 write_ref(&git_dir, "refs/remotes/origin/main", &first).expect("remote");
5973 write_symbolic_ref(
5974 &git_dir,
5975 "refs/remotes/origin/HEAD",
5976 "refs/remotes/origin/main",
5977 )
5978 .expect("symbolic");
5979 let tag = repo
5980 .odb()
5981 .write(
5982 ObjectKind::Tag,
5983 format!("object {first}\ntype commit\n\nremote tag\n").as_bytes(),
5984 )
5985 .expect("tag object");
5986 write_ref(&git_dir, "refs/remotes/origin/tag", &tag).expect("tagged remote");
5987 let hook_dir = git_dir.clone();
5988 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
5989 write_ref(&hook_dir, "refs/tags/race", &second).expect("concurrent update");
5990 })));
5991 pack_remote_tracking_refs_for_clone(&git_dir, "origin").expect("pack");
5992 assert_eq!(
5993 resolve_ref(&git_dir, "refs/tags/race").expect("resolve"),
5994 second
5995 );
5996 assert!(git_dir.join("refs/heads/main").exists());
5997 assert!(git_dir.join("refs/remotes/origin/HEAD").exists());
5998 assert!(!git_dir.join("refs/remotes/origin/main").exists());
5999 let body = fs::read_to_string(store::paths::packed_refs_path(&git_dir)).expect("packed");
6000 assert!(!body.contains("refs/tags/race"));
6001 assert!(!body.contains("refs/remotes/origin/HEAD"));
6002 assert!(
6003 body.contains(&format!("{tag} refs/remotes/origin/tag\n^{first}\n")),
6004 "{body}"
6005 );
6006 }
6007
6008 #[test]
6009 fn clone_packing_routes_reftable_without_creating_packed_refs() {
6010 let dir = tempfile::tempdir().expect("tempdir");
6011 let repo = init_repository(
6012 dir.path(),
6013 false,
6014 "main",
6015 None,
6016 crate::RefStorageFormat::Reftable,
6017 )
6018 .expect("init");
6019 let commit = write_commit(&repo, vec![]);
6020 write_ref(repo.git_dir(), "refs/tags/tip", &commit).expect("tag");
6021 pack_remote_tracking_refs_for_clone(repo.git_dir(), "origin").expect("pack");
6022 assert_eq!(
6023 resolve_ref(repo.git_dir(), "refs/tags/tip").expect("resolve"),
6024 commit
6025 );
6026 assert!(!store::paths::packed_refs_path(repo.git_dir()).exists());
6027 }
6028
6029 #[test]
6030 fn pack_refs_nested_tag_peel_line_points_at_commit() {
6031 let dir = tempfile::tempdir().expect("tempdir");
6032 let repo = init_repository(
6033 dir.path(),
6034 false,
6035 "main",
6036 None,
6037 crate::RefStorageFormat::Files,
6038 )
6039 .expect("init");
6040 let commit = write_commit(&repo, vec![]);
6041 let inner_tag_body = format!(
6042 "object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
6043 commit.to_hex()
6044 );
6045 let inner = repo
6046 .odb()
6047 .write(ObjectKind::Tag, inner_tag_body.as_bytes())
6048 .expect("inner tag");
6049 let outer_tag_body = format!(
6050 "object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
6051 inner.to_hex()
6052 );
6053 let outer = repo
6054 .odb()
6055 .write(ObjectKind::Tag, outer_tag_body.as_bytes())
6056 .expect("outer tag");
6057 write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
6058
6059 pack_refs_files(repo.git_dir(), repo.odb(), false, false).expect("pack");
6060 let body =
6061 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
6062 let mut lines = body.lines();
6063 let tag_line = lines
6064 .find(|l| l.contains("refs/tags/outer"))
6065 .expect("tag line");
6066 assert!(tag_line.starts_with(&outer.to_hex()));
6067 let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
6068 assert!(
6069 peel_line.contains(&commit.to_hex()),
6070 "peel must reach commit through nested tags, got {peel_line}"
6071 );
6072 }
6073
6074 #[test]
6075 fn pack_refs_tree_target_tag_has_peel_line() {
6076 let dir = tempfile::tempdir().expect("tempdir");
6077 let repo = init_repository(
6078 dir.path(),
6079 false,
6080 "main",
6081 None,
6082 crate::RefStorageFormat::Files,
6083 )
6084 .expect("init");
6085 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
6086 let tag_body = format!(
6087 "object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
6088 tree.to_hex()
6089 );
6090 let tag = repo
6091 .odb()
6092 .write(ObjectKind::Tag, tag_body.as_bytes())
6093 .expect("tag");
6094 write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
6095
6096 pack_refs_files(repo.git_dir(), repo.odb(), true, false).expect("pack");
6097 let body =
6098 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
6099 assert!(body.contains("refs/tags/tree-tag"));
6100 assert!(
6101 body.contains(&format!("^{}", tree.to_hex())),
6102 "expected peel to tree: {body}"
6103 );
6104 }
6105
6106 #[test]
6107 fn write_packed_refs_file_survives_publish_failure() {
6108 let dir = tempfile::tempdir().expect("tempdir");
6109 let git_dir = dir.path();
6110 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
6111 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
6112 fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
6113 fs::create_dir_all(store::paths::packed_refs_path(git_dir))
6114 .expect("block packed-refs file");
6115 let err = write_packed_refs_file(
6116 git_dir,
6117 &[PackedRefRecord {
6118 refname: "refs/heads/keep".into(),
6119 oid,
6120 peeled: None,
6121 }],
6122 )
6123 .expect_err("publish should fail");
6124 assert!(matches!(err, Error::Io(_)));
6125 assert!(git_dir.join("refs/heads/keep").is_file());
6126 }
6127
6128 #[test]
6129 fn write_packed_refs_file_replaces_loose_and_sorts() {
6130 let dir = tempfile::tempdir().expect("tempdir");
6131 let git_dir = dir.path();
6132 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
6133 let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
6134 let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
6135 fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
6136 write_packed_refs_file(
6137 git_dir,
6138 &[
6139 PackedRefRecord {
6140 refname: "refs/heads/z-last".into(),
6141 oid: oid_b,
6142 peeled: None,
6143 },
6144 PackedRefRecord {
6145 refname: "refs/heads/a-first".into(),
6146 oid: oid_a,
6147 peeled: None,
6148 },
6149 ],
6150 )
6151 .expect("write packed-refs");
6152 assert!(!git_dir.join("refs/heads/a-first").exists());
6153 let body =
6154 fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
6155 assert!(body.contains("refs/heads/a-first"));
6156 assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
6157 }
6158}
6159
6160#[cfg(test)]
6161mod packed_ref_peels_tests {
6162 use super::*;
6163
6164 fn oid(n: u8) -> ObjectId {
6165 ObjectId::from_bytes(&[n; 20]).expect("oid")
6166 }
6167
6168 fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
6169 let dir = tempfile::tempdir().expect("tempdir");
6170 fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
6171 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
6172 (dir, peels)
6173 }
6174
6175 fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
6176 let mut out = String::from(header);
6177 for (name, oid, peel) in records {
6178 out.push_str(&format!("{oid} {name}\n"));
6179 if let Some(peel) = peel {
6180 out.push_str(&format!("^{peel}\n"));
6181 }
6182 }
6183 out
6184 }
6185
6186 #[test]
6187 fn fully_peeled_file_answers_for_every_ref() {
6188 for header in [
6189 "# pack-refs with: peeled fully-peeled sorted \n",
6190 "# pack-refs with: peeled fully-peeled \n",
6191 ] {
6192 let records = [
6193 ("refs/heads/main", oid(1), None),
6194 ("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
6195 ("refs/tags/v1", oid(4), Some(oid(1))),
6196 ("refs/tags/light", oid(1), None),
6197 ];
6198 let mut ordered = records;
6200 if header.contains(" sorted") {
6201 ordered.sort_by_key(|(name, _, _)| *name);
6202 } else {
6203 ordered.reverse();
6204 }
6205 let (_dir, peels) = peels_for(&body(header, &ordered));
6206 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
6207 assert_eq!(
6208 peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
6209 Some(Some(oid(3)))
6210 );
6211 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
6212 assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
6213 assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
6215 assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
6216 }
6217 }
6218
6219 #[test]
6220 fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
6221 let records = [
6222 ("refs/heads/main", oid(1), None),
6223 ("refs/tags/v1", oid(4), Some(oid(1))),
6224 ];
6225 let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
6226 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
6227 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
6228
6229 for header in [
6230 "",
6231 "# pack-refs with: sorted\n",
6232 "# some comment\n",
6233 "#pack-refs with: peeled fully-peeled\n",
6234 "# pack-refs with: peeled fully-peeled\n",
6235 ] {
6236 let (_dir, peels) = peels_for(&body(header, &records));
6237 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
6238 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
6239 }
6240 }
6241
6242 #[test]
6243 fn missing_file_and_other_backends_know_nothing() {
6244 let dir = tempfile::tempdir().expect("tempdir");
6245 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
6246 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
6247
6248 let records = [("refs/heads/main", oid(1), None)];
6249 fs::write(
6250 store::paths::packed_refs_path(dir.path()),
6251 body("# pack-refs with: peeled fully-peeled sorted\n", &records),
6252 )
6253 .expect("packed-refs");
6254 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
6255 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
6256 }
6257
6258 #[test]
6259 fn lookups_in_any_order_agree_with_a_fresh_snapshot() {
6260 let records = [
6261 ("refs/heads/main", oid(1), None),
6262 ("refs/tags/a", oid(2), Some(oid(1))),
6263 ("refs/tags/b", oid(1), None),
6264 ("refs/tags/c", oid(3), Some(oid(4))),
6265 ("refs/tags/d", oid(5), None),
6266 ];
6267 let text = body("# pack-refs with: peeled fully-peeled sorted \n", &records);
6268 let (_dir, peels) = peels_for(&text);
6269 let fresh = |name: &str, value: &ObjectId| peels_for(&text).1.peeled(name, value);
6270 let queries = [
6271 ("refs/tags/a", oid(2)),
6272 ("refs/tags/b", oid(1)),
6273 ("refs/tags/b", oid(1)),
6274 ("refs/tags/bb", oid(1)),
6275 ("refs/tags/c", oid(3)),
6276 ("refs/tags/d", oid(9)),
6277 ("refs/tags/d", oid(5)),
6278 ("refs/heads/main", oid(1)),
6279 ("refs/tags/c", oid(3)),
6280 ("refs/tags/a", oid(2)),
6281 ("refs/tags/zzz", oid(1)),
6282 ("refs/tags/a", oid(2)),
6283 ("refs/tags/b", oid(1)),
6284 ];
6285 for (name, value) in queries {
6286 assert_eq!(
6287 peels.peeled(name, &value),
6288 fresh(name, &value),
6289 "{name} {value}"
6290 );
6291 }
6292 assert_eq!(peels.peeled("refs/tags/c", &oid(3)), Some(Some(oid(4))));
6293 assert_eq!(peels.peeled("refs/tags/d", &oid(5)), Some(None));
6294 }
6295
6296 #[test]
6297 fn store_snapshot_reuses_the_files_store_view_and_follows_rewrites() {
6298 let dir = tempfile::tempdir().expect("tempdir");
6299 let path = store::paths::packed_refs_path(dir.path());
6300 let header = "# pack-refs with: peeled fully-peeled sorted \n";
6301 let records = [
6302 ("refs/heads/main", oid(1), None),
6303 ("refs/tags/v1", oid(2), Some(oid(1))),
6304 ("refs/tags/v2", oid(3), None),
6305 ];
6306 fs::write(&path, body(header, &records)).expect("packed-refs");
6307 let files = store::FilesRefStore::from_git_dir(dir.path()).expect("files store");
6308
6309 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
6310 let loaded = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
6311 for (name, value, _) in records {
6312 assert_eq!(
6313 peels.peeled(name, &value),
6314 loaded.peeled(name, &value),
6315 "{name}"
6316 );
6317 }
6318 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), Some(Some(oid(1))));
6319 let cached = files
6320 .packed_refs_view()
6321 .expect("view")
6322 .expect("packed-refs");
6323 let shared = peels.view.as_ref().expect("snapshot view");
6324 assert!(std::sync::Arc::ptr_eq(&cached.data, &shared.data));
6325
6326 let rewritten = [
6327 ("refs/heads/main", oid(1), None),
6328 ("refs/tags/v1", oid(4), Some(oid(5))),
6329 ("refs/tags/v2", oid(3), None),
6330 ("refs/tags/v3", oid(6), Some(oid(1))),
6331 ];
6332 fs::write(&path, body(header, &rewritten)).expect("rewrite packed-refs");
6333 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
6334 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), None);
6335 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(5))));
6336 assert_eq!(peels.peeled("refs/tags/v3", &oid(6)), Some(Some(oid(1))));
6337
6338 let memory = store::MemoryRefStore::new();
6339 let fallback = PackedRefPeels::for_store(&memory, dir.path()).expect("fallback");
6340 let direct = PackedRefPeels::load(dir.path(), memory.format()).expect("load");
6341 for (name, value, _) in rewritten {
6342 assert_eq!(
6343 fallback.peeled(name, &value),
6344 direct.peeled(name, &value),
6345 "{name}"
6346 );
6347 }
6348 }
6349
6350 fn raw_view(data: &[u8], hex_len: usize) -> PackedRefsFileView {
6352 PackedRefsFileView {
6353 data: crate::pack_map::PackData::from_owned(data.to_vec()),
6354 sorted: true,
6355 hex_len,
6356 body_start: 0,
6357 peel_trait: PackedPeelTrait::None,
6358 }
6359 }
6360
6361 fn naive_record_starts(data: &[u8], hex_len: usize) -> Vec<usize> {
6363 let mut out = Vec::new();
6364 let mut start = 0;
6365 for line in data.split(|&b| b == b'\n') {
6366 if start >= data.len() {
6367 break;
6368 }
6369 if packed_line_is_record(line, hex_len) {
6370 out.push(start);
6371 }
6372 start += line.len() + 1;
6373 }
6374 out
6375 }
6376
6377 fn assert_record_walks_match(data: &[u8], hex_len: usize) {
6378 let view = raw_view(data, hex_len);
6379 let want = naive_record_starts(data, hex_len);
6380 assert_eq!(
6381 view.records().collect::<Vec<_>>(),
6382 want,
6383 "{:?} hex_len={hex_len}",
6384 String::from_utf8_lossy(data)
6385 );
6386 let mut line_starts: Vec<usize> = memchr::memchr_iter(b'\n', data)
6387 .map(|nl| nl + 1)
6388 .filter(|&s| s < data.len())
6389 .collect();
6390 line_starts.insert(0, 0);
6391 if data.ends_with(b"\n") {
6393 line_starts.push(data.len());
6394 }
6395 for before in line_starts {
6396 let prev = want.iter().copied().filter(|&s| s < before).last();
6397 assert_eq!(view.prev_record(before), prev, "{data:?} before={before}");
6398 }
6399 }
6400
6401 fn xorshift(state: &mut u64) -> u64 {
6402 *state ^= *state << 13;
6403 *state ^= *state >> 7;
6404 *state ^= *state << 17;
6405 *state
6406 }
6407
6408 #[test]
6409 fn record_walks_match_a_line_by_line_scan() {
6410 let hex = oid(1).to_hex();
6411 let inputs = [
6412 String::new(),
6413 "\n".to_owned(),
6414 format!("# pack-refs with: peeled \n{hex} refs/heads/a\n^{hex}\n{hex} refs/tags/b\n"),
6415 format!("{hex} refs/heads/z\n{hex} refs/heads/a\n"),
6416 format!("{hex} refs/heads/a\n\n{hex} refs/heads/b"),
6417 format!("{hex}\n{hex} \n{hex} x\nshort\n#{hex} refs/heads/c\n"),
6418 format!("{hex} refs/tags/a\n^{hex}\n^{hex}\n{hex} refs/tags/b\n^{hex}"),
6419 format!("^ab\n{}\n{hex} refs/heads/a\n", "x".repeat(37)),
6420 format!("^\n{hex} refs/heads/a\n^{hex}x\n{hex} refs/heads/b\n"),
6421 format!("^{hex}\n"),
6422 format!("^{}", &hex[1..]),
6423 ];
6424 for input in &inputs {
6425 assert_record_walks_match(input.as_bytes(), 40);
6426 }
6427 }
6428
6429 #[test]
6430 fn record_walks_match_a_line_by_line_scan_on_random_bytes() {
6431 let alphabet = b"^\n #ab";
6432 let mut state = 0x2545_f491_4f6c_dd1du64;
6433 for len in 0..400 {
6434 let input: Vec<u8> = (0..len % 97)
6435 .map(|_| alphabet[(xorshift(&mut state) % alphabet.len() as u64) as usize])
6436 .collect();
6437 for hex_len in [1, 2, 3] {
6438 assert_record_walks_match(&input, hex_len);
6439 }
6440 }
6441 }
6442
6443 #[test]
6446 fn search_matches_a_linear_scan_on_sorted_records() {
6447 let hex_len = 2;
6448 let mut state = 0x9e37_79b9_7f4a_7c15u64;
6449 for _ in 0..300 {
6450 let mut names: Vec<String> = (0..xorshift(&mut state) % 12)
6451 .map(|_| format!("n{}", xorshift(&mut state) % 40))
6452 .collect();
6453 names.sort();
6454 names.dedup();
6455 let mut data = b"# h\n".to_vec();
6456 for name in &names {
6457 data.extend_from_slice(format!("ab {name}\n").as_bytes());
6458 match xorshift(&mut state) % 4 {
6459 0 => data.extend_from_slice(b"^ab\n"),
6460 1 => data.extend_from_slice(b"junk\n"),
6461 _ => {}
6462 }
6463 }
6464 let mut view = raw_view(&data, hex_len);
6465 view.body_start = 4;
6466 let records: Vec<usize> = view.records().collect();
6467 for probe in 0..42 {
6468 let target = format!("n{probe}");
6469 let want = records
6470 .iter()
6471 .copied()
6472 .find(|&s| view.record_cmp(s, &target).is_ge())
6473 .unwrap_or(data.len());
6474 assert_eq!(view.lower_bound(&target), want, "{target} in {data:?}");
6475 let hit = view.search(&target).ok();
6476 assert_eq!(
6477 hit,
6478 names.contains(&target).then_some(want),
6479 "{target} in {data:?}"
6480 );
6481 }
6482 }
6483 }
6484}