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 std::collections::{BTreeSet, HashMap, HashSet};
57use std::fs;
58use std::io;
59use std::ops::ControlFlow;
60use std::path::{Path, PathBuf};
61use store::{
62 open_ref_store, Expected, RawRef as StoreRawRef, RefCaseOnlyRename, RefEntry, RefReflogRename,
63 RefStorageFormat, RefStore, RefTransaction, RefUpdate, RefUpdateFlags, ReflogUpdate,
64};
65
66use crate::check_ref_format::{check_refname_format, RefNameOptions};
67use crate::config::ConfigSet;
68use crate::error::{Error, RefLockError, Result};
69use crate::ident::Signature;
70use crate::objects::{HashAlgo, ObjectId};
71use crate::pack;
72use store::format_reflog_identity;
73
74pub const SYMREF_MAXDEPTH: usize = 5;
76
77fn map_public_ref_store_error(err: Error, refname: &str) -> Error {
78 match err {
79 Error::RefStore(store::RefStoreError::SymrefLoop) => {
80 Error::InvalidRef(format!("ref symlink too deep: {refname}"))
81 }
82 other => other,
83 }
84}
85
86fn files_legacy_write_precheck(git_dir: &Path, refname: &str) -> Result<()> {
90 let storage_dir = ref_storage_dir(git_dir, refname);
91 let stor = crate::ref_namespace::storage_ref_name(refname);
92 let path = storage_dir.join(stor);
93 remove_empty_ref_directory(&path);
94 if fs::symlink_metadata(&path)
95 .map(|m| m.file_type().is_dir())
96 .unwrap_or(false)
97 {
98 let display = ref_path_for_display(&path);
99 return Err(crate::error::RefLockError::DirectoryInTheWay {
100 refname: refname.to_owned(),
101 path: display,
102 }
103 .into());
104 }
105 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
106 return Err(Error::InvalidRef(format!(
107 "cannot update ref '{refname}': reference broken"
108 )));
109 }
110 Ok(())
111}
112
113fn legacy_ref_update_flags(refname: &str) -> RefUpdateFlags {
114 let mut flags = RefUpdateFlags::default();
115 if refname == "HEAD" {
117 flags.no_deref = true;
118 }
119 flags
120}
121
122pub(crate) fn commit_store_update(git_dir: &Path, update: RefUpdate) -> Result<()> {
123 let store = open_ref_store(git_dir)?;
124 commit_ref_update_on_store(store.as_ref(), update)
125}
126
127pub fn commit_ref_update_on_store(store: &dyn RefStore, update: RefUpdate) -> Result<()> {
134 if let Some(ref log) = update.reflog {
135 validate_reflog_update(log)?;
136 }
137 let refname = update.name.clone();
138 let txn = RefTransaction::new()
139 .update(update)
140 .map_err(crate::error::Error::from)?;
141 let prepared = store
142 .prepare(txn)
143 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))?;
144 prepared
145 .commit()
146 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))
147}
148
149pub fn update_direct_ref_on_store(
157 store: &dyn RefStore,
158 git_dir: &Path,
159 refname: &str,
160 oid: &ObjectId,
161) -> Result<()> {
162 store_write_value_on_store(
163 store,
164 git_dir,
165 refname,
166 Some(StoreRawRef::Direct(*oid)),
167 Expected::Any,
168 None,
169 )
170}
171
172pub(crate) fn store_write_value_on_store(
173 store: &dyn RefStore,
174 git_dir: &Path,
175 refname: &str,
176 new_value: Option<StoreRawRef>,
177 expected: Expected,
178 reflog: Option<ReflogUpdate>,
179) -> Result<()> {
180 ensure_refname_safe_for_storage(refname)?;
181 if new_value.is_some() && store.format() == RefStorageFormat::Files {
182 files_legacy_write_precheck(git_dir, refname)?;
183 }
184 commit_ref_update_on_store(
185 store,
186 RefUpdate {
187 name: refname.to_owned(),
188 new_value,
189 expected,
190 reflog,
191 flags: legacy_ref_update_flags(refname),
192 },
193 )
194}
195
196pub fn commit_ref_store_batch(git_dir: &Path, updates: &[RefUpdate]) -> Result<()> {
205 if updates.is_empty() {
206 return Ok(());
207 }
208 let store = open_ref_store(git_dir)?;
209 commit_ref_updates_on_store(store.as_ref(), updates, &[])
210}
211
212pub fn commit_ref_updates_on_store(
218 store: &dyn RefStore,
219 updates: &[RefUpdate],
220 reflog_renames: &[RefReflogRename],
221) -> Result<()> {
222 commit_ref_batch_on_store(store, updates, reflog_renames, &[])
223}
224
225pub fn commit_ref_batch_on_store(
231 store: &dyn RefStore,
232 updates: &[RefUpdate],
233 reflog_renames: &[RefReflogRename],
234 case_only_renames: &[RefCaseOnlyRename],
235) -> Result<()> {
236 if updates.is_empty() && reflog_renames.is_empty() && case_only_renames.is_empty() {
237 return Ok(());
238 }
239 commit_owned_ref_batch_on_store(
240 store,
241 updates.to_vec(),
242 reflog_renames.to_vec(),
243 case_only_renames.to_vec(),
244 )
245}
246
247pub(crate) fn commit_owned_ref_updates_on_store(
253 store: &dyn RefStore,
254 updates: Vec<RefUpdate>,
255) -> Result<()> {
256 if updates.is_empty() {
257 return Ok(());
258 }
259 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new())
260}
261
262fn commit_owned_ref_batch_on_store(
263 store: &dyn RefStore,
264 updates: Vec<RefUpdate>,
265 reflog_renames: Vec<RefReflogRename>,
266 case_only_renames: Vec<RefCaseOnlyRename>,
267) -> Result<()> {
268 for update in &updates {
269 if let Some(ref log) = update.reflog {
270 validate_reflog_update(log)?;
271 }
272 }
273 for rename in &reflog_renames {
274 validate_reflog_update(&rename.append)?;
275 }
276 let label = updates
277 .first()
278 .map(|u| u.name.clone())
279 .or_else(|| reflog_renames.first().map(|r| r.from.clone()))
280 .or_else(|| case_only_renames.first().map(|r| r.from.clone()))
281 .unwrap_or_else(|| "refs".to_owned());
282 let mut txn = RefTransaction::from_updates(updates).map_err(|e| {
283 let name = match &e {
284 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
285 _ => label.clone(),
286 };
287 map_public_ref_store_error(Error::from(e), &name)
288 })?;
289 for rename in reflog_renames {
290 txn = txn.with_reflog_rename(rename);
291 }
292 for rename in case_only_renames {
293 txn = txn.with_case_only_rename(rename);
294 }
295 let prepared = store
296 .prepare(txn)
297 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
298 prepared
299 .commit()
300 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))
301}
302
303fn store_write_value(
304 git_dir: &Path,
305 refname: &str,
306 new_value: Option<StoreRawRef>,
307 expected: Expected,
308 reflog: Option<ReflogUpdate>,
309) -> Result<()> {
310 let store = open_ref_store(git_dir)?;
311 store_write_value_on_store(
312 store.as_ref(),
313 git_dir,
314 refname,
315 new_value,
316 expected,
317 reflog,
318 )
319}
320
321fn collect_resolved_refs(store: &dyn RefStore, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
322 collect_resolved_ref_entries(store, |f| store.for_each_ref(prefix, f))
323}
324
325fn collect_resolved_ref_entries(
327 store: &dyn RefStore,
328 for_each: impl FnOnce(&mut dyn FnMut(&RefEntry) -> ControlFlow<()>) -> Result<()>,
329) -> Result<Vec<(String, ObjectId)>> {
330 let mut results = Vec::new();
331 for_each(&mut |entry: &RefEntry| {
332 let oid = entry
333 .peeled
334 .or_else(|| match &entry.value {
335 StoreRawRef::Direct(oid) => Some(*oid),
336 StoreRawRef::Symbolic(target) => store.resolve(target).ok(),
337 })
338 .or_else(|| store.resolve(&entry.name).ok());
339 if let Some(oid) = oid {
340 results.push((entry.name.clone(), oid));
341 }
342 ControlFlow::Continue(())
343 })?;
344 Ok(results)
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
349pub enum Ref {
350 Direct(ObjectId),
352 Symbolic(String),
354}
355
356pub fn read_ref_file(path: &Path) -> Result<Ref> {
363 if let Ok(target) = fs::read_link(path) {
364 return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
365 }
366
367 let content = match fs::read_to_string(path) {
368 Ok(c) => c,
369 Err(e)
373 if e.kind() == io::ErrorKind::IsADirectory
374 || e.kind() == io::ErrorKind::NotADirectory
375 || e.raw_os_error() == Some(libc::EISDIR)
376 || e.raw_os_error() == Some(libc::ENOTDIR) =>
377 {
378 return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, e)));
379 }
380 Err(e) => return Err(Error::Io(e)),
381 };
382 let content = content.trim_end_matches('\n');
383 parse_ref_content(content)
384}
385
386pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
388 if let Some(target) = content.strip_prefix("ref: ") {
389 Ok(Ref::Symbolic(target.trim().to_owned()))
390 } else if let Some(token) = content.split_whitespace().next() {
391 if ObjectId::is_full_hex(token) {
392 let oid: ObjectId = token.parse()?;
393 Ok(Ref::Direct(oid))
394 } else if content == "unknown-oid" {
395 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
396 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
397 Ok(Ref::Direct(oid))
398 } else {
399 Err(Error::InvalidRef(content.to_owned()))
400 }
401 } else {
402 Err(Error::InvalidRef(content.to_owned()))
403 }
404}
405
406pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
421 open_ref_store(git_dir)?
422 .resolve(refname)
423 .map_err(|err| map_public_ref_store_error(err, refname))
424}
425
426pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
431 let commondir_file = git_dir.join("commondir");
432 if !commondir_file.is_file() {
433 return None;
434 }
435 let raw = fs::read_to_string(commondir_file).ok()?;
436 let rel = raw.trim();
437 let path = if Path::new(rel).is_absolute() {
440 PathBuf::from(rel)
441 } else {
442 git_dir.join(rel)
443 };
444 path.canonicalize().ok()
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub enum RawRefLookup {
454 Exists,
456 NotFound,
458 IsDirectory,
460}
461
462pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
477 let store = open_ref_store(git_dir)?;
478 match store.read_raw(refname)? {
479 Some(_) => Ok(RawRefLookup::Exists),
480 None => {
481 let (storage_dir, stor_name) =
482 crate::worktree_ref::resolve_ref_storage(git_dir, refname);
483 let path = storage_dir.join(&stor_name);
484 if fs::symlink_metadata(&path)
485 .map(|m| m.is_dir())
486 .unwrap_or(false)
487 {
488 Ok(RawRefLookup::IsDirectory)
489 } else {
490 Ok(RawRefLookup::NotFound)
491 }
492 }
493 }
494}
495
496#[must_use]
498pub fn lock_path_for_ref(path: &Path) -> PathBuf {
499 let mut s = path.as_os_str().to_owned();
500 s.push(".lock");
501 PathBuf::from(s)
502}
503
504fn first_packed_descendant_ref(store: &Path, refname: &str) -> Result<Option<String>> {
506 let leaf_dir = format!("{refname}/");
507 let map = read_packed_refs_map_for_prefix(store, refname)?;
508 let sorted: BTreeSet<String> = map.keys().cloned().collect();
509 Ok(sorted
510 .range(leaf_dir.clone()..)
511 .next()
512 .filter(|name| name.starts_with(&leaf_dir))
513 .cloned())
514}
515
516fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
517 Ok(lookup_packed_ref(git_dir, refname)?.is_some())
518}
519
520pub(crate) fn packed_refname_namespace_conflict(
525 sorted_packed: &BTreeSet<String>,
526 candidate: &str,
527) -> bool {
528 let segments: Vec<&str> = candidate.split('/').filter(|s| !s.is_empty()).collect();
529 if segments.len() > 1 {
530 let mut dirname = String::new();
531 for part in &segments[..segments.len() - 1] {
532 if !dirname.is_empty() {
533 dirname.push('/');
534 }
535 dirname.push_str(part);
536 if sorted_packed.contains(&dirname) {
537 return true;
538 }
539 }
540 }
541
542 let leaf_dir = format!("{candidate}/");
543 if let Some(existing) = sorted_packed.range(leaf_dir.clone()..).next() {
544 if existing.starts_with(&leaf_dir) {
545 return true;
546 }
547 }
548 false
549}
550
551fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
552 Ok(PackedRefs::load(git_dir)?.has_namespace_conflict(refname))
553}
554
555pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
567 if refname == "HEAD" {
568 return Ok(false);
569 }
570 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
571 return Ok(false);
572 }
573 let storage_dir = ref_storage_dir(git_dir, refname);
574 packed_ref_name_exists(&storage_dir, refname)
575}
576
577#[derive(Debug, Clone, PartialEq, Eq)]
579pub enum RefnameUnavailable {
580 AncestorExists {
582 blocking: String,
584 new_ref: String,
586 },
587 DescendantExists {
589 blocking: String,
591 new_ref: String,
593 },
594 SameBatch {
596 refname: String,
598 other: String,
600 },
601}
602
603impl RefnameUnavailable {
604 #[must_use]
606 pub fn lock_message_suffix(&self) -> String {
607 match self {
608 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
609 format!("'{blocking}' exists; cannot create '{new_ref}'")
610 }
611 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
612 format!("'{blocking}' exists; cannot create '{new_ref}'")
613 }
614 RefnameUnavailable::SameBatch { refname, other } => {
615 format!("cannot process '{refname}' and '{other}' at the same time")
616 }
617 }
618 }
619}
620
621fn find_descendant_in_sorted_extras(
622 dirname_with_slash: &str,
623 extras: &BTreeSet<String>,
624) -> Option<String> {
625 let start = extras
626 .range(dirname_with_slash.to_string()..)
627 .next()
628 .cloned()?;
629 if start.starts_with(dirname_with_slash) {
630 Some(start)
631 } else {
632 None
633 }
634}
635
636#[must_use]
640pub fn is_valid_fetch_advertised_ref(name: &str) -> bool {
641 crate::check_ref_format::is_valid_fetch_advertised_ref(name)
642}
643
644#[must_use]
649pub fn is_valid_ls_refs_advertised_name(name: &str) -> bool {
650 name == "HEAD" || is_valid_fetch_advertised_ref(name)
651}
652
653#[must_use]
655pub fn is_valid_storable_ref_name(name: &str) -> bool {
656 is_valid_fetch_advertised_ref(name)
657}
658
659#[must_use]
661pub fn is_valid_advertised_symref_target(name: &str) -> bool {
662 crate::check_ref_format::is_valid_advertised_symref_target(name)
663}
664
665pub fn verify_refname_available_for_create(
678 git_dir: &Path,
679 refname: &str,
680 extras: &BTreeSet<String>,
681 skip: &HashSet<String>,
682) -> std::result::Result<(), RefnameUnavailable> {
683 let git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
686 let mut seen_dirnames: HashSet<String> = HashSet::new();
687 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
688 if segments.len() <= 1 {
689 } else {
691 let mut dirname = String::new();
692 for part in &segments[..segments.len() - 1] {
693 if !dirname.is_empty() {
694 dirname.push('/');
695 }
696 dirname.push_str(part);
697
698 if !seen_dirnames.insert(dirname.clone()) {
699 continue;
700 }
701
702 if skip.contains(&dirname) {
703 continue;
704 }
705
706 match read_raw_ref(&git_dir, &dirname) {
707 Ok(RawRefLookup::Exists) => {
708 return Err(RefnameUnavailable::AncestorExists {
709 blocking: dirname.clone(),
710 new_ref: refname.to_owned(),
711 });
712 }
713 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
716 Err(_) => {}
717 }
718
719 if extras.contains(&dirname) {
720 return Err(RefnameUnavailable::SameBatch {
721 refname: refname.to_owned(),
722 other: dirname.clone(),
723 });
724 }
725 }
726 }
727
728 let mut leaf_dir = String::with_capacity(refname.len() + 1);
729 leaf_dir.push_str(refname);
730 leaf_dir.push('/');
731
732 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
738 let may_have_loose_descendants = packed_dir.join("reftable").is_dir()
739 || git_dir.join(refname).is_dir()
740 || packed_dir.join(refname).is_dir();
741 let under = if may_have_loose_descendants {
742 list_refs(&git_dir, &leaf_dir).unwrap_or_default()
743 } else {
744 Vec::new()
745 };
746 if under.is_empty() {
747 if let Ok(Some(name)) = first_packed_descendant_ref(&packed_dir, refname) {
748 if !skip.contains(&name) {
749 return Err(RefnameUnavailable::DescendantExists {
750 blocking: name,
751 new_ref: refname.to_owned(),
752 });
753 }
754 }
755 if packed_dir != git_dir {
756 if let Ok(Some(name)) = first_packed_descendant_ref(&git_dir, refname) {
757 if !skip.contains(&name) {
758 return Err(RefnameUnavailable::DescendantExists {
759 blocking: name,
760 new_ref: refname.to_owned(),
761 });
762 }
763 }
764 }
765 }
766 if under.is_empty()
767 && fs::symlink_metadata(git_dir.join(refname))
768 .map(|m| m.is_dir())
769 .unwrap_or(false)
770 {
771 let mut blocking: Option<String> = None;
772 let dir_path = git_dir.join(refname);
773 if let Ok(read) = fs::read_dir(&dir_path) {
774 for entry in read.flatten() {
775 let path = entry.path();
776 let Ok(meta) = fs::metadata(&path) else {
777 continue;
778 };
779 if !meta.is_file() {
780 continue;
781 }
782 let name = entry.file_name().to_string_lossy().into_owned();
783 let full = format!("{refname}/{name}");
784 blocking = Some(full);
785 break;
786 }
787 }
788 if let Some(b) = blocking {
789 if !skip.contains(&b) {
790 return Err(RefnameUnavailable::DescendantExists {
791 blocking: b,
792 new_ref: refname.to_owned(),
793 });
794 }
795 }
796 }
797
798 for (existing, _) in under {
799 if skip.contains(&existing) {
800 continue;
801 }
802 return Err(RefnameUnavailable::DescendantExists {
803 blocking: existing,
804 new_ref: refname.to_owned(),
805 });
806 }
807
808 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
809 if !skip.contains(&extra) {
810 return Err(RefnameUnavailable::SameBatch {
811 refname: refname.to_owned(),
812 other: extra,
813 });
814 }
815 }
816
817 Ok(())
818}
819
820#[derive(Clone, Debug)]
822pub struct RefBatchItem {
823 pub name: String,
825 pub new_oid: Option<ObjectId>,
827}
828
829pub fn verify_ref_transaction_batch(
840 git_dir: &Path,
841 items: &[RefBatchItem],
842) -> std::result::Result<(), RefnameUnavailable> {
843 if let Some((parent, child)) =
844 batch_refname_prefix_conflict(items.iter().map(|i| i.name.as_str()))
845 {
846 return Err(RefnameUnavailable::SameBatch {
847 refname: child,
848 other: parent,
849 });
850 }
851
852 if crate::ref_storage::detect_format(git_dir).is_ok_and(|f| f.is_reftable()) {
853 return crate::reftable::verify_ref_transaction_batch_reftable(git_dir, items);
854 }
855
856 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
857 let empty_skip = HashSet::new();
858 for item in items {
859 let Some(_oid) = item.new_oid else {
860 continue;
861 };
862 if resolve_ref(git_dir, &item.name).is_ok() {
863 continue;
864 }
865 verify_refname_available_for_create(git_dir, &item.name, &extras, &empty_skip)?;
866 }
867 Ok(())
868}
869
870pub(crate) fn batch_refname_prefix_conflict<'a>(
872 names: impl Iterator<Item = &'a str>,
873) -> Option<(String, String)> {
874 let mut sorted: Vec<&str> = names.collect();
875 sorted.sort_unstable();
876 for pair in sorted.windows(2) {
877 let (a, b) = (pair[0], pair[1]);
878 if refname_is_strict_prefix(a, b) {
879 return Some((a.to_owned(), b.to_owned()));
880 }
881 if refname_is_strict_prefix(b, a) {
882 return Some((b.to_owned(), a.to_owned()));
883 }
884 }
885 None
886}
887
888fn refname_is_strict_prefix(parent: &str, child: &str) -> bool {
889 child.len() > parent.len()
890 && child.as_bytes().get(parent.len()) == Some(&b'/')
891 && child.starts_with(parent)
892}
893
894fn packed_refs_unexpected_line(path: &Path, line: impl Into<String>) -> Error {
895 Error::PackedRefsUnexpectedLine {
896 path: path.display().to_string(),
897 line: line.into(),
898 }
899}
900
901fn packed_refs_unterminated_line(path: &Path, line: impl Into<String>) -> Error {
902 Error::PackedRefsUnexpectedLine {
903 path: path.display().to_string(),
904 line: line.into(),
905 }
906}
907
908fn parse_packed_ref_peel_line(path: &Path, line: &str, expect_peel: bool) -> Result<ObjectId> {
910 if !expect_peel {
911 return Err(packed_refs_unexpected_line(path, line));
912 }
913 line.strip_prefix('^')
914 .and_then(|inner| ObjectId::from_hex(inner).ok())
915 .ok_or_else(|| packed_refs_unexpected_line(path, line))
916}
917
918fn packed_line_oid(line: &str) -> Option<(usize, ObjectId)> {
920 let bytes = line.as_bytes();
921 let hex_len = [HashAlgo::Sha1.hex_len(), HashAlgo::Sha256.hex_len()]
922 .into_iter()
923 .find(|&len| bytes.get(len) == Some(&b' '))?;
924 let oid = ObjectId::from_hex(&line[..hex_len]).ok()?;
926 Some((hex_len, oid))
927}
928
929fn packed_ref_data_line_refname(line: &str) -> Option<&str> {
930 let bytes = line.as_bytes();
931 let mut j = 0usize;
932 while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
933 j += 1;
934 }
935 if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
936 return None;
937 }
938 let refname = &line[j + 1..];
939 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
940 return None;
941 }
942 Some(refname)
943}
944
945fn parse_packed_ref_data_line(
946 path: &Path,
947 line: &str,
948 ref_opts: &RefNameOptions,
949) -> Result<(String, ObjectId)> {
950 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, ref_opts)?;
951 Ok((refname.to_owned(), oid))
952}
953
954fn parse_packed_ref_data_line_borrowed<'a>(
955 path: &Path,
956 line: &'a str,
957 ref_opts: &RefNameOptions,
958) -> Result<(&'a str, ObjectId)> {
959 let Some((hex_len, oid)) = packed_line_oid(line) else {
960 return Err(packed_refs_unexpected_line(path, line));
961 };
962 let refname = &line[hex_len + 1..];
963 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
964 return Err(packed_refs_unexpected_line(path, line));
965 }
966 if check_refname_format(refname, ref_opts).is_err() {
967 return Err(packed_refs_unexpected_line(path, line));
968 }
969 Ok((refname, oid))
970}
971
972#[derive(Debug, Clone)]
975pub(crate) struct PackedRefsFileView {
976 data: std::sync::Arc<crate::pack_map::PackData>,
977 sorted: bool,
980 hex_len: usize,
982 body_start: usize,
984 peel_trait: PackedPeelTrait,
986}
987
988impl PackedRefsFileView {
989 #[cfg(test)]
991 fn from_bytes(path: &Path, data: Vec<u8>, hash_algo: crate::objects::HashAlgo) -> Result<Self> {
992 Self::from_data(path, crate::pack_map::PackData::from_owned(data), hash_algo)
993 }
994
995 fn from_data(
1004 path: &Path,
1005 data: std::sync::Arc<crate::pack_map::PackData>,
1006 hash_algo: crate::objects::HashAlgo,
1007 ) -> Result<Self> {
1008 let hex_len = hash_algo.hex_len();
1009 if data.is_empty() {
1010 return Ok(Self {
1011 data,
1012 sorted: false,
1013 hex_len,
1014 body_start: 0,
1015 peel_trait: PackedPeelTrait::None,
1016 });
1017 }
1018 if !data.ends_with(b"\n") {
1019 let tail = std::str::from_utf8(&data)
1020 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1021 .unwrap_or("<invalid utf-8>");
1022 return Err(packed_refs_unterminated_line(path, tail));
1023 }
1024 let text = std::str::from_utf8(&data[..packed_refs_leading_comments_end(&data)])
1027 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1028 let body_start = if data.starts_with(b"#") {
1031 memchr::memchr(b'\n', &data).map_or(data.len(), |nl| nl + 1)
1032 } else {
1033 0
1034 };
1035 let body = &data[body_start..];
1036 if let Some(rest) = body.strip_suffix(b"\n") {
1037 let last_line = memchr::memrchr(b'\n', rest).map_or(rest, |nl| &rest[nl + 1..]);
1038 if last_line.len() < hex_len + 1 {
1039 return Err(packed_refs_unexpected_line(
1040 path,
1041 String::from_utf8_lossy(last_line),
1042 ));
1043 }
1044 }
1045 Ok(Self {
1046 sorted: packed_refs_file_declares_sorted(text),
1047 hex_len,
1048 body_start,
1049 peel_trait: packed_refs_peel_trait(text),
1050 data,
1051 })
1052 }
1053
1054 fn record_name(&self, line_start: usize) -> &[u8] {
1056 let end = packed_line_end(&self.data, line_start);
1057 &self.data[line_start + self.hex_len + 1..end]
1058 }
1059
1060 fn record_cmp(&self, line_start: usize, name: &str) -> std::cmp::Ordering {
1061 self.record_name(line_start).cmp(name.as_bytes())
1062 }
1063
1064 fn next_record(&self, from: usize, limit: usize) -> Option<usize> {
1067 let hex_len = self.hex_len;
1068 let mut start = from;
1069 while start < limit {
1070 let rest = &self.data[start..];
1071 if rest[0] == b'^' && rest.get(hex_len + 1) == Some(&b'\n') {
1074 start += hex_len + 2;
1075 continue;
1076 }
1077 let end = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
1078 if packed_line_is_record(&rest[..end], hex_len) {
1079 return Some(start);
1080 }
1081 start += end + 1;
1082 }
1083 None
1084 }
1085
1086 fn prev_record(&self, before: usize) -> Option<usize> {
1088 let mut end = before;
1089 while end > 0 {
1090 let line_end = end - 1;
1091 let start = memchr::memrchr(b'\n', &self.data[..line_end]).map_or(0, |nl| nl + 1);
1092 if packed_line_is_record(&self.data[start..line_end], self.hex_len) {
1093 return Some(start);
1094 }
1095 end = start;
1096 }
1097 None
1098 }
1099
1100 fn records(&self) -> impl Iterator<Item = usize> + '_ {
1102 let len = self.data.len();
1103 std::iter::successors(self.next_record(0, len), move |&start| {
1104 self.next_record(packed_line_end(&self.data, start) + 1, len)
1105 })
1106 }
1107
1108 fn search(&self, name: &str) -> std::result::Result<usize, usize> {
1116 let len = self.data.len();
1117 let mut lo = self.body_start;
1118 let mut hi = len;
1119 while lo < hi {
1120 let mid = lo + (hi - lo) / 2;
1121 let line = memchr::memrchr(b'\n', &self.data[lo..mid]).map_or(lo, |nl| lo + nl + 1);
1122 let Some(record) = self.next_record(line, hi) else {
1123 hi = line;
1124 continue;
1125 };
1126 match self.record_cmp(record, name) {
1127 std::cmp::Ordering::Less => lo = packed_line_end(&self.data, record) + 1,
1128 std::cmp::Ordering::Greater => hi = record,
1129 std::cmp::Ordering::Equal => return Ok(record),
1130 }
1131 }
1132 Err(self.next_record(lo, len).unwrap_or(len))
1133 }
1134
1135 fn lower_bound(&self, name: &str) -> usize {
1137 let (Ok(start) | Err(start)) = self.search(name);
1138 start
1139 }
1140
1141 fn lookup_entry(
1143 &self,
1144 path: &Path,
1145 refname: &str,
1146 ) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1147 let ref_opts = RefNameOptions {
1148 allow_onelevel: false,
1149 refspec_pattern: false,
1150 normalize: false,
1151 };
1152 let line_start = if self.sorted {
1153 self.search(refname).ok()
1154 } else {
1155 self.records()
1156 .find(|&start| self.record_cmp(start, refname).is_eq())
1157 };
1158 let Some(start) = line_start else {
1159 return Ok(None);
1160 };
1161 let (name, oid) = parse_packed_line_at(path, &self.data, start, &ref_opts)?;
1162 debug_assert_eq!(name, refname);
1163 let line_end = packed_line_end(&self.data, start);
1164 if line_end + 1 < self.data.len() && self.data[line_end + 1] == b'^' {
1165 let peel_line = packed_line_as_str(&self.data, line_end + 1);
1166 parse_packed_ref_peel_line(path, peel_line, true)?;
1167 }
1168 let peel = peel_oid_following_line(&self.data, start);
1169 Ok(Some((oid, peel)))
1170 }
1171
1172 fn lookup_oid(&self, path: &Path, refname: &str) -> Result<Option<ObjectId>> {
1173 Ok(self.lookup_entry(path, refname)?.map(|(oid, _)| oid))
1174 }
1175
1176 pub(crate) fn first_descendant(
1180 &self,
1181 path: &Path,
1182 refname: &str,
1183 ) -> Result<Option<(String, ObjectId)>> {
1184 let leaf_dir = format!("{refname}/");
1185 Ok(self
1186 .map_for_prefix(path, Some(refname))?
1187 .into_iter()
1188 .filter(|(name, _)| name.starts_with(&leaf_dir))
1189 .min_by(|a, b| a.0.cmp(&b.0)))
1190 }
1191
1192 fn map_for_prefix(
1201 &self,
1202 path: &Path,
1203 prefix: Option<&str>,
1204 ) -> Result<HashMap<String, ObjectId>> {
1205 self.map_for_prefix_filtered(path, prefix, None)
1206 }
1207
1208 fn map_for_prefix_filtered(
1211 &self,
1212 path: &Path,
1213 prefix: Option<&str>,
1214 keep: Option<PackedRecordFilter<'_>>,
1215 ) -> Result<HashMap<String, ObjectId>> {
1216 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1217 let range_prefix = prefix_trim.filter(|p| self.sorted && !p.is_empty() && *p != "refs");
1218 let (begin, end) = match range_prefix {
1219 Some(prefix) => {
1220 let first = self.lower_bound(prefix);
1221 let end = list_prefix_upper_bound(prefix)
1223 .map_or(self.data.len(), |upper| self.lower_bound(&upper))
1224 .max(first);
1225 let begin = self.prev_record(first).map_or(0, |prev| {
1228 let after = packed_line_end(&self.data, prev) + 1;
1229 if self.data.get(after) == Some(&b'^') {
1230 packed_line_end(&self.data, after) + 1
1231 } else {
1232 after
1233 }
1234 });
1235 (begin, end)
1236 }
1237 None => (0, self.data.len()),
1238 };
1239 let region = std::str::from_utf8(&self.data[begin..end])
1241 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1242 parse_packed_refs_region(path, region, prefix_trim, keep, self.peel_trait)
1243 }
1244}
1245
1246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1249pub(crate) enum PackedPeel {
1250 Peeled(ObjectId),
1252 NotTag,
1254 Unknown,
1256}
1257
1258pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
1261
1262fn parse_packed_refs_region(
1267 path: &Path,
1268 region: &str,
1269 prefix: Option<&str>,
1270 keep: Option<PackedRecordFilter<'_>>,
1271 peel_trait: PackedPeelTrait,
1272) -> Result<HashMap<String, ObjectId>> {
1273 let ref_opts = RefNameOptions {
1274 allow_onelevel: false,
1275 refspec_pattern: false,
1276 normalize: false,
1277 };
1278 let mut map = HashMap::new();
1279 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
1281 let flush = |map: &mut HashMap<String, ObjectId>,
1282 pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
1283 let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
1284 return;
1285 };
1286 let peel = if peel_trait.covers(refname) {
1287 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1288 } else {
1289 PackedPeel::Unknown
1290 };
1291 if keep(refname, &oid, peel) {
1292 map.insert(refname.to_owned(), oid);
1293 }
1294 };
1295 let mut expect_peel = false;
1296 let mut lines = region.split('\n').peekable();
1297 while let Some(line) = lines.next() {
1298 if line.is_empty() {
1299 if lines.peek().is_some() {
1300 return Err(packed_refs_unexpected_line(path, line));
1301 }
1302 break;
1303 }
1304 if line.starts_with('#') {
1305 flush(&mut map, &mut pending);
1306 expect_peel = false;
1307 continue;
1308 }
1309 if line.starts_with('^') {
1310 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
1311 if let Some((_, _, peeled)) = pending.as_mut() {
1312 *peeled = Some(peel);
1313 }
1314 expect_peel = false;
1315 continue;
1316 }
1317 flush(&mut map, &mut pending);
1318 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1319 if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
1320 if keep.is_some() {
1321 pending = Some((refname, oid, None));
1322 } else {
1323 map.insert(refname.to_owned(), oid);
1324 }
1325 }
1326 expect_peel = true;
1327 }
1328 flush(&mut map, &mut pending);
1329 Ok(map)
1330}
1331
1332fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
1333 let packed_path = store::paths::packed_refs_path(store);
1334 let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
1337 Ok(d) => d,
1338 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
1339 Err(e) => return Err(e),
1340 };
1341 let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
1342 .map(|format| format.hash_algo())
1343 .unwrap_or(crate::objects::HashAlgo::Sha1);
1344 Ok(Some(PackedRefsFileView::from_data(
1345 &packed_path,
1346 data,
1347 hash_algo,
1348 )?))
1349}
1350
1351fn packed_line_is_record(line: &[u8], hex_len: usize) -> bool {
1354 line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^')
1355}
1356
1357fn packed_line_end(data: &[u8], line_start: usize) -> usize {
1358 memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
1359}
1360
1361fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
1362 std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
1363}
1364
1365fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
1366 let line_end = packed_line_end(data, line_start);
1367 if line_end >= data.len() || data[line_end] != b'\n' {
1368 return None;
1369 }
1370 let peel_start = line_end + 1;
1371 if peel_start >= data.len() || data[peel_start] != b'^' {
1372 return None;
1373 }
1374 let peel_end = packed_line_end(data, peel_start);
1375 let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
1376 ObjectId::from_hex(inner.trim()).ok()
1377}
1378
1379fn parse_packed_line_at(
1380 path: &Path,
1381 data: &[u8],
1382 line_start: usize,
1383 ref_opts: &RefNameOptions,
1384) -> Result<(String, ObjectId)> {
1385 let line = packed_line_as_str(data, line_start);
1386 parse_packed_ref_data_line(path, line, ref_opts)
1387}
1388
1389pub(crate) fn parse_packed_refs_to_map(
1390 path: &Path,
1391 data: &[u8],
1392) -> Result<HashMap<String, ObjectId>> {
1393 parse_packed_refs_to_map_with_prefix(path, data, None)
1394}
1395
1396fn packed_refs_leading_comments_end(data: &[u8]) -> usize {
1398 let mut end = 0;
1399 while data.get(end) == Some(&b'#') {
1400 match memchr::memchr(b'\n', &data[end..]) {
1401 Some(nl) => end += nl + 1,
1402 None => return data.len(),
1403 }
1404 }
1405 end
1406}
1407
1408fn packed_refs_file_declares_sorted(text: &str) -> bool {
1410 for line in text.split('\n') {
1411 if line.is_empty() {
1412 break;
1413 }
1414 if !line.starts_with('#') {
1415 break;
1416 }
1417 let body = line[1..].trim();
1418 let Some(traits) = body.strip_prefix("pack-refs with:") else {
1419 continue;
1420 };
1421 return traits.split_whitespace().any(|t| t == "sorted");
1422 }
1423 false
1424}
1425
1426fn parse_packed_refs_to_map_with_prefix(
1427 path: &Path,
1428 data: &[u8],
1429 prefix_filter: Option<&str>,
1430) -> Result<HashMap<String, ObjectId>> {
1431 if data.is_empty() {
1432 return Ok(HashMap::new());
1433 }
1434 if data.contains(&b'\r') {
1435 return Err(packed_refs_unexpected_line(
1436 path,
1437 "<contains carriage return>",
1438 ));
1439 }
1440 if !data.ends_with(b"\n") {
1441 let tail = std::str::from_utf8(data)
1442 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1443 .unwrap_or("<invalid utf-8>");
1444 return Err(packed_refs_unterminated_line(path, tail));
1445 }
1446 let text = std::str::from_utf8(data)
1447 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1448
1449 let ref_opts = RefNameOptions {
1450 allow_onelevel: false,
1451 refspec_pattern: false,
1452 normalize: false,
1453 };
1454
1455 let mut map = HashMap::new();
1456 let mut expect_peel = false;
1457 let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
1458 let packed_sorted = packed_refs_file_declares_sorted(text);
1459 let prefix_upper = if packed_sorted {
1460 prefix_trim.and_then(list_prefix_upper_bound)
1461 } else {
1462 None
1463 };
1464 let mut lines = text.split('\n').peekable();
1465 while let Some(line) = lines.next() {
1466 if line.is_empty() {
1467 if lines.peek().is_some() {
1468 return Err(packed_refs_unexpected_line(path, line));
1469 }
1470 break;
1471 }
1472 if line.starts_with('#') {
1473 expect_peel = false;
1474 continue;
1475 }
1476 if line.starts_with('^') {
1477 parse_packed_ref_peel_line(path, line, expect_peel)?;
1478 expect_peel = false;
1479 continue;
1480 }
1481
1482 if let Some(prefix) = prefix_trim {
1483 let Some(refname) = packed_ref_data_line_refname(line) else {
1484 return Err(packed_refs_unexpected_line(path, line));
1485 };
1486 if packed_sorted {
1487 if refname < prefix {
1488 expect_peel = true;
1489 continue;
1490 }
1491 if let Some(upper) = &prefix_upper {
1492 if refname >= upper.as_str() {
1493 break;
1494 }
1495 }
1496 }
1497 if !ref_name_matches_list_prefix(refname, prefix) {
1498 expect_peel = true;
1499 continue;
1500 }
1501 }
1502 let (refname, oid) = parse_packed_ref_data_line(path, line, &ref_opts)?;
1503 map.insert(refname, oid);
1504 expect_peel = true;
1505 }
1506 Ok(map)
1507}
1508
1509pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
1510 read_packed_refs_map_for_prefix(store, "")
1511}
1512
1513pub(crate) fn read_packed_refs_map_for_prefix(
1514 store: &Path,
1515 prefix: &str,
1516) -> Result<HashMap<String, ObjectId>> {
1517 read_packed_refs_map_for_prefix_filtered(store, prefix, None)
1518}
1519
1520pub(crate) fn read_packed_refs_map_for_prefix_filtered(
1522 store: &Path,
1523 prefix: &str,
1524 keep: Option<PackedRecordFilter<'_>>,
1525) -> Result<HashMap<String, ObjectId>> {
1526 let Some(view) = read_packed_refs_file_view(store)? else {
1527 return Ok(HashMap::new());
1528 };
1529 let packed_path = store::paths::packed_refs_path(store);
1530 let filter = if prefix.is_empty() {
1531 None
1532 } else {
1533 Some(prefix)
1534 };
1535 view.map_for_prefix_filtered(&packed_path, filter, keep)
1536}
1537
1538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1540enum PackedPeelTrait {
1541 None,
1543 Tags,
1545 All,
1547}
1548
1549impl PackedPeelTrait {
1550 fn covers(self, refname: &str) -> bool {
1552 match self {
1553 Self::None => false,
1554 Self::Tags => refname.starts_with("refs/tags/"),
1555 Self::All => true,
1556 }
1557 }
1558}
1559
1560pub(crate) struct PackedRefPeels {
1570 view: Option<PackedRefsFileView>,
1571 unsorted: Option<HashMap<Box<[u8]>, usize>>,
1573 peel_trait: PackedPeelTrait,
1574 next: std::cell::Cell<usize>,
1577}
1578
1579impl PackedRefPeels {
1580 pub(crate) fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
1590 if format != RefStorageFormat::Files {
1591 return Ok(Self::from_view(None));
1592 }
1593 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1594 Ok(Self::from_view(read_packed_refs_file_view(&store)?))
1595 }
1596
1597 pub(crate) fn for_store(store: &dyn store::RefStore, git_dir: &Path) -> Result<Self> {
1606 match store.as_files() {
1607 Some(files) => Ok(Self::from_view(files.packed_refs_view()?)),
1608 None => Self::load(git_dir, store.format()),
1609 }
1610 }
1611
1612 fn from_view(view: Option<PackedRefsFileView>) -> Self {
1613 let empty = Self {
1614 view: None,
1615 unsorted: None,
1616 peel_trait: PackedPeelTrait::None,
1617 next: std::cell::Cell::new(0),
1618 };
1619 let Some(view) = view else {
1620 return empty;
1621 };
1622 let peel_trait = view.peel_trait;
1623 if peel_trait == PackedPeelTrait::None {
1624 return empty;
1625 }
1626 let unsorted = (!view.sorted).then(|| {
1627 view.records()
1628 .map(|start| (Box::<[u8]>::from(view.record_name(start)), start))
1629 .collect()
1630 });
1631 let first = view
1632 .next_record(view.body_start, view.data.len())
1633 .unwrap_or(view.data.len());
1634 Self {
1635 view: Some(view),
1636 unsorted,
1637 peel_trait,
1638 next: std::cell::Cell::new(first),
1639 }
1640 }
1641
1642 pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
1654 if !self.peel_trait.covers(refname) {
1655 return None;
1656 }
1657 let view = self.view.as_ref()?;
1658 let start = match &self.unsorted {
1659 Some(map) => *map.get(refname.as_bytes())?,
1660 None => {
1661 let len = view.data.len();
1662 let hint = self.next.get();
1663 let start = if hint < len && view.record_cmp(hint, refname).is_eq() {
1664 hint
1665 } else {
1666 view.search(refname).ok()?
1667 };
1668 let after = packed_line_end(&view.data, start) + 1;
1669 self.next.set(view.next_record(after, len).unwrap_or(len));
1670 start
1671 }
1672 };
1673 let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
1674 let packed_oid = ObjectId::from_hex(hex).ok()?;
1675 (packed_oid == *oid).then(|| peel_oid_following_line(&view.data, start))
1676 }
1677}
1678
1679fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
1681 let Some(header) = text.strip_prefix("# pack-refs with: ") else {
1682 return PackedPeelTrait::None;
1683 };
1684 let traits = header.split('\n').next().unwrap_or_default();
1685 let mut peel_trait = PackedPeelTrait::None;
1686 for t in traits.split_whitespace() {
1687 match t {
1688 "fully-peeled" => return PackedPeelTrait::All,
1689 "peeled" => peel_trait = PackedPeelTrait::Tags,
1690 _ => {}
1691 }
1692 }
1693 peel_trait
1694}
1695
1696fn acquire_packed_refs_lock(
1697 git_dir: &Path,
1698 lock: &Path,
1699 lock_display: &Path,
1700) -> Result<crate::lockfile::LockFile> {
1701 use crate::lockfile::{LockError, LockFile, LockWait};
1702 let wait = LockWait::packed_refs_for_git_dir(git_dir);
1703 LockFile::acquire(lock, wait).map_err(|err| match err {
1704 LockError::Held { .. } => Error::from(RefLockError::PackedRefsLockHeld {
1705 lock_path: lock_display.display().to_string(),
1706 }),
1707 LockError::Io { source, .. } => Error::Io(source),
1708 })
1709}
1710
1711pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
1713 let Some(view) = read_packed_refs_file_view(git_dir)? else {
1714 return Ok(None);
1715 };
1716 let packed_path = store::paths::packed_refs_path(git_dir);
1717 view.lookup_oid(&packed_path, refname)
1718}
1719
1720#[cfg(test)]
1722pub(crate) fn lookup_packed_ref_with_peel(
1723 git_dir: &Path,
1724 refname: &str,
1725) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1726 let Some(view) = read_packed_refs_file_view(git_dir)? else {
1727 return Ok(None);
1728 };
1729 let packed_path = store::paths::packed_refs_path(git_dir);
1730 view.lookup_entry(&packed_path, refname)
1731}
1732
1733pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
1742 store_write_value(
1743 git_dir,
1744 refname,
1745 Some(StoreRawRef::Symbolic(target.to_owned())),
1746 Expected::Any,
1747 None,
1748 )
1749}
1750
1751pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
1752 if let Some(rest) = refname.strip_prefix("refs/") {
1753 if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
1754 return Err(Error::InvalidRef(format!(
1755 "refusing to update ref with bad name '{refname}'"
1756 )));
1757 }
1758 return Ok(());
1759 }
1760 if crate::check_ref_format::refname_is_safe(refname) {
1761 Ok(())
1762 } else {
1763 Err(Error::InvalidRef(format!(
1764 "refusing to update ref with bad name '{refname}'"
1765 )))
1766 }
1767}
1768
1769pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
1784 store_write_value(
1785 git_dir,
1786 refname,
1787 Some(StoreRawRef::Direct(*oid)),
1788 Expected::Any,
1789 None,
1790 )
1791}
1792
1793#[must_use]
1795pub fn ref_update_flags(refname: &str) -> RefUpdateFlags {
1796 legacy_ref_update_flags(refname)
1797}
1798
1799pub fn create_ref_with_reflog(
1805 git_dir: &Path,
1806 refname: &str,
1807 oid: &ObjectId,
1808 identity: &str,
1809 message: &str,
1810) -> Result<()> {
1811 let config = ConfigSet::load(
1812 &crate::environment::Environment::empty(),
1813 Some(git_dir),
1814 true,
1815 )
1816 .unwrap_or_default();
1817 let store = open_ref_store(git_dir)?;
1818 create_ref_with_reflog_on_store(
1819 store.as_ref(),
1820 git_dir,
1821 &config,
1822 refname,
1823 oid,
1824 identity,
1825 message,
1826 )
1827}
1828
1829pub(crate) fn create_ref_with_reflog_on_store(
1832 store: &dyn RefStore,
1833 git_dir: &Path,
1834 config: &ConfigSet,
1835 refname: &str,
1836 oid: &ObjectId,
1837 identity: &str,
1838 message: &str,
1839) -> Result<()> {
1840 let reflog =
1841 should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
1842 identity: identity.to_owned(),
1843 message: message.to_owned(),
1844 time: time::OffsetDateTime::now_utc(),
1845 old_oid: None,
1846 new_oid: None,
1847 });
1848 store_write_value_on_store(
1849 store,
1850 git_dir,
1851 refname,
1852 Some(StoreRawRef::Direct(*oid)),
1853 Expected::Missing,
1854 reflog,
1855 )
1856}
1857
1858fn write_ref_at_storage(
1861 git_dir: &Path,
1862 storage_dir: &Path,
1863 refname: &str,
1864 oid: &ObjectId,
1865) -> Result<()> {
1866 let stor = crate::ref_namespace::storage_ref_name(refname);
1867 let path = storage_dir.join(stor);
1868 remove_empty_ref_directory(&path);
1875 if fs::symlink_metadata(&path)
1882 .map(|m| m.file_type().is_dir())
1883 .unwrap_or(false)
1884 {
1885 let display = ref_path_for_display(&path);
1886 return Err(crate::error::RefLockError::DirectoryInTheWay {
1887 refname: refname.to_owned(),
1888 path: display,
1889 }
1890 .into());
1891 }
1892 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
1893 return Err(Error::InvalidRef(format!(
1894 "cannot update ref '{refname}': reference broken"
1895 )));
1896 }
1897 if let Some(parent) = path.parent() {
1898 crate::git_dir::io_error::io_with_git_path(
1899 git_dir,
1900 parent,
1901 crate::git_dir::GitDirPathOperation::CreateDir,
1902 fs::create_dir_all(parent),
1903 )?;
1904 }
1905 let content = format!("{oid}\n");
1906 let lock_path = lock_path_for_ref(&path);
1909 use crate::lockfile::{LockError, LockFile, LockWait};
1910 use std::io::Write as _;
1911 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
1912 let mut lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
1913 held @ LockError::Held { .. } => Error::from(held),
1914 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
1915 git_dir,
1916 &lock_path,
1917 crate::git_dir::GitDirPathOperation::UpdateRef,
1918 source,
1919 ),
1920 })?;
1921 crate::git_dir::io_error::io_with_git_path(
1922 git_dir,
1923 &path,
1924 crate::git_dir::GitDirPathOperation::UpdateRef,
1925 lock.write_all(content.as_bytes()),
1926 )?;
1927 lock.commit_to(&path).map_err(|err| match err {
1928 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
1929 git_dir,
1930 &path,
1931 crate::git_dir::GitDirPathOperation::UpdateRef,
1932 source,
1933 ),
1934 held @ LockError::Held { .. } => Error::from(held),
1935 })?;
1936 Ok(())
1937}
1938
1939pub fn write_ref_cas(
1947 git_dir: &Path,
1948 refname: &str,
1949 oid: &ObjectId,
1950 expected_old: ObjectId,
1951) -> Result<()> {
1952 store_write_value(
1953 git_dir,
1954 refname,
1955 Some(StoreRawRef::Direct(*oid)),
1956 Expected::Oid(expected_old),
1957 None,
1958 )
1959}
1960
1961pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
1968 store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
1969}
1970
1971pub(crate) fn prune_loose_ref_cas(
1981 git_dir: &Path,
1982 refname: &str,
1983 expected: ObjectId,
1984) -> Result<bool> {
1985 ensure_refname_safe_for_storage(refname)?;
1986 let storage_dir = ref_storage_dir(git_dir, refname);
1987 let stor = crate::ref_namespace::storage_ref_name(refname);
1988 let path = storage_dir.join(&stor);
1989 let lock_path = lock_path_for_ref(&path);
1990 use crate::lockfile::{LockFile, LockWait};
1991 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
1992 let _lock = LockFile::acquire(&lock_path, wait)?;
1993 let outcome = (|| -> Result<bool> {
1994 if !path.is_file() {
1995 return Ok(true);
1996 }
1997 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
1998 return Ok(false);
1999 };
2000 if oid != expected {
2001 return Ok(false);
2002 }
2003 remove_empty_ref_directory(&path);
2004 fs::remove_file(&path).map_err(Error::Io)?;
2005 if let Some(parent) = path.parent() {
2006 remove_empty_ref_directory(parent);
2007 }
2008 Ok(true)
2009 })();
2010 outcome
2011}
2012
2013pub struct PackedRefs {
2029 map: HashMap<String, ObjectId>,
2030 sorted_names: BTreeSet<String>,
2031}
2032
2033fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
2034 let sorted_names = map.keys().cloned().collect();
2035 PackedRefs { map, sorted_names }
2036}
2037
2038impl PackedRefs {
2039 pub fn load(git_dir: &Path) -> Result<Self> {
2048 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2049 let map = read_packed_refs_map(&store)?;
2050 Ok(packed_refs_from_map(map))
2051 }
2052
2053 #[must_use]
2055 pub fn get(&self, refname: &str) -> Option<ObjectId> {
2056 self.map.get(refname).copied()
2057 }
2058
2059 #[must_use]
2063 pub fn has_namespace_conflict(&self, refname: &str) -> bool {
2064 packed_refname_namespace_conflict(&self.sorted_names, refname)
2065 }
2066
2067 pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
2070 self.map.insert(refname.to_owned(), oid);
2071 self.sorted_names.insert(refname.to_owned());
2072 }
2073}
2074
2075pub fn resolve_ref_cached(
2085 git_dir: &Path,
2086 refname: &str,
2087 packed: &PackedRefs,
2088) -> Result<Option<ObjectId>> {
2089 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2090 return Ok(resolve_ref(git_dir, refname).ok());
2091 }
2092 resolve_files_ref_cached(git_dir, refname, packed)
2093}
2094
2095fn resolve_files_ref_cached(
2096 git_dir: &Path,
2097 refname: &str,
2098 packed: &PackedRefs,
2099) -> Result<Option<ObjectId>> {
2100 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
2101 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
2102 let try_names: Vec<&str> = if storage_owned != stor_name {
2103 vec![storage_owned.as_str(), stor_name.as_str()]
2104 } else {
2105 vec![stor_name.as_str()]
2106 };
2107
2108 for name in &try_names {
2109 let path = store.join(name);
2110 match read_ref_file(&path) {
2111 Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
2112 Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
2113 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
2114 Err(e) => return Err(e),
2115 }
2116 }
2117
2118 for name in &try_names {
2119 if let Some(oid) = packed.get(name) {
2120 return Ok(Some(oid));
2121 }
2122 }
2123 Ok(None)
2124}
2125
2126pub fn write_ref_cached(
2135 git_dir: &Path,
2136 refname: &str,
2137 oid: &ObjectId,
2138 packed: &mut PackedRefs,
2139) -> Result<()> {
2140 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2141 return write_ref(git_dir, refname, oid);
2142 }
2143 ensure_refname_safe_for_storage(refname)?;
2144 if packed.has_namespace_conflict(refname) {
2145 return Err(Error::InvalidRef(format!(
2146 "cannot update ref '{refname}': reference namespace conflict"
2147 )));
2148 }
2149 let storage_dir = ref_storage_dir(git_dir, refname);
2150 write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
2151 packed.record_loose_write(refname, *oid);
2152 Ok(())
2153}
2154
2155fn ref_path_for_display(path: &Path) -> String {
2160 let cwd = crate::environment::Environment::empty().cwd.clone();
2161 if let Ok(rel) = path.strip_prefix(&cwd) {
2162 return rel.to_string_lossy().into_owned();
2163 }
2164 if let (Ok(cwd_c), Ok(path_c)) = (cwd.canonicalize(), path.canonicalize()) {
2168 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
2169 return rel.to_string_lossy().into_owned();
2170 }
2171 }
2172 path.to_string_lossy().into_owned()
2173}
2174
2175pub(crate) fn remove_empty_ref_directory(path: &Path) {
2181 match fs::symlink_metadata(path) {
2182 Ok(meta) if meta.file_type().is_dir() => {}
2183 _ => return,
2184 }
2185 if dir_tree_has_files(path) {
2188 return;
2189 }
2190 let _ = remove_dir_tree(path);
2191}
2192
2193fn dir_tree_has_files(dir: &Path) -> bool {
2195 let Ok(entries) = fs::read_dir(dir) else {
2196 return true;
2198 };
2199 for entry in entries.flatten() {
2200 match entry.file_type() {
2201 Ok(ft) if ft.is_dir() => {
2202 if dir_tree_has_files(&entry.path()) {
2203 return true;
2204 }
2205 }
2206 Ok(_) => return true, Err(_) => return true,
2208 }
2209 }
2210 false
2211}
2212
2213pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
2215 if let Some(dir) = ref_path.parent() {
2216 prune_empty_ref_dirs(storage_dir, dir);
2217 }
2218}
2219
2220pub(crate) fn prune_empty_ref_dirs(storage_dir: &Path, dir: &Path) {
2222 let refs_root = storage_dir.join("refs");
2223 let mut current = Some(dir);
2224 while let Some(dir) = current {
2225 if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
2226 break;
2227 }
2228 if dir_tree_has_files(dir) {
2229 break;
2230 }
2231 if fs::remove_dir(dir).is_err() {
2232 break;
2233 }
2234 current = dir.parent();
2235 }
2236}
2237
2238fn remove_dir_tree(dir: &Path) -> io::Result<()> {
2240 for entry in fs::read_dir(dir)? {
2241 let entry = entry?;
2242 if entry.file_type()?.is_dir() {
2243 remove_dir_tree(&entry.path())?;
2244 }
2245 }
2246 fs::remove_dir(dir)
2247}
2248
2249pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
2259 if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
2260 return Err(store::RefStoreError::not_found(refname).into());
2261 }
2262 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2263 let store = open_ref_store(git_dir)?;
2264 let mut flags = legacy_ref_update_flags(refname);
2265 if refname != "HEAD" {
2266 if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
2267 flags.no_deref = true;
2268 }
2269 }
2270 return commit_store_update(
2271 git_dir,
2272 RefUpdate {
2273 name: refname.to_owned(),
2274 new_value: None,
2275 expected: Expected::Exists,
2276 reflog: None,
2277 flags,
2278 },
2279 )
2280 .map_err(|e| map_public_ref_store_error(e, refname));
2281 }
2282 ensure_refname_safe_for_storage(refname)?;
2283 let storage_dir = ref_storage_dir(git_dir, refname);
2284 let stor = crate::ref_namespace::storage_ref_name(refname);
2285 let path = storage_dir.join(&stor);
2286 let lock = lock_path_for_ref(&path);
2287 if fs::symlink_metadata(&lock).is_ok() {
2288 return Err(Error::from(crate::lockfile::LockError::Held { path: lock }));
2289 }
2290
2291 remove_packed_ref(&storage_dir, &stor)?;
2297
2298 remove_empty_ref_directory(&path);
2302 match fs::remove_file(&path) {
2303 Ok(()) => {}
2304 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
2305 Err(e)
2306 if e.kind() == io::ErrorKind::NotADirectory
2307 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
2308 Err(e)
2311 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
2312 Err(e) => return Err(Error::Io(e)),
2313 }
2314
2315 prune_empty_loose_ref_parents(&storage_dir, &path);
2316
2317 let log_path = storage_dir.join("logs").join(&stor);
2318
2319 let _ = fs::remove_file(&log_path);
2325
2326 let logs_root = storage_dir.join("logs");
2327 let mut parent = log_path.parent();
2328 while let Some(p) = parent {
2329 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
2330 break;
2331 }
2332 if fs::remove_dir(p).is_err() {
2333 break; }
2335 parent = p.parent();
2336 }
2337
2338 Ok(())
2339}
2340
2341pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
2343 remove_packed_refs_inner(git_dir, &|name| name == refname, false, false).map(drop)
2344}
2345
2346pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
2347 remove_packed_refs_inner(git_dir, &|name| name == refname, false, true).map(drop)
2348}
2349
2350pub(crate) fn remove_packed_refs_under_lock(
2360 git_dir: &Path,
2361 refnames: &HashSet<String>,
2362) -> Result<Vec<String>> {
2363 if refnames.is_empty() {
2364 return Ok(Vec::new());
2365 }
2366 remove_packed_refs_inner(git_dir, &|name| refnames.contains(name), true, true)
2367}
2368
2369pub(crate) fn packed_record_name(record: &str) -> &str {
2371 let line = record.lines().next().unwrap_or("");
2372 line.split_once(' ').map_or("", |(_, name)| name.trim())
2373}
2374
2375pub(crate) fn packed_record_oid(record: &str) -> Option<ObjectId> {
2377 let line = record.lines().next()?;
2378 ObjectId::from_hex(line.split_once(' ')?.0).ok()
2379}
2380
2381pub(crate) fn restore_packed_records_under_lock(git_dir: &Path, records: &[&str]) -> Result<()> {
2388 if records.is_empty() {
2389 return Ok(());
2390 }
2391 let packed_path = store::paths::packed_refs_path(git_dir);
2392 let content = match fs::read(&packed_path) {
2393 Ok(c) => c,
2394 Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
2395 Err(e) => return Err(Error::Io(e)),
2396 };
2397 parse_packed_refs_to_map(&packed_path, &content)?;
2398 let content = String::from_utf8(content)
2399 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
2400
2401 let mut blocks: Vec<String> = Vec::new();
2402 for line in content.lines() {
2403 if line.starts_with('#') {
2404 continue;
2405 }
2406 match blocks.last_mut() {
2407 Some(block) if line.starts_with('^') => block.push_str(line),
2408 _ => blocks.push(line.to_owned()),
2409 }
2410 if let Some(block) = blocks.last_mut() {
2411 block.push('\n');
2412 }
2413 }
2414 blocks.extend(records.iter().map(|r| (*r).to_owned()));
2415 blocks.sort_by(|a, b| packed_record_name(a).cmp(packed_record_name(b)));
2416
2417 let mut out =
2418 String::with_capacity(content.len() + records.iter().map(|r| r.len()).sum::<usize>());
2419 out.push_str(PACKED_REFS_HEADER);
2420 for block in &blocks {
2421 out.push_str(block);
2422 }
2423 replace_packed_refs_file(git_dir, &packed_path, &out, true)
2424}
2425
2426const PACKED_REFS_HEADER: &str = "# pack-refs with: peeled fully-peeled sorted\n";
2427
2428fn remove_packed_refs_inner(
2429 git_dir: &Path,
2430 remove: &dyn Fn(&str) -> bool,
2431 keep_removed: bool,
2432 lock_already_held: bool,
2433) -> Result<Vec<String>> {
2434 let mut removed: Vec<String> = Vec::new();
2435 let packed_path = store::paths::packed_refs_path(git_dir);
2436 let content = match fs::read(&packed_path) {
2437 Ok(c) => c,
2438 Err(e)
2439 if e.kind() == io::ErrorKind::NotFound
2440 || e.kind() == io::ErrorKind::NotADirectory
2441 || e.raw_os_error() == Some(libc::ENOTDIR) =>
2442 {
2443 return Ok(removed);
2444 }
2445 Err(e) => return Err(Error::Io(e)),
2446 };
2447 parse_packed_refs_to_map(&packed_path, &content)?;
2449
2450 let content = String::from_utf8(content)
2451 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
2452
2453 let mut out = String::with_capacity(content.len());
2454 let mut skip_peeled = false;
2455 let mut changed = false;
2456 let mut header_written = false;
2459
2460 for line in content.lines() {
2461 if skip_peeled {
2462 if line.starts_with('^') {
2463 if let Some(record) = removed.last_mut().filter(|_| keep_removed) {
2464 record.push_str(line);
2465 record.push('\n');
2466 }
2467 changed = true;
2468 continue;
2469 }
2470 skip_peeled = false;
2471 }
2472
2473 if line.starts_with('#') {
2474 continue;
2476 }
2477 if line.starts_with('^') {
2478 out.push_str(line);
2479 out.push('\n');
2480 continue;
2481 }
2482
2483 if !header_written {
2485 out.insert_str(0, PACKED_REFS_HEADER);
2486 header_written = true;
2487 }
2488
2489 let name = line.split_once(' ').map_or("", |(_, name)| name.trim());
2490 if remove(name) {
2491 if keep_removed {
2492 removed.push(format!("{line}\n"));
2493 }
2494 changed = true;
2495 skip_peeled = true;
2496 continue;
2497 }
2498
2499 out.push_str(line);
2500 out.push('\n');
2501 }
2502
2503 if changed {
2504 replace_packed_refs_file(git_dir, &packed_path, &out, lock_already_held)?;
2505 }
2506
2507 Ok(removed)
2508}
2509
2510fn replace_packed_refs_file(
2512 git_dir: &Path,
2513 packed_path: &Path,
2514 body: &str,
2515 lock_already_held: bool,
2516) -> Result<()> {
2517 let lock = lock_path_for_ref(packed_path); let _packed_lock = if !lock_already_held {
2525 let abs_lock = fs::canonicalize(git_dir)
2526 .map(|d| d.join("packed-refs.lock"))
2527 .unwrap_or_else(|_| lock.clone());
2528 Some(acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?)
2529 } else {
2530 None
2531 };
2532
2533 let tmp = packed_path.with_extension("new");
2534 let mut created_tmp = false;
2535 let write_result = (|| -> Result<()> {
2536 let mut file = std::fs::OpenOptions::new()
2538 .write(true)
2539 .create_new(true)
2540 .open(&tmp)
2541 .map_err(Error::Io)?;
2542 created_tmp = true;
2543 use std::io::Write as _;
2544 file.write_all(body.as_bytes()).map_err(Error::Io)?;
2545 drop(file);
2546 fs::rename(&tmp, packed_path).map_err(Error::Io)?;
2547 created_tmp = false; Ok(())
2549 })();
2550
2551 if write_result.is_err() && created_tmp {
2552 let _ = fs::remove_file(&tmp);
2553 }
2554 write_result
2555}
2556
2557pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
2565 read_symbolic_ref(git_dir, "HEAD")
2566}
2567
2568pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
2575 match open_ref_store(git_dir)?.read_raw(refname)? {
2576 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
2577 Some(StoreRawRef::Direct(_)) | None => Ok(None),
2578 }
2579}
2580
2581pub fn read_symbolic_ref_for_repository(
2583 repo: &crate::repo::Repository,
2584 refname: &str,
2585) -> Result<Option<String>> {
2586 match repo.refs().read_raw(refname)? {
2587 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
2588 Some(StoreRawRef::Direct(_)) | None => Ok(None),
2589 }
2590}
2591
2592#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2594pub enum LogRefsConfig {
2595 Unset,
2597 None,
2599 Normal,
2601 Always,
2603}
2604
2605impl LogRefsConfig {
2606 pub(crate) fn parse(raw: &str) -> Option<Self> {
2608 match raw.trim().to_ascii_lowercase().as_str() {
2609 "always" => Some(Self::Always),
2610 "1" | "true" | "yes" | "on" => Some(Self::Normal),
2611 "0" | "false" | "no" | "off" | "never" => Some(Self::None),
2612 _ => None,
2613 }
2614 }
2615}
2616
2617pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
2621 ConfigSet::load(
2622 &crate::environment::Environment::empty(),
2623 Some(git_dir),
2624 true,
2625 )
2626 .map(|cfg| cfg.log_refs_config())
2627 .unwrap_or(LogRefsConfig::Unset)
2628}
2629
2630pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
2634 ConfigSet::load(
2635 &crate::environment::Environment::empty(),
2636 Some(git_dir),
2637 true,
2638 )
2639 .map(|cfg| cfg.effective_log_refs_config())
2640 .unwrap_or(LogRefsConfig::Normal)
2641}
2642
2643#[must_use]
2646pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
2647 match mode {
2648 LogRefsConfig::Always => true,
2649 LogRefsConfig::Normal => {
2650 refname == "HEAD"
2651 || refname.starts_with("refs/heads/")
2652 || refname.starts_with("refs/remotes/")
2653 || refname.starts_with("refs/notes/")
2654 }
2655 LogRefsConfig::None | LogRefsConfig::Unset => false,
2656 }
2657}
2658
2659#[must_use]
2661pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
2662 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
2663}
2664
2665#[must_use]
2667pub fn should_autocreate_reflog_with_config(
2668 config: &ConfigSet,
2669 _git_dir: &Path,
2670 refname: &str,
2671) -> bool {
2672 should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
2673}
2674
2675fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
2701 let Ok(rel) = target.strip_prefix(logs_root) else {
2702 return;
2703 };
2704 let mut cur = logs_root.to_path_buf();
2705 for component in rel.components() {
2706 cur.push(component);
2707 match fs::symlink_metadata(&cur) {
2708 Ok(meta) if meta.file_type().is_dir() => {}
2709 Ok(_) => {
2710 let _ = fs::remove_file(&cur);
2713 }
2714 Err(_) => break, }
2716 }
2717}
2718
2719pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
2722 let width = if !old.is_zero() {
2723 old.algo().hex_len()
2724 } else if !new.is_zero() {
2725 new.algo().hex_len()
2726 } else {
2727 old.algo().hex_len()
2728 };
2729 let render = |oid: &ObjectId| {
2730 if oid.is_zero() {
2731 "0".repeat(width)
2732 } else {
2733 oid.to_hex()
2734 }
2735 };
2736 (render(old), render(new))
2737}
2738
2739fn map_reflog_store_validation(err: store::RefStoreError) -> Error {
2740 match err {
2741 store::RefStoreError::InvalidReflog(sig) => Error::InvalidSignature(sig),
2742 other => Error::from(other),
2743 }
2744}
2745
2746fn validate_reflog_message(message: &str) -> Result<()> {
2747 store::validate_reflog_message(message).map_err(map_reflog_store_validation)
2748}
2749
2750fn validate_reflog_identity_string(identity: &str) -> Result<()> {
2751 store::validate_reflog_identity_string(identity).map_err(map_reflog_store_validation)
2752}
2753
2754fn validate_reflog_update(log: &ReflogUpdate) -> Result<()> {
2756 store::validate_reflog_update(log).map_err(map_reflog_store_validation)
2757}
2758
2759pub fn append_reflog_with_signature(
2765 git_dir: &Path,
2766 refname: &str,
2767 old_oid: &ObjectId,
2768 new_oid: &ObjectId,
2769 identity: &Signature,
2770 message: &str,
2771 force_create: bool,
2772) -> Result<()> {
2773 identity.validate().map_err(Error::InvalidSignature)?;
2774 let formatted = format_reflog_identity(&identity.reflog_name_email(), identity.time);
2775 append_reflog_with_config(
2776 git_dir,
2777 refname,
2778 old_oid,
2779 new_oid,
2780 &formatted,
2781 message,
2782 force_create,
2783 None,
2784 )
2785}
2786
2787pub fn append_reflog(
2796 git_dir: &Path,
2797 refname: &str,
2798 old_oid: &ObjectId,
2799 new_oid: &ObjectId,
2800 identity: &str,
2801 message: &str,
2802 force_create: bool,
2803) -> Result<()> {
2804 append_reflog_with_config(
2805 git_dir,
2806 refname,
2807 old_oid,
2808 new_oid,
2809 identity,
2810 message,
2811 force_create,
2812 None,
2813 )
2814}
2815
2816#[allow(clippy::too_many_arguments)]
2818pub fn append_reflog_with_config(
2819 git_dir: &Path,
2820 refname: &str,
2821 old_oid: &ObjectId,
2822 new_oid: &ObjectId,
2823 identity: &str,
2824 message: &str,
2825 force_create: bool,
2826 config: Option<&ConfigSet>,
2827) -> Result<()> {
2828 validate_reflog_identity_string(identity)?;
2829 validate_reflog_message(message)?;
2830 test_inject_reflog_fail(refname)?;
2831 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2832 return crate::reftable::reftable_append_reflog(
2833 git_dir,
2834 refname,
2835 old_oid,
2836 new_oid,
2837 identity,
2838 message,
2839 force_create,
2840 );
2841 }
2842 let storage_dir = ref_storage_dir(git_dir, refname);
2843 let stor = crate::ref_namespace::storage_ref_name(refname);
2844 let log_path = storage_dir.join("logs").join(&stor);
2845 let may_create = force_create
2846 || match config {
2847 Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
2848 None => should_autocreate_reflog(git_dir, refname),
2849 };
2850 if !may_create && !log_path.exists() {
2851 return Ok(());
2852 }
2853 if let Some(parent) = log_path.parent() {
2854 let logs_root = storage_dir.join("logs");
2859 clear_conflicting_reflog_files(&logs_root, parent);
2860 crate::git_dir::io_error::io_with_git_path(
2861 git_dir,
2862 parent,
2863 crate::git_dir::GitDirPathOperation::CreateDir,
2864 fs::create_dir_all(parent),
2865 )?;
2866 }
2867 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
2872 let line = if message.is_empty() {
2873 format!("{old_hex} {new_hex} {identity}\n")
2874 } else {
2875 format!("{old_hex} {new_hex} {identity}\t{message}\n")
2876 };
2877 let mut file = fs::OpenOptions::new()
2878 .create(true)
2879 .append(true)
2880 .open(&log_path)
2881 .map_err(|source| {
2882 crate::git_dir::io_error::map_git_dir_io(
2883 git_dir,
2884 &log_path,
2885 crate::git_dir::GitDirPathOperation::AppendReflog,
2886 source,
2887 )
2888 })?;
2889 use io::Write;
2890 crate::git_dir::io_error::io_with_git_path(
2891 git_dir,
2892 &log_path,
2893 crate::git_dir::GitDirPathOperation::AppendReflog,
2894 file.write_all(line.as_bytes()),
2895 )?;
2896 Ok(())
2897}
2898
2899fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
2900 if all {
2901 b"*"
2902 } else {
2903 b"refs/tags/*"
2904 }
2905}
2906
2907fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
2908 crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
2909}
2910
2911fn collect_loose_ref_files(
2912 dir: &Path,
2913 prefix: &str,
2914 out: &mut Vec<(String, PathBuf)>,
2915) -> Result<()> {
2916 let read = match fs::read_dir(dir) {
2917 Ok(r) => r,
2918 Err(e)
2919 if e.kind() == io::ErrorKind::NotFound
2920 || e.kind() == io::ErrorKind::NotADirectory
2921 || e.raw_os_error() == Some(libc::ENOTDIR) =>
2922 {
2923 return Ok(());
2924 }
2925 Err(e) => return Err(Error::Io(e)),
2926 };
2927 for entry in read {
2928 let entry = entry?;
2929 let name = entry.file_name().to_string_lossy().to_string();
2930 let refname = format!("{prefix}{name}");
2931 let path = entry.path();
2932 if path.is_dir() {
2933 collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
2934 } else if path.is_file() {
2935 out.push((refname, path));
2936 }
2937 }
2938 Ok(())
2939}
2940
2941fn load_packed_refs_with_peel(
2942 packed_path: &Path,
2943 merged: &mut HashMap<String, (ObjectId, Option<ObjectId>)>,
2944) -> Result<()> {
2945 if !packed_path.is_file() {
2946 return Ok(());
2947 }
2948 let data = fs::read(packed_path)?;
2949 parse_packed_refs_to_map(packed_path, &data)?;
2950 let content = String::from_utf8(data)
2951 .map_err(|_| packed_refs_unexpected_line(packed_path, "<invalid utf-8>"))?;
2952 let mut last_name: Option<String> = None;
2953 for line in content.lines() {
2954 if line.starts_with('#') {
2955 continue;
2956 }
2957 if let Some(hex) = line.strip_prefix('^') {
2958 let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
2959 continue;
2960 };
2961 if let Some(name) = &last_name {
2962 if let Some(entry) = merged.get_mut(name) {
2963 entry.1 = Some(peel_oid);
2964 }
2965 }
2966 continue;
2967 }
2968 let mut parts = line.splitn(2, ' ');
2969 let Some(hex) = parts.next() else {
2970 continue;
2971 };
2972 let Some(name) = parts.next() else {
2973 continue;
2974 };
2975 let name = name.trim();
2976 let Ok(oid) = ObjectId::from_hex(hex) else {
2977 continue;
2978 };
2979 merged.insert(name.to_owned(), (oid, None));
2980 last_name = Some(name.to_owned());
2981 }
2982 Ok(())
2983}
2984
2985pub(crate) fn pack_refs_for_repository(
2987 repo: &crate::repo::Repository,
2988 all: bool,
2989 dry_run: bool,
2990) -> Result<(PathBuf, usize, usize)> {
2991 let git_dir = repo.git_dir();
2992 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2993 return pack_refs_reftable(git_dir, dry_run);
2994 }
2995 pack_refs_files(git_dir, repo.odb(), all, dry_run)
2996}
2997
2998fn pack_refs_reftable(git_dir: &Path, dry_run: bool) -> Result<(PathBuf, usize, usize)> {
2999 use crate::reftable::ReftableStack;
3000 let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3001 let rt_dir = store_git_dir.join("reftable");
3002 let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
3003 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3004 .unwrap_or(0);
3005 if dry_run {
3006 let tables_pruned = tables_before.saturating_sub(1);
3007 return Ok((rt_dir, 0, tables_pruned));
3008 }
3009 let mut stack = ReftableStack::open(&store_git_dir)?;
3010 stack.compact()?;
3011 let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
3012 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3013 .unwrap_or(0);
3014 Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
3015}
3016
3017fn pack_refs_files(
3018 git_dir: &Path,
3019 odb: &crate::odb::Odb,
3020 all: bool,
3021 dry_run: bool,
3022) -> Result<(PathBuf, usize, usize)> {
3023 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3024 let packed_path = store::paths::packed_refs_path(&storage_git_dir);
3025
3026 let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
3027 load_packed_refs_with_peel(&packed_path, &mut merged)?;
3028
3029 let mut loose_files = Vec::new();
3030 collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
3031
3032 let mut packed_loose = 0usize;
3033 let mut to_prune: Vec<(String, ObjectId)> = Vec::new();
3034
3035 for (refname, path) in loose_files {
3036 if crate::worktree_ref::is_per_worktree_ref(&refname) {
3037 continue;
3038 }
3039 if !ref_matches_pack_include(&refname, all) {
3040 continue;
3041 }
3042 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
3043 continue;
3044 };
3045 if odb.read(&oid).is_err() {
3046 continue;
3047 }
3048 let peel = if refname.starts_with("refs/tags/") {
3049 packed_ref_peel(odb, oid)
3050 } else {
3051 None
3052 };
3053 merged.insert(refname.clone(), (oid, peel));
3054 to_prune.push((refname, oid));
3055 let _ = path;
3056 packed_loose += 1;
3057 }
3058
3059 if packed_loose == 0 && merged.is_empty() {
3060 return Ok((packed_path, 0, 0));
3061 }
3062
3063 if dry_run {
3064 return Ok((packed_path, packed_loose, to_prune.len()));
3065 }
3066
3067 let mut names: Vec<String> = merged.keys().cloned().collect();
3068 names.sort();
3069
3070 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
3071 for name in &names {
3072 let Some((oid, peel)) = merged.get(name) else {
3073 continue;
3074 };
3075 body.push_str(&oid.to_hex());
3076 body.push(' ');
3077 body.push_str(name);
3078 body.push('\n');
3079 if let Some(p) = peel {
3080 body.push('^');
3081 body.push_str(&p.to_hex());
3082 body.push('\n');
3083 }
3084 }
3085
3086 atomic_rewrite_packed_refs(&storage_git_dir, &body)?;
3087
3088 #[cfg(test)]
3089 pack_refs_test_hooks::run_before_prune();
3090
3091 let mut pruned = 0usize;
3092 for (refname, expected) in &to_prune {
3093 match prune_loose_ref_cas(git_dir, refname, *expected) {
3094 Ok(true) => pruned += 1,
3095 Ok(false) => {
3096 remove_packed_ref(git_dir, refname)?;
3097 }
3098 Err(e) => return Err(e),
3099 }
3100 }
3101
3102 Ok((packed_path, packed_loose, pruned))
3103}
3104
3105#[cfg(test)]
3107mod pack_refs_test_hooks {
3108 use std::cell::RefCell;
3109
3110 thread_local! {
3111 static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
3112 }
3113
3114 pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
3115 BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
3116 }
3117
3118 pub fn run_before_prune() {
3119 BEFORE_PRUNE.with(|c| {
3120 if let Some(f) = c.borrow_mut().take() {
3121 f();
3122 }
3123 });
3124 }
3125}
3126
3127#[cfg(test)]
3131mod reflog_fail_inject {
3132 use std::cell::RefCell;
3133
3134 thread_local! {
3136 pub static INJECT: RefCell<Option<String>> = const { RefCell::new(None) };
3137 }
3138}
3139
3140#[cfg(test)]
3142pub fn set_test_inject_reflog_fail(refname: Option<&str>) {
3143 reflog_fail_inject::INJECT.with(|c| {
3144 *c.borrow_mut() = refname.map(str::to_owned);
3145 });
3146}
3147
3148#[cfg(test)]
3149fn test_inject_reflog_fail(refname: &str) -> Result<()> {
3150 reflog_fail_inject::INJECT.with(|c| {
3151 if c.borrow().as_deref() == Some(refname) {
3152 return Err(Error::Io(std::io::Error::other("injected reflog failure")));
3153 }
3154 Ok(())
3155 })
3156}
3157
3158#[cfg(not(test))]
3159fn test_inject_reflog_fail(_refname: &str) -> Result<()> {
3160 Ok(())
3161}
3162
3163pub struct BranchCommitRefUpdate<'a> {
3176 pub branch_ref: &'a str,
3178 pub expected_old: Option<ObjectId>,
3180 pub new_oid: ObjectId,
3182 pub identity: &'a str,
3184 pub reflog_message: &'a str,
3186}
3187
3188pub fn verify_branch_commit_cas(
3198 expected: Option<ObjectId>,
3199 current: Option<ObjectId>,
3200 refname: &str,
3201) -> Result<()> {
3202 verify_branch_cas(expected, current, refname)
3203}
3204
3205fn verify_branch_cas(
3206 expected: Option<ObjectId>,
3207 current: Option<ObjectId>,
3208 refname: &str,
3209) -> Result<()> {
3210 if expected == current {
3211 return Ok(());
3212 }
3213 Err(store::RefStoreError::ExpectedMismatch {
3214 name: refname.to_owned(),
3215 expected: expected.map_or(Expected::Missing, Expected::Oid),
3216 actual: current.map(StoreRawRef::Direct),
3217 }
3218 .into())
3219}
3220
3221fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
3222 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
3223 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
3224 let try_names: Vec<&str> = if storage_owned != stor_name {
3225 vec![storage_owned.as_str(), stor_name.as_str()]
3226 } else {
3227 vec![stor_name.as_str()]
3228 };
3229 for name in try_names {
3230 let path = store.join(name);
3231 if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
3232 return Ok(Some(oid));
3233 }
3234 if let Some(oid) = lookup_packed_ref(&store, name)? {
3235 return Ok(Some(oid));
3236 }
3237 }
3238 Ok(None)
3239}
3240
3241#[cfg(test)]
3242mod test_pause_inject_serial {
3243 use std::sync::{Mutex, OnceLock};
3244
3245 static SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
3247
3248 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
3249 SERIAL
3250 .get_or_init(|| Mutex::new(()))
3251 .lock()
3252 .unwrap_or_else(|e| e.into_inner())
3253 }
3254}
3255
3256#[cfg(test)]
3258pub fn test_pause_injection_serial_guard() -> std::sync::MutexGuard<'static, ()> {
3259 test_pause_inject_serial::lock()
3260}
3261
3262#[cfg(test)]
3263mod branch_ref_pause_inject {
3264 use std::sync::{Condvar, Mutex, OnceLock};
3265
3266 pub struct PauseGate {
3267 pub wait: Mutex<bool>,
3268 pub cv: Condvar,
3269 }
3270
3271 static PAUSE_BEFORE_COMMIT: OnceLock<Mutex<Option<std::sync::Arc<PauseGate>>>> =
3273 OnceLock::new();
3274
3275 pub fn pause_slot() -> &'static Mutex<Option<std::sync::Arc<PauseGate>>> {
3276 PAUSE_BEFORE_COMMIT.get_or_init(|| Mutex::new(None))
3277 }
3278}
3279
3280#[cfg(test)]
3282pub type BranchRefPauseGate = std::sync::Arc<branch_ref_pause_inject::PauseGate>;
3283
3284#[cfg(test)]
3286pub fn set_test_inject_branch_ref_pause_before_commit(gate: Option<BranchRefPauseGate>) {
3287 *branch_ref_pause_inject::pause_slot()
3288 .lock()
3289 .unwrap_or_else(|e| e.into_inner()) = gate;
3290}
3291
3292#[cfg(test)]
3293fn test_branch_ref_pause_before_commit() {
3294 let gate = branch_ref_pause_inject::pause_slot()
3295 .lock()
3296 .unwrap_or_else(|e| e.into_inner())
3297 .clone();
3298 if let Some(gate) = gate {
3299 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
3300 *wait = true;
3301 gate.cv.notify_all();
3302 while *wait {
3303 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
3304 }
3305 }
3306}
3307
3308#[cfg(not(test))]
3309fn test_branch_ref_pause_before_commit() {}
3310
3311#[cfg(test)]
3312mod ref_store_batch_pause_inject {
3313 use std::sync::{Condvar, Mutex, OnceLock};
3314
3315 pub struct PauseGate {
3316 pub wait: Mutex<bool>,
3317 pub cv: Condvar,
3318 }
3319
3320 pub type PauseSlot = Option<(usize, std::path::PathBuf, std::sync::Arc<PauseGate>)>;
3323
3324 static PAUSE_AFTER_REF_WRITE: OnceLock<Mutex<PauseSlot>> = OnceLock::new();
3325
3326 pub fn pause_slot() -> &'static Mutex<PauseSlot> {
3327 PAUSE_AFTER_REF_WRITE.get_or_init(|| Mutex::new(None))
3328 }
3329}
3330
3331#[cfg(test)]
3333pub type RefStoreBatchPauseGate = std::sync::Arc<ref_store_batch_pause_inject::PauseGate>;
3334
3335#[cfg(test)]
3337pub fn new_ref_store_batch_pause_gate() -> RefStoreBatchPauseGate {
3338 std::sync::Arc::new(ref_store_batch_pause_inject::PauseGate {
3339 wait: std::sync::Mutex::new(false),
3340 cv: std::sync::Condvar::new(),
3341 })
3342}
3343
3344#[cfg(test)]
3349pub fn set_test_inject_ref_store_batch_pause_after_apply(
3350 apply_index: usize,
3351 git_dir: &Path,
3352 gate: Option<RefStoreBatchPauseGate>,
3353) {
3354 *ref_store_batch_pause_inject::pause_slot()
3355 .lock()
3356 .unwrap_or_else(|e| e.into_inner()) = gate.map(|g| (apply_index, git_dir.to_path_buf(), g));
3357}
3358
3359#[cfg(test)]
3360struct RefStorePauseTestSerial {
3361 holder: Option<std::thread::ThreadId>,
3363}
3364
3365#[cfg(test)]
3366static REF_STORE_PAUSE_TEST_SERIAL: std::sync::Mutex<RefStorePauseTestSerial> =
3367 std::sync::Mutex::new(RefStorePauseTestSerial { holder: None });
3368
3369#[cfg(test)]
3371pub(crate) struct RefStoreBatchPauseTestScope {
3372 _serial: std::sync::MutexGuard<'static, RefStorePauseTestSerial>,
3373}
3374
3375#[cfg(test)]
3376pub(crate) fn ref_store_batch_pause_test_scope() -> RefStoreBatchPauseTestScope {
3377 let mut serial = REF_STORE_PAUSE_TEST_SERIAL
3378 .lock()
3379 .unwrap_or_else(|e| e.into_inner());
3380 assert!(
3381 serial.holder.is_none(),
3382 "ref_store_batch_pause_test_scope must not nest"
3383 );
3384 serial.holder = Some(std::thread::current().id());
3385 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
3386 RefStoreBatchPauseTestScope { _serial: serial }
3387}
3388
3389#[cfg(test)]
3390impl Drop for RefStoreBatchPauseTestScope {
3391 fn drop(&mut self) {
3392 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
3393 self._serial.holder = None;
3394 }
3395}
3396
3397#[cfg(test)]
3398pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
3399 let slot = ref_store_batch_pause_inject::pause_slot()
3400 .lock()
3401 .unwrap_or_else(|e| e.into_inner())
3402 .clone();
3403 if let Some((target, target_dir, gate)) = slot {
3404 let same_repo = |a: &Path, b: &Path| {
3405 a == b || matches!((a.canonicalize(), b.canonicalize()), (Ok(a), Ok(b)) if a == b)
3406 };
3407 if idx != target || !same_repo(git_dir, &target_dir) {
3408 return;
3409 }
3410 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
3411 *wait = true;
3412 gate.cv.notify_all();
3413 while *wait {
3414 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
3415 }
3416 }
3417}
3418
3419#[cfg(not(test))]
3420pub(crate) fn test_ref_store_batch_pause_after_ref_write(_idx: usize, _git_dir: &Path) {}
3421
3422pub struct BranchCommitRefLock {
3426 lock: Option<crate::lockfile::LockFile>,
3427 disarmed: bool,
3428}
3429
3430impl BranchCommitRefLock {
3431 pub fn acquire(
3437 git_dir: &Path,
3438 branch_ref: &str,
3439 expected_old: Option<ObjectId>,
3440 ) -> Result<Self> {
3441 ensure_refname_safe_for_storage(branch_ref)?;
3442 let storage_dir = ref_storage_dir(git_dir, branch_ref);
3443 if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
3444 return Err(Error::InvalidRef(format!(
3445 "cannot update ref '{}': reference namespace conflict",
3446 branch_ref
3447 )));
3448 }
3449 let stor = crate::ref_namespace::storage_ref_name(branch_ref);
3450 let path = storage_dir.join(&stor);
3451 remove_empty_ref_directory(&path);
3452 if fs::symlink_metadata(&path)
3453 .map(|m| m.file_type().is_dir())
3454 .unwrap_or(false)
3455 {
3456 let display = ref_path_for_display(&path);
3457 return Err(crate::error::RefLockError::DirectoryInTheWay {
3458 refname: branch_ref.to_owned(),
3459 path: display,
3460 }
3461 .into());
3462 }
3463 if let Some(parent) = path.parent() {
3464 crate::git_dir::io_error::io_with_git_path(
3465 git_dir,
3466 parent,
3467 crate::git_dir::GitDirPathOperation::CreateDir,
3468 fs::create_dir_all(parent),
3469 )?;
3470 }
3471
3472 let lock_path = lock_path_for_ref(&path);
3473 use crate::lockfile::{LockError, LockFile, LockWait};
3474 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
3475 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
3476 held @ LockError::Held { .. } => Error::from(held),
3477 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
3478 git_dir,
3479 path.as_path(),
3480 crate::git_dir::GitDirPathOperation::UpdateRef,
3481 source,
3482 ),
3483 })?;
3484
3485 let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
3486 verify_branch_cas(expected_old, current, branch_ref)?;
3487
3488 Ok(Self {
3489 lock: Some(lock),
3490 disarmed: false,
3491 })
3492 }
3493
3494 pub fn write_reflogs(
3504 &mut self,
3505 git_dir: &Path,
3506 update: &BranchCommitRefUpdate<'_>,
3507 reflog_old: &ObjectId,
3508 config: &ConfigSet,
3509 ) -> Result<()> {
3510 write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
3511 }
3512
3513 pub fn finish_ref_commit(
3521 mut self,
3522 git_dir: &Path,
3523 update: &BranchCommitRefUpdate<'_>,
3524 ) -> Result<()> {
3525 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
3526 self.disarmed = true;
3527 Ok(())
3528 }
3529
3530 pub fn commit(
3536 mut self,
3537 git_dir: &Path,
3538 update: &BranchCommitRefUpdate<'_>,
3539 reflog_old: &ObjectId,
3540 config: &ConfigSet,
3541 ) -> Result<()> {
3542 write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
3543 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
3544 self.disarmed = true;
3545 Ok(())
3546 }
3547}
3548
3549impl Drop for BranchCommitRefLock {
3550 fn drop(&mut self) {
3551 drop(self.lock.take());
3552 }
3553}
3554
3555fn write_branch_commit_reflogs(
3556 git_dir: &Path,
3557 lock: &mut BranchCommitRefLock,
3558 update: &BranchCommitRefUpdate<'_>,
3559 reflog_old: &ObjectId,
3560 config: &ConfigSet,
3561) -> Result<()> {
3562 let mut branch_reflog_written = false;
3563 if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
3564 match append_reflog_with_config(
3565 git_dir,
3566 update.branch_ref,
3567 reflog_old,
3568 &update.new_oid,
3569 update.identity,
3570 update.reflog_message,
3571 false,
3572 Some(config),
3573 ) {
3574 Ok(()) => branch_reflog_written = true,
3575 Err(err) => {
3576 drop(lock.lock.take());
3577 return Err(err);
3578 }
3579 }
3580 }
3581
3582 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
3583 if let Err(err) = append_reflog_with_config(
3584 git_dir,
3585 "HEAD",
3586 reflog_old,
3587 &update.new_oid,
3588 update.identity,
3589 update.reflog_message,
3590 false,
3591 Some(config),
3592 ) {
3593 if branch_reflog_written {
3594 let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
3595 }
3596 drop(lock.lock.take());
3597 return Err(err);
3598 }
3599 }
3600
3601 Ok(())
3602}
3603
3604fn finish_branch_commit_ref_lock_files(
3605 git_dir: &Path,
3606 lock: &mut BranchCommitRefLock,
3607 update: &BranchCommitRefUpdate<'_>,
3608) -> Result<()> {
3609 test_branch_ref_pause_before_commit();
3610
3611 let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
3612 let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
3613 let path = storage_dir.join(&stor);
3614 let content = format!("{}\n", update.new_oid);
3615 use crate::lockfile::LockError;
3616 use std::io::Write as _;
3617 let mut file_lock = lock
3618 .lock
3619 .take()
3620 .ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
3621 crate::git_dir::io_error::io_with_git_path(
3622 git_dir,
3623 &path,
3624 crate::git_dir::GitDirPathOperation::UpdateRef,
3625 file_lock.write_all(content.as_bytes()),
3626 )?;
3627 file_lock.commit_to(&path).map_err(|err| match err {
3628 LockError::Io {
3629 source,
3630 path: lock_path,
3631 } => crate::git_dir::io_error::map_git_dir_io(
3632 git_dir,
3633 lock_path.as_path(),
3634 crate::git_dir::GitDirPathOperation::UpdateRef,
3635 source,
3636 ),
3637 held @ LockError::Held { .. } => Error::from(held),
3638 })?;
3639 Ok(())
3640}
3641
3642pub(crate) fn update_branch_for_commit_files(
3643 git_dir: &Path,
3644 update: &BranchCommitRefUpdate<'_>,
3645 reflog_old: &ObjectId,
3646 config: &ConfigSet,
3647) -> Result<()> {
3648 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
3649 lock.commit(git_dir, update, reflog_old, config)
3650}
3651
3652pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
3657 let config = ConfigSet::load(
3658 &crate::environment::Environment::empty(),
3659 Some(git_dir),
3660 true,
3661 )
3662 .unwrap_or_default();
3663 update_branch_for_commit_with_config(git_dir, update, &config)
3664}
3665
3666pub fn update_branch_for_commit_with_config(
3670 git_dir: &Path,
3671 update: &BranchCommitRefUpdate<'_>,
3672 config: &ConfigSet,
3673) -> Result<()> {
3674 update_branch_for_commit_on_store(open_ref_store(git_dir)?.as_ref(), update, config)
3675}
3676
3677pub(crate) fn update_detached_head_for_commit_files(
3680 git_dir: &Path,
3681 expected_old: Option<ObjectId>,
3682 new_oid: ObjectId,
3683 identity: &str,
3684 reflog_message: &str,
3685 config: &ConfigSet,
3686) -> Result<()> {
3687 let current = resolve_ref(git_dir, "HEAD").ok();
3688 verify_branch_cas(expected_old, current, "HEAD")?;
3689 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
3690
3691 write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
3692 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
3693 if let Err(err) = append_reflog_with_config(
3694 git_dir,
3695 "HEAD",
3696 &reflog_old,
3697 &new_oid,
3698 identity,
3699 reflog_message,
3700 false,
3701 Some(config),
3702 ) {
3703 let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
3704 return Err(err);
3705 }
3706 }
3707 Ok(())
3708}
3709
3710#[must_use]
3716pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
3717 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
3718 store.join("logs").join(stor_name)
3719}
3720
3721fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
3722 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
3723}
3724
3725fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
3734 if prefix.is_empty() {
3735 return String::new();
3736 }
3737 if prefix.ends_with('/') {
3738 return prefix.to_string();
3739 }
3740 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
3741 if candidate.is_file() {
3742 prefix.to_string()
3743 } else {
3744 format!("{prefix}/")
3745 }
3746}
3747
3748fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
3751 odb.peel_tag_target(oid).ok().flatten()
3752}
3753
3754pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
3762 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3763 return Ok(());
3764 }
3765 let prefix = format!("refs/remotes/{remote}/");
3766 let head_sym = format!("refs/remotes/{remote}/HEAD");
3767 let mut entries = list_refs(git_dir, &prefix)?;
3768 entries.retain(|(name, _)| name != &head_sym);
3769 if entries.is_empty() {
3770 return Ok(());
3771 }
3772 entries.sort_by(|a, b| a.0.cmp(&b.0));
3773
3774 let odb =
3775 crate::odb::Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf());
3776 let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
3777 let packed_path = store::paths::packed_refs_path(git_dir);
3778 if let Ok(data) = fs::read(&packed_path) {
3779 parse_packed_refs_to_map(&packed_path, &data)?;
3780 let content = String::from_utf8(data)
3781 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3782 let mut last_name: Option<String> = None;
3783 for line in content.split('\n') {
3784 if line.starts_with('#') {
3785 continue;
3786 }
3787 if let Some(hex) = line.strip_prefix('^') {
3788 let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
3789 continue;
3790 };
3791 if let Some(name) = &last_name {
3792 if let Some(entry) = merged.get_mut(name) {
3793 entry.1 = Some(peel_oid);
3794 }
3795 }
3796 continue;
3797 }
3798 let mut parts = line.splitn(2, ' ');
3799 let Some(hex) = parts.next() else {
3800 continue;
3801 };
3802 let Some(name) = parts.next() else {
3803 continue;
3804 };
3805 let name = name.trim();
3806 let Ok(oid) = ObjectId::from_hex(hex) else {
3807 continue;
3808 };
3809 if name.starts_with(&prefix) {
3810 last_name = None;
3811 continue;
3812 }
3813 merged.insert(name.to_owned(), (oid, None));
3814 last_name = Some(name.to_owned());
3815 }
3816 }
3817
3818 for (name, oid) in &entries {
3819 merged.insert(name.clone(), (*oid, packed_ref_peel(&odb, *oid)));
3820 }
3821
3822 let mut names: Vec<String> = merged.keys().cloned().collect();
3823 names.sort();
3824
3825 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
3826 for name in &names {
3827 let Some((oid, peel)) = merged.get(name) else {
3828 continue;
3829 };
3830 body.push_str(&oid.to_hex());
3831 body.push(' ');
3832 body.push_str(name);
3833 body.push('\n');
3834 if let Some(p) = peel {
3835 body.push('^');
3836 body.push_str(&p.to_hex());
3837 body.push('\n');
3838 }
3839 }
3840
3841 atomic_rewrite_packed_refs(git_dir, &body)?;
3842
3843 for (name, _) in &entries {
3844 let storage_dir = ref_storage_dir(git_dir, name);
3845 let stor = crate::ref_namespace::storage_ref_name(name);
3846 let path = storage_dir.join(&stor);
3847 if path.is_file() {
3848 let _ = fs::remove_file(&path);
3849 }
3850 if let Some(parent) = path.parent() {
3851 remove_empty_ref_directory(parent);
3852 }
3853 }
3854 Ok(())
3855}
3856
3857fn atomic_rewrite_packed_refs(git_dir: &Path, body: &str) -> Result<()> {
3859 let packed_path = store::paths::packed_refs_path(git_dir);
3860 let lock = lock_path_for_ref(&packed_path);
3861 let abs_lock = fs::canonicalize(git_dir)
3862 .map(|d| d.join("packed-refs.lock"))
3863 .unwrap_or_else(|_| lock.clone());
3864 let _packed_lock = acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?;
3865
3866 let tmp = packed_path.with_extension("new");
3867 let mut created_tmp = false;
3868 let write_result = (|| -> Result<()> {
3869 let mut file = std::fs::OpenOptions::new()
3871 .write(true)
3872 .create_new(true)
3873 .open(&tmp)
3874 .map_err(Error::Io)?;
3875 created_tmp = true;
3876 use std::io::Write as _;
3877 file.write_all(body.as_bytes()).map_err(Error::Io)?;
3878 drop(file);
3879 fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
3880 created_tmp = false;
3881 Ok(())
3882 })();
3883 if write_result.is_err() && created_tmp {
3884 let _ = fs::remove_file(&tmp);
3885 }
3886 write_result
3887}
3888
3889#[derive(Debug, Clone, PartialEq, Eq)]
3891pub struct PackedRefRecord {
3892 pub refname: String,
3894 pub oid: ObjectId,
3896 pub peeled: Option<ObjectId>,
3898}
3899
3900pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
3909 let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
3910 sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
3911 for rec in &sorted {
3912 ensure_refname_safe_for_storage(&rec.refname)?;
3913 }
3914 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
3915 for rec in &sorted {
3916 body.push_str(&rec.oid.to_hex());
3917 body.push(' ');
3918 body.push_str(&rec.refname);
3919 body.push('\n');
3920 if let Some(peeled) = rec.peeled {
3921 body.push('^');
3922 body.push_str(&peeled.to_hex());
3923 body.push('\n');
3924 }
3925 }
3926 atomic_rewrite_packed_refs(git_dir, &body)?;
3927 for rec in &sorted {
3928 let storage_dir = ref_storage_dir(git_dir, &rec.refname);
3929 let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
3930 let loose = storage_dir.join(stor);
3931 if loose.is_file() {
3932 fs::remove_file(&loose).map_err(Error::Io)?;
3933 }
3934 if let Some(parent) = loose.parent() {
3935 remove_empty_ref_directory(parent);
3936 }
3937 }
3938 Ok(())
3939}
3940
3941pub fn list_refs_for_repository(
3952 repo: &crate::repo::Repository,
3953 prefix: &str,
3954) -> Result<Vec<(String, ObjectId)>> {
3955 let prefix_norm = normalize_list_refs_prefix(repo.git_dir(), prefix);
3956 list_store_refs_with_name_prefix(repo, &prefix_norm)
3957}
3958
3959fn list_store_refs_with_name_prefix(
3961 repo: &crate::repo::Repository,
3962 prefix: &str,
3963) -> Result<Vec<(String, ObjectId)>> {
3964 let git_dir = repo.git_dir();
3965 let mut results = collect_resolved_refs(repo.refs(), prefix)?;
3966 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
3967 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
3968 }
3969 results.sort_by(|a, b| a.0.cmp(&b.0));
3970 Ok(results)
3971}
3972
3973pub(crate) fn list_refs_in(
3984 repo: &crate::repo::Repository,
3985 prefix: &str,
3986) -> Result<Vec<(String, ObjectId)>> {
3987 if repo.refs_on_disk() {
3988 list_refs(repo.git_dir(), prefix)
3989 } else {
3990 list_refs_for_repository(repo, prefix)
3991 }
3992}
3993
3994pub(crate) fn resolve_ref_in(repo: &crate::repo::Repository, refname: &str) -> Result<ObjectId> {
4001 if repo.refs_on_disk() {
4002 resolve_ref(repo.git_dir(), refname)
4003 } else {
4004 repo.resolve_ref_oid(refname)
4005 }
4006}
4007
4008pub(crate) fn list_refs_glob_in(
4015 repo: &crate::repo::Repository,
4016 pattern: &str,
4017) -> Result<Vec<(String, ObjectId)>> {
4018 if repo.refs_on_disk() {
4019 list_refs_glob(repo.git_dir(), pattern)
4020 } else {
4021 list_refs_glob_for_repository(repo, pattern)
4022 }
4023}
4024
4025pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4034 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4035 let prefix = prefix_norm.as_str();
4036 let store = open_ref_store(git_dir)?;
4037 if store.format() != RefStorageFormat::Files {
4038 return crate::reftable::reftable_list_refs(git_dir, prefix);
4039 }
4040 let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
4041 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4042 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4043 }
4044 results.sort_by(|a, b| a.0.cmp(&b.0));
4045 Ok(results)
4046}
4047
4048pub(crate) fn list_refs_filtering_packed(
4059 git_dir: &Path,
4060 prefix: &str,
4061 keep: PackedRecordFilter<'_>,
4062) -> Result<Vec<(String, ObjectId)>> {
4063 let Some(store) = store::open_files_ref_store(git_dir)? else {
4064 return list_refs(git_dir, prefix);
4065 };
4066 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4067 let prefix = prefix_norm.as_str();
4068 let mut results = collect_resolved_ref_entries(&store, |f| {
4069 store.for_each_ref_filtered(prefix, Some(keep), f)
4070 })?;
4071 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4072 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4073 }
4074 results.sort_by(|a, b| a.0.cmp(&b.0));
4075 Ok(results)
4076}
4077
4078pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
4080 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
4081}
4082
4083pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4088 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4089 return crate::reftable::reftable_list_refs(git_dir, prefix);
4090 }
4091 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
4092 let stored_prefix = prefix.to_owned();
4093
4094 if let Some(cdir) = common_dir(git_dir) {
4095 if cdir != git_dir {
4096 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
4097 let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
4098 collect_loose_refs_into_map(
4099 &loose_root,
4100 &loose_prefix,
4101 &stored_prefix,
4102 &cdir,
4103 true,
4104 &mut by_name,
4105 )?;
4106 }
4107 }
4108
4109 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
4110 let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
4111 collect_loose_refs_into_map(
4112 &loose_root,
4113 &loose_prefix,
4114 &stored_prefix,
4115 git_dir,
4116 true,
4117 &mut by_name,
4118 )?;
4119
4120 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
4121 results.sort_by(|a, b| a.0.cmp(&b.0));
4122 Ok(results)
4123}
4124
4125pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
4134 let config = ConfigSet::load(
4135 &crate::environment::Environment::empty(),
4136 Some(receiving_git_dir),
4137 true,
4138 )?;
4139 let objects_dir = receiving_git_dir.join("objects");
4140 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
4141 let mut out = Vec::new();
4142 let mut seen = std::collections::HashSet::new();
4143 for alt_objects in alternates {
4144 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
4145 continue;
4146 };
4147 if !alt_git_dir.join("refs").is_dir() {
4148 continue;
4149 }
4150 if let Some(prefixes) = config
4151 .get("core.alternaterefsprefixes")
4152 .or_else(|| config.get("core.alternateRefsPrefixes"))
4153 {
4154 for part in prefixes.split_whitespace() {
4155 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
4156 if seen.insert(oid) {
4157 out.push(oid);
4158 }
4159 continue;
4160 }
4161 for (_, oid) in list_refs(&alt_git_dir, part)? {
4162 if seen.insert(oid) {
4163 out.push(oid);
4164 }
4165 }
4166 }
4167 } else {
4168 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
4169 if seen.insert(oid) {
4170 out.push(oid);
4171 }
4172 }
4173 }
4174 }
4175 Ok(out)
4176}
4177
4178pub fn list_refs_glob_for_repository(
4180 repo: &crate::repo::Repository,
4181 pattern: &str,
4182) -> Result<Vec<(String, ObjectId)>> {
4183 let all = match pattern.find(['*', '?', '[']) {
4187 Some(pos) => list_store_refs_with_name_prefix(repo, &pattern[..pos])?,
4188 None => {
4189 let mut dir = pattern.trim_end_matches('/').to_owned();
4190 if !dir.is_empty() {
4191 dir.push('/');
4192 }
4193 list_refs_for_repository(repo, &dir)?
4194 }
4195 };
4196 Ok(all
4197 .into_iter()
4198 .filter(|(refname, _)| ref_matches_glob(refname, pattern))
4199 .collect())
4200}
4201
4202pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
4204 let glob_pos = pattern.find(['*', '?', '[']);
4205 let prefix_owned: String = match glob_pos {
4206 Some(pos) => pattern[..pos].to_owned(),
4208 None => {
4209 let mut p = pattern.trim_end_matches('/').to_owned();
4210 if !p.is_empty() {
4211 p.push('/');
4212 }
4213 p
4214 }
4215 };
4216 let prefix = prefix_owned.as_str();
4217 let all = list_refs(git_dir, prefix)?;
4218 let mut results = Vec::new();
4219 for (refname, oid) in all {
4220 if ref_matches_glob(&refname, pattern) {
4221 results.push((refname, oid));
4222 }
4223 }
4224 Ok(results)
4225}
4226
4227pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
4231 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
4233 return refname == pattern
4234 || refname.ends_with(&format!("/{pattern}"))
4235 || refname.starts_with(&format!("{pattern}/"));
4236 }
4237 glob_match(pattern, refname)
4238}
4239
4240fn glob_match(pattern: &str, text: &str) -> bool {
4241 let pat = pattern.as_bytes();
4242 let txt = text.as_bytes();
4243 let (mut pi, mut ti) = (0, 0);
4244 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
4245 while ti < txt.len() {
4246 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
4247 pi += 1;
4248 ti += 1;
4249 } else if pi < pat.len() && pat[pi] == b'*' {
4250 star_pi = pi;
4251 star_ti = ti;
4252 pi += 1;
4253 } else if star_pi != usize::MAX {
4254 pi = star_pi + 1;
4255 star_ti += 1;
4256 ti = star_ti;
4257 } else {
4258 return false;
4259 }
4260 }
4261 while pi < pat.len() && pat[pi] == b'*' {
4262 pi += 1;
4263 }
4264 pi == pat.len()
4265}
4266
4267fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
4269 let content = fs::read_to_string(path).ok()?;
4270 let content = content.trim_end_matches('\n').trim();
4271 if ObjectId::is_full_hex(content) {
4272 content.parse().ok()
4273 } else {
4274 None
4275 }
4276}
4277
4278fn collect_loose_refs_into_map(
4279 dir: &Path,
4280 prefix: &str,
4281 filter_prefix: &str,
4282 resolve_git_dir: &Path,
4283 physical_keys: bool,
4284 out: &mut HashMap<String, ObjectId>,
4285) -> Result<()> {
4286 let read = match fs::read_dir(dir) {
4287 Ok(r) => r,
4288 Err(e)
4289 if e.kind() == io::ErrorKind::NotFound
4290 || e.kind() == io::ErrorKind::NotADirectory
4291 || e.raw_os_error() == Some(libc::ENOTDIR) =>
4292 {
4293 return Ok(());
4294 }
4295 Err(e) => return Err(Error::Io(e)),
4296 };
4297
4298 for entry in read {
4299 let entry = entry?;
4300 let name = entry.file_name();
4301 let name_str = name.to_string_lossy();
4302 let refname = format!("{prefix}{name_str}");
4303 let path = entry.path();
4304 let meta = match fs::metadata(&path) {
4305 Ok(m) => m,
4306 Err(_) => continue,
4307 };
4308
4309 if meta.is_dir() {
4310 collect_loose_refs_into_map(
4311 &path,
4312 &format!("{refname}/"),
4313 filter_prefix,
4314 resolve_git_dir,
4315 physical_keys,
4316 out,
4317 )?;
4318 } else if meta.is_file() {
4319 if physical_keys {
4320 if let Some(oid) = loose_ref_file_direct_oid(&path) {
4321 out.insert(refname, oid);
4322 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
4323 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
4324 out.insert(refname, oid);
4325 }
4326 }
4327 } else {
4328 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
4329 .unwrap_or_else(|| refname.clone());
4330 if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
4331 continue;
4332 }
4333 if let Some(oid) = loose_ref_file_direct_oid(&path) {
4334 out.insert(logical, oid);
4335 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
4336 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
4337 out.insert(logical, oid);
4338 }
4339 }
4340 }
4341 }
4342 }
4343 Ok(())
4344}
4345
4346pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
4349 let inner = spec
4351 .strip_prefix("@{-")
4352 .and_then(|s| s.strip_suffix('}'))
4353 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
4354 let n: usize = inner
4355 .parse()
4356 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
4357 if n == 0 {
4358 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
4359 }
4360 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
4361 let mut count = 0usize;
4362 for entry in entries.iter().rev() {
4363 let msg = &entry.message;
4364 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
4365 count += 1;
4366 if count == n {
4367 if let Some(to_pos) = rest.find(" to ") {
4368 return Ok(rest[..to_pos].to_string());
4369 }
4370 }
4371 }
4372 }
4373 Err(Error::InvalidRef(format!(
4374 "{spec}: only {count} checkout(s) in reflog"
4375 )))
4376}
4377
4378fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
4379 let prefix = prefix.trim_end_matches('/');
4380 if prefix.is_empty() {
4381 return true;
4382 }
4383 if refname.starts_with(prefix) {
4384 return true;
4385 }
4386 if refname == prefix {
4387 return true;
4388 }
4389 false
4390}
4391
4392fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
4393 let base = prefix.trim_end_matches('/');
4394 let (head, tail) = base.rsplit_once('/')?;
4395 let mut chars: Vec<char> = tail.chars().collect();
4396 while let Some(last) = chars.pop() {
4397 if last != '\u{10ffff}' {
4398 chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
4399 return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
4400 }
4401 }
4402 None
4403}
4404
4405pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
4408 let trimmed = prefix.trim_end_matches('/');
4409 if trimmed.is_empty() {
4410 return (git_dir.join("refs"), "refs/".to_owned());
4411 }
4412 let direct = git_dir.join(trimmed);
4413 if direct.is_dir() {
4414 return (direct, format!("{trimmed}/"));
4415 }
4416 if let Some(slash) = trimmed.rfind('/') {
4417 let parent = &trimmed[..slash];
4418 return (git_dir.join(parent), format!("{parent}/"));
4419 }
4420 (git_dir.to_path_buf(), String::new())
4421}
4422
4423fn collect_packed_refs_into_map(
4424 git_dir: &Path,
4425 prefix: &str,
4426 physical_keys: bool,
4427 out: &mut HashMap<String, ObjectId>,
4428) -> Result<()> {
4429 let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
4430 for (refname, oid) in map {
4431 let key = if physical_keys {
4432 refname.clone()
4433 } else {
4434 crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
4435 };
4436 out.insert(key, oid);
4437 }
4438 Ok(())
4439}
4440
4441#[cfg(test)]
4442mod update_branch_for_commit_tests {
4443 use super::*;
4444 use crate::reflog::read_reflog;
4445 use crate::repo::init_repository;
4446 use std::sync::{Arc, Condvar, Mutex};
4447 use std::thread;
4448 use tempfile::TempDir;
4449
4450 fn sample_oid(hex: &str) -> ObjectId {
4451 hex.parse().unwrap()
4452 }
4453
4454 #[test]
4455 fn update_branch_for_commit_holds_lock_until_ref_commit() {
4456 let _pause_serial = super::test_pause_injection_serial_guard();
4457 let tmp = TempDir::new().unwrap();
4458 let repo = init_repository(
4459 tmp.path(),
4460 false,
4461 "main",
4462 None,
4463 crate::RefStorageFormat::Files,
4464 )
4465 .unwrap();
4466 let git_dir = repo.git_dir().to_path_buf();
4467 let oid1 = sample_oid("67bf698f3ab735e92fb011a99cff3497c44d30c1");
4468 let oid2 = sample_oid("1111111111111111111111111111111111111111");
4469 let oid3 = sample_oid("2222222222222222222222222222222222222222");
4470 write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
4471
4472 let gate = Arc::new(branch_ref_pause_inject::PauseGate {
4473 wait: Mutex::new(false),
4474 cv: Condvar::new(),
4475 });
4476 set_test_inject_branch_ref_pause_before_commit(Some(gate.clone()));
4477
4478 let git_dir_thread = git_dir.clone();
4479 let handle = thread::spawn(move || {
4480 update_branch_for_commit(
4481 &git_dir_thread,
4482 &BranchCommitRefUpdate {
4483 branch_ref: "refs/heads/main",
4484 expected_old: Some(oid1),
4485 new_oid: oid2,
4486 identity: "T <t@e.com> 1 +0000",
4487 reflog_message: "commit: test",
4488 },
4489 )
4490 });
4491
4492 let mut wait = gate.wait.lock().unwrap();
4493 while !*wait {
4494 wait = gate.cv.wait(wait).unwrap();
4495 }
4496
4497 assert!(
4498 write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
4499 "concurrent branch update must block on the held ref lock"
4500 );
4501
4502 *wait = false;
4503 gate.cv.notify_all();
4504 drop(wait);
4505 handle.join().unwrap().unwrap();
4506 set_test_inject_branch_ref_pause_before_commit(None);
4507
4508 assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
4509 }
4510
4511 #[test]
4512 fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
4513 let tmp = TempDir::new().unwrap();
4514 let repo = init_repository(
4515 tmp.path(),
4516 false,
4517 "main",
4518 None,
4519 crate::RefStorageFormat::Reftable,
4520 )
4521 .unwrap();
4522 let git_dir = repo.git_dir().to_path_buf();
4523 let new_oid = sample_oid("3333333333333333333333333333333333333333");
4524
4525 crate::reftable::set_test_inject_reftable_transaction_fail(true);
4526 let err = update_branch_for_commit(
4527 &git_dir,
4528 &BranchCommitRefUpdate {
4529 branch_ref: "refs/heads/main",
4530 expected_old: None,
4531 new_oid,
4532 identity: "T <t@e.com> 1 +0000",
4533 reflog_message: "commit (initial): test",
4534 },
4535 )
4536 .unwrap_err();
4537 crate::reftable::set_test_inject_reftable_transaction_fail(false);
4538 assert!(!matches!(err, Error::Message(_)));
4539
4540 assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
4541 assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
4542 assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
4543 }
4544}
4545
4546#[cfg(test)]
4547mod refname_available_tests {
4548 use super::*;
4549 use std::collections::{BTreeSet, HashSet};
4550 use tempfile::tempdir;
4551
4552 #[test]
4553 fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
4554 let dir = tempdir().unwrap();
4555 let git_dir = dir.path();
4556 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4557 let items = vec![
4558 RefBatchItem {
4559 name: "refs/heads/c".to_owned(),
4560 new_oid: Some(sample_oid()),
4561 },
4562 RefBatchItem {
4563 name: "refs/heads/c/x".to_owned(),
4564 new_oid: Some(sample_oid()),
4565 },
4566 ];
4567 let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
4568 assert!(matches!(
4569 err,
4570 RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
4571 ));
4572 }
4573
4574 fn sample_oid() -> ObjectId {
4575 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
4576 }
4577
4578 #[test]
4579 fn loose_parent_blocks_child_create() {
4580 let dir = tempdir().unwrap();
4581 let git_dir = dir.path();
4582 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
4583 fs::write(
4584 git_dir.join("refs/1l/c"),
4585 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4586 )
4587 .unwrap();
4588 assert_eq!(
4589 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
4590 RawRefLookup::Exists
4591 );
4592 let extras = BTreeSet::from([
4593 "refs/1l/b".to_string(),
4594 "refs/1l/c/x".to_string(),
4595 "refs/1l/d".to_string(),
4596 ]);
4597 let skip = HashSet::new();
4598 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
4599 .unwrap_err();
4600 assert!(matches!(
4601 err,
4602 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
4603 ));
4604 }
4605
4606 #[test]
4607 fn verify_sees_loose_ref_after_canonical_git_dir() {
4608 let dir = tempdir().unwrap();
4609 let git_dir = dir.path().join(".git");
4610 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
4611 fs::write(
4612 git_dir.join("refs/1l/c"),
4613 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4614 )
4615 .unwrap();
4616 let skip = HashSet::new();
4617 let extras = BTreeSet::new();
4618 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
4619 .unwrap_err();
4620 assert!(matches!(
4621 err,
4622 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
4623 ));
4624 }
4625
4626 fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
4627 if existing == candidate {
4628 return false;
4629 }
4630 existing
4631 .strip_prefix(candidate)
4632 .is_some_and(|rest| rest.starts_with('/'))
4633 || candidate
4634 .strip_prefix(existing)
4635 .is_some_and(|rest| rest.starts_with('/'))
4636 }
4637
4638 #[test]
4639 fn packed_namespace_index_matches_pairwise_scan() {
4640 let mut sorted: BTreeSet<String> = BTreeSet::new();
4641 for i in 0..500 {
4642 sorted.insert(format!("refs/heads/b{i:05}"));
4643 }
4644 sorted.insert("refs/heads/parent".into());
4645 sorted.insert("refs/heads/a/b".into());
4646 let candidates = [
4647 "refs/heads/b00001",
4648 "refs/heads/parent/child",
4649 "refs/heads/new",
4650 "refs/heads/a",
4651 ];
4652 for candidate in candidates {
4653 let indexed = packed_refname_namespace_conflict(&sorted, candidate);
4654 let linear = sorted
4655 .iter()
4656 .any(|existing| refname_namespace_conflicts(existing, candidate));
4657 assert_eq!(indexed, linear, "candidate {candidate}");
4658 }
4659 }
4660
4661 #[test]
4662 fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
4663 let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
4664 if !git_dir.is_dir() {
4665 return;
4666 }
4667 let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
4668 assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
4669 assert!(listed
4670 .iter()
4671 .all(|(name, _)| name.starts_with("refs/heads/b05")));
4672 }
4673
4674 #[test]
4675 fn list_refs_finds_sibling_under_parent_directory() {
4676 let dir = tempdir().unwrap();
4677 let git_dir = dir.path();
4678 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
4679 fs::write(
4680 git_dir.join("refs/ns/p/x"),
4681 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4682 )
4683 .unwrap();
4684 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
4685 assert!(
4686 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
4687 "got {listed:?}"
4688 );
4689 }
4690
4691 #[test]
4692 fn verify_blocks_parent_when_child_ref_exists() {
4693 let dir = tempdir().unwrap();
4694 let git_dir = dir.path();
4695 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
4696 fs::write(
4697 git_dir.join("refs/ns/p/x"),
4698 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4699 )
4700 .unwrap();
4701 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
4702 let skip = HashSet::new();
4703 let err =
4704 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
4705 assert!(matches!(
4706 err,
4707 RefnameUnavailable::DescendantExists { ref blocking, .. }
4708 if blocking == "refs/ns/p/x"
4709 ));
4710 }
4711
4712 #[test]
4713 fn verify_blocks_parent_when_only_a_packed_child_exists() {
4714 let dir = tempdir().unwrap();
4717 let git_dir = dir.path();
4718 fs::create_dir_all(git_dir.join("refs")).unwrap();
4719 fs::write(
4720 store::paths::packed_refs_path(git_dir),
4721 "# pack-refs with: peeled fully-peeled\n\
4722 67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
4723 )
4724 .unwrap();
4725 let err = verify_refname_available_for_create(
4726 git_dir,
4727 "refs/ns/q",
4728 &BTreeSet::new(),
4729 &HashSet::new(),
4730 )
4731 .unwrap_err();
4732 assert!(matches!(
4733 err,
4734 RefnameUnavailable::DescendantExists { ref blocking, .. }
4735 if blocking == "refs/ns/q/x"
4736 ));
4737 }
4738
4739 #[test]
4740 fn verify_allows_name_whose_siblings_share_a_prefix() {
4741 let dir = tempdir().unwrap();
4742 let git_dir = dir.path();
4743 fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
4744 for name in ["v1.0.1", "v1.0-rc1"] {
4745 fs::write(
4746 git_dir.join("refs/tags").join(name),
4747 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4748 )
4749 .unwrap();
4750 }
4751 verify_refname_available_for_create(
4752 git_dir,
4753 "refs/tags/v1.0",
4754 &BTreeSet::new(),
4755 &HashSet::new(),
4756 )
4757 .expect("siblings are not descendants");
4758 }
4759
4760 #[test]
4761 fn verify_blocks_parent_git_style_nested_path() {
4762 let dir = tempdir().unwrap();
4763 let git_dir = dir.path();
4764 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
4765 fs::write(
4766 git_dir.join("refs/3l/c/x"),
4767 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4768 )
4769 .unwrap();
4770 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
4771 let skip = HashSet::new();
4772 let err =
4773 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
4774 assert!(matches!(
4775 err,
4776 RefnameUnavailable::DescendantExists { ref blocking, .. }
4777 if blocking == "refs/3l/c/x"
4778 ));
4779 }
4780
4781 #[test]
4782 fn intermediate_directory_does_not_block_nested_create() {
4783 let dir = tempdir().unwrap();
4784 let git_dir = dir.path();
4785 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
4786 fs::write(
4787 git_dir.join("refs/ns/existing"),
4788 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
4789 )
4790 .unwrap();
4791 assert_eq!(
4792 read_raw_ref(git_dir, "refs/ns").unwrap(),
4793 RawRefLookup::IsDirectory
4794 );
4795 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
4796 let skip = HashSet::new();
4797 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
4798 }
4799}
4800
4801#[cfg(test)]
4802mod ref_storage_traversal_guard_tests {
4803 use super::*;
4804 use crate::objects::ObjectId;
4805 use tempfile::tempdir;
4806
4807 const TRAVERSAL_REF: &str = "refs/heads/../../config";
4808
4809 fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
4810 let dir = tempdir().unwrap();
4811 let git_dir = dir.path().to_path_buf();
4812 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4813 fs::write(
4814 git_dir.join("config"),
4815 "[core]\nrepositoryformatversion = 0\n",
4816 )
4817 .unwrap();
4818 (dir, git_dir)
4819 }
4820
4821 fn sample_oid() -> ObjectId {
4822 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
4823 }
4824
4825 fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
4826 assert!(matches!(result, Err(Error::InvalidRef(_))));
4827 assert!(
4828 git_dir.join("config").is_file(),
4829 "repository config must not be touched"
4830 );
4831 }
4832
4833 #[test]
4834 fn write_ref_rejects_traversal_refname() {
4835 let (_dir, git_dir) = bare_repo_with_config();
4836 let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
4837 assert_invalid_ref_preserves_config(&git_dir, result);
4838 }
4839
4840 #[test]
4841 fn write_ref_cached_rejects_traversal_refname() {
4842 let (_dir, git_dir) = bare_repo_with_config();
4843 let mut packed = PackedRefs::load(&git_dir).unwrap();
4844 let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
4845 assert_invalid_ref_preserves_config(&git_dir, result);
4846 }
4847
4848 #[test]
4849 fn write_symbolic_ref_rejects_traversal_refname() {
4850 let (_dir, git_dir) = bare_repo_with_config();
4851 let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
4852 assert_invalid_ref_preserves_config(&git_dir, result);
4853 }
4854
4855 #[test]
4856 fn delete_ref_rejects_traversal_refname() {
4857 let (_dir, git_dir) = bare_repo_with_config();
4858 let result = delete_ref(&git_dir, TRAVERSAL_REF);
4859 assert_invalid_ref_preserves_config(&git_dir, result);
4860 }
4861
4862 #[test]
4863 fn write_ref_rejects_illegal_branch_refname() {
4864 let (_dir, git_dir) = bare_repo_with_config();
4865 for refname in [
4866 "refs/heads/bad name",
4867 "refs/heads/x..y",
4868 "refs/heads/a~b",
4869 "refs/heads/a.lock",
4870 ] {
4871 let result = write_ref(&git_dir, refname, &sample_oid());
4872 assert_invalid_ref_preserves_config(&git_dir, result);
4873 assert!(
4874 !git_dir
4875 .join(refname.strip_prefix("refs/").unwrap())
4876 .exists(),
4877 "must not write loose ref for {refname}"
4878 );
4879 }
4880 }
4881
4882 #[test]
4883 fn write_ref_accepts_refs_heads_head() {
4884 let (_dir, git_dir) = bare_repo_with_config();
4885 write_ref(&git_dir, "refs/heads/HEAD", &sample_oid())
4886 .expect("refs/heads/HEAD is valid ref syntax");
4887 assert!(git_dir.join("refs/heads/HEAD").is_file());
4888 }
4889}
4890
4891#[cfg(test)]
4892mod log_refs_config_tests {
4893 use super::*;
4894 use crate::config::{ConfigFile, ConfigScope};
4895
4896 fn set(text: &str) -> ConfigSet {
4897 let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
4898 let mut set = ConfigSet::new();
4899 set.merge(&file);
4900 set
4901 }
4902
4903 #[test]
4904 fn parse_accepts_modes_and_bool_spellings() {
4905 for (raw, mode) in [
4906 ("always", LogRefsConfig::Always),
4907 (" ALWAYS ", LogRefsConfig::Always),
4908 ("true", LogRefsConfig::Normal),
4909 ("Yes", LogRefsConfig::Normal),
4910 ("on", LogRefsConfig::Normal),
4911 ("1", LogRefsConfig::Normal),
4912 ("false", LogRefsConfig::None),
4913 ("no", LogRefsConfig::None),
4914 ("OFF", LogRefsConfig::None),
4915 ("0", LogRefsConfig::None),
4916 ("never", LogRefsConfig::None),
4917 ] {
4918 assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
4919 }
4920 assert_eq!(LogRefsConfig::parse("sometimes"), None);
4921 }
4922
4923 #[test]
4924 fn effective_mode_applies_bare_default_only_when_unset() {
4925 assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
4926 assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
4927 let bare = set("[core]\n\tbare = true\n");
4928 assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
4929 let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
4930 assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
4931 assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
4932 let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
4933 assert_eq!(
4934 bare_logged.effective_log_refs_config(),
4935 LogRefsConfig::Normal
4936 );
4937 let always = set("[core]\n\tlogAllRefUpdates = always\n");
4938 assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
4939 }
4940
4941 #[test]
4942 fn autocreate_with_config_matches_mode() {
4943 let normal = set("[core]\n\tlogAllRefUpdates = true\n");
4944 let git_dir = Path::new(".git");
4945 assert!(should_autocreate_reflog_with_config(
4946 &normal,
4947 git_dir,
4948 "refs/heads/main"
4949 ));
4950 assert!(!should_autocreate_reflog_with_config(
4951 &normal,
4952 git_dir,
4953 "refs/tags/v1"
4954 ));
4955 let none = set("[core]\n\tlogAllRefUpdates = false\n");
4956 assert!(!should_autocreate_reflog_with_config(
4957 &none, git_dir, "HEAD"
4958 ));
4959 }
4960}
4961
4962#[cfg(test)]
4963mod read_raw_ref_tests {
4964 use super::*;
4965 use tempfile::tempdir;
4966
4967 #[test]
4968 fn loose_ref_file_is_exists() {
4969 let dir = tempdir().unwrap();
4970 let git_dir = dir.path();
4971 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4972 fs::write(
4973 git_dir.join("refs/heads/side"),
4974 "0000000000000000000000000000000000000000\n",
4975 )
4976 .unwrap();
4977 assert_eq!(
4978 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
4979 RawRefLookup::Exists
4980 );
4981 }
4982
4983 #[test]
4984 fn missing_ref_is_not_found() {
4985 let dir = tempdir().unwrap();
4986 let git_dir = dir.path();
4987 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4988 assert_eq!(
4989 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
4990 RawRefLookup::NotFound
4991 );
4992 }
4993
4994 #[test]
4995 fn directory_where_ref_expected_is_is_directory() {
4996 let dir = tempdir().unwrap();
4997 let git_dir = dir.path();
4998 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
4999 assert_eq!(
5000 read_raw_ref(git_dir, "refs/heads").unwrap(),
5001 RawRefLookup::IsDirectory
5002 );
5003 }
5004
5005 #[test]
5006 fn packed_ref_name_is_exists() {
5007 let dir = tempdir().unwrap();
5008 let git_dir = dir.path();
5009 fs::write(
5010 store::paths::packed_refs_path(git_dir),
5011 "# pack-refs with: peeled fully-peeled \n\
5012 0000000000000000000000000000000000000000 refs/heads/packed\n",
5013 )
5014 .unwrap();
5015 assert_eq!(
5016 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
5017 RawRefLookup::Exists
5018 );
5019 }
5020}
5021
5022#[cfg(test)]
5023mod packed_refs_parse_tests {
5024 use super::*;
5025
5026 use tempfile::tempdir;
5027
5028 #[test]
5029 fn parse_rejects_garbage_line() {
5030 let dir = tempdir().unwrap();
5031 let path = store::paths::packed_refs_path(dir.path());
5032 fs::write(
5033 &path,
5034 "# pack-refs with: peeled\n\
5035 0000000000000000000000000000000000000000 refs/heads/main\n\
5036 not a ref line\n",
5037 )
5038 .unwrap();
5039 let data = fs::read(&path).unwrap();
5040 assert!(matches!(
5041 parse_packed_refs_to_map(&path, &data),
5042 Err(Error::PackedRefsUnexpectedLine { .. })
5043 ));
5044 }
5045
5046 #[test]
5047 fn parse_rejects_carriage_return_bytes() {
5048 let dir = tempdir().unwrap();
5049 let path = store::paths::packed_refs_path(dir.path());
5050 fs::write(
5051 &path,
5052 "# pack-refs with: peeled\n\
5053 0000000000000000000000000000000000000000 refs/heads/main\r\n",
5054 )
5055 .unwrap();
5056 let data = fs::read(&path).unwrap();
5057 assert!(matches!(
5058 parse_packed_refs_to_map(&path, &data),
5059 Err(Error::PackedRefsUnexpectedLine { .. })
5060 ));
5061 }
5062
5063 #[test]
5064 fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
5065 let dir = tempdir().unwrap();
5066 let git_dir = dir.path();
5067 let oid = "1111111111111111111111111111111111111111";
5068 fs::write(
5069 store::paths::packed_refs_path(git_dir),
5070 format!(
5071 "# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5072 ),
5073 )
5074 .unwrap();
5075 let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
5076 assert_eq!(listed.len(), 1);
5077 assert_eq!(listed[0].0, "refs/heads/aaa");
5078 }
5079
5080 #[test]
5081 fn parse_rejects_blank_line_and_missing_final_newline() {
5082 let dir = tempdir().unwrap();
5083 let path = store::paths::packed_refs_path(dir.path());
5084 fs::write(
5085 &path,
5086 "# pack-refs with: peeled\n\n\
5087 0000000000000000000000000000000000000000 refs/heads/main\n",
5088 )
5089 .unwrap();
5090 let data = fs::read(&path).unwrap();
5091 assert!(matches!(
5092 parse_packed_refs_to_map(&path, &data),
5093 Err(Error::PackedRefsUnexpectedLine { .. })
5094 ));
5095
5096 fs::write(
5097 &path,
5098 "# pack-refs with: peeled\n\
5099 0000000000000000000000000000000000000000 refs/heads/main",
5100 )
5101 .unwrap();
5102 let data = fs::read(&path).unwrap();
5103 assert!(matches!(
5104 parse_packed_refs_to_map(&path, &data),
5105 Err(Error::PackedRefsUnexpectedLine { .. })
5106 ));
5107 }
5108
5109 #[test]
5110 fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
5111 let dir = tempdir().unwrap();
5112 let path = store::paths::packed_refs_path(dir.path());
5113 let a = "1111111111111111111111111111111111111111";
5114 let b = "2222222222222222222222222222222222222222";
5115 let peel = "3333333333333333333333333333333333333333";
5116 let body = |header: &str| {
5117 format!(
5118 "{header}{a} refs/heads/main\n\
5119 {b} refs/heads/tagged\n\
5120 ^{peel}\n\
5121 {a} refs/tags/light\n\
5122 {b} refs/tags/v1\n\
5123 ^{peel}\n"
5124 )
5125 };
5126 let a: ObjectId = a.parse().unwrap();
5127 let b: ObjectId = b.parse().unwrap();
5128 let peel: ObjectId = peel.parse().unwrap();
5129 let cases = [
5130 (
5131 "# pack-refs with: peeled fully-peeled sorted \n",
5132 [
5133 PackedPeel::NotTag,
5134 PackedPeel::Peeled(peel),
5135 PackedPeel::NotTag,
5136 PackedPeel::Peeled(peel),
5137 ],
5138 ),
5139 (
5140 "# pack-refs with: peeled sorted \n",
5141 [
5142 PackedPeel::Unknown,
5143 PackedPeel::Unknown,
5144 PackedPeel::NotTag,
5145 PackedPeel::Peeled(peel),
5146 ],
5147 ),
5148 ("", [PackedPeel::Unknown; 4]),
5149 ];
5150 for (header, want) in cases {
5151 let data = body(header).into_bytes();
5152 let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
5153 let seen = std::cell::RefCell::new(Vec::new());
5154 let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
5155 seen.borrow_mut().push((name.to_owned(), *oid, peel));
5156 name.starts_with("refs/tags/")
5157 };
5158 let kept = view
5159 .map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
5160 .unwrap();
5161 assert_eq!(
5162 seen.into_inner(),
5163 vec![
5164 ("refs/heads/main".to_owned(), a, want[0]),
5165 ("refs/heads/tagged".to_owned(), b, want[1]),
5166 ("refs/tags/light".to_owned(), a, want[2]),
5167 ("refs/tags/v1".to_owned(), b, want[3]),
5168 ],
5169 "header {header:?}"
5170 );
5171 let mut kept: Vec<_> = kept.into_iter().collect();
5172 kept.sort();
5173 assert_eq!(
5174 kept,
5175 vec![
5176 ("refs/tags/light".to_owned(), a),
5177 ("refs/tags/v1".to_owned(), b)
5178 ]
5179 );
5180 assert_eq!(
5181 view.map_for_prefix_filtered(&path, Some("refs"), None)
5182 .unwrap(),
5183 view.map_for_prefix(&path, Some("refs")).unwrap()
5184 );
5185 }
5186
5187 let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
5189 let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
5190 assert!(matches!(
5191 view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
5192 Err(Error::PackedRefsUnexpectedLine { .. })
5193 ));
5194 }
5195
5196 #[test]
5197 fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
5198 let dir = tempdir().unwrap();
5199 let path = store::paths::packed_refs_path(dir.path());
5200 let oid = "1111111111111111111111111111111111111111";
5201 let peel = "2222222222222222222222222222222222222222";
5202 let sorted_body = format!(
5203 "# pack-refs with: peeled fully-peeled sorted\n\
5204 {oid} refs/heads/aaa\n\
5205 {oid} refs/heads/main\n\
5206 {oid} refs/heads/zzz\n\
5207 {oid} refs/tags/v1\n\
5208 ^{peel}\n"
5209 );
5210 fs::write(&path, &sorted_body).unwrap();
5211 let data = fs::read(&path).unwrap();
5212 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5213 let full = parse_packed_refs_to_map(&path, &data).unwrap();
5214 for (name, want) in &full {
5215 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
5216 }
5217 assert_eq!(
5218 lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
5219 .unwrap()
5220 .map(|(o, p)| (o, p.unwrap())),
5221 Some((oid.parse().unwrap(), peel.parse().unwrap()))
5222 );
5223
5224 let unsorted_body = format!(
5225 "# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
5226 );
5227 fs::write(&path, unsorted_body).unwrap();
5228 let data = fs::read(&path).unwrap();
5229 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5230 let full = parse_packed_refs_to_map(&path, &data).unwrap();
5231 for (name, want) in &full {
5232 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
5233 }
5234 assert_eq!(
5235 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
5236 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
5237 );
5238 }
5239
5240 #[test]
5241 fn sorted_prefix_without_upper_bound_filters_names() {
5242 let dir = tempdir().unwrap();
5243 let path = store::paths::packed_refs_path(dir.path());
5244 let oid = "1111111111111111111111111111111111111111";
5245 let sorted_body = format!(
5246 "# pack-refs with: peeled fully-peeled sorted\n\
5247 {oid} refs/heads/aaa\n\
5248 {oid} refs/heads/zzz\n\
5249 {oid} refs/tags/v1\n"
5250 );
5251 fs::write(&path, sorted_body).unwrap();
5252 let data = fs::read(&path).unwrap();
5253 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
5254 assert_eq!(
5255 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
5256 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
5257 );
5258 assert!(
5259 !view
5260 .map_for_prefix(&path, Some("refs/heads/a"))
5261 .unwrap()
5262 .contains_key("refs/heads/zzz"),
5263 "prefix refs/heads/a must not include refs/heads/zzz"
5264 );
5265 assert_eq!(
5266 view.map_for_prefix(&path, Some("refs")).unwrap(),
5267 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
5268 );
5269 }
5270
5271 #[test]
5272 fn sorted_prefix_scan_validates_only_its_range() {
5273 let dir = tempdir().unwrap();
5274 let path = store::paths::packed_refs_path(dir.path());
5275 let oid = "1111111111111111111111111111111111111111";
5276 let view_of =
5277 |body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
5278
5279 let bad_peel_on_branch = view_of(format!(
5280 "# pack-refs with: peeled fully-peeled sorted\n\
5281 {oid} refs/heads/main\n\
5282 ^not-hex\n\
5283 {oid} refs/tags/v1\n"
5284 ))
5285 .unwrap();
5286 assert!(matches!(
5287 bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
5288 Err(Error::PackedRefsUnexpectedLine { .. })
5289 ));
5290 let tags = bad_peel_on_branch
5291 .map_for_prefix(&path, Some("refs/tags"))
5292 .unwrap();
5293 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
5294 assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
5295
5296 let blank_among_tags = view_of(format!(
5297 "# pack-refs with: peeled fully-peeled sorted\n\
5298 {oid} refs/heads/main\n\
5299 {oid} refs/tags/v1\n\
5300 \n\
5301 {oid} refs/tags/v2\n"
5302 ))
5303 .unwrap();
5304 assert_eq!(
5305 blank_among_tags
5306 .map_for_prefix(&path, Some("refs/heads"))
5307 .unwrap()
5308 .len(),
5309 1
5310 );
5311 assert!(blank_among_tags
5312 .map_for_prefix(&path, Some("refs/tags"))
5313 .is_err());
5314
5315 let no_match = blank_among_tags
5316 .map_for_prefix(&path, Some("refs/notes"))
5317 .unwrap();
5318 assert!(no_match.is_empty());
5319 }
5320
5321 #[test]
5322 fn non_utf8_record_only_breaks_reads_that_cover_it() {
5323 let dir = tempdir().unwrap();
5324 let path = store::paths::packed_refs_path(dir.path());
5325 let oid = "1111111111111111111111111111111111111111";
5326 let mut body =
5327 format!("# pack-refs with: peeled fully-peeled sorted \n{oid} refs/heads/main\n")
5328 .into_bytes();
5329 body.extend_from_slice(format!("{oid} refs/tags/v1\n{oid} refs/tags/").as_bytes());
5330 body.extend_from_slice(b"\xff\n");
5331 let view = PackedRefsFileView::from_bytes(&path, body, HashAlgo::Sha1).unwrap();
5332 assert!(view.sorted);
5333 for name in ["refs/heads/main", "refs/tags/v1"] {
5334 assert_eq!(
5335 view.lookup_oid(&path, name).unwrap(),
5336 Some(oid.parse().unwrap())
5337 );
5338 }
5339 assert_eq!(view.lookup_oid(&path, "refs/tags/v2").unwrap(), None);
5340 let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
5341 assert_eq!(heads.keys().collect::<Vec<_>>(), ["refs/heads/main"]);
5342 assert!(matches!(
5343 view.map_for_prefix(&path, Some("refs/tags")),
5344 Err(Error::PackedRefsUnexpectedLine { .. })
5345 ));
5346 assert!(view.map_for_prefix(&path, None).is_err());
5347
5348 let mut bad_header = b"# pack-refs with: \xff sorted \n".to_vec();
5349 bad_header.extend_from_slice(format!("{oid} refs/heads/main\n").as_bytes());
5350 assert!(matches!(
5351 PackedRefsFileView::from_bytes(&path, bad_header, HashAlgo::Sha1),
5352 Err(Error::PackedRefsUnexpectedLine { .. })
5353 ));
5354 }
5355
5356 #[test]
5357 fn scan_of_file_without_ref_lines_still_validates_it() {
5358 let dir = tempdir().unwrap();
5359 let path = store::paths::packed_refs_path(dir.path());
5360 let view = PackedRefsFileView::from_bytes(
5361 &path,
5362 b"# pack-refs with: peeled fully-peeled sorted\n\
5363 ^1111111111111111111111111111111111111111\n"
5364 .to_vec(),
5365 HashAlgo::Sha1,
5366 )
5367 .unwrap();
5368 for prefix in [None, Some("refs/heads")] {
5369 assert!(matches!(
5370 view.map_for_prefix(&path, prefix),
5371 Err(Error::PackedRefsUnexpectedLine { .. })
5372 ));
5373 }
5374 let header_only = PackedRefsFileView::from_bytes(
5375 &path,
5376 b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
5377 HashAlgo::Sha1,
5378 )
5379 .unwrap();
5380 assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
5381 }
5382
5383 #[test]
5384 fn view_checks_last_line_length_against_the_repository_hash() {
5385 let dir = tempdir().unwrap();
5386 let path = store::paths::packed_refs_path(dir.path());
5387 let sha1 = "1111111111111111111111111111111111111111";
5388 let sha256 = "2".repeat(64);
5389 let view =
5390 |body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
5391 let header = "# pack-refs with: peeled fully-peeled sorted\n";
5392 let fifty_bytes = "x".repeat(49);
5393
5394 for (body, algo) in [
5395 (
5396 format!("{header}{sha1} refs/heads/main\ngarbage\n"),
5397 HashAlgo::Sha1,
5398 ),
5399 (
5400 format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
5401 HashAlgo::Sha1,
5402 ),
5403 (
5404 format!("{header}{sha1} refs/heads/main\n#\n"),
5405 HashAlgo::Sha1,
5406 ),
5407 (
5408 format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
5409 HashAlgo::Sha256,
5410 ),
5411 (format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
5412 ] {
5413 assert!(
5414 matches!(
5415 view(body.clone(), algo),
5416 Err(Error::PackedRefsUnexpectedLine { .. })
5417 ),
5418 "{algo:?}: {body:?}"
5419 );
5420 }
5421 for (body, algo) in [
5422 (header.to_owned(), HashAlgo::Sha1),
5423 (header.to_owned(), HashAlgo::Sha256),
5424 ("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
5425 (
5426 format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
5427 HashAlgo::Sha1,
5428 ),
5429 (
5430 format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
5431 HashAlgo::Sha1,
5432 ),
5433 (
5434 format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
5435 HashAlgo::Sha256,
5436 ),
5437 ] {
5438 assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
5439 }
5440 }
5441
5442 #[test]
5443 fn sha256_short_last_record_fails_branch_listing() {
5444 let dir = tempdir().unwrap();
5445 let path = store::paths::packed_refs_path(dir.path());
5446 let sha256 = "2".repeat(64);
5447 let body = format!(
5448 "# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
5449 "x".repeat(49)
5450 );
5451 assert!(
5452 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
5453 );
5454 }
5455
5456 #[test]
5457 fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
5458 let dir = tempdir().unwrap();
5459 let path = store::paths::packed_refs_path(dir.path());
5460 let oid = "1111111111111111111111111111111111111111";
5461 let header = "# pack-refs with: peeled fully-peeled sorted\n";
5462 let view = |body: String| {
5463 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
5464 };
5465
5466 let garbage_after_first_branch = view(format!(
5467 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
5468 {oid} refs/tags/t1\n"
5469 ));
5470 assert!(matches!(
5471 garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
5472 Err(Error::PackedRefsUnexpectedLine { .. })
5473 ));
5474 let tags = garbage_after_first_branch
5475 .map_for_prefix(&path, Some("refs/tags"))
5476 .unwrap();
5477 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
5478
5479 let garbage_before_range = view(format!(
5480 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
5481 ));
5482 for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
5483 assert!(
5484 garbage_before_range
5485 .map_for_prefix(&path, Some(prefix))
5486 .is_err(),
5487 "{prefix}"
5488 );
5489 }
5490 assert_eq!(
5491 garbage_before_range
5492 .map_for_prefix(&path, Some("refs/zz"))
5493 .unwrap()
5494 .len(),
5495 0
5496 );
5497
5498 let peeled_tag_before_range = view(format!(
5499 "{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
5500 ));
5501 let orphan_peel_before_range = view(format!(
5502 "{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
5503 ));
5504 assert!(orphan_peel_before_range
5505 .map_for_prefix(&path, Some("refs/tags"))
5506 .is_err());
5507 assert_eq!(
5508 peeled_tag_before_range
5509 .map_for_prefix(&path, Some("refs/tags"))
5510 .unwrap()
5511 .len(),
5512 1
5513 );
5514 }
5515
5516 #[test]
5517 fn sorted_trait_is_trusted_even_when_names_are_out_of_order() {
5518 let dir = tempdir().unwrap();
5519 let path = store::paths::packed_refs_path(dir.path());
5520 let oid = "1111111111111111111111111111111111111111";
5521 let records = format!("{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n");
5522 let view_of = |header: &str| {
5523 PackedRefsFileView::from_bytes(
5524 &path,
5525 format!("{header}{records}").into_bytes(),
5526 HashAlgo::Sha1,
5527 )
5528 .unwrap()
5529 };
5530
5531 let lying = view_of("# pack-refs with: peeled fully-peeled sorted\n");
5532 assert!(lying.sorted);
5533 assert!(lying.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
5535 assert_eq!(lying.lookup_oid(&path, "refs/heads/b").unwrap(), None);
5536 let heads = lying.map_for_prefix(&path, Some("refs/heads")).unwrap();
5537 assert_eq!(heads.len(), 2);
5538
5539 let unsorted = view_of("# pack-refs with: peeled fully-peeled\n");
5540 assert!(!unsorted.sorted);
5541 for name in ["refs/heads/a", "refs/heads/b", "refs/tags/t"] {
5542 assert!(
5543 unsorted.lookup_oid(&path, name).unwrap().is_some(),
5544 "{name}"
5545 );
5546 }
5547 }
5548
5549 #[test]
5550 fn records_are_only_record_shaped_lines() {
5551 let dir = tempdir().unwrap();
5552 let path = store::paths::packed_refs_path(dir.path());
5553 let view = |data: &[u8]| {
5554 PackedRefsFileView::from_bytes(&path, data.to_vec(), HashAlgo::Sha1).unwrap()
5555 };
5556 let hex = "1".repeat(40);
5557 let data = format!("# header\n{hex} x\n^peel\n\n{hex} y\n#c\nshort\n{hex}z\n{hex} z\n");
5558 let starts: Vec<usize> = view(data.as_bytes()).records().collect();
5559 assert_eq!(starts, [9, 59, 153]);
5560 assert_eq!(view(b"").records().count(), 0);
5561 let out_of_order = format!("{hex} b\n{hex} a\n");
5562 assert_eq!(
5563 view(out_of_order.as_bytes()).records().collect::<Vec<_>>(),
5564 [0, 43]
5565 );
5566 }
5567
5568 #[test]
5569 fn parse_rejects_orphan_peel_and_double_space() {
5570 let dir = tempdir().unwrap();
5571 let path = store::paths::packed_refs_path(dir.path());
5572 fs::write(
5573 &path,
5574 "# pack-refs with: peeled\n\
5575 ^0000000000000000000000000000000000000000\n\
5576 0000000000000000000000000000000000000000 refs/heads/main\n",
5577 )
5578 .unwrap();
5579 let data = fs::read(&path).unwrap();
5580 assert!(matches!(
5581 parse_packed_refs_to_map(&path, &data),
5582 Err(Error::PackedRefsUnexpectedLine { .. })
5583 ));
5584
5585 fs::write(
5586 &path,
5587 "# pack-refs with: peeled\n\
5588 0000000000000000000000000000000000000000 refs/heads/main\n",
5589 )
5590 .unwrap();
5591 let data = fs::read(&path).unwrap();
5592 assert!(matches!(
5593 parse_packed_refs_to_map(&path, &data),
5594 Err(Error::PackedRefsUnexpectedLine { .. })
5595 ));
5596 }
5597}
5598
5599#[cfg(test)]
5600mod pack_refs_tests {
5601 use super::*;
5602 use crate::objects::{serialize_commit, CommitData, ObjectKind};
5603 use crate::repo::{init_repository, Repository};
5604 use std::fs;
5605 use std::sync::Arc;
5606
5607 fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
5608 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
5609 let raw = serialize_commit(&CommitData {
5610 tree,
5611 parents,
5612 author: "T <t@example.com> 1 +0000".into(),
5613 committer: "T <t@example.com> 1 +0000".into(),
5614 author_raw: Vec::new(),
5615 committer_raw: Vec::new(),
5616 encoding: None,
5617 message: "m\n".into(),
5618 raw_message: None,
5619 extra_headers: Vec::new(),
5620 });
5621 repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
5622 }
5623
5624 #[test]
5625 fn pack_refs_skips_concurrent_loose_update() {
5626 let dir = tempfile::tempdir().expect("tempdir");
5627 let repo = init_repository(
5628 dir.path(),
5629 false,
5630 "main",
5631 None,
5632 crate::RefStorageFormat::Files,
5633 )
5634 .expect("init");
5635 let git_dir = repo.git_dir().to_path_buf();
5636 let c1 = write_commit(&repo, vec![]);
5637 let c2 = write_commit(&repo, vec![c1]);
5638 write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
5639
5640 let git_dir_hook = git_dir.clone();
5641 let c2 = Arc::new(c2);
5642 let c2_hook = Arc::clone(&c2);
5643 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
5644 write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
5645 })));
5646
5647 let (_path, packed, pruned) =
5648 pack_refs_files(&git_dir, repo.odb(), true, false).expect("pack");
5649 pack_refs_test_hooks::set_before_prune(None);
5650 assert!(packed >= 1);
5651 assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
5652
5653 let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
5654 assert_eq!(resolved, *c2);
5655 let packed_body =
5656 fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
5657 assert!(
5658 !packed_body.contains("refs/heads/race"),
5659 "packed entry for concurrently updated ref must be removed"
5660 );
5661 }
5662
5663 #[test]
5664 fn pack_refs_nested_tag_peel_line_points_at_commit() {
5665 let dir = tempfile::tempdir().expect("tempdir");
5666 let repo = init_repository(
5667 dir.path(),
5668 false,
5669 "main",
5670 None,
5671 crate::RefStorageFormat::Files,
5672 )
5673 .expect("init");
5674 let commit = write_commit(&repo, vec![]);
5675 let inner_tag_body = format!(
5676 "object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
5677 commit.to_hex()
5678 );
5679 let inner = repo
5680 .odb()
5681 .write(ObjectKind::Tag, inner_tag_body.as_bytes())
5682 .expect("inner tag");
5683 let outer_tag_body = format!(
5684 "object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
5685 inner.to_hex()
5686 );
5687 let outer = repo
5688 .odb()
5689 .write(ObjectKind::Tag, outer_tag_body.as_bytes())
5690 .expect("outer tag");
5691 write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
5692
5693 pack_refs_files(repo.git_dir(), repo.odb(), false, false).expect("pack");
5694 let body =
5695 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
5696 let mut lines = body.lines();
5697 let tag_line = lines
5698 .find(|l| l.contains("refs/tags/outer"))
5699 .expect("tag line");
5700 assert!(tag_line.starts_with(&outer.to_hex()));
5701 let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
5702 assert!(
5703 peel_line.contains(&commit.to_hex()),
5704 "peel must reach commit through nested tags, got {peel_line}"
5705 );
5706 }
5707
5708 #[test]
5709 fn pack_refs_tree_target_tag_has_peel_line() {
5710 let dir = tempfile::tempdir().expect("tempdir");
5711 let repo = init_repository(
5712 dir.path(),
5713 false,
5714 "main",
5715 None,
5716 crate::RefStorageFormat::Files,
5717 )
5718 .expect("init");
5719 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
5720 let tag_body = format!(
5721 "object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
5722 tree.to_hex()
5723 );
5724 let tag = repo
5725 .odb()
5726 .write(ObjectKind::Tag, tag_body.as_bytes())
5727 .expect("tag");
5728 write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
5729
5730 pack_refs_files(repo.git_dir(), repo.odb(), true, false).expect("pack");
5731 let body =
5732 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
5733 assert!(body.contains("refs/tags/tree-tag"));
5734 assert!(
5735 body.contains(&format!("^{}", tree.to_hex())),
5736 "expected peel to tree: {body}"
5737 );
5738 }
5739
5740 #[test]
5741 fn write_packed_refs_file_survives_publish_failure() {
5742 let dir = tempfile::tempdir().expect("tempdir");
5743 let git_dir = dir.path();
5744 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
5745 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
5746 fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
5747 fs::create_dir_all(store::paths::packed_refs_path(git_dir))
5748 .expect("block packed-refs file");
5749 let err = write_packed_refs_file(
5750 git_dir,
5751 &[PackedRefRecord {
5752 refname: "refs/heads/keep".into(),
5753 oid,
5754 peeled: None,
5755 }],
5756 )
5757 .expect_err("publish should fail");
5758 assert!(matches!(err, Error::Io(_)));
5759 assert!(git_dir.join("refs/heads/keep").is_file());
5760 }
5761
5762 #[test]
5763 fn write_packed_refs_file_replaces_loose_and_sorts() {
5764 let dir = tempfile::tempdir().expect("tempdir");
5765 let git_dir = dir.path();
5766 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
5767 let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
5768 let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
5769 fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
5770 write_packed_refs_file(
5771 git_dir,
5772 &[
5773 PackedRefRecord {
5774 refname: "refs/heads/z-last".into(),
5775 oid: oid_b,
5776 peeled: None,
5777 },
5778 PackedRefRecord {
5779 refname: "refs/heads/a-first".into(),
5780 oid: oid_a,
5781 peeled: None,
5782 },
5783 ],
5784 )
5785 .expect("write packed-refs");
5786 assert!(!git_dir.join("refs/heads/a-first").exists());
5787 let body =
5788 fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
5789 assert!(body.contains("refs/heads/a-first"));
5790 assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
5791 }
5792}
5793
5794#[cfg(test)]
5795mod packed_ref_peels_tests {
5796 use super::*;
5797
5798 fn oid(n: u8) -> ObjectId {
5799 ObjectId::from_bytes(&[n; 20]).expect("oid")
5800 }
5801
5802 fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
5803 let dir = tempfile::tempdir().expect("tempdir");
5804 fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
5805 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
5806 (dir, peels)
5807 }
5808
5809 fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
5810 let mut out = String::from(header);
5811 for (name, oid, peel) in records {
5812 out.push_str(&format!("{oid} {name}\n"));
5813 if let Some(peel) = peel {
5814 out.push_str(&format!("^{peel}\n"));
5815 }
5816 }
5817 out
5818 }
5819
5820 #[test]
5821 fn fully_peeled_file_answers_for_every_ref() {
5822 for header in [
5823 "# pack-refs with: peeled fully-peeled sorted \n",
5824 "# pack-refs with: peeled fully-peeled \n",
5825 ] {
5826 let records = [
5827 ("refs/heads/main", oid(1), None),
5828 ("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
5829 ("refs/tags/v1", oid(4), Some(oid(1))),
5830 ("refs/tags/light", oid(1), None),
5831 ];
5832 let mut ordered = records;
5834 if header.contains(" sorted") {
5835 ordered.sort_by_key(|(name, _, _)| *name);
5836 } else {
5837 ordered.reverse();
5838 }
5839 let (_dir, peels) = peels_for(&body(header, &ordered));
5840 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
5841 assert_eq!(
5842 peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
5843 Some(Some(oid(3)))
5844 );
5845 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
5846 assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
5847 assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
5849 assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
5850 }
5851 }
5852
5853 #[test]
5854 fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
5855 let records = [
5856 ("refs/heads/main", oid(1), None),
5857 ("refs/tags/v1", oid(4), Some(oid(1))),
5858 ];
5859 let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
5860 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
5861 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
5862
5863 for header in [
5864 "",
5865 "# pack-refs with: sorted\n",
5866 "# some comment\n",
5867 "#pack-refs with: peeled fully-peeled\n",
5868 "# pack-refs with: peeled fully-peeled\n",
5869 ] {
5870 let (_dir, peels) = peels_for(&body(header, &records));
5871 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
5872 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
5873 }
5874 }
5875
5876 #[test]
5877 fn missing_file_and_other_backends_know_nothing() {
5878 let dir = tempfile::tempdir().expect("tempdir");
5879 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
5880 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
5881
5882 let records = [("refs/heads/main", oid(1), None)];
5883 fs::write(
5884 store::paths::packed_refs_path(dir.path()),
5885 body("# pack-refs with: peeled fully-peeled sorted\n", &records),
5886 )
5887 .expect("packed-refs");
5888 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
5889 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
5890 }
5891
5892 #[test]
5893 fn lookups_in_any_order_agree_with_a_fresh_snapshot() {
5894 let records = [
5895 ("refs/heads/main", oid(1), None),
5896 ("refs/tags/a", oid(2), Some(oid(1))),
5897 ("refs/tags/b", oid(1), None),
5898 ("refs/tags/c", oid(3), Some(oid(4))),
5899 ("refs/tags/d", oid(5), None),
5900 ];
5901 let text = body("# pack-refs with: peeled fully-peeled sorted \n", &records);
5902 let (_dir, peels) = peels_for(&text);
5903 let fresh = |name: &str, value: &ObjectId| peels_for(&text).1.peeled(name, value);
5904 let queries = [
5905 ("refs/tags/a", oid(2)),
5906 ("refs/tags/b", oid(1)),
5907 ("refs/tags/b", oid(1)),
5908 ("refs/tags/bb", oid(1)),
5909 ("refs/tags/c", oid(3)),
5910 ("refs/tags/d", oid(9)),
5911 ("refs/tags/d", oid(5)),
5912 ("refs/heads/main", oid(1)),
5913 ("refs/tags/c", oid(3)),
5914 ("refs/tags/a", oid(2)),
5915 ("refs/tags/zzz", oid(1)),
5916 ("refs/tags/a", oid(2)),
5917 ("refs/tags/b", oid(1)),
5918 ];
5919 for (name, value) in queries {
5920 assert_eq!(
5921 peels.peeled(name, &value),
5922 fresh(name, &value),
5923 "{name} {value}"
5924 );
5925 }
5926 assert_eq!(peels.peeled("refs/tags/c", &oid(3)), Some(Some(oid(4))));
5927 assert_eq!(peels.peeled("refs/tags/d", &oid(5)), Some(None));
5928 }
5929
5930 #[test]
5931 fn store_snapshot_reuses_the_files_store_view_and_follows_rewrites() {
5932 let dir = tempfile::tempdir().expect("tempdir");
5933 let path = store::paths::packed_refs_path(dir.path());
5934 let header = "# pack-refs with: peeled fully-peeled sorted \n";
5935 let records = [
5936 ("refs/heads/main", oid(1), None),
5937 ("refs/tags/v1", oid(2), Some(oid(1))),
5938 ("refs/tags/v2", oid(3), None),
5939 ];
5940 fs::write(&path, body(header, &records)).expect("packed-refs");
5941 let files = store::FilesRefStore::from_git_dir(dir.path()).expect("files store");
5942
5943 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
5944 let loaded = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
5945 for (name, value, _) in records {
5946 assert_eq!(
5947 peels.peeled(name, &value),
5948 loaded.peeled(name, &value),
5949 "{name}"
5950 );
5951 }
5952 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), Some(Some(oid(1))));
5953 let cached = files
5954 .packed_refs_view()
5955 .expect("view")
5956 .expect("packed-refs");
5957 let shared = peels.view.as_ref().expect("snapshot view");
5958 assert!(std::sync::Arc::ptr_eq(&cached.data, &shared.data));
5959
5960 let rewritten = [
5961 ("refs/heads/main", oid(1), None),
5962 ("refs/tags/v1", oid(4), Some(oid(5))),
5963 ("refs/tags/v2", oid(3), None),
5964 ("refs/tags/v3", oid(6), Some(oid(1))),
5965 ];
5966 fs::write(&path, body(header, &rewritten)).expect("rewrite packed-refs");
5967 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
5968 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), None);
5969 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(5))));
5970 assert_eq!(peels.peeled("refs/tags/v3", &oid(6)), Some(Some(oid(1))));
5971
5972 let memory = store::MemoryRefStore::new();
5973 let fallback = PackedRefPeels::for_store(&memory, dir.path()).expect("fallback");
5974 let direct = PackedRefPeels::load(dir.path(), memory.format()).expect("load");
5975 for (name, value, _) in rewritten {
5976 assert_eq!(
5977 fallback.peeled(name, &value),
5978 direct.peeled(name, &value),
5979 "{name}"
5980 );
5981 }
5982 }
5983
5984 fn raw_view(data: &[u8], hex_len: usize) -> PackedRefsFileView {
5986 PackedRefsFileView {
5987 data: crate::pack_map::PackData::from_owned(data.to_vec()),
5988 sorted: true,
5989 hex_len,
5990 body_start: 0,
5991 peel_trait: PackedPeelTrait::None,
5992 }
5993 }
5994
5995 fn naive_record_starts(data: &[u8], hex_len: usize) -> Vec<usize> {
5997 let mut out = Vec::new();
5998 let mut start = 0;
5999 for line in data.split(|&b| b == b'\n') {
6000 if start >= data.len() {
6001 break;
6002 }
6003 if packed_line_is_record(line, hex_len) {
6004 out.push(start);
6005 }
6006 start += line.len() + 1;
6007 }
6008 out
6009 }
6010
6011 fn assert_record_walks_match(data: &[u8], hex_len: usize) {
6012 let view = raw_view(data, hex_len);
6013 let want = naive_record_starts(data, hex_len);
6014 assert_eq!(
6015 view.records().collect::<Vec<_>>(),
6016 want,
6017 "{:?} hex_len={hex_len}",
6018 String::from_utf8_lossy(data)
6019 );
6020 let mut line_starts: Vec<usize> = memchr::memchr_iter(b'\n', data)
6021 .map(|nl| nl + 1)
6022 .filter(|&s| s < data.len())
6023 .collect();
6024 line_starts.insert(0, 0);
6025 if data.ends_with(b"\n") {
6027 line_starts.push(data.len());
6028 }
6029 for before in line_starts {
6030 let prev = want.iter().copied().filter(|&s| s < before).last();
6031 assert_eq!(view.prev_record(before), prev, "{data:?} before={before}");
6032 }
6033 }
6034
6035 fn xorshift(state: &mut u64) -> u64 {
6036 *state ^= *state << 13;
6037 *state ^= *state >> 7;
6038 *state ^= *state << 17;
6039 *state
6040 }
6041
6042 #[test]
6043 fn record_walks_match_a_line_by_line_scan() {
6044 let hex = oid(1).to_hex();
6045 let inputs = [
6046 String::new(),
6047 "\n".to_owned(),
6048 format!("# pack-refs with: peeled \n{hex} refs/heads/a\n^{hex}\n{hex} refs/tags/b\n"),
6049 format!("{hex} refs/heads/z\n{hex} refs/heads/a\n"),
6050 format!("{hex} refs/heads/a\n\n{hex} refs/heads/b"),
6051 format!("{hex}\n{hex} \n{hex} x\nshort\n#{hex} refs/heads/c\n"),
6052 format!("{hex} refs/tags/a\n^{hex}\n^{hex}\n{hex} refs/tags/b\n^{hex}"),
6053 format!("^ab\n{}\n{hex} refs/heads/a\n", "x".repeat(37)),
6054 format!("^\n{hex} refs/heads/a\n^{hex}x\n{hex} refs/heads/b\n"),
6055 format!("^{hex}\n"),
6056 format!("^{}", &hex[1..]),
6057 ];
6058 for input in &inputs {
6059 assert_record_walks_match(input.as_bytes(), 40);
6060 }
6061 }
6062
6063 #[test]
6064 fn record_walks_match_a_line_by_line_scan_on_random_bytes() {
6065 let alphabet = b"^\n #ab";
6066 let mut state = 0x2545_f491_4f6c_dd1du64;
6067 for len in 0..400 {
6068 let input: Vec<u8> = (0..len % 97)
6069 .map(|_| alphabet[(xorshift(&mut state) % alphabet.len() as u64) as usize])
6070 .collect();
6071 for hex_len in [1, 2, 3] {
6072 assert_record_walks_match(&input, hex_len);
6073 }
6074 }
6075 }
6076
6077 #[test]
6080 fn search_matches_a_linear_scan_on_sorted_records() {
6081 let hex_len = 2;
6082 let mut state = 0x9e37_79b9_7f4a_7c15u64;
6083 for _ in 0..300 {
6084 let mut names: Vec<String> = (0..xorshift(&mut state) % 12)
6085 .map(|_| format!("n{}", xorshift(&mut state) % 40))
6086 .collect();
6087 names.sort();
6088 names.dedup();
6089 let mut data = b"# h\n".to_vec();
6090 for name in &names {
6091 data.extend_from_slice(format!("ab {name}\n").as_bytes());
6092 match xorshift(&mut state) % 4 {
6093 0 => data.extend_from_slice(b"^ab\n"),
6094 1 => data.extend_from_slice(b"junk\n"),
6095 _ => {}
6096 }
6097 }
6098 let mut view = raw_view(&data, hex_len);
6099 view.body_start = 4;
6100 let records: Vec<usize> = view.records().collect();
6101 for probe in 0..42 {
6102 let target = format!("n{probe}");
6103 let want = records
6104 .iter()
6105 .copied()
6106 .find(|&s| view.record_cmp(s, &target).is_ge())
6107 .unwrap_or(data.len());
6108 assert_eq!(view.lower_bound(&target), want, "{target} in {data:?}");
6109 let hit = view.search(&target).ok();
6110 assert_eq!(
6111 hit,
6112 names.contains(&target).then_some(want),
6113 "{target} in {data:?}"
6114 );
6115 }
6116 }
6117 }
6118}