1pub mod handle;
41pub mod listing;
42pub mod query_reachability;
43pub mod shorten;
44pub mod store;
45pub mod version_sort;
46
47pub use shorten::{
48 expand_ref_dwim_full_name, peel_symref_to_terminal_ref, shorten_unambiguous,
49 warn_ambiguous_refs_from_config,
50};
51
52mod domain_reexports;
53mod on_store;
54pub use domain_reexports::*;
55pub use handle::References;
56pub use on_store::update_branch_for_commit_on_store;
57pub(crate) use on_store::{
58 append_reflog_on_store, delete_ref_on_store, reflog_wanted,
59 update_detached_head_for_commit_on_store, verify_new_tip, write_symbolic_ref_on_store,
60};
61
62pub mod gc {
64 pub use crate::gc::*;
65}
66
67use crate::git_path::CanonicalizeExt;
68use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
69use std::fs;
70use std::io;
71use std::ops::ControlFlow;
72use std::path::{Path, PathBuf};
73use store::{
74 open_ref_store, Expected, PreparedRefTransaction, RawRef as StoreRawRef, RefCaseOnlyRename,
75 RefEntry, RefReflogRename, RefStorageFormat, RefStore, RefStoreError, RefTransaction,
76 RefUpdate, RefUpdateFlags, ReflogUpdate,
77};
78
79use crate::check_ref_format::{check_refname_format_borrowed, RefNameOptions};
80use crate::config::ConfigSet;
81use crate::error::{Error, RefLockError, Result};
82use crate::ident::Signature;
83use crate::lockfile::LockWait;
84use crate::objects::{HashAlgo, ObjectId};
85use crate::pack;
86use store::format_reflog_identity;
87
88pub const SYMREF_MAXDEPTH: usize = 5;
90
91fn map_public_ref_store_error(err: Error, refname: &str) -> Error {
92 match err {
93 Error::RefStore(store::RefStoreError::SymrefLoop) => {
94 Error::InvalidRef(format!("ref symlink too deep: {refname}"))
95 }
96 other => other,
97 }
98}
99
100fn files_legacy_write_precheck(git_dir: &Path, refname: &str) -> Result<()> {
104 let storage_dir = ref_storage_dir(git_dir, refname);
105 let stor = crate::ref_namespace::storage_ref_name(refname);
106 let path = storage_dir.join(stor);
107 remove_empty_ref_directory(&path);
108 if fs::symlink_metadata(&path)
109 .map(|m| m.file_type().is_dir())
110 .unwrap_or(false)
111 {
112 return Err(directory_in_the_way(git_dir, refname, &path));
113 }
114 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
115 return Err(Error::InvalidRef(format!(
116 "cannot update ref '{refname}': reference broken"
117 )));
118 }
119 Ok(())
120}
121
122fn directory_in_the_way(git_dir: &Path, refname: &str, path: &Path) -> Error {
129 match verify_refname_available_for_create(git_dir, refname, &BTreeSet::new(), &HashSet::new()) {
130 Err(reason) => Error::RefStore(store::RefStoreError::NameUnavailable { reason }),
131 Ok(()) => RefLockError::DirectoryInTheWay {
132 refname: refname.to_owned(),
133 path: ref_path_for_display(path),
134 }
135 .into(),
136 }
137}
138
139fn legacy_ref_update_flags(refname: &str) -> RefUpdateFlags {
140 let mut flags = RefUpdateFlags::default();
141 if refname == "HEAD" {
143 flags.no_deref = true;
144 }
145 flags
146}
147
148pub(crate) fn commit_store_update(git_dir: &Path, update: RefUpdate) -> Result<()> {
149 commit_ref_update_on_store(&store::open_checked_ref_store(git_dir)?, update)
150}
151
152pub fn commit_ref_update_on_store(store: &dyn RefStore, update: RefUpdate) -> Result<()> {
159 commit_ref_update_on_store_with_wait(store, update, || default_ref_lock_wait(store))
160}
161
162pub(crate) fn commit_ref_update_on_store_with_wait(
165 store: &dyn RefStore,
166 update: RefUpdate,
167 wait: impl FnOnce() -> LockWait,
168) -> Result<()> {
169 if let Some(ref log) = update.reflog {
170 validate_reflog_update(log)?;
171 }
172 let refname = update.name.clone();
173 let txn = RefTransaction::new()
174 .update(update)
175 .map_err(crate::error::Error::from)?;
176 let prepared = prepare_retrying(store, txn, wait)
177 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))?;
178 prepared
179 .commit()
180 .map_err(|e| map_public_ref_store_error(Error::from(e), &refname))
181}
182
183pub fn prepare_with_lock_wait(
234 store: &dyn RefStore,
235 txn: RefTransaction,
236 wait: LockWait,
237) -> std::result::Result<Box<dyn PreparedRefTransaction + Send + '_>, RefStoreError> {
238 prepare_retrying(store, txn, || wait)
239}
240
241fn prepare_retrying(
242 store: &dyn RefStore,
243 txn: RefTransaction,
244 wait: impl FnOnce() -> LockWait,
245) -> std::result::Result<Box<dyn PreparedRefTransaction + Send + '_>, RefStoreError> {
246 use crate::lockfile::{retry_while_held, SystemLockClock, SystemLockJitter};
247 retry_while_held(
248 Path::new("refs"),
249 wait,
250 &SystemLockClock,
251 &SystemLockJitter,
252 || store.prepare(txn.clone()),
253 |err| matches!(err, RefStoreError::LockHeld { .. }),
254 )
255}
256
257fn default_ref_lock_wait(store: &dyn RefStore) -> LockWait {
259 store.ref_lock_wait()
260}
261
262pub fn update_direct_ref_on_store(
270 store: &dyn RefStore,
271 git_dir: &Path,
272 refname: &str,
273 oid: &ObjectId,
274) -> Result<()> {
275 store_write_value_on_store(
276 store,
277 git_dir,
278 refname,
279 Some(StoreRawRef::Direct(*oid)),
280 Expected::Any,
281 None,
282 )
283}
284
285pub(crate) fn store_write_value_on_store(
286 store: &dyn RefStore,
287 git_dir: &Path,
288 refname: &str,
289 new_value: Option<StoreRawRef>,
290 expected: Expected,
291 reflog: Option<ReflogUpdate>,
292) -> Result<()> {
293 ensure_refname_safe_for_storage(refname)?;
294 if new_value.is_some() && store.format() == RefStorageFormat::Files {
295 files_legacy_write_precheck(git_dir, refname)?;
296 }
297 commit_ref_update_on_store(
298 store,
299 RefUpdate {
300 name: refname.to_owned(),
301 new_value,
302 expected,
303 reflog,
304 flags: legacy_ref_update_flags(refname),
305 },
306 )
307}
308
309pub fn commit_ref_store_batch(git_dir: &Path, updates: &[RefUpdate]) -> Result<()> {
329 if updates.is_empty() {
330 return Ok(());
331 }
332 commit_ref_updates_on_store(&store::open_checked_ref_store(git_dir)?, updates, &[])
333}
334
335pub fn commit_ref_updates_non_atomic(
341 git_dir: &Path,
342 updates: Vec<RefUpdate>,
343) -> Result<Vec<RefUpdateCommitOutcome>> {
344 commit_ref_updates_non_atomic_on_store(&store::open_checked_ref_store(git_dir)?, updates)
345}
346
347pub fn commit_ref_updates_on_store(
356 store: &dyn RefStore,
357 updates: &[RefUpdate],
358 reflog_renames: &[RefReflogRename],
359) -> Result<()> {
360 commit_ref_batch_on_store(store, updates, reflog_renames, &[])
361}
362
363pub fn commit_ref_batch_on_store(
369 store: &dyn RefStore,
370 updates: &[RefUpdate],
371 reflog_renames: &[RefReflogRename],
372 case_only_renames: &[RefCaseOnlyRename],
373) -> Result<()> {
374 if updates.is_empty() && reflog_renames.is_empty() && case_only_renames.is_empty() {
375 return Ok(());
376 }
377 commit_owned_ref_batch_on_store(
378 store,
379 updates.to_vec(),
380 reflog_renames.to_vec(),
381 case_only_renames.to_vec(),
382 false,
383 || default_ref_lock_wait(store),
384 )
385}
386
387#[derive(Debug, Clone, PartialEq, Eq)]
389pub struct RefUpdateCommitOutcome {
390 pub name: String,
392 pub result: std::result::Result<(), store::RefStoreError>,
394}
395
396pub fn commit_ref_updates_non_atomic_on_store(
407 store: &dyn RefStore,
408 updates: Vec<RefUpdate>,
409) -> Result<Vec<RefUpdateCommitOutcome>> {
410 if updates.is_empty() {
411 return Ok(Vec::new());
412 }
413 let order: Vec<String> = updates.iter().map(|u| u.name.clone()).collect();
414 let (deletes, rest): (Vec<_>, Vec<_>) = updates
415 .into_iter()
416 .partition(|u| u.new_value.is_none() && !u.flags.log_only);
417 let mut results: std::collections::HashMap<String, store::RefStoreError> =
418 std::collections::HashMap::new();
419 for phase in [deletes, rest] {
420 if phase.is_empty() {
421 continue;
422 }
423 for outcome in commit_ref_updates_allowing_failures_on_store(store, phase)? {
424 if let Err(err) = outcome.result {
425 results.insert(outcome.name, err);
426 }
427 }
428 }
429 Ok(order
430 .into_iter()
431 .map(|name| RefUpdateCommitOutcome {
432 result: results.remove(&name).map_or(Ok(()), Err),
433 name,
434 })
435 .collect())
436}
437
438pub fn commit_ref_updates_allowing_failures_on_store(
445 store: &dyn RefStore,
446 updates: Vec<RefUpdate>,
447) -> Result<Vec<RefUpdateCommitOutcome>> {
448 if updates.is_empty() {
449 return Ok(Vec::new());
450 }
451 let order: Vec<String> = updates.iter().map(|u| u.name.clone()).collect();
452 for update in &updates {
453 if let Some(ref log) = update.reflog {
454 validate_reflog_update(log)?;
455 }
456 }
457 let label = updates
458 .first()
459 .map(|u| u.name.clone())
460 .unwrap_or_else(|| "refs".to_owned());
461 let txn = RefTransaction::from_updates(updates).map_err(|e| {
462 let name = match &e {
463 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
464 _ => label.clone(),
465 };
466 map_public_ref_store_error(Error::from(e), &name)
467 })?;
468 let txn = txn.allowing_failures();
469 let prepared = prepare_retrying(store, txn, || default_ref_lock_wait(store))
470 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
471 let rejections: std::collections::HashMap<String, store::RefStoreError> = prepared
472 .rejected_updates()
473 .iter()
474 .map(|(name, err)| (name.clone(), err.clone()))
475 .collect();
476 prepared
477 .commit()
478 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
479
480 Ok(order
481 .into_iter()
482 .map(|name| {
483 let result = rejections.get(&name).cloned().map_or(Ok(()), Err);
484 RefUpdateCommitOutcome { name, result }
485 })
486 .collect())
487}
488
489pub(crate) fn commit_ref_updates_on_store_with_wait(
492 store: &dyn RefStore,
493 updates: &[RefUpdate],
494 wait: impl FnOnce() -> LockWait,
495) -> Result<()> {
496 if updates.is_empty() {
497 return Ok(());
498 }
499 commit_owned_ref_batch_on_store(store, updates.to_vec(), Vec::new(), Vec::new(), false, wait)
500}
501
502pub(crate) fn commit_owned_ref_updates_on_store(
508 store: &dyn RefStore,
509 updates: Vec<RefUpdate>,
510) -> Result<()> {
511 if updates.is_empty() {
512 return Ok(());
513 }
514 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new(), false, || {
515 default_ref_lock_wait(store)
516 })
517}
518
519pub(crate) fn commit_initial_ref_updates_on_store(
527 store: &dyn RefStore,
528 updates: Vec<RefUpdate>,
529) -> Result<()> {
530 commit_initial_ref_updates_on_store_with_wait(store, updates, || default_ref_lock_wait(store))
531}
532
533pub(crate) fn commit_initial_ref_updates_on_store_with_wait(
536 store: &dyn RefStore,
537 updates: Vec<RefUpdate>,
538 wait: impl FnOnce() -> LockWait,
539) -> Result<()> {
540 if updates.is_empty() {
541 return Ok(());
542 }
543 commit_owned_ref_batch_on_store(store, updates, Vec::new(), Vec::new(), true, wait)
544}
545
546fn commit_owned_ref_batch_on_store(
547 store: &dyn RefStore,
548 updates: Vec<RefUpdate>,
549 reflog_renames: Vec<RefReflogRename>,
550 case_only_renames: Vec<RefCaseOnlyRename>,
551 initial: bool,
552 wait: impl FnOnce() -> LockWait,
553) -> Result<()> {
554 for update in &updates {
555 if let Some(ref log) = update.reflog {
556 validate_reflog_update(log)?;
557 }
558 }
559 for rename in &reflog_renames {
560 validate_reflog_update(&rename.append)?;
561 }
562 let label = updates
563 .first()
564 .map(|u| u.name.clone())
565 .or_else(|| reflog_renames.first().map(|r| r.from.clone()))
566 .or_else(|| case_only_renames.first().map(|r| r.from.clone()))
567 .unwrap_or_else(|| "refs".to_owned());
568 let mut txn = RefTransaction::from_updates(updates).map_err(|e| {
569 let name = match &e {
570 store::RefStoreError::DuplicateUpdate { name } => name.clone(),
571 _ => label.clone(),
572 };
573 map_public_ref_store_error(Error::from(e), &name)
574 })?;
575 for rename in reflog_renames {
576 txn = txn.with_reflog_rename(rename);
577 }
578 for rename in case_only_renames {
579 txn = txn.with_case_only_rename(rename);
580 }
581 if initial {
582 txn = txn.initial();
583 }
584 let prepared = prepare_retrying(store, txn, wait)
585 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))?;
586 prepared
587 .commit()
588 .map_err(|e| map_public_ref_store_error(Error::from(e), &label))
589}
590
591fn store_write_value(
592 git_dir: &Path,
593 refname: &str,
594 new_value: Option<StoreRawRef>,
595 expected: Expected,
596 reflog: Option<ReflogUpdate>,
597) -> Result<()> {
598 store_write_value_on_store(
599 &store::open_checked_ref_store(git_dir)?,
600 git_dir,
601 refname,
602 new_value,
603 expected,
604 reflog,
605 )
606}
607
608pub(crate) fn collect_resolved_refs(
609 store: &dyn RefStore,
610 prefix: &str,
611) -> Result<Vec<(String, ObjectId)>> {
612 collect_resolved_ref_entries(store, |f| store.for_each_ref(prefix, f))
613}
614
615fn collect_resolved_ref_entries(
617 store: &dyn RefStore,
618 for_each: impl FnOnce(&mut dyn FnMut(&RefEntry) -> ControlFlow<()>) -> Result<()>,
619) -> Result<Vec<(String, ObjectId)>> {
620 let mut results = Vec::new();
621 for_each(&mut |entry: &RefEntry| {
622 let oid = match &entry.value {
623 StoreRawRef::Direct(oid) => Some(*oid),
624 StoreRawRef::Symbolic(target) => entry
625 .peeled
626 .or_else(|| store.resolve(target).ok())
627 .or_else(|| store.resolve(&entry.name).ok()),
628 };
629 if let Some(oid) = oid {
630 results.push((entry.name.clone(), oid));
631 }
632 ControlFlow::Continue(())
633 })?;
634 Ok(results)
635}
636
637#[derive(Debug, Clone, PartialEq, Eq)]
639pub enum Ref {
640 Direct(ObjectId),
642 Symbolic(String),
644}
645
646pub fn read_ref_file(path: &Path) -> Result<Ref> {
653 let meta = fs::symlink_metadata(path).map_err(|e| loose_ref_io_error(e, path))?;
655 if meta.file_type().is_symlink() {
656 if let Ok(target) = fs::read_link(path) {
657 return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
658 }
659 }
660 if meta.is_dir() {
661 return Err(Error::Io(io::Error::new(
662 io::ErrorKind::NotFound,
663 "loose ref path is a directory",
664 )));
665 }
666 read_regular_ref_file(path)
667}
668
669pub(crate) fn read_regular_ref_file(path: &Path) -> Result<Ref> {
671 let content = fs::read_to_string(path).map_err(|e| loose_ref_io_error(e, path))?;
672 parse_ref_content(content.trim_end_matches('\n'))
673}
674
675fn loose_ref_io_error(e: io::Error, path: &Path) -> Error {
679 if io_error_is_missing_file(&e, path) {
680 return Error::Io(io::Error::new(io::ErrorKind::NotFound, e));
681 }
682 Error::Io(e)
683}
684
685pub(crate) fn io_error_is_missing_file(err: &io::Error, path: &Path) -> bool {
693 match err.kind() {
694 io::ErrorKind::NotFound | io::ErrorKind::IsADirectory | io::ErrorKind::NotADirectory => {
695 true
696 }
697 io::ErrorKind::PermissionDenied => fs::symlink_metadata(path).is_ok_and(|m| m.is_dir()),
698 _ => matches!(err.raw_os_error(), Some(libc::EISDIR | libc::ENOTDIR)),
699 }
700}
701
702pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
704 if let Some(target) = content.strip_prefix("ref: ") {
705 Ok(Ref::Symbolic(target.trim().to_owned()))
706 } else if let Some(token) = content.split_whitespace().next() {
707 if ObjectId::is_full_hex(token) {
708 let oid: ObjectId = token.parse()?;
709 Ok(Ref::Direct(oid))
710 } else if content == "unknown-oid" {
711 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
712 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
713 Ok(Ref::Direct(oid))
714 } else {
715 Err(Error::InvalidRef(content.to_owned()))
716 }
717 } else {
718 Err(Error::InvalidRef(content.to_owned()))
719 }
720}
721
722pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
742 open_ref_store(git_dir)?
743 .resolve(refname)
744 .map_err(|err| map_public_ref_store_error(err, refname))
745}
746
747pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
752 let commondir_file = git_dir.join("commondir");
753 if !commondir_file.is_file() {
754 return None;
755 }
756 let raw = fs::read_to_string(commondir_file).ok()?;
757 let rel = raw.trim();
758 let path = if Path::new(rel).is_absolute() {
761 PathBuf::from(rel)
762 } else {
763 git_dir.join(rel)
764 };
765 path.canonicalize_simplified().ok()
766}
767
768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub enum RawRefLookup {
775 Exists,
777 NotFound,
779 IsDirectory,
781}
782
783pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
804 let store = open_ref_store(git_dir)?;
805 match store.read_raw(refname)? {
806 Some(_) => Ok(RawRefLookup::Exists),
807 None => {
808 let (storage_dir, stor_name) =
809 crate::worktree_ref::resolve_ref_storage(git_dir, refname);
810 let path = storage_dir.join(&stor_name);
811 if fs::symlink_metadata(&path)
812 .map(|m| m.is_dir())
813 .unwrap_or(false)
814 {
815 Ok(RawRefLookup::IsDirectory)
816 } else {
817 Ok(RawRefLookup::NotFound)
818 }
819 }
820 }
821}
822
823#[must_use]
825pub fn lock_path_for_ref(path: &Path) -> PathBuf {
826 let mut name = path
827 .file_name()
828 .map(|s| s.to_os_string())
829 .unwrap_or_default();
830 name.push(".lock");
831 match path.parent() {
832 Some(parent) => parent.join(&name),
833 None => PathBuf::from(name),
834 }
835}
836
837fn first_packed_descendant_ref(store: &Path, refname: &str) -> Result<Option<String>> {
839 let leaf_dir = format!("{refname}/");
840 let map = read_packed_refs_map_for_prefix(store, refname)?;
841 let sorted: BTreeSet<String> = map.keys().cloned().collect();
842 Ok(sorted
843 .range(leaf_dir.clone()..)
844 .next()
845 .filter(|name| name.starts_with(&leaf_dir))
846 .cloned())
847}
848
849fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
850 Ok(lookup_packed_ref(git_dir, refname)?.is_some())
851}
852
853pub(crate) fn packed_refname_namespace_conflict(
858 sorted_packed: &BTreeSet<String>,
859 candidate: &str,
860) -> bool {
861 let segments: Vec<&str> = candidate.split('/').filter(|s| !s.is_empty()).collect();
862 if segments.len() > 1 {
863 let mut dirname = String::new();
864 for part in &segments[..segments.len() - 1] {
865 if !dirname.is_empty() {
866 dirname.push('/');
867 }
868 dirname.push_str(part);
869 if sorted_packed.contains(&dirname) {
870 return true;
871 }
872 }
873 }
874
875 let leaf_dir = format!("{candidate}/");
876 if let Some(existing) = sorted_packed.range(leaf_dir.clone()..).next() {
877 if existing.starts_with(&leaf_dir) {
878 return true;
879 }
880 }
881 false
882}
883
884fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
885 Ok(PackedRefs::load(git_dir)?.has_namespace_conflict(refname))
886}
887
888pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
904 if refname == "HEAD" {
905 return Ok(false);
906 }
907 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
908 return Ok(false);
909 }
910 let storage_dir = ref_storage_dir(git_dir, refname);
911 packed_ref_name_exists(&storage_dir, refname)
912}
913
914#[derive(Debug, Clone, PartialEq, Eq)]
916pub enum RefnameUnavailable {
917 AncestorExists {
919 blocking: String,
921 new_ref: String,
923 },
924 DescendantExists {
926 blocking: String,
928 new_ref: String,
930 },
931 SameBatch {
933 refname: String,
935 other: String,
937 },
938}
939
940impl RefnameUnavailable {
941 #[must_use]
943 pub fn refname(&self) -> &str {
944 match self {
945 RefnameUnavailable::AncestorExists { new_ref, .. }
946 | RefnameUnavailable::DescendantExists { new_ref, .. } => new_ref,
947 RefnameUnavailable::SameBatch { refname, .. } => refname,
948 }
949 }
950
951 #[must_use]
953 pub fn lock_message_suffix(&self) -> String {
954 match self {
955 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
956 format!("'{blocking}' exists; cannot create '{new_ref}'")
957 }
958 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
959 format!("'{blocking}' exists; cannot create '{new_ref}'")
960 }
961 RefnameUnavailable::SameBatch { refname, other } => {
962 format!("cannot process '{refname}' and '{other}' at the same time")
963 }
964 }
965 }
966}
967
968fn find_descendant_in_sorted_extras(
969 dirname_with_slash: &str,
970 extras: &BTreeSet<String>,
971) -> Option<String> {
972 let start = extras
973 .range(dirname_with_slash.to_string()..)
974 .next()
975 .cloned()?;
976 if start.starts_with(dirname_with_slash) {
977 Some(start)
978 } else {
979 None
980 }
981}
982
983#[must_use]
987pub fn is_valid_fetch_advertised_ref(name: &str) -> bool {
988 crate::check_ref_format::is_valid_fetch_advertised_ref(name)
989}
990
991#[must_use]
996pub fn is_valid_ls_refs_advertised_name(name: &str) -> bool {
997 name == "HEAD" || is_valid_fetch_advertised_ref(name)
998}
999
1000#[must_use]
1002pub fn is_valid_storable_ref_name(name: &str) -> bool {
1003 is_valid_fetch_advertised_ref(name)
1004}
1005
1006#[must_use]
1008pub fn is_valid_advertised_symref_target(name: &str) -> bool {
1009 crate::check_ref_format::is_valid_advertised_symref_target(name)
1010}
1011
1012pub fn verify_refname_available_for_create(
1025 git_dir: &Path,
1026 refname: &str,
1027 extras: &BTreeSet<String>,
1028 skip: &HashSet<String>,
1029) -> std::result::Result<(), RefnameUnavailable> {
1030 let git_dir = git_dir
1033 .canonicalize_simplified()
1034 .unwrap_or_else(|_| git_dir.to_path_buf());
1035 let mut seen_dirnames: HashSet<String> = HashSet::new();
1036 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
1037 if segments.len() <= 1 {
1038 } else {
1040 let mut dirname = String::new();
1041 for part in &segments[..segments.len() - 1] {
1042 if !dirname.is_empty() {
1043 dirname.push('/');
1044 }
1045 dirname.push_str(part);
1046
1047 if !seen_dirnames.insert(dirname.clone()) {
1048 continue;
1049 }
1050
1051 if skip.contains(&dirname) {
1052 continue;
1053 }
1054
1055 match read_raw_ref(&git_dir, &dirname) {
1056 Ok(RawRefLookup::Exists) => {
1057 return Err(RefnameUnavailable::AncestorExists {
1058 blocking: dirname.clone(),
1059 new_ref: refname.to_owned(),
1060 });
1061 }
1062 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
1065 Err(_) => {}
1066 }
1067
1068 if extras.contains(&dirname) {
1069 return Err(RefnameUnavailable::SameBatch {
1070 refname: refname.to_owned(),
1071 other: dirname.clone(),
1072 });
1073 }
1074 }
1075 }
1076
1077 let mut leaf_dir = String::with_capacity(refname.len() + 1);
1078 leaf_dir.push_str(refname);
1079 leaf_dir.push('/');
1080
1081 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
1087 let may_have_loose_descendants = packed_dir.join("reftable").is_dir()
1088 || git_dir.join(refname).is_dir()
1089 || packed_dir.join(refname).is_dir();
1090 let under = if may_have_loose_descendants {
1091 list_refs(&git_dir, &leaf_dir).unwrap_or_default()
1092 } else {
1093 Vec::new()
1094 };
1095 if under.is_empty() {
1096 if let Ok(Some(name)) = first_packed_descendant_ref(&packed_dir, refname) {
1097 if !skip.contains(&name) {
1098 return Err(RefnameUnavailable::DescendantExists {
1099 blocking: name,
1100 new_ref: refname.to_owned(),
1101 });
1102 }
1103 }
1104 if packed_dir != git_dir {
1105 if let Ok(Some(name)) = first_packed_descendant_ref(&git_dir, refname) {
1106 if !skip.contains(&name) {
1107 return Err(RefnameUnavailable::DescendantExists {
1108 blocking: name,
1109 new_ref: refname.to_owned(),
1110 });
1111 }
1112 }
1113 }
1114 }
1115 if under.is_empty()
1116 && fs::symlink_metadata(git_dir.join(refname))
1117 .map(|m| m.is_dir())
1118 .unwrap_or(false)
1119 {
1120 let mut blocking: Option<String> = None;
1121 let dir_path = git_dir.join(refname);
1122 if let Ok(read) = fs::read_dir(&dir_path) {
1123 for entry in read.flatten() {
1124 let path = entry.path();
1125 let Ok(meta) = fs::metadata(&path) else {
1126 continue;
1127 };
1128 let name = entry.file_name().to_string_lossy().into_owned();
1129 if !meta.is_file() || name.ends_with(".lock") {
1130 continue;
1131 }
1132 let full = format!("{refname}/{name}");
1133 blocking = Some(full);
1134 break;
1135 }
1136 }
1137 if let Some(b) = blocking {
1138 if !skip.contains(&b) {
1139 return Err(RefnameUnavailable::DescendantExists {
1140 blocking: b,
1141 new_ref: refname.to_owned(),
1142 });
1143 }
1144 }
1145 }
1146
1147 for (existing, _) in under {
1148 if skip.contains(&existing) {
1149 continue;
1150 }
1151 return Err(RefnameUnavailable::DescendantExists {
1152 blocking: existing,
1153 new_ref: refname.to_owned(),
1154 });
1155 }
1156
1157 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
1158 if !skip.contains(&extra) {
1159 return Err(RefnameUnavailable::SameBatch {
1160 refname: refname.to_owned(),
1161 other: extra,
1162 });
1163 }
1164 }
1165
1166 Ok(())
1167}
1168
1169#[derive(Clone, Debug)]
1171pub struct RefBatchItem {
1172 pub name: String,
1174 pub new_oid: Option<ObjectId>,
1176}
1177
1178pub fn verify_ref_transaction_batch(
1189 git_dir: &Path,
1190 items: &[RefBatchItem],
1191) -> std::result::Result<(), RefnameUnavailable> {
1192 if let Some((parent, child)) =
1193 batch_refname_prefix_conflict(items.iter().map(|i| i.name.as_str()))
1194 {
1195 return Err(RefnameUnavailable::SameBatch {
1196 refname: child,
1197 other: parent,
1198 });
1199 }
1200
1201 if crate::ref_storage::detect_format(git_dir).is_ok_and(|f| f.is_reftable()) {
1202 return crate::reftable::verify_ref_transaction_batch_reftable(git_dir, items);
1203 }
1204
1205 let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
1206 let empty_skip = HashSet::new();
1207 for item in items {
1208 let Some(_oid) = item.new_oid else {
1209 continue;
1210 };
1211 if resolve_ref(git_dir, &item.name).is_ok() {
1212 continue;
1213 }
1214 verify_refname_available_for_create(git_dir, &item.name, &extras, &empty_skip)?;
1215 }
1216 Ok(())
1217}
1218
1219pub(crate) fn batch_refname_prefix_conflict<'a>(
1221 names: impl Iterator<Item = &'a str>,
1222) -> Option<(String, String)> {
1223 let mut sorted: Vec<&str> = names.collect();
1224 sorted.sort_unstable();
1225 for pair in sorted.windows(2) {
1226 let (a, b) = (pair[0], pair[1]);
1227 if refname_is_strict_prefix(a, b) {
1228 return Some((a.to_owned(), b.to_owned()));
1229 }
1230 if refname_is_strict_prefix(b, a) {
1231 return Some((b.to_owned(), a.to_owned()));
1232 }
1233 }
1234 None
1235}
1236
1237fn refname_is_strict_prefix(parent: &str, child: &str) -> bool {
1238 child.len() > parent.len()
1239 && child.as_bytes().get(parent.len()) == Some(&b'/')
1240 && child.starts_with(parent)
1241}
1242
1243fn packed_refs_unexpected_line(path: &Path, line: impl Into<String>) -> Error {
1244 Error::PackedRefsUnexpectedLine {
1245 path: path.display().to_string(),
1246 line: line.into(),
1247 }
1248}
1249
1250fn packed_refs_unterminated_line(path: &Path, line: impl Into<String>) -> Error {
1251 Error::PackedRefsUnexpectedLine {
1252 path: path.display().to_string(),
1253 line: line.into(),
1254 }
1255}
1256
1257fn parse_packed_ref_peel_line(path: &Path, line: &str, expect_peel: bool) -> Result<ObjectId> {
1259 if !expect_peel {
1260 return Err(packed_refs_unexpected_line(path, line));
1261 }
1262 line.strip_prefix('^')
1263 .and_then(|inner| ObjectId::from_hex(inner).ok())
1264 .ok_or_else(|| packed_refs_unexpected_line(path, line))
1265}
1266
1267fn packed_line_oid(line: &str) -> Option<(usize, ObjectId)> {
1269 let bytes = line.as_bytes();
1270 let hex_len = [HashAlgo::Sha1.hex_len(), HashAlgo::Sha256.hex_len()]
1271 .into_iter()
1272 .find(|&len| bytes.get(len) == Some(&b' '))?;
1273 let oid = ObjectId::from_hex(&line[..hex_len]).ok()?;
1275 Some((hex_len, oid))
1276}
1277
1278fn packed_ref_data_line_refname(line: &str) -> Option<&str> {
1279 let bytes = line.as_bytes();
1280 let mut j = 0usize;
1281 while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
1282 j += 1;
1283 }
1284 if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
1285 return None;
1286 }
1287 let refname = &line[j + 1..];
1288 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
1289 return None;
1290 }
1291 Some(refname)
1292}
1293
1294fn parse_packed_ref_data_line(
1295 path: &Path,
1296 line: &str,
1297 ref_opts: &RefNameOptions,
1298) -> Result<(String, ObjectId)> {
1299 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, ref_opts)?;
1300 Ok((refname.to_owned(), oid))
1301}
1302
1303fn parse_packed_ref_data_line_borrowed<'a>(
1304 path: &Path,
1305 line: &'a str,
1306 ref_opts: &RefNameOptions,
1307) -> Result<(&'a str, ObjectId)> {
1308 let Some((hex_len, oid)) = packed_line_oid(line) else {
1309 return Err(packed_refs_unexpected_line(path, line));
1310 };
1311 let refname = &line[hex_len + 1..];
1312 if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
1313 return Err(packed_refs_unexpected_line(path, line));
1314 }
1315 if check_refname_format_borrowed(refname, ref_opts).is_err() {
1316 return Err(packed_refs_unexpected_line(path, line));
1317 }
1318 Ok((refname, oid))
1319}
1320
1321enum StreamRecordsError {
1340 OutOfOrder,
1341 Packed(Error),
1342}
1343
1344#[derive(Debug, Clone)]
1345pub(crate) struct PackedRefsFileView {
1346 data: std::sync::Arc<crate::pack_map::PackData>,
1347 sorted: bool,
1350 hex_len: usize,
1352 body_start: usize,
1354 peel_trait: PackedPeelTrait,
1356}
1357
1358impl PackedRefsFileView {
1359 #[cfg(test)]
1361 fn from_bytes(path: &Path, data: Vec<u8>, hash_algo: crate::objects::HashAlgo) -> Result<Self> {
1362 Self::from_data(path, crate::pack_map::PackData::from_owned(data), hash_algo)
1363 }
1364
1365 fn from_data(
1374 path: &Path,
1375 data: std::sync::Arc<crate::pack_map::PackData>,
1376 hash_algo: crate::objects::HashAlgo,
1377 ) -> Result<Self> {
1378 let hex_len = hash_algo.hex_len();
1379 if data.is_empty() {
1380 return Ok(Self {
1381 data,
1382 sorted: false,
1383 hex_len,
1384 body_start: 0,
1385 peel_trait: PackedPeelTrait::None,
1386 });
1387 }
1388 if !data.ends_with(b"\n") {
1389 let tail = std::str::from_utf8(&data)
1390 .map(|s| s.rsplit('\n').next().unwrap_or(s))
1391 .unwrap_or("<invalid utf-8>");
1392 return Err(packed_refs_unterminated_line(path, tail));
1393 }
1394 let text = std::str::from_utf8(&data[..packed_refs_leading_comments_end(&data)])
1397 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1398 let body_start = if data.starts_with(b"#") {
1401 memchr::memchr(b'\n', &data).map_or(data.len(), |nl| nl + 1)
1402 } else {
1403 0
1404 };
1405 let body = &data[body_start..];
1406 if let Some(rest) = body.strip_suffix(b"\n") {
1407 let last_line = memchr::memrchr(b'\n', rest).map_or(rest, |nl| &rest[nl + 1..]);
1408 if last_line.len() < hex_len + 1 {
1409 return Err(packed_refs_unexpected_line(
1410 path,
1411 String::from_utf8_lossy(last_line),
1412 ));
1413 }
1414 }
1415 Ok(Self {
1416 sorted: packed_refs_file_declares_sorted(text),
1417 hex_len,
1418 body_start,
1419 peel_trait: packed_refs_peel_trait(text),
1420 data,
1421 })
1422 }
1423
1424 fn record_name(&self, line_start: usize) -> &[u8] {
1426 let end = packed_line_end(&self.data, line_start);
1427 &self.data[line_start + self.hex_len + 1..end]
1428 }
1429
1430 fn record_cmp(&self, line_start: usize, name: &str) -> std::cmp::Ordering {
1431 self.record_name(line_start).cmp(name.as_bytes())
1432 }
1433
1434 fn next_record(&self, from: usize, limit: usize) -> Option<usize> {
1437 let hex_len = self.hex_len;
1438 let mut start = from;
1439 while start < limit {
1440 let rest = &self.data[start..];
1441 if rest[0] == b'^' && rest.get(hex_len + 1) == Some(&b'\n') {
1444 start += hex_len + 2;
1445 continue;
1446 }
1447 let end = memchr::memchr(b'\n', rest).unwrap_or(rest.len());
1448 if packed_line_is_record(&rest[..end], hex_len) {
1449 return Some(start);
1450 }
1451 start += end + 1;
1452 }
1453 None
1454 }
1455
1456 fn prev_record(&self, before: usize) -> Option<usize> {
1458 let mut end = before;
1459 while end > 0 {
1460 let line_end = end - 1;
1461 let start = memchr::memrchr(b'\n', &self.data[..line_end]).map_or(0, |nl| nl + 1);
1462 if packed_line_is_record(&self.data[start..line_end], self.hex_len) {
1463 return Some(start);
1464 }
1465 end = start;
1466 }
1467 None
1468 }
1469
1470 fn records(&self) -> impl Iterator<Item = usize> + '_ {
1472 let len = self.data.len();
1473 std::iter::successors(self.next_record(0, len), move |&start| {
1474 self.next_record(packed_line_end(&self.data, start) + 1, len)
1475 })
1476 }
1477
1478 fn search(&self, name: &str) -> std::result::Result<usize, usize> {
1486 let len = self.data.len();
1487 let mut lo = self.body_start;
1488 let mut hi = len;
1489 while lo < hi {
1490 let mid = lo + (hi - lo) / 2;
1491 let line = memchr::memrchr(b'\n', &self.data[lo..mid]).map_or(lo, |nl| lo + nl + 1);
1492 let Some(record) = self.next_record(line, hi) else {
1493 hi = line;
1494 continue;
1495 };
1496 match self.record_cmp(record, name) {
1497 std::cmp::Ordering::Less => lo = packed_line_end(&self.data, record) + 1,
1498 std::cmp::Ordering::Greater => hi = record,
1499 std::cmp::Ordering::Equal => return Ok(record),
1500 }
1501 }
1502 Err(self.next_record(lo, len).unwrap_or(len))
1503 }
1504
1505 fn lower_bound(&self, name: &str) -> usize {
1507 let (Ok(start) | Err(start)) = self.search(name);
1508 start
1509 }
1510
1511 fn lookup_entry(
1513 &self,
1514 path: &Path,
1515 refname: &str,
1516 ) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
1517 let ref_opts = RefNameOptions {
1518 allow_onelevel: false,
1519 refspec_pattern: false,
1520 normalize: false,
1521 };
1522 let line_start = if self.sorted {
1523 self.search(refname).ok()
1524 } else {
1525 self.records()
1526 .find(|&start| self.record_cmp(start, refname).is_eq())
1527 };
1528 let Some(start) = line_start else {
1529 return Ok(None);
1530 };
1531 let (name, oid) = parse_packed_line_at(path, &self.data, start, &ref_opts)?;
1532 debug_assert_eq!(name, refname);
1533 let line_end = packed_line_end(&self.data, start);
1534 if line_end + 1 < self.data.len() && self.data[line_end + 1] == b'^' {
1535 let peel_line = packed_line_as_str(&self.data, line_end + 1);
1536 parse_packed_ref_peel_line(path, peel_line, true)?;
1537 }
1538 let peel = peel_oid_following_line(&self.data, start);
1539 Ok(Some((oid, peel)))
1540 }
1541
1542 fn lookup_oid(&self, path: &Path, refname: &str) -> Result<Option<ObjectId>> {
1543 Ok(self.lookup_entry(path, refname)?.map(|(oid, _)| oid))
1544 }
1545
1546 pub(crate) fn first_descendant(
1550 &self,
1551 path: &Path,
1552 refname: &str,
1553 ) -> Result<Option<(String, ObjectId)>> {
1554 let leaf_dir = format!("{refname}/");
1555 Ok(self
1556 .map_for_prefix(path, Some(refname))?
1557 .into_iter()
1558 .filter(|(name, _)| name.starts_with(&leaf_dir))
1559 .min_by(|a, b| a.0.cmp(&b.0)))
1560 }
1561
1562 fn map_for_prefix(
1573 &self,
1574 path: &Path,
1575 prefix: Option<&str>,
1576 ) -> Result<HashMap<String, ObjectId>> {
1577 self.map_for_prefix_filtered(path, prefix, None)
1578 }
1579
1580 pub(crate) fn full_map(&self, path: &Path) -> Result<BTreeMap<String, ObjectId>> {
1583 let (mut records, in_order) =
1584 self.parse_region::<Vec<_>>(path, 0, self.data.len(), None, None)?;
1585 if !in_order {
1586 records.sort_by(|a, b| a.0.cmp(&b.0));
1587 }
1588 records.dedup_by(|later, earlier| {
1590 if later.0 != earlier.0 {
1591 return false;
1592 }
1593 earlier.1 = later.1;
1594 true
1595 });
1596 Ok(records.into_iter().collect())
1597 }
1598
1599 fn map_for_prefix_filtered(
1602 &self,
1603 path: &Path,
1604 prefix: Option<&str>,
1605 keep: Option<PackedRecordFilter<'_>>,
1606 ) -> Result<HashMap<String, ObjectId>> {
1607 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1608 let range_prefix = prefix_trim.filter(|p| self.sorted && !p.is_empty() && *p != "refs");
1609 if let Some((begin, end)) = range_prefix.and_then(|prefix| self.prefix_region(prefix)) {
1610 let (map, in_order) = self.parse_region(path, begin, end, prefix_trim, keep)?;
1611 if in_order {
1612 return Ok(map);
1613 }
1614 }
1615 let (map, _) = self.parse_region(path, 0, self.data.len(), prefix_trim, keep)?;
1617 Ok(map)
1618 }
1619
1620 pub(crate) fn for_each_record(
1631 &self,
1632 path: &Path,
1633 prefix: Option<&str>,
1634 keep: Option<PackedRecordFilter<'_>>,
1635 mut f: impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1636 ) -> Result<ControlFlow<()>> {
1637 if !self.sorted {
1638 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1639 }
1640 if keep.is_some() {
1641 return self.for_each_kept_record(path, prefix, keep, f);
1642 }
1643 let mut prove = |_: &str, _: ObjectId, _: PackedPeel| ControlFlow::Continue(());
1644 match self.walk_records(path, prefix, keep, false, &mut prove) {
1645 Err(StreamRecordsError::OutOfOrder) => {
1646 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1647 }
1648 Err(StreamRecordsError::Packed(e)) => return Err(e),
1649 Ok(_) => {}
1650 }
1651 match self.walk_records(path, prefix, keep, true, &mut f) {
1652 Ok(flow) => Ok(flow),
1653 Err(StreamRecordsError::OutOfOrder) => {
1654 self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f)
1655 }
1656 Err(StreamRecordsError::Packed(e)) => Err(e),
1657 }
1658 }
1659
1660 fn for_each_kept_record(
1667 &self,
1668 path: &Path,
1669 prefix: Option<&str>,
1670 keep: Option<PackedRecordFilter<'_>>,
1671 mut f: impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1672 ) -> Result<ControlFlow<()>> {
1673 let mut kept = Vec::new();
1674 let mut hold = |name, oid, peel| {
1675 kept.push((name, oid, peel));
1676 ControlFlow::Continue(())
1677 };
1678 match self.walk_records(path, prefix, keep, true, &mut hold) {
1679 Ok(_) => {}
1680 Err(StreamRecordsError::OutOfOrder) => {
1681 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1682 }
1683 Err(StreamRecordsError::Packed(e)) => return Err(e),
1684 }
1685 for (name, oid, peel) in kept {
1686 if f(name, oid, peel).is_break() {
1687 return Ok(ControlFlow::Break(()));
1688 }
1689 }
1690 Ok(ControlFlow::Continue(()))
1691 }
1692
1693 fn for_each_record_fallback(
1694 &self,
1695 path: &Path,
1696 prefix: Option<&str>,
1697 keep: Option<PackedRecordFilter<'_>>,
1698 already_emitted: &BTreeSet<String>,
1699 f: &mut impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1700 ) -> Result<ControlFlow<()>> {
1701 let map = self.map_for_prefix_filtered(path, prefix, keep)?;
1702 let mut names: Vec<String> = map.keys().cloned().collect();
1703 names.sort();
1704 for name in names {
1705 if already_emitted.contains(&name) {
1706 continue;
1707 }
1708 let oid = map[&name];
1709 if f(&name, oid, PackedPeel::Unknown).is_break() {
1710 return Ok(ControlFlow::Break(()));
1711 }
1712 }
1713 Ok(ControlFlow::Continue(()))
1714 }
1715
1716 fn walk_records<'s>(
1717 &'s self,
1718 path: &Path,
1719 prefix: Option<&str>,
1720 keep: Option<PackedRecordFilter<'_>>,
1721 emit: bool,
1722 f: &mut impl FnMut(&'s str, ObjectId, PackedPeel) -> ControlFlow<()>,
1723 ) -> std::result::Result<ControlFlow<()>, StreamRecordsError> {
1724 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1725 let prefix_upper = if self.sorted {
1726 prefix_trim.and_then(list_prefix_upper_bound)
1727 } else {
1728 None
1729 };
1730 let ref_opts = RefNameOptions {
1731 allow_onelevel: false,
1732 refspec_pattern: false,
1733 normalize: false,
1734 };
1735 let (mut pos, end) = self.record_scan_range(prefix_trim, prefix_upper.as_deref());
1736 let mut prev_name = "";
1737 let mut expect_peel = false;
1738 let mut pending: Option<(&'s str, ObjectId, Option<ObjectId>)> = None;
1739 let mut flush = |pending: &mut Option<(&'s str, ObjectId, Option<ObjectId>)>| {
1740 let Some((refname, oid, peeled)) = pending.take() else {
1741 return Ok(ControlFlow::Continue(()));
1742 };
1743 let peel = if self.peel_trait.covers(refname) {
1744 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1745 } else {
1746 PackedPeel::Unknown
1747 };
1748 if keep.is_some_and(|keep| !keep(refname, &oid, peel)) {
1749 return Ok(ControlFlow::Continue(()));
1750 }
1751 if !emit {
1752 return Ok(ControlFlow::Continue(()));
1753 }
1754 if f(refname, oid, peel).is_break() {
1755 Ok(ControlFlow::Break(()))
1756 } else {
1757 Ok(ControlFlow::Continue(()))
1758 }
1759 };
1760
1761 while pos < end {
1762 let line_end = packed_line_end(&self.data, pos).min(end);
1763 if self.data[pos..line_end].contains(&b'\r') {
1764 return Err(StreamRecordsError::Packed(packed_refs_unexpected_line(
1765 path,
1766 "<contains carriage return>",
1767 )));
1768 }
1769 let line = std::str::from_utf8(&self.data[pos..line_end]).map_err(|_| {
1770 StreamRecordsError::Packed(packed_refs_unexpected_line(path, "<invalid utf-8>"))
1771 })?;
1772 if line.is_empty() {
1773 if pos + 1 < end {
1774 return Err(StreamRecordsError::Packed(packed_refs_unexpected_line(
1775 path, line,
1776 )));
1777 }
1778 break;
1779 }
1780 if line.starts_with('#') {
1781 if flush(&mut pending)?.is_break() {
1782 return Ok(ControlFlow::Break(()));
1783 }
1784 expect_peel = false;
1785 pos = line_end + 1;
1786 continue;
1787 }
1788 if line.starts_with('^') {
1789 let peel = parse_packed_ref_peel_line(path, line, expect_peel)
1790 .map_err(StreamRecordsError::Packed)?;
1791 if let Some((_, _, stored)) = pending.as_mut() {
1792 *stored = Some(peel);
1793 }
1794 expect_peel = false;
1795 pos = line_end + 1;
1796 continue;
1797 }
1798 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)
1799 .map_err(StreamRecordsError::Packed)?;
1800 if self.sorted && !prev_name.is_empty() && refname < prev_name {
1801 return Err(StreamRecordsError::OutOfOrder);
1802 }
1803 if flush(&mut pending)?.is_break() {
1804 return Ok(ControlFlow::Break(()));
1805 }
1806 prev_name = refname;
1807 let in_prefix = prefix_trim.is_none_or(|p| ref_name_matches_list_prefix(refname, p));
1808 if in_prefix {
1809 pending = Some((refname, oid, None));
1810 } else if self.sorted {
1811 if let Some(upper) = prefix_upper.as_deref() {
1812 if refname >= upper {
1813 break;
1814 }
1815 }
1816 }
1817 expect_peel = true;
1818 pos = line_end + 1;
1819 }
1820 if flush(&mut pending)?.is_break() {
1821 return Ok(ControlFlow::Break(()));
1822 }
1823 Ok(ControlFlow::Continue(()))
1824 }
1825
1826 fn record_scan_range(
1827 &self,
1828 prefix_trim: Option<&str>,
1829 prefix_upper: Option<&str>,
1830 ) -> (usize, usize) {
1831 let len = self.data.len();
1832 if self.sorted {
1833 if let Some(p) = prefix_trim.filter(|p| !p.is_empty() && *p != "refs") {
1834 let first = self.lower_bound(p);
1835 let end = prefix_upper.map_or(len, |upper| self.lower_bound(upper));
1836 if end < first {
1837 return (len, len);
1838 }
1839 return (first, end);
1840 }
1841 }
1842 (self.body_start, len)
1843 }
1844
1845 fn prefix_region(&self, prefix: &str) -> Option<(usize, usize)> {
1853 let len = self.data.len();
1854 let first = self.lower_bound(prefix);
1855 let upper = list_prefix_upper_bound(prefix);
1856 let end = upper
1857 .as_deref()
1858 .map_or(len, |upper| self.lower_bound(upper));
1859 if end < first || (first < end && self.record_cmp(first, prefix).is_lt()) {
1860 return None;
1861 }
1862 if let Some(upper) = upper.as_deref() {
1863 if end < len && self.record_cmp(end, upper).is_lt() {
1864 return None;
1865 }
1866 }
1867 let begin = match self.prev_record(first) {
1868 Some(prev) => {
1869 if !self.record_cmp(prev, prefix).is_lt() {
1870 return None;
1871 }
1872 prev
1873 }
1874 None => 0,
1875 };
1876 let end = if end < len {
1877 let after = packed_line_end(&self.data, end) + 1;
1878 if self.data.get(after) == Some(&b'^') {
1879 packed_line_end(&self.data, after) + 1
1880 } else {
1881 after
1882 }
1883 } else {
1884 end
1885 };
1886 Some((begin, end))
1887 }
1888
1889 fn parse_region<C: Default + Extend<(String, ObjectId)>>(
1893 &self,
1894 path: &Path,
1895 begin: usize,
1896 end: usize,
1897 prefix: Option<&str>,
1898 keep: Option<PackedRecordFilter<'_>>,
1899 ) -> Result<(C, bool)> {
1900 let bytes = &self.data[begin..end];
1901 if memchr::memchr(b'\r', bytes).is_some() {
1902 return Err(packed_refs_unexpected_line(
1903 path,
1904 "<contains carriage return>",
1905 ));
1906 }
1907 let region = std::str::from_utf8(bytes)
1908 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1909 parse_packed_refs_region(path, region, prefix, keep, self.peel_trait)
1910 }
1911}
1912
1913#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1916pub(crate) enum PackedPeel {
1917 Peeled(ObjectId),
1919 NotTag,
1921 Unknown,
1923}
1924
1925pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
1928
1929fn parse_packed_refs_region<C: Default + Extend<(String, ObjectId)>>(
1935 path: &Path,
1936 region: &str,
1937 prefix: Option<&str>,
1938 keep: Option<PackedRecordFilter<'_>>,
1939 peel_trait: PackedPeelTrait,
1940) -> Result<(C, bool)> {
1941 let ref_opts = RefNameOptions {
1942 allow_onelevel: false,
1943 refspec_pattern: false,
1944 normalize: false,
1945 };
1946 let mut map = C::default();
1947 let mut in_order = true;
1948 let mut prev_name = "";
1949 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
1951 let flush = |map: &mut C, pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
1952 let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
1953 return;
1954 };
1955 let peel = if peel_trait.covers(refname) {
1956 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1957 } else {
1958 PackedPeel::Unknown
1959 };
1960 if keep(refname, &oid, peel) {
1961 map.extend(Some((refname.to_owned(), oid)));
1962 }
1963 };
1964 let mut expect_peel = false;
1965 let mut lines = region.split('\n').peekable();
1966 while let Some(line) = lines.next() {
1967 if line.is_empty() {
1968 if lines.peek().is_some() {
1969 return Err(packed_refs_unexpected_line(path, line));
1970 }
1971 break;
1972 }
1973 if line.starts_with('#') {
1974 flush(&mut map, &mut pending);
1975 expect_peel = false;
1976 continue;
1977 }
1978 if line.starts_with('^') {
1979 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
1980 if let Some((_, _, peeled)) = pending.as_mut() {
1981 *peeled = Some(peel);
1982 }
1983 expect_peel = false;
1984 continue;
1985 }
1986 flush(&mut map, &mut pending);
1987 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1988 in_order &= prev_name <= refname;
1989 prev_name = refname;
1990 if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
1991 if keep.is_some() {
1992 pending = Some((refname, oid, None));
1993 } else {
1994 map.extend(Some((refname.to_owned(), oid)));
1995 }
1996 }
1997 expect_peel = true;
1998 }
1999 flush(&mut map, &mut pending);
2000 Ok((map, in_order))
2001}
2002
2003fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
2004 let packed_path = store::paths::packed_refs_path(store);
2005 let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
2008 Ok(d) => d,
2009 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
2010 Err(e) => return Err(e),
2011 };
2012 let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
2013 .map(|format| format.hash_algo())
2014 .unwrap_or(crate::objects::HashAlgo::Sha1);
2015 Ok(Some(PackedRefsFileView::from_data(
2016 &packed_path,
2017 data,
2018 hash_algo,
2019 )?))
2020}
2021
2022fn packed_line_is_record(line: &[u8], hex_len: usize) -> bool {
2025 line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^')
2026}
2027
2028fn packed_line_end(data: &[u8], line_start: usize) -> usize {
2029 memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
2030}
2031
2032fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
2033 std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
2034}
2035
2036fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
2037 let line_end = packed_line_end(data, line_start);
2038 if line_end >= data.len() || data[line_end] != b'\n' {
2039 return None;
2040 }
2041 let peel_start = line_end + 1;
2042 if peel_start >= data.len() || data[peel_start] != b'^' {
2043 return None;
2044 }
2045 let peel_end = packed_line_end(data, peel_start);
2046 let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
2047 ObjectId::from_hex(inner.trim()).ok()
2048}
2049
2050fn parse_packed_line_at(
2051 path: &Path,
2052 data: &[u8],
2053 line_start: usize,
2054 ref_opts: &RefNameOptions,
2055) -> Result<(String, ObjectId)> {
2056 let line = packed_line_as_str(data, line_start);
2057 parse_packed_ref_data_line(path, line, ref_opts)
2058}
2059
2060pub(crate) fn parse_packed_refs_to_map(
2061 path: &Path,
2062 data: &[u8],
2063) -> Result<HashMap<String, ObjectId>> {
2064 parse_packed_refs_to_map_with_prefix(path, data, None)
2065}
2066
2067fn packed_refs_leading_comments_end(data: &[u8]) -> usize {
2069 let mut end = 0;
2070 while data.get(end) == Some(&b'#') {
2071 match memchr::memchr(b'\n', &data[end..]) {
2072 Some(nl) => end += nl + 1,
2073 None => return data.len(),
2074 }
2075 }
2076 end
2077}
2078
2079fn packed_refs_file_declares_sorted(text: &str) -> bool {
2081 for line in text.split('\n') {
2082 if line.is_empty() {
2083 break;
2084 }
2085 if !line.starts_with('#') {
2086 break;
2087 }
2088 let body = line[1..].trim();
2089 let Some(traits) = body.strip_prefix("pack-refs with:") else {
2090 continue;
2091 };
2092 return traits.split_whitespace().any(|t| t == "sorted");
2093 }
2094 false
2095}
2096
2097fn parse_packed_refs_to_map_with_prefix(
2098 path: &Path,
2099 data: &[u8],
2100 prefix_filter: Option<&str>,
2101) -> Result<HashMap<String, ObjectId>> {
2102 let mut map = HashMap::new();
2103 visit_packed_refs_with_prefix(path, data, prefix_filter, |refname, oid| {
2104 map.insert(refname.to_owned(), oid);
2105 })?;
2106 Ok(map)
2107}
2108
2109fn visit_packed_refs_with_prefix<'a>(
2111 path: &Path,
2112 data: &'a [u8],
2113 prefix_filter: Option<&str>,
2114 mut visit: impl FnMut(&'a str, ObjectId),
2115) -> Result<()> {
2116 scan_packed_refs(path, data, prefix_filter, |line| {
2117 if let PackedLine::Record(name, oid) = line {
2118 visit(name, oid);
2119 }
2120 })
2121}
2122
2123enum PackedLine<'a> {
2125 Record(&'a str, ObjectId),
2127 Peel(ObjectId),
2129}
2130
2131#[derive(Clone, Copy)]
2133struct PackedEntry<'a> {
2134 name: &'a str,
2135 oid: ObjectId,
2136 peel: Option<ObjectId>,
2137}
2138
2139fn packed_entries_with_peel<'a>(path: &Path, data: &'a [u8]) -> Result<Vec<PackedEntry<'a>>> {
2143 let mut entries: Vec<PackedEntry<'a>> = Vec::with_capacity(data.len() / 64);
2144 scan_packed_refs(path, data, None, |line| match line {
2145 PackedLine::Record(name, oid) => entries.push(PackedEntry {
2146 name,
2147 oid,
2148 peel: None,
2149 }),
2150 PackedLine::Peel(peel) => {
2151 if let Some(entry) = entries.last_mut() {
2152 entry.peel = Some(peel);
2153 }
2154 }
2155 })?;
2156 Ok(entries)
2157}
2158
2159fn scan_packed_refs<'a>(
2162 path: &Path,
2163 data: &'a [u8],
2164 prefix_filter: Option<&str>,
2165 mut visit: impl FnMut(PackedLine<'a>),
2166) -> Result<()> {
2167 if data.is_empty() {
2168 return Ok(());
2169 }
2170 if data.contains(&b'\r') {
2171 return Err(packed_refs_unexpected_line(
2172 path,
2173 "<contains carriage return>",
2174 ));
2175 }
2176 if !data.ends_with(b"\n") {
2177 let tail = std::str::from_utf8(data)
2178 .map(|s| s.rsplit('\n').next().unwrap_or(s))
2179 .unwrap_or("<invalid utf-8>");
2180 return Err(packed_refs_unterminated_line(path, tail));
2181 }
2182 let text = std::str::from_utf8(data)
2183 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
2184
2185 let ref_opts = RefNameOptions {
2186 allow_onelevel: false,
2187 refspec_pattern: false,
2188 normalize: false,
2189 };
2190
2191 let mut expect_peel = false;
2192 let mut peel_follows_visited = false;
2193 let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
2194 let packed_sorted = packed_refs_file_declares_sorted(text);
2195 let prefix_upper = if packed_sorted {
2196 prefix_trim.and_then(list_prefix_upper_bound)
2197 } else {
2198 None
2199 };
2200 let mut lines = text.split('\n').peekable();
2201 while let Some(line) = lines.next() {
2202 if line.is_empty() {
2203 if lines.peek().is_some() {
2204 return Err(packed_refs_unexpected_line(path, line));
2205 }
2206 break;
2207 }
2208 if line.starts_with('#') {
2209 expect_peel = false;
2210 continue;
2211 }
2212 if line.starts_with('^') {
2213 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
2214 if peel_follows_visited {
2215 visit(PackedLine::Peel(peel));
2216 }
2217 expect_peel = false;
2218 peel_follows_visited = false;
2219 continue;
2220 }
2221 peel_follows_visited = false;
2222
2223 if let Some(prefix) = prefix_trim {
2224 let Some(refname) = packed_ref_data_line_refname(line) else {
2225 return Err(packed_refs_unexpected_line(path, line));
2226 };
2227 if packed_sorted {
2228 if refname < prefix {
2229 expect_peel = true;
2230 continue;
2231 }
2232 if let Some(upper) = &prefix_upper {
2233 if refname >= upper.as_str() {
2234 break;
2235 }
2236 }
2237 }
2238 if !ref_name_matches_list_prefix(refname, prefix) {
2239 expect_peel = true;
2240 continue;
2241 }
2242 }
2243 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
2244 visit(PackedLine::Record(refname, oid));
2245 expect_peel = true;
2246 peel_follows_visited = true;
2247 }
2248 Ok(())
2249}
2250
2251pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
2252 read_packed_refs_map_for_prefix(store, "")
2253}
2254
2255pub(crate) fn read_packed_refs_map_for_prefix(
2256 store: &Path,
2257 prefix: &str,
2258) -> Result<HashMap<String, ObjectId>> {
2259 read_packed_refs_map_for_prefix_filtered(store, prefix, None)
2260}
2261
2262pub(crate) fn read_packed_refs_map_for_prefix_filtered(
2264 store: &Path,
2265 prefix: &str,
2266 keep: Option<PackedRecordFilter<'_>>,
2267) -> Result<HashMap<String, ObjectId>> {
2268 let Some(view) = read_packed_refs_file_view(store)? else {
2269 return Ok(HashMap::new());
2270 };
2271 let packed_path = store::paths::packed_refs_path(store);
2272 let filter = if prefix.is_empty() {
2273 None
2274 } else {
2275 Some(prefix)
2276 };
2277 view.map_for_prefix_filtered(&packed_path, filter, keep)
2278}
2279
2280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2282enum PackedPeelTrait {
2283 None,
2285 Tags,
2287 All,
2289}
2290
2291impl PackedPeelTrait {
2292 fn covers(self, refname: &str) -> bool {
2294 match self {
2295 Self::None => false,
2296 Self::Tags => refname.starts_with("refs/tags/"),
2297 Self::All => true,
2298 }
2299 }
2300}
2301
2302pub(crate) struct PackedRefPeels {
2312 view: Option<PackedRefsFileView>,
2313 unsorted: Option<HashMap<Box<[u8]>, usize>>,
2315 peel_trait: PackedPeelTrait,
2316 next: std::cell::Cell<usize>,
2319}
2320
2321impl PackedRefPeels {
2322 pub(crate) fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
2332 if format != RefStorageFormat::Files {
2333 return Ok(Self::from_view(None));
2334 }
2335 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2336 Ok(Self::from_view(read_packed_refs_file_view(&store)?))
2337 }
2338
2339 pub(crate) fn for_store(store: &dyn store::RefStore, git_dir: &Path) -> Result<Self> {
2348 match store.as_files() {
2349 Some(files) => Ok(Self::from_view(files.packed_refs_view()?)),
2350 None => Self::load(git_dir, store.format()),
2351 }
2352 }
2353
2354 fn from_view(view: Option<PackedRefsFileView>) -> Self {
2355 let empty = Self {
2356 view: None,
2357 unsorted: None,
2358 peel_trait: PackedPeelTrait::None,
2359 next: std::cell::Cell::new(0),
2360 };
2361 let Some(view) = view else {
2362 return empty;
2363 };
2364 let peel_trait = view.peel_trait;
2365 let unsorted = (!view.sorted).then(|| {
2366 view.records()
2367 .map(|start| (Box::<[u8]>::from(view.record_name(start)), start))
2368 .collect()
2369 });
2370 let first = view
2371 .next_record(view.body_start, view.data.len())
2372 .unwrap_or(view.data.len());
2373 Self {
2374 view: Some(view),
2375 unsorted,
2376 peel_trait,
2377 next: std::cell::Cell::new(first),
2378 }
2379 }
2380
2381 pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
2393 if !self.peel_trait.covers(refname) {
2394 return None;
2395 }
2396 self.record(refname, oid)?.peeled
2397 }
2398
2399 pub(crate) fn record(&self, refname: &str, oid: &ObjectId) -> Option<PackedRecord> {
2405 let view = self.view.as_ref()?;
2406 let start = match &self.unsorted {
2407 Some(map) => *map.get(refname.as_bytes())?,
2408 None => {
2409 let len = view.data.len();
2410 let hint = self.next.get();
2411 let start = if hint < len && view.record_cmp(hint, refname).is_eq() {
2412 hint
2413 } else {
2414 view.search(refname).ok()?
2415 };
2416 let after = packed_line_end(&view.data, start) + 1;
2417 self.next.set(view.next_record(after, len).unwrap_or(len));
2418 start
2419 }
2420 };
2421 let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
2422 let packed_oid = ObjectId::from_hex(hex).ok()?;
2423 (packed_oid == *oid).then(|| PackedRecord {
2424 peeled: self
2425 .peel_trait
2426 .covers(refname)
2427 .then(|| peel_oid_following_line(&view.data, start)),
2428 })
2429 }
2430}
2431
2432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2434pub(crate) struct PackedRecord {
2435 pub(crate) peeled: Option<Option<ObjectId>>,
2437}
2438
2439fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
2441 let Some(header) = text.strip_prefix("# pack-refs with: ") else {
2442 return PackedPeelTrait::None;
2443 };
2444 let traits = header.split('\n').next().unwrap_or_default();
2445 let mut peel_trait = PackedPeelTrait::None;
2446 for t in traits.split_whitespace() {
2447 match t {
2448 "fully-peeled" => return PackedPeelTrait::All,
2449 "peeled" => peel_trait = PackedPeelTrait::Tags,
2450 _ => {}
2451 }
2452 }
2453 peel_trait
2454}
2455
2456fn acquire_packed_refs_lock(
2459 git_dir: &Path,
2460 wait: crate::lockfile::LockWait,
2461) -> Result<crate::lockfile::LockFile> {
2462 use crate::lockfile::{LockError, LockFile};
2463 let lock = lock_path_for_ref(&store::paths::packed_refs_path(git_dir));
2464 LockFile::acquire(&lock, wait).map_err(|err| match err {
2465 LockError::Held { .. } => {
2466 let lock_path = git_dir
2467 .canonicalize_simplified()
2468 .map(|d| d.join("packed-refs.lock"))
2469 .unwrap_or(lock);
2470 Error::from(RefLockError::PackedRefsLockHeld {
2471 lock_path: lock_path.display().to_string(),
2472 })
2473 }
2474 LockError::Io { source, .. } => Error::Io(source),
2475 })
2476}
2477
2478pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
2480 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2481 return Ok(None);
2482 };
2483 let packed_path = store::paths::packed_refs_path(git_dir);
2484 view.lookup_oid(&packed_path, refname)
2485}
2486
2487#[cfg(test)]
2489pub(crate) fn lookup_packed_ref_with_peel(
2490 git_dir: &Path,
2491 refname: &str,
2492) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
2493 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2494 return Ok(None);
2495 };
2496 let packed_path = store::paths::packed_refs_path(git_dir);
2497 view.lookup_entry(&packed_path, refname)
2498}
2499
2500#[cfg(test)]
2503pub(crate) fn test_commit(git_dir: &Path, message: &str) -> ObjectId {
2504 let common = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2505 let body = format!(
2506 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
2507 author T <t@example.com> 0 +0000\n\
2508 committer T <t@example.com> 0 +0000\n\n{message}\n"
2509 );
2510 crate::odb::Odb::new(&common.join("objects"))
2511 .write(crate::objects::ObjectKind::Commit, body.as_bytes())
2512 .unwrap_or_else(|err| panic!("store test commit {message:?}: {err}"))
2513}
2514
2515pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
2530 store_write_value(
2531 git_dir,
2532 refname,
2533 Some(StoreRawRef::Symbolic(target.to_owned())),
2534 Expected::Any,
2535 None,
2536 )
2537}
2538
2539pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
2540 if let Some(rest) = refname.strip_prefix("refs/") {
2541 if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
2542 return Err(Error::InvalidRef(format!(
2543 "refusing to update ref with bad name '{refname}'"
2544 )));
2545 }
2546 return Ok(());
2547 }
2548 if crate::check_ref_format::refname_is_safe(refname) {
2549 Ok(())
2550 } else {
2551 Err(Error::InvalidRef(format!(
2552 "refusing to update ref with bad name '{refname}'"
2553 )))
2554 }
2555}
2556
2557pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
2580 store_write_value(
2581 git_dir,
2582 refname,
2583 Some(StoreRawRef::Direct(*oid)),
2584 Expected::Any,
2585 None,
2586 )
2587}
2588
2589#[must_use]
2591pub fn ref_update_flags(refname: &str) -> RefUpdateFlags {
2592 legacy_ref_update_flags(refname)
2593}
2594
2595pub(crate) fn create_ref_with_reflog_on_store(
2599 store: &dyn RefStore,
2600 git_dir: &Path,
2601 config: &ConfigSet,
2602 refname: &str,
2603 oid: &ObjectId,
2604 identity: &str,
2605 message: &str,
2606) -> Result<()> {
2607 let reflog =
2608 should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
2609 identity: identity.to_owned(),
2610 message: message.to_owned(),
2611 time: time::OffsetDateTime::now_utc(),
2612 old_oid: None,
2613 new_oid: None,
2614 });
2615 store_write_value_on_store(
2616 store,
2617 git_dir,
2618 refname,
2619 Some(StoreRawRef::Direct(*oid)),
2620 Expected::Missing,
2621 reflog,
2622 )
2623}
2624
2625fn write_ref_at_storage(
2628 git_dir: &Path,
2629 storage_dir: &Path,
2630 refname: &str,
2631 oid: &ObjectId,
2632) -> Result<()> {
2633 let stor = crate::ref_namespace::storage_ref_name(refname);
2634 let path = storage_dir.join(stor);
2635 remove_empty_ref_directory(&path);
2642 if fs::symlink_metadata(&path)
2645 .map(|m| m.file_type().is_dir())
2646 .unwrap_or(false)
2647 {
2648 return Err(directory_in_the_way(git_dir, refname, &path));
2649 }
2650 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
2651 return Err(Error::InvalidRef(format!(
2652 "cannot update ref '{refname}': reference broken"
2653 )));
2654 }
2655 if let Some(parent) = path.parent() {
2656 crate::git_dir::io_error::io_with_git_path(
2657 git_dir,
2658 parent,
2659 crate::git_dir::GitDirPathOperation::CreateDir,
2660 fs::create_dir_all(parent),
2661 )?;
2662 }
2663 let content = format!("{oid}\n");
2664 let lock_path = lock_path_for_ref(&path);
2667 use crate::lockfile::{LockError, LockFile, LockWait};
2668 use std::io::Write as _;
2669 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
2670 let mut lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
2671 held @ LockError::Held { .. } => Error::from(held),
2672 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2673 git_dir,
2674 &lock_path,
2675 crate::git_dir::GitDirPathOperation::UpdateRef,
2676 source,
2677 ),
2678 })?;
2679 crate::git_dir::io_error::io_with_git_path(
2680 git_dir,
2681 &path,
2682 crate::git_dir::GitDirPathOperation::UpdateRef,
2683 lock.write_all(content.as_bytes()),
2684 )?;
2685 lock.commit_to(&path).map_err(|err| match err {
2686 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2687 git_dir,
2688 &path,
2689 crate::git_dir::GitDirPathOperation::UpdateRef,
2690 source,
2691 ),
2692 held @ LockError::Held { .. } => Error::from(held),
2693 })?;
2694 Ok(())
2695}
2696
2697pub fn write_ref_cas(
2711 git_dir: &Path,
2712 refname: &str,
2713 oid: &ObjectId,
2714 expected_old: ObjectId,
2715) -> Result<()> {
2716 store_write_value(
2717 git_dir,
2718 refname,
2719 Some(StoreRawRef::Direct(*oid)),
2720 Expected::Oid(expected_old),
2721 None,
2722 )
2723}
2724
2725pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
2738 store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
2739}
2740
2741fn prune_loose_ref_cas(
2751 git_dir: &Path,
2752 refname: &str,
2753 expected: ObjectId,
2754 wait: crate::lockfile::LockWait,
2755) -> Result<bool> {
2756 ensure_refname_safe_for_storage(refname)?;
2757 let storage_dir = ref_storage_dir(git_dir, refname);
2758 let stor = crate::ref_namespace::storage_ref_name(refname);
2759 let path = storage_dir.join(&stor);
2760 let lock_path = lock_path_for_ref(&path);
2761 use crate::lockfile::LockFile;
2762 let _lock = LockFile::acquire(&lock_path, wait)?;
2763 if !path.is_file() {
2764 return Ok(true);
2765 }
2766 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
2767 return Ok(false);
2768 };
2769 if oid != expected {
2770 return Ok(false);
2771 }
2772 fs::remove_file(&path).map_err(Error::Io)?;
2773 Ok(true)
2774}
2775
2776pub struct PackedRefs {
2792 map: HashMap<String, ObjectId>,
2793 sorted_names: BTreeSet<String>,
2794}
2795
2796fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
2797 let sorted_names = map.keys().cloned().collect();
2798 PackedRefs { map, sorted_names }
2799}
2800
2801impl PackedRefs {
2802 pub fn load(git_dir: &Path) -> Result<Self> {
2811 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2812 let map = read_packed_refs_map(&store)?;
2813 Ok(packed_refs_from_map(map))
2814 }
2815
2816 #[must_use]
2818 pub fn get(&self, refname: &str) -> Option<ObjectId> {
2819 self.map.get(refname).copied()
2820 }
2821
2822 #[must_use]
2826 pub fn has_namespace_conflict(&self, refname: &str) -> bool {
2827 packed_refname_namespace_conflict(&self.sorted_names, refname)
2828 }
2829
2830 pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
2833 self.map.insert(refname.to_owned(), oid);
2834 self.sorted_names.insert(refname.to_owned());
2835 }
2836}
2837
2838pub fn resolve_ref_cached(
2848 git_dir: &Path,
2849 refname: &str,
2850 packed: &PackedRefs,
2851) -> Result<Option<ObjectId>> {
2852 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2853 return Ok(resolve_ref(git_dir, refname).ok());
2854 }
2855 resolve_files_ref_cached(git_dir, refname, packed)
2856}
2857
2858fn resolve_files_ref_cached(
2859 git_dir: &Path,
2860 refname: &str,
2861 packed: &PackedRefs,
2862) -> Result<Option<ObjectId>> {
2863 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
2864 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
2865 let try_names: Vec<&str> = if storage_owned != stor_name {
2866 vec![storage_owned.as_str(), stor_name.as_str()]
2867 } else {
2868 vec![stor_name.as_str()]
2869 };
2870
2871 for name in &try_names {
2872 let path = store.join(name);
2873 match read_ref_file(&path) {
2874 Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
2875 Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
2876 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
2877 Err(e) => return Err(e),
2878 }
2879 }
2880
2881 for name in &try_names {
2882 if let Some(oid) = packed.get(name) {
2883 return Ok(Some(oid));
2884 }
2885 }
2886 Ok(None)
2887}
2888
2889pub fn write_ref_cached(
2898 git_dir: &Path,
2899 refname: &str,
2900 oid: &ObjectId,
2901 packed: &mut PackedRefs,
2902) -> Result<()> {
2903 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2904 return write_ref(git_dir, refname, oid);
2905 }
2906 ensure_refname_safe_for_storage(refname)?;
2907 if packed.has_namespace_conflict(refname) {
2908 return Err(Error::InvalidRef(format!(
2909 "cannot update ref '{refname}': reference namespace conflict"
2910 )));
2911 }
2912 let storage_dir = ref_storage_dir(git_dir, refname);
2913 write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
2914 packed.record_loose_write(refname, *oid);
2915 Ok(())
2916}
2917
2918fn ref_path_for_display(path: &Path) -> String {
2923 let cwd = crate::environment::Environment::empty().cwd.clone();
2924 if let Ok(rel) = path.strip_prefix(&cwd) {
2925 return rel.to_string_lossy().into_owned();
2926 }
2927 if let (Ok(cwd_c), Ok(path_c)) = (
2931 cwd.canonicalize_simplified(),
2932 path.canonicalize_simplified(),
2933 ) {
2934 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
2935 return rel.to_string_lossy().into_owned();
2936 }
2937 }
2938 path.to_string_lossy().into_owned()
2939}
2940
2941pub(crate) fn remove_empty_ref_directory(path: &Path) {
2947 match fs::symlink_metadata(path) {
2948 Ok(meta) if meta.file_type().is_dir() => {}
2949 _ => return,
2950 }
2951 if dir_tree_has_files(path) {
2954 return;
2955 }
2956 let _ = remove_dir_tree(path);
2957}
2958
2959pub(crate) fn dir_tree_has_files(dir: &Path) -> bool {
2961 let Ok(entries) = fs::read_dir(dir) else {
2962 return true;
2964 };
2965 for entry in entries.flatten() {
2966 match entry.file_type() {
2967 Ok(ft) if ft.is_dir() => {
2968 if dir_tree_has_files(&entry.path()) {
2969 return true;
2970 }
2971 }
2972 Ok(_) => return true, Err(_) => return true,
2974 }
2975 }
2976 false
2977}
2978
2979pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
2981 if let Some(dir) = ref_path.parent() {
2982 prune_empty_ref_dirs(storage_dir, dir);
2983 }
2984}
2985
2986pub(crate) fn prune_empty_ref_dirs(storage_dir: &Path, dir: &Path) {
2991 let refs_root = storage_dir.join("refs");
2992 let mut current = Some(dir);
2993 while let Some(dir) = current {
2994 if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
2995 break;
2996 }
2997 if dir
2998 .parent()
2999 .is_some_and(|parent| parent == refs_root.as_path())
3000 {
3001 break;
3002 }
3003 if dir_tree_has_files(dir) {
3004 break;
3005 }
3006 if remove_dir_tree(dir).is_err() {
3007 break;
3008 }
3009 current = dir.parent();
3010 }
3011}
3012
3013fn remove_dir_tree(dir: &Path) -> io::Result<()> {
3015 for entry in fs::read_dir(dir)? {
3016 let entry = entry?;
3017 if entry.file_type()?.is_dir() {
3018 remove_dir_tree(&entry.path())?;
3019 }
3020 }
3021 fs::remove_dir(dir)
3022}
3023
3024pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
3040 if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
3041 return Err(store::RefStoreError::not_found(refname).into());
3042 }
3043 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3044 let store = open_ref_store(git_dir)?;
3045 let mut flags = legacy_ref_update_flags(refname);
3046 if refname != "HEAD" {
3047 if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
3048 flags.no_deref = true;
3049 }
3050 }
3051 return commit_store_update(
3052 git_dir,
3053 RefUpdate {
3054 name: refname.to_owned(),
3055 new_value: None,
3056 expected: Expected::Exists,
3057 reflog: None,
3058 flags,
3059 },
3060 )
3061 .map_err(|e| map_public_ref_store_error(e, refname));
3062 }
3063 ensure_refname_safe_for_storage(refname)?;
3064 let storage_dir = ref_storage_dir(git_dir, refname);
3065 let stor = crate::ref_namespace::storage_ref_name(refname);
3066 let path = storage_dir.join(&stor);
3067 let lock = lock_path_for_ref(&path);
3068 if fs::symlink_metadata(&lock).is_ok() {
3069 return Err(Error::from(crate::lockfile::LockError::Held { path: lock }));
3070 }
3071
3072 remove_packed_ref(&storage_dir, &stor)?;
3078
3079 remove_empty_ref_directory(&path);
3083 match fs::remove_file(&path) {
3084 Ok(()) => {}
3085 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
3086 Err(e)
3087 if e.kind() == io::ErrorKind::NotADirectory
3088 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
3089 Err(e)
3092 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
3093 Err(e) => return Err(Error::Io(e)),
3094 }
3095
3096 prune_empty_loose_ref_parents(&storage_dir, &path);
3097
3098 let log_path = storage_dir.join("logs").join(&stor);
3099
3100 let _ = fs::remove_file(&log_path);
3106
3107 let logs_root = storage_dir.join("logs");
3108 let mut parent = log_path.parent();
3109 while let Some(p) = parent {
3110 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
3111 break;
3112 }
3113 if fs::remove_dir(p).is_err() {
3114 break; }
3116 parent = p.parent();
3117 }
3118
3119 Ok(())
3120}
3121
3122pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
3124 remove_packed_refs_inner(git_dir, &|name| name == refname, false, false).map(drop)
3125}
3126
3127pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
3128 remove_packed_refs_inner(git_dir, &|name| name == refname, false, true).map(drop)
3129}
3130
3131pub(crate) fn remove_packed_refs_under_lock(
3141 git_dir: &Path,
3142 refnames: &HashSet<String>,
3143) -> Result<Vec<String>> {
3144 if refnames.is_empty() {
3145 return Ok(Vec::new());
3146 }
3147 remove_packed_refs_inner(git_dir, &|name| refnames.contains(name), true, true)
3148}
3149
3150pub(crate) fn packed_record_name(record: &str) -> &str {
3152 let line = record.lines().next().unwrap_or("");
3153 line.split_once(' ').map_or("", |(_, name)| name.trim())
3154}
3155
3156pub(crate) fn packed_record_oid(record: &str) -> Option<ObjectId> {
3158 let line = record.lines().next()?;
3159 ObjectId::from_hex(line.split_once(' ')?.0).ok()
3160}
3161
3162pub(crate) fn restore_packed_records_under_lock(git_dir: &Path, records: &[&str]) -> Result<()> {
3169 if records.is_empty() {
3170 return Ok(());
3171 }
3172 let packed_path = store::paths::packed_refs_path(git_dir);
3173 let content = match fs::read(&packed_path) {
3174 Ok(c) => c,
3175 Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
3176 Err(e) => return Err(Error::Io(e)),
3177 };
3178 parse_packed_refs_to_map(&packed_path, &content)?;
3179 let content = String::from_utf8(content)
3180 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3181
3182 let mut blocks: Vec<String> = Vec::new();
3183 for line in content.lines() {
3184 if line.starts_with('#') {
3185 continue;
3186 }
3187 match blocks.last_mut() {
3188 Some(block) if line.starts_with('^') => block.push_str(line),
3189 _ => blocks.push(line.to_owned()),
3190 }
3191 if let Some(block) = blocks.last_mut() {
3192 block.push('\n');
3193 }
3194 }
3195 blocks.extend(records.iter().map(|r| (*r).to_owned()));
3196 blocks.sort_by(|a, b| packed_record_name(a).cmp(packed_record_name(b)));
3197
3198 let mut out =
3199 String::with_capacity(content.len() + records.iter().map(|r| r.len()).sum::<usize>());
3200 out.push_str(PACKED_REFS_HEADER);
3201 for block in &blocks {
3202 out.push_str(block);
3203 }
3204 replace_packed_refs_file(git_dir, &packed_path, &out, true)
3205}
3206
3207const PACKED_REFS_HEADER: &str = "# pack-refs with: peeled fully-peeled sorted\n";
3208
3209fn remove_packed_refs_inner(
3210 git_dir: &Path,
3211 remove: &dyn Fn(&str) -> bool,
3212 keep_removed: bool,
3213 lock_already_held: bool,
3214) -> Result<Vec<String>> {
3215 let mut removed: Vec<String> = Vec::new();
3216 let packed_path = store::paths::packed_refs_path(git_dir);
3217 let content = match fs::read(&packed_path) {
3218 Ok(c) => c,
3219 Err(e)
3220 if e.kind() == io::ErrorKind::NotFound
3221 || e.kind() == io::ErrorKind::NotADirectory
3222 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3223 {
3224 return Ok(removed);
3225 }
3226 Err(e) => return Err(Error::Io(e)),
3227 };
3228 let mut has_removed_record = false;
3231 visit_packed_refs_with_prefix(&packed_path, &content, None, |name, _| {
3232 has_removed_record |= remove(name);
3233 })?;
3234 if !has_removed_record {
3235 return Ok(removed);
3236 }
3237
3238 let content = String::from_utf8(content)
3239 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3240
3241 let mut out = String::with_capacity(content.len());
3242 let mut skip_peeled = false;
3243 let mut changed = false;
3244 let mut header_written = false;
3247
3248 for line in content.lines() {
3249 if skip_peeled {
3250 if line.starts_with('^') {
3251 if let Some(record) = removed.last_mut().filter(|_| keep_removed) {
3252 record.push_str(line);
3253 record.push('\n');
3254 }
3255 changed = true;
3256 continue;
3257 }
3258 skip_peeled = false;
3259 }
3260
3261 if line.starts_with('#') {
3262 continue;
3264 }
3265 if line.starts_with('^') {
3266 out.push_str(line);
3267 out.push('\n');
3268 continue;
3269 }
3270
3271 if !header_written {
3273 out.insert_str(0, PACKED_REFS_HEADER);
3274 header_written = true;
3275 }
3276
3277 let name = line.split_once(' ').map_or("", |(_, name)| name.trim());
3278 if remove(name) {
3279 if keep_removed {
3280 removed.push(format!("{line}\n"));
3281 }
3282 changed = true;
3283 skip_peeled = true;
3284 continue;
3285 }
3286
3287 out.push_str(line);
3288 out.push('\n');
3289 }
3290
3291 if changed {
3292 replace_packed_refs_file(git_dir, &packed_path, &out, lock_already_held)?;
3293 }
3294
3295 Ok(removed)
3296}
3297
3298fn replace_packed_refs_file(
3300 git_dir: &Path,
3301 packed_path: &Path,
3302 body: &str,
3303 lock_already_held: bool,
3304) -> Result<()> {
3305 let _packed_lock = if !lock_already_held {
3312 Some(acquire_packed_refs_lock(
3313 git_dir,
3314 crate::lockfile::LockWait::packed_refs_for_git_dir(git_dir),
3315 )?)
3316 } else {
3317 None
3318 };
3319 write_packed_refs_tmp_and_rename(packed_path, body)
3320}
3321
3322fn write_packed_refs_tmp_and_rename(packed_path: &Path, body: &str) -> Result<()> {
3325 let tmp = packed_path.with_extension("new");
3326 let mut created_tmp = false;
3327 let write_result = (|| -> Result<()> {
3328 let mut file = std::fs::OpenOptions::new()
3330 .write(true)
3331 .create_new(true)
3332 .open(&tmp)
3333 .map_err(Error::Io)?;
3334 created_tmp = true;
3335 use std::io::Write as _;
3336 file.write_all(body.as_bytes()).map_err(Error::Io)?;
3337 drop(file);
3338 fs::rename(&tmp, packed_path).map_err(Error::Io)?;
3339 created_tmp = false; Ok(())
3341 })();
3342
3343 if write_result.is_err() && created_tmp {
3344 let _ = fs::remove_file(&tmp);
3345 }
3346 write_result
3347}
3348
3349pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
3362 read_symbolic_ref(git_dir, "HEAD")
3363}
3364
3365pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
3377 match open_ref_store(git_dir)?.read_raw(refname)? {
3378 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3379 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3380 }
3381}
3382
3383pub fn read_symbolic_ref_for_repository(
3385 repo: &crate::repo::Repository,
3386 refname: &str,
3387) -> Result<Option<String>> {
3388 match repo.refs().read_raw(refname)? {
3389 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3390 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3391 }
3392}
3393
3394#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3396pub enum LogRefsConfig {
3397 Unset,
3399 None,
3401 Normal,
3403 Always,
3405}
3406
3407impl LogRefsConfig {
3408 pub(crate) fn parse(raw: &str) -> Option<Self> {
3410 match raw.trim().to_ascii_lowercase().as_str() {
3411 "always" => Some(Self::Always),
3412 "1" | "true" | "yes" | "on" => Some(Self::Normal),
3413 "0" | "false" | "no" | "off" | "never" => Some(Self::None),
3414 _ => None,
3415 }
3416 }
3417}
3418
3419pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3423 ConfigSet::load(
3424 &crate::environment::Environment::empty(),
3425 Some(git_dir),
3426 true,
3427 )
3428 .map(|cfg| cfg.log_refs_config())
3429 .unwrap_or(LogRefsConfig::Unset)
3430}
3431
3432pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3436 ConfigSet::load(
3437 &crate::environment::Environment::empty(),
3438 Some(git_dir),
3439 true,
3440 )
3441 .map(|cfg| cfg.effective_log_refs_config())
3442 .unwrap_or(LogRefsConfig::Normal)
3443}
3444
3445#[must_use]
3448pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
3449 match mode {
3450 LogRefsConfig::Always => true,
3451 LogRefsConfig::Normal => {
3452 refname == "HEAD"
3453 || refname.starts_with("refs/heads/")
3454 || refname.starts_with("refs/remotes/")
3455 || refname.starts_with("refs/notes/")
3456 }
3457 LogRefsConfig::None | LogRefsConfig::Unset => false,
3458 }
3459}
3460
3461#[must_use]
3463pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
3464 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
3465}
3466
3467#[must_use]
3469pub fn should_autocreate_reflog_with_config(
3470 config: &ConfigSet,
3471 _git_dir: &Path,
3472 refname: &str,
3473) -> bool {
3474 should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
3475}
3476
3477fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
3503 let Ok(rel) = target.strip_prefix(logs_root) else {
3504 return;
3505 };
3506 let mut cur = logs_root.to_path_buf();
3507 for component in rel.components() {
3508 cur.push(component);
3509 match fs::symlink_metadata(&cur) {
3510 Ok(meta) if meta.file_type().is_dir() => {}
3511 Ok(_) => {
3512 let _ = fs::remove_file(&cur);
3515 }
3516 Err(_) => break, }
3518 }
3519}
3520
3521pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
3524 let width = if !old.is_zero() {
3525 old.algo().hex_len()
3526 } else if !new.is_zero() {
3527 new.algo().hex_len()
3528 } else {
3529 old.algo().hex_len()
3530 };
3531 let render = |oid: &ObjectId| {
3532 if oid.is_zero() {
3533 "0".repeat(width)
3534 } else {
3535 oid.to_hex()
3536 }
3537 };
3538 (render(old), render(new))
3539}
3540
3541fn map_reflog_store_validation(err: store::RefStoreError) -> Error {
3542 match err {
3543 store::RefStoreError::InvalidReflog(sig) => Error::InvalidSignature(sig),
3544 other => Error::from(other),
3545 }
3546}
3547
3548fn validate_reflog_message(message: &str) -> Result<()> {
3549 store::validate_reflog_message(message).map_err(map_reflog_store_validation)
3550}
3551
3552fn validate_reflog_identity_string(identity: &str) -> Result<()> {
3553 store::validate_reflog_identity_string(identity).map_err(map_reflog_store_validation)
3554}
3555
3556fn validate_reflog_update(log: &ReflogUpdate) -> Result<()> {
3558 store::validate_reflog_update(log).map_err(map_reflog_store_validation)
3559}
3560
3561pub fn append_reflog_with_signature(
3567 git_dir: &Path,
3568 refname: &str,
3569 old_oid: &ObjectId,
3570 new_oid: &ObjectId,
3571 identity: &Signature,
3572 message: &str,
3573 force_create: bool,
3574) -> Result<()> {
3575 identity.validate().map_err(Error::InvalidSignature)?;
3576 let formatted = format_reflog_identity(&identity.reflog_name_email(), identity.time);
3577 append_reflog_with_config(
3578 git_dir,
3579 refname,
3580 old_oid,
3581 new_oid,
3582 &formatted,
3583 message,
3584 force_create,
3585 None,
3586 )
3587}
3588
3589pub fn append_reflog(
3598 git_dir: &Path,
3599 refname: &str,
3600 old_oid: &ObjectId,
3601 new_oid: &ObjectId,
3602 identity: &str,
3603 message: &str,
3604 force_create: bool,
3605) -> Result<()> {
3606 append_reflog_with_config(
3607 git_dir,
3608 refname,
3609 old_oid,
3610 new_oid,
3611 identity,
3612 message,
3613 force_create,
3614 None,
3615 )
3616}
3617
3618#[allow(clippy::too_many_arguments)]
3620pub fn append_reflog_with_config(
3621 git_dir: &Path,
3622 refname: &str,
3623 old_oid: &ObjectId,
3624 new_oid: &ObjectId,
3625 identity: &str,
3626 message: &str,
3627 force_create: bool,
3628 config: Option<&ConfigSet>,
3629) -> Result<()> {
3630 validate_reflog_identity_string(identity)?;
3631 validate_reflog_message(message)?;
3632 test_inject_reflog_fail(refname)?;
3633 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3634 return crate::reftable::reftable_append_reflog(
3635 git_dir,
3636 refname,
3637 old_oid,
3638 new_oid,
3639 identity,
3640 message,
3641 force_create,
3642 );
3643 }
3644 let storage_dir = ref_storage_dir(git_dir, refname);
3645 let stor = crate::ref_namespace::storage_ref_name(refname);
3646 let log_path = storage_dir.join("logs").join(&stor);
3647 let may_create = force_create
3648 || match config {
3649 Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
3650 None => should_autocreate_reflog(git_dir, refname),
3651 };
3652 if !may_create && !log_path.exists() {
3653 return Ok(());
3654 }
3655 if let Some(parent) = log_path.parent() {
3656 let logs_root = storage_dir.join("logs");
3661 clear_conflicting_reflog_files(&logs_root, parent);
3662 crate::git_dir::io_error::io_with_git_path(
3663 git_dir,
3664 parent,
3665 crate::git_dir::GitDirPathOperation::CreateDir,
3666 fs::create_dir_all(parent),
3667 )?;
3668 }
3669 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
3674 let line = if message.is_empty() {
3675 format!("{old_hex} {new_hex} {identity}\n")
3676 } else {
3677 format!("{old_hex} {new_hex} {identity}\t{message}\n")
3678 };
3679 let mut file = fs::OpenOptions::new()
3680 .create(true)
3681 .append(true)
3682 .open(&log_path)
3683 .map_err(|source| {
3684 crate::git_dir::io_error::map_git_dir_io(
3685 git_dir,
3686 &log_path,
3687 crate::git_dir::GitDirPathOperation::AppendReflog,
3688 source,
3689 )
3690 })?;
3691 use io::Write;
3692 crate::git_dir::io_error::io_with_git_path(
3693 git_dir,
3694 &log_path,
3695 crate::git_dir::GitDirPathOperation::AppendReflog,
3696 file.write_all(line.as_bytes()),
3697 )?;
3698 Ok(())
3699}
3700
3701fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
3702 if all {
3703 b"*"
3704 } else {
3705 b"refs/tags/*"
3706 }
3707}
3708
3709fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
3710 crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
3711}
3712
3713fn collect_loose_ref_files(
3714 dir: &Path,
3715 prefix: &str,
3716 out: &mut Vec<(String, PathBuf)>,
3717) -> Result<()> {
3718 let read = match fs::read_dir(dir) {
3719 Ok(r) => r,
3720 Err(e)
3721 if e.kind() == io::ErrorKind::NotFound
3722 || e.kind() == io::ErrorKind::NotADirectory
3723 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3724 {
3725 return Ok(());
3726 }
3727 Err(e) => return Err(Error::Io(e)),
3728 };
3729 for entry in read {
3730 let entry = entry?;
3731 let name = entry.file_name().to_string_lossy().to_string();
3732 let refname = format!("{prefix}{name}");
3733 let path = entry.path();
3734 let file_type = match entry.file_type() {
3736 Ok(ft) if ft.is_symlink() => fs::metadata(&path).map(|m| m.file_type()),
3737 other => other,
3738 };
3739 let Ok(file_type) = file_type else {
3740 continue;
3741 };
3742 if file_type.is_dir() {
3743 collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
3744 } else if file_type.is_file() {
3745 out.push((refname, path));
3746 }
3747 }
3748 Ok(())
3749}
3750
3751fn read_packed_refs_bytes(packed_path: &Path) -> Result<Vec<u8>> {
3753 if !packed_path.is_file() {
3754 return Ok(Vec::new());
3755 }
3756 Ok(fs::read(packed_path)?)
3757}
3758
3759fn merged_packed_refs_body(
3763 mut existing: Vec<PackedEntry<'_>>,
3764 loose: &[(String, ObjectId, Option<ObjectId>)],
3765) -> String {
3766 if !existing.windows(2).all(|w| w[0].name < w[1].name) {
3767 existing.sort_by(|a, b| a.name.cmp(b.name));
3768 existing.dedup_by(|later, earlier| {
3769 if later.name != earlier.name {
3770 return false;
3771 }
3772 *earlier = *later;
3773 true
3774 });
3775 }
3776 let estimate = |name: &str| name.len() + 2 + HashAlgo::Sha256.hex_len();
3778 let capacity = PACKED_REFS_HEADER.len()
3779 + existing.iter().map(|e| estimate(e.name)).sum::<usize>()
3780 + loose.iter().map(|(name, ..)| estimate(name)).sum::<usize>();
3781 let mut body = String::with_capacity(capacity);
3782 body.push_str(PACKED_REFS_HEADER);
3783
3784 let mut packed = existing.iter().peekable();
3785 for (name, oid, peel) in loose {
3786 while let Some(entry) = packed.next_if(|e| e.name < name.as_str()) {
3787 push_packed_record(&mut body, entry.name, &entry.oid, entry.peel.as_ref());
3788 }
3789 packed.next_if(|e| e.name == name.as_str());
3790 push_packed_record(&mut body, name, oid, peel.as_ref());
3791 }
3792 for entry in packed {
3793 push_packed_record(&mut body, entry.name, &entry.oid, entry.peel.as_ref());
3794 }
3795 body
3796}
3797
3798pub(crate) fn pack_refs_for_repository(
3800 repo: &crate::repo::Repository,
3801 all: bool,
3802 dry_run: bool,
3803) -> Result<(PathBuf, usize, usize)> {
3804 let git_dir = repo.git_dir();
3805 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3806 return pack_refs_reftable(git_dir, dry_run);
3807 }
3808 let config = repo.config().ok();
3809 pack_refs_files(git_dir, repo.odb(), config.as_deref(), all, dry_run)
3810}
3811
3812fn pack_refs_reftable(git_dir: &Path, dry_run: bool) -> Result<(PathBuf, usize, usize)> {
3813 use crate::reftable::ReftableStack;
3814 let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3815 let rt_dir = store_git_dir.join("reftable");
3816 let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
3817 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3818 .unwrap_or(0);
3819 if dry_run {
3820 let tables_pruned = tables_before.saturating_sub(1);
3821 return Ok((rt_dir, 0, tables_pruned));
3822 }
3823 let mut stack = ReftableStack::open(&store_git_dir)?;
3824 stack.compact()?;
3825 let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
3826 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3827 .unwrap_or(0);
3828 Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
3829}
3830
3831fn pack_refs_files(
3832 git_dir: &Path,
3833 odb: &crate::odb::Odb,
3834 config: Option<&crate::config::ConfigSet>,
3835 all: bool,
3836 dry_run: bool,
3837) -> Result<(PathBuf, usize, usize)> {
3838 pack_refs_files_matching(
3839 git_dir,
3840 odb,
3841 config,
3842 |name| ref_matches_pack_include(name, all),
3843 dry_run,
3844 false,
3845 )
3846}
3847
3848fn pack_refs_files_matching(
3851 git_dir: &Path,
3852 odb: &crate::odb::Odb,
3853 config: Option<&crate::config::ConfigSet>,
3854 include: impl Fn(&str) -> bool,
3855 dry_run: bool,
3856 allow_missing: bool,
3857) -> Result<(PathBuf, usize, usize)> {
3858 use crate::lockfile::LockWait;
3859
3860 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3861 let packed_path = store::paths::packed_refs_path(&storage_git_dir);
3862
3863 let mut loose_files = Vec::new();
3864 collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
3865
3866 let mut loose: Vec<(String, ObjectId, Option<ObjectId>)> = Vec::new();
3867 for (refname, path) in loose_files {
3868 if crate::worktree_ref::is_per_worktree_ref(&refname) {
3869 continue;
3870 }
3871 if !include(&refname) {
3872 continue;
3873 }
3874 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
3875 continue;
3876 };
3877 let kind = odb
3879 .try_read_info_once(&oid)
3880 .ok()
3881 .flatten()
3882 .map(|info| info.kind);
3883 if kind.is_none() && !allow_missing {
3884 continue;
3885 }
3886 let peel = if kind == Some(crate::objects::ObjectKind::Tag) {
3887 packed_ref_peel(odb, oid)
3888 } else {
3889 None
3890 };
3891 loose.push((refname, oid, peel));
3892 }
3893 loose.sort_by(|a, b| a.0.cmp(&b.0));
3894 let packed_loose = loose.len();
3895
3896 if packed_loose == 0 || dry_run {
3897 let data = read_packed_refs_bytes(&packed_path)?;
3899 visit_packed_refs_with_prefix(&packed_path, &data, None, |_, _| {})?;
3900 return Ok((packed_path, packed_loose, packed_loose));
3901 }
3902
3903 {
3904 let _packed_lock =
3906 acquire_packed_refs_lock(&storage_git_dir, LockWait::packed_refs(config))?;
3907 let data = read_packed_refs_bytes(&packed_path)?;
3908 let existing = packed_entries_with_peel(&packed_path, &data)?;
3909 let body = merged_packed_refs_body(existing, &loose);
3910 write_packed_refs_tmp_and_rename(&packed_path, &body)?;
3911 }
3912
3913 #[cfg(test)]
3914 pack_refs_test_hooks::run_before_prune();
3915
3916 let mut pruned = 0usize;
3917 let mut parents = HashSet::new();
3918 let wait = LockWait::files_ref_lock(config);
3919 for (refname, expected, _) in &loose {
3920 match prune_loose_ref_cas(git_dir, refname, *expected, wait) {
3921 Ok(true) => {
3922 pruned += 1;
3923 let path = storage_git_dir.join(crate::ref_namespace::storage_ref_name(refname));
3924 if let Some(parent) = path.parent() {
3925 parents.insert(parent.to_path_buf());
3926 }
3927 }
3928 Ok(false) => {
3929 remove_packed_ref(git_dir, refname)?;
3930 }
3931 Err(e) => return Err(e),
3932 }
3933 }
3934
3935 for parent in &parents {
3938 prune_empty_ref_dirs(&storage_git_dir, parent);
3939 }
3940
3941 Ok((packed_path, packed_loose, pruned))
3942}
3943
3944#[cfg(test)]
3946mod pack_refs_test_hooks {
3947 use std::cell::RefCell;
3948
3949 thread_local! {
3950 static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
3951 }
3952
3953 pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
3954 BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
3955 }
3956
3957 pub fn run_before_prune() {
3958 BEFORE_PRUNE.with(|c| {
3959 if let Some(f) = c.borrow_mut().take() {
3960 f();
3961 }
3962 });
3963 }
3964}
3965
3966#[cfg(test)]
3970mod reflog_fail_inject {
3971 use std::cell::RefCell;
3972
3973 thread_local! {
3975 pub static INJECT: RefCell<Option<String>> = const { RefCell::new(None) };
3976 }
3977}
3978
3979#[cfg(test)]
3981pub fn set_test_inject_reflog_fail(refname: Option<&str>) {
3982 reflog_fail_inject::INJECT.with(|c| {
3983 *c.borrow_mut() = refname.map(str::to_owned);
3984 });
3985}
3986
3987#[cfg(test)]
3988fn test_inject_reflog_fail(refname: &str) -> Result<()> {
3989 reflog_fail_inject::INJECT.with(|c| {
3990 if c.borrow().as_deref() == Some(refname) {
3991 return Err(Error::Io(std::io::Error::other("injected reflog failure")));
3992 }
3993 Ok(())
3994 })
3995}
3996
3997#[cfg(not(test))]
3998fn test_inject_reflog_fail(_refname: &str) -> Result<()> {
3999 Ok(())
4000}
4001
4002pub struct BranchCommitRefUpdate<'a> {
4015 pub branch_ref: &'a str,
4017 pub expected_old: Option<ObjectId>,
4019 pub new_oid: ObjectId,
4021 pub identity: &'a str,
4023 pub reflog_message: &'a str,
4025}
4026
4027pub fn verify_branch_commit_cas(
4037 expected: Option<ObjectId>,
4038 current: Option<ObjectId>,
4039 refname: &str,
4040) -> Result<()> {
4041 verify_branch_cas(expected, current, refname)
4042}
4043
4044fn verify_branch_cas(
4045 expected: Option<ObjectId>,
4046 current: Option<ObjectId>,
4047 refname: &str,
4048) -> Result<()> {
4049 if expected == current {
4050 return Ok(());
4051 }
4052 Err(store::RefStoreError::ExpectedMismatch {
4053 name: refname.to_owned(),
4054 expected: expected.map_or(Expected::Missing, Expected::Oid),
4055 actual: current.map(StoreRawRef::Direct),
4056 }
4057 .into())
4058}
4059
4060fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
4061 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
4062 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
4063 let try_names: Vec<&str> = if storage_owned != stor_name {
4064 vec![storage_owned.as_str(), stor_name.as_str()]
4065 } else {
4066 vec![stor_name.as_str()]
4067 };
4068 for name in try_names {
4069 let path = store.join(name);
4070 if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
4071 return Ok(Some(oid));
4072 }
4073 if let Some(oid) = lookup_packed_ref(&store, name)? {
4074 return Ok(Some(oid));
4075 }
4076 }
4077 Ok(None)
4078}
4079
4080#[cfg(test)]
4081mod test_pause_inject_serial {
4082 use std::sync::{Mutex, OnceLock};
4083
4084 static SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
4086
4087 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
4088 SERIAL
4089 .get_or_init(|| Mutex::new(()))
4090 .lock()
4091 .unwrap_or_else(|e| e.into_inner())
4092 }
4093}
4094
4095#[cfg(test)]
4097pub fn test_pause_injection_serial_guard() -> std::sync::MutexGuard<'static, ()> {
4098 test_pause_inject_serial::lock()
4099}
4100
4101#[cfg(test)]
4102mod branch_ref_pause_inject {
4103 use std::sync::{Condvar, Mutex, OnceLock};
4104
4105 pub struct PauseGate {
4106 pub wait: Mutex<bool>,
4107 pub cv: Condvar,
4108 }
4109
4110 static PAUSE_BEFORE_COMMIT: OnceLock<Mutex<Option<std::sync::Arc<PauseGate>>>> =
4112 OnceLock::new();
4113
4114 pub fn pause_slot() -> &'static Mutex<Option<std::sync::Arc<PauseGate>>> {
4115 PAUSE_BEFORE_COMMIT.get_or_init(|| Mutex::new(None))
4116 }
4117}
4118
4119#[cfg(test)]
4121pub type BranchRefPauseGate = std::sync::Arc<branch_ref_pause_inject::PauseGate>;
4122
4123#[cfg(test)]
4125pub fn set_test_inject_branch_ref_pause_before_commit(gate: Option<BranchRefPauseGate>) {
4126 *branch_ref_pause_inject::pause_slot()
4127 .lock()
4128 .unwrap_or_else(|e| e.into_inner()) = gate;
4129}
4130
4131#[cfg(test)]
4132fn test_branch_ref_pause_before_commit() {
4133 let gate = branch_ref_pause_inject::pause_slot()
4134 .lock()
4135 .unwrap_or_else(|e| e.into_inner())
4136 .clone();
4137 if let Some(gate) = gate {
4138 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
4139 *wait = true;
4140 gate.cv.notify_all();
4141 while *wait {
4142 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
4143 }
4144 }
4145}
4146
4147#[cfg(not(test))]
4148fn test_branch_ref_pause_before_commit() {}
4149
4150#[cfg(test)]
4151mod ref_store_batch_pause_inject {
4152 use std::sync::{Condvar, Mutex, OnceLock};
4153
4154 pub struct PauseGate {
4155 pub wait: Mutex<bool>,
4156 pub cv: Condvar,
4157 }
4158
4159 pub type PauseSlot = Option<(usize, std::path::PathBuf, std::sync::Arc<PauseGate>)>;
4162
4163 static PAUSE_AFTER_REF_WRITE: OnceLock<Mutex<PauseSlot>> = OnceLock::new();
4164
4165 pub fn pause_slot() -> &'static Mutex<PauseSlot> {
4166 PAUSE_AFTER_REF_WRITE.get_or_init(|| Mutex::new(None))
4167 }
4168}
4169
4170#[cfg(test)]
4172pub type RefStoreBatchPauseGate = std::sync::Arc<ref_store_batch_pause_inject::PauseGate>;
4173
4174#[cfg(test)]
4176pub fn new_ref_store_batch_pause_gate() -> RefStoreBatchPauseGate {
4177 std::sync::Arc::new(ref_store_batch_pause_inject::PauseGate {
4178 wait: std::sync::Mutex::new(false),
4179 cv: std::sync::Condvar::new(),
4180 })
4181}
4182
4183#[cfg(test)]
4188pub fn set_test_inject_ref_store_batch_pause_after_apply(
4189 apply_index: usize,
4190 git_dir: &Path,
4191 gate: Option<RefStoreBatchPauseGate>,
4192) {
4193 *ref_store_batch_pause_inject::pause_slot()
4194 .lock()
4195 .unwrap_or_else(|e| e.into_inner()) = gate.map(|g| (apply_index, git_dir.to_path_buf(), g));
4196}
4197
4198#[cfg(test)]
4199struct RefStorePauseTestSerial {
4200 holder: Option<std::thread::ThreadId>,
4202}
4203
4204#[cfg(test)]
4205static REF_STORE_PAUSE_TEST_SERIAL: std::sync::Mutex<RefStorePauseTestSerial> =
4206 std::sync::Mutex::new(RefStorePauseTestSerial { holder: None });
4207
4208#[cfg(test)]
4210pub(crate) struct RefStoreBatchPauseTestScope {
4211 _serial: std::sync::MutexGuard<'static, RefStorePauseTestSerial>,
4212}
4213
4214#[cfg(test)]
4215pub(crate) fn ref_store_batch_pause_test_scope() -> RefStoreBatchPauseTestScope {
4216 let mut serial = REF_STORE_PAUSE_TEST_SERIAL
4217 .lock()
4218 .unwrap_or_else(|e| e.into_inner());
4219 assert!(
4220 serial.holder.is_none(),
4221 "ref_store_batch_pause_test_scope must not nest"
4222 );
4223 serial.holder = Some(std::thread::current().id());
4224 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
4225 RefStoreBatchPauseTestScope { _serial: serial }
4226}
4227
4228#[cfg(test)]
4229impl Drop for RefStoreBatchPauseTestScope {
4230 fn drop(&mut self) {
4231 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
4232 self._serial.holder = None;
4233 }
4234}
4235
4236#[cfg(test)]
4237pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
4238 let slot = ref_store_batch_pause_inject::pause_slot()
4239 .lock()
4240 .unwrap_or_else(|e| e.into_inner())
4241 .clone();
4242 if let Some((target, target_dir, gate)) = slot {
4243 let same_repo = |a: &Path, b: &Path| {
4244 a == b
4245 || matches!((a.canonicalize_simplified(), b.canonicalize_simplified()), (Ok(a), Ok(b)) if a == b)
4246 };
4247 if idx != target || !same_repo(git_dir, &target_dir) {
4248 return;
4249 }
4250 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
4251 *wait = true;
4252 gate.cv.notify_all();
4253 while *wait {
4254 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
4255 }
4256 }
4257}
4258
4259#[cfg(not(test))]
4260pub(crate) fn test_ref_store_batch_pause_after_ref_write(_idx: usize, _git_dir: &Path) {}
4261
4262pub struct BranchCommitRefLock {
4266 lock: Option<crate::lockfile::LockFile>,
4267 disarmed: bool,
4268}
4269
4270impl BranchCommitRefLock {
4271 pub fn acquire(
4277 git_dir: &Path,
4278 branch_ref: &str,
4279 expected_old: Option<ObjectId>,
4280 ) -> Result<Self> {
4281 ensure_refname_safe_for_storage(branch_ref)?;
4282 let storage_dir = ref_storage_dir(git_dir, branch_ref);
4283 if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
4284 return Err(Error::InvalidRef(format!(
4285 "cannot update ref '{}': reference namespace conflict",
4286 branch_ref
4287 )));
4288 }
4289 let stor = crate::ref_namespace::storage_ref_name(branch_ref);
4290 let path = storage_dir.join(&stor);
4291 remove_empty_ref_directory(&path);
4292 if fs::symlink_metadata(&path)
4293 .map(|m| m.file_type().is_dir())
4294 .unwrap_or(false)
4295 {
4296 return Err(directory_in_the_way(git_dir, branch_ref, &path));
4297 }
4298 if let Some(parent) = path.parent() {
4299 crate::git_dir::io_error::io_with_git_path(
4300 git_dir,
4301 parent,
4302 crate::git_dir::GitDirPathOperation::CreateDir,
4303 fs::create_dir_all(parent),
4304 )?;
4305 }
4306
4307 let lock_path = lock_path_for_ref(&path);
4308 use crate::lockfile::{LockError, LockFile, LockWait};
4309 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4310 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4311 held @ LockError::Held { .. } => Error::from(held),
4312 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4313 git_dir,
4314 path.as_path(),
4315 crate::git_dir::GitDirPathOperation::UpdateRef,
4316 source,
4317 ),
4318 })?;
4319
4320 let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
4321 verify_branch_cas(expected_old, current, branch_ref)?;
4322
4323 Ok(Self {
4324 lock: Some(lock),
4325 disarmed: false,
4326 })
4327 }
4328
4329 pub fn write_reflogs(
4339 &mut self,
4340 git_dir: &Path,
4341 update: &BranchCommitRefUpdate<'_>,
4342 reflog_old: &ObjectId,
4343 config: &ConfigSet,
4344 ) -> Result<()> {
4345 write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
4346 }
4347
4348 pub fn finish_ref_commit(
4356 mut self,
4357 git_dir: &Path,
4358 update: &BranchCommitRefUpdate<'_>,
4359 ) -> Result<()> {
4360 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4361 self.disarmed = true;
4362 Ok(())
4363 }
4364
4365 pub fn commit(
4371 mut self,
4372 git_dir: &Path,
4373 update: &BranchCommitRefUpdate<'_>,
4374 reflog_old: &ObjectId,
4375 config: &ConfigSet,
4376 ) -> Result<()> {
4377 write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
4378 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4379 self.disarmed = true;
4380 Ok(())
4381 }
4382}
4383
4384impl Drop for BranchCommitRefLock {
4385 fn drop(&mut self) {
4386 drop(self.lock.take());
4387 }
4388}
4389
4390fn write_branch_commit_reflogs(
4391 git_dir: &Path,
4392 lock: &mut BranchCommitRefLock,
4393 update: &BranchCommitRefUpdate<'_>,
4394 reflog_old: &ObjectId,
4395 config: &ConfigSet,
4396) -> Result<()> {
4397 let mut branch_reflog_written = false;
4398 if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
4399 match append_reflog_with_config(
4400 git_dir,
4401 update.branch_ref,
4402 reflog_old,
4403 &update.new_oid,
4404 update.identity,
4405 update.reflog_message,
4406 false,
4407 Some(config),
4408 ) {
4409 Ok(()) => branch_reflog_written = true,
4410 Err(err) => {
4411 drop(lock.lock.take());
4412 return Err(err);
4413 }
4414 }
4415 }
4416
4417 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4418 if let Err(err) = append_reflog_with_config(
4419 git_dir,
4420 "HEAD",
4421 reflog_old,
4422 &update.new_oid,
4423 update.identity,
4424 update.reflog_message,
4425 false,
4426 Some(config),
4427 ) {
4428 if branch_reflog_written {
4429 let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
4430 }
4431 drop(lock.lock.take());
4432 return Err(err);
4433 }
4434 }
4435
4436 Ok(())
4437}
4438
4439fn finish_branch_commit_ref_lock_files(
4440 git_dir: &Path,
4441 lock: &mut BranchCommitRefLock,
4442 update: &BranchCommitRefUpdate<'_>,
4443) -> Result<()> {
4444 test_branch_ref_pause_before_commit();
4445
4446 let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
4447 let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
4448 let path = storage_dir.join(&stor);
4449 let content = format!("{}\n", update.new_oid);
4450 use crate::lockfile::LockError;
4451 use std::io::Write as _;
4452 let mut file_lock = lock
4453 .lock
4454 .take()
4455 .ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
4456 crate::git_dir::io_error::io_with_git_path(
4457 git_dir,
4458 &path,
4459 crate::git_dir::GitDirPathOperation::UpdateRef,
4460 file_lock.write_all(content.as_bytes()),
4461 )?;
4462 file_lock.commit_to(&path).map_err(|err| match err {
4463 LockError::Io {
4464 source,
4465 path: lock_path,
4466 } => crate::git_dir::io_error::map_git_dir_io(
4467 git_dir,
4468 lock_path.as_path(),
4469 crate::git_dir::GitDirPathOperation::UpdateRef,
4470 source,
4471 ),
4472 held @ LockError::Held { .. } => Error::from(held),
4473 })?;
4474 Ok(())
4475}
4476
4477pub(crate) fn update_branch_for_commit_files(
4478 git_dir: &Path,
4479 update: &BranchCommitRefUpdate<'_>,
4480 reflog_old: &ObjectId,
4481 config: &ConfigSet,
4482) -> Result<()> {
4483 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4484 lock.commit(git_dir, update, reflog_old, config)
4485}
4486
4487pub(crate) fn update_branch_for_commit_files_with_reference_transaction_hooks(
4490 git_dir: &Path,
4491 update: &BranchCommitRefUpdate<'_>,
4492 reflog_old: &ObjectId,
4493 config: &ConfigSet,
4494 ctx: &crate::hooks::RefHookContext,
4495 lines: &[store::reference_transaction::ReferenceTransactionLine],
4496) -> Result<()> {
4497 use crate::hooks::ReferenceTransactionPhase;
4498 use store::reference_transaction::stdin_from_lines;
4499
4500 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4501 if lines.is_empty() {
4502 return lock.commit(git_dir, update, reflog_old, config);
4503 }
4504 let stdin = stdin_from_lines(lines);
4505 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4506 .map_err(Error::Hook)?;
4507 match lock.commit(git_dir, update, reflog_old, config) {
4508 Ok(()) => {
4509 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4510 Ok(())
4511 }
4512 Err(err) => {
4513 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4514 Err(err)
4515 }
4516 }
4517}
4518
4519pub(crate) struct DetachedHeadRefLock {
4521 lock: Option<crate::lockfile::LockFile>,
4522 disarmed: bool,
4523}
4524
4525impl DetachedHeadRefLock {
4526 fn acquire(git_dir: &Path, expected_old: Option<ObjectId>) -> Result<Self> {
4527 let current = resolve_ref(git_dir, "HEAD").ok();
4528 verify_branch_cas(expected_old, current, "HEAD")?;
4529 let path = git_dir.join("HEAD");
4530 let lock_path = lock_path_for_ref(&path);
4531 use crate::lockfile::{LockError, LockFile, LockWait};
4532 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4533 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4534 held @ LockError::Held { .. } => Error::from(held),
4535 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4536 git_dir,
4537 path.as_path(),
4538 crate::git_dir::GitDirPathOperation::UpdateRef,
4539 source,
4540 ),
4541 })?;
4542 Ok(Self {
4543 lock: Some(lock),
4544 disarmed: false,
4545 })
4546 }
4547
4548 fn commit(
4549 mut self,
4550 git_dir: &Path,
4551 new_oid: ObjectId,
4552 reflog_old: &ObjectId,
4553 identity: &str,
4554 reflog_message: &str,
4555 config: &ConfigSet,
4556 ) -> Result<()> {
4557 let path = git_dir.join("HEAD");
4558 let content = format!("{new_oid}\n");
4559 use crate::lockfile::LockError;
4560 use std::io::Write as _;
4561 let mut file_lock = self
4562 .lock
4563 .take()
4564 .ok_or_else(|| Error::Message("HEAD ref lock already released".into()))?;
4565 crate::git_dir::io_error::io_with_git_path(
4566 git_dir,
4567 &path,
4568 crate::git_dir::GitDirPathOperation::UpdateRef,
4569 file_lock.write_all(content.as_bytes()),
4570 )?;
4571 file_lock.commit_to(&path).map_err(|err| match err {
4572 LockError::Io {
4573 source,
4574 path: lock_path,
4575 } => crate::git_dir::io_error::map_git_dir_io(
4576 git_dir,
4577 lock_path.as_path(),
4578 crate::git_dir::GitDirPathOperation::UpdateRef,
4579 source,
4580 ),
4581 held @ LockError::Held { .. } => Error::from(held),
4582 })?;
4583 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4584 if let Err(err) = append_reflog_with_config(
4585 git_dir,
4586 "HEAD",
4587 reflog_old,
4588 &new_oid,
4589 identity,
4590 reflog_message,
4591 false,
4592 Some(config),
4593 ) {
4594 let _ = write_ref_cas(git_dir, "HEAD", reflog_old, new_oid);
4595 return Err(err);
4596 }
4597 }
4598 self.disarmed = true;
4599 Ok(())
4600 }
4601}
4602
4603impl Drop for DetachedHeadRefLock {
4604 fn drop(&mut self) {
4605 drop(self.lock.take());
4606 }
4607}
4608
4609#[allow(clippy::too_many_arguments)]
4610pub(crate) fn update_detached_head_for_commit_files_with_reference_transaction_hooks(
4611 git_dir: &Path,
4612 expected_old: Option<ObjectId>,
4613 new_oid: ObjectId,
4614 identity: &str,
4615 reflog_message: &str,
4616 config: &ConfigSet,
4617 ctx: &crate::hooks::RefHookContext,
4618 lines: &[store::reference_transaction::ReferenceTransactionLine],
4619) -> Result<()> {
4620 use crate::hooks::ReferenceTransactionPhase;
4621 use store::reference_transaction::stdin_from_lines;
4622
4623 let lock = DetachedHeadRefLock::acquire(git_dir, expected_old)?;
4624 if lines.is_empty() {
4625 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4626 return lock.commit(
4627 git_dir,
4628 new_oid,
4629 &reflog_old,
4630 identity,
4631 reflog_message,
4632 config,
4633 );
4634 }
4635 let stdin = stdin_from_lines(lines);
4636 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4637 .map_err(Error::Hook)?;
4638 let reflog_old = expected_old.unwrap_or_else(|| ctx.null_oid());
4639 match lock.commit(
4640 git_dir,
4641 new_oid,
4642 &reflog_old,
4643 identity,
4644 reflog_message,
4645 config,
4646 ) {
4647 Ok(()) => {
4648 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4649 Ok(())
4650 }
4651 Err(err) => {
4652 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4653 Err(err)
4654 }
4655 }
4656}
4657
4658pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
4666 let config = ConfigSet::load(
4667 &crate::environment::Environment::empty(),
4668 Some(git_dir),
4669 true,
4670 )
4671 .unwrap_or_default();
4672 update_branch_for_commit_with_config(git_dir, update, &config)
4673}
4674
4675pub fn update_branch_for_commit_with_config(
4684 git_dir: &Path,
4685 update: &BranchCommitRefUpdate<'_>,
4686 config: &ConfigSet,
4687) -> Result<()> {
4688 update_branch_for_commit_on_store(&store::open_checked_ref_store(git_dir)?, update, config)
4689}
4690
4691pub(crate) fn update_detached_head_for_commit_files(
4694 git_dir: &Path,
4695 expected_old: Option<ObjectId>,
4696 new_oid: ObjectId,
4697 identity: &str,
4698 reflog_message: &str,
4699 config: &ConfigSet,
4700) -> Result<()> {
4701 let current = resolve_ref(git_dir, "HEAD").ok();
4702 verify_branch_cas(expected_old, current, "HEAD")?;
4703 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4704
4705 write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
4706 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4707 if let Err(err) = append_reflog_with_config(
4708 git_dir,
4709 "HEAD",
4710 &reflog_old,
4711 &new_oid,
4712 identity,
4713 reflog_message,
4714 false,
4715 Some(config),
4716 ) {
4717 let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
4718 return Err(err);
4719 }
4720 }
4721 Ok(())
4722}
4723
4724#[must_use]
4730pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
4731 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
4732 store.join("logs").join(stor_name)
4733}
4734
4735fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
4736 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
4737}
4738
4739fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
4748 if prefix.is_empty() {
4749 return String::new();
4750 }
4751 if prefix.ends_with('/') {
4752 return prefix.to_string();
4753 }
4754 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
4755 if candidate.is_file() {
4756 prefix.to_string()
4757 } else {
4758 format!("{prefix}/")
4759 }
4760}
4761
4762pub(crate) fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
4765 odb.peel_stored_tag_target(oid).ok().flatten()
4766}
4767
4768pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
4779 pack_imported_refs_for_clone(git_dir, Some(remote))
4780}
4781
4782pub(crate) fn pack_imported_refs_for_clone(git_dir: &Path, remote: Option<&str>) -> Result<()> {
4784 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4785 return Ok(());
4786 }
4787 let prefix = remote.map(|name| format!("refs/remotes/{name}/"));
4788 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
4789 let odb = crate::odb::Odb::new(&storage_git_dir.join("objects"))
4790 .with_config_git_dir(git_dir.to_path_buf());
4791 let config = crate::config::ConfigSet::load(
4792 &crate::environment::Environment::empty(),
4793 Some(git_dir),
4794 true,
4795 )
4796 .ok();
4797 odb.with_pack_read_context(|| {
4798 pack_refs_files_matching(
4799 git_dir,
4800 &odb,
4801 config.as_ref(),
4802 |name| {
4803 name.starts_with("refs/tags/")
4804 || prefix
4805 .as_ref()
4806 .is_some_and(|prefix| name.starts_with(prefix))
4807 },
4808 false,
4809 true,
4811 )
4812 })?;
4813 Ok(())
4814}
4815
4816fn push_packed_record(body: &mut String, name: &str, oid: &ObjectId, peel: Option<&ObjectId>) {
4818 oid.push_hex_to(body);
4819 body.push(' ');
4820 body.push_str(name);
4821 body.push('\n');
4822 if let Some(peel) = peel {
4823 body.push('^');
4824 peel.push_hex_to(body);
4825 body.push('\n');
4826 }
4827}
4828
4829#[derive(Debug, Clone, PartialEq, Eq)]
4831pub struct PackedRefRecord {
4832 pub refname: String,
4834 pub oid: ObjectId,
4836 pub peeled: Option<ObjectId>,
4838}
4839
4840pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
4850 let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
4851 sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
4852 for rec in &sorted {
4853 ensure_refname_safe_for_storage(&rec.refname)?;
4854 }
4855 let mut body = String::from(PACKED_REFS_HEADER);
4856 for rec in &sorted {
4857 push_packed_record(&mut body, &rec.refname, &rec.oid, rec.peeled.as_ref());
4858 }
4859 let packed_path = store::paths::packed_refs_path(git_dir);
4860 replace_packed_refs_file(git_dir, &packed_path, &body, false)?;
4861 for rec in &sorted {
4862 let storage_dir = ref_storage_dir(git_dir, &rec.refname);
4863 let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
4864 let loose = storage_dir.join(stor);
4865 if loose.is_file() {
4866 fs::remove_file(&loose).map_err(Error::Io)?;
4867 }
4868 if let Some(parent) = loose.parent() {
4869 prune_empty_ref_dirs(&storage_dir, parent);
4870 }
4871 }
4872 Ok(())
4873}
4874
4875pub fn list_refs_for_repository(
4886 repo: &crate::repo::Repository,
4887 prefix: &str,
4888) -> Result<Vec<(String, ObjectId)>> {
4889 let prefix_norm = normalize_list_refs_prefix(repo.git_dir(), prefix);
4890 list_store_refs_with_name_prefix(repo, &prefix_norm)
4891}
4892
4893fn list_store_refs_with_name_prefix(
4895 repo: &crate::repo::Repository,
4896 prefix: &str,
4897) -> Result<Vec<(String, ObjectId)>> {
4898 let git_dir = repo.git_dir();
4899 let mut results = collect_resolved_refs(repo.refs(), prefix)?;
4900 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4901 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4902 }
4903 results.sort_by(|a, b| a.0.cmp(&b.0));
4904 Ok(results)
4905}
4906
4907pub(crate) fn list_refs_in(
4918 repo: &crate::repo::Repository,
4919 prefix: &str,
4920) -> Result<Vec<(String, ObjectId)>> {
4921 if repo.refs_on_disk() {
4922 list_refs(repo.git_dir(), prefix)
4923 } else {
4924 list_refs_for_repository(repo, prefix)
4925 }
4926}
4927
4928pub(crate) fn resolve_ref_in(repo: &crate::repo::Repository, refname: &str) -> Result<ObjectId> {
4935 if repo.refs_on_disk() {
4936 resolve_ref(repo.git_dir(), refname)
4937 } else {
4938 repo.resolve_ref_oid(refname)
4939 }
4940}
4941
4942pub(crate) fn list_refs_glob_in(
4949 repo: &crate::repo::Repository,
4950 pattern: &str,
4951) -> Result<Vec<(String, ObjectId)>> {
4952 if repo.refs_on_disk() {
4953 list_refs_glob(repo.git_dir(), pattern)
4954 } else {
4955 list_refs_glob_for_repository(repo, pattern)
4956 }
4957}
4958
4959pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4979 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4980 let prefix = prefix_norm.as_str();
4981 let store = open_ref_store(git_dir)?;
4982 if store.format() != RefStorageFormat::Files {
4983 return crate::reftable::reftable_list_refs(git_dir, prefix);
4984 }
4985 let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
4986 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4987 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4988 }
4989 results.sort_by(|a, b| a.0.cmp(&b.0));
4990 Ok(results)
4991}
4992
4993pub(crate) fn list_refs_filtering_packed(
5004 git_dir: &Path,
5005 prefix: &str,
5006 keep: PackedRecordFilter<'_>,
5007) -> Result<Vec<(String, ObjectId)>> {
5008 let Some(store) = store::open_files_ref_store(git_dir)? else {
5009 return list_refs(git_dir, prefix);
5010 };
5011 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
5012 let prefix = prefix_norm.as_str();
5013 let mut results = collect_resolved_ref_entries(&store, |f| {
5014 store.for_each_ref_filtered(prefix, Some(keep), f)
5015 })?;
5016 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
5017 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
5018 }
5019 results.sort_by(|a, b| a.0.cmp(&b.0));
5020 Ok(results)
5021}
5022
5023pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
5031 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
5032}
5033
5034pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
5044 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
5045 return crate::reftable::reftable_list_refs(git_dir, prefix);
5046 }
5047 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
5048 let stored_prefix = prefix.to_owned();
5049
5050 if let Some(cdir) = common_dir(git_dir) {
5051 if cdir != git_dir {
5052 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
5053 let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
5054 collect_loose_refs_into_map(
5055 &loose_root,
5056 &loose_prefix,
5057 &stored_prefix,
5058 &cdir,
5059 true,
5060 &mut by_name,
5061 )?;
5062 }
5063 }
5064
5065 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
5066 let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
5067 collect_loose_refs_into_map(
5068 &loose_root,
5069 &loose_prefix,
5070 &stored_prefix,
5071 git_dir,
5072 true,
5073 &mut by_name,
5074 )?;
5075
5076 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
5077 results.sort_by(|a, b| a.0.cmp(&b.0));
5078 Ok(results)
5079}
5080
5081pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
5094 let config = ConfigSet::load(
5095 &crate::environment::Environment::empty(),
5096 Some(receiving_git_dir),
5097 true,
5098 )?;
5099 let objects_dir = receiving_git_dir.join("objects");
5100 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
5101 let mut out = Vec::new();
5102 let mut seen = std::collections::HashSet::new();
5103 for alt_objects in alternates {
5104 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
5105 continue;
5106 };
5107 if !alt_git_dir.join("refs").is_dir() {
5108 continue;
5109 }
5110 if let Some(prefixes) = config.get("core.alternateRefsPrefixes") {
5111 for part in prefixes.split_whitespace() {
5112 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
5113 if seen.insert(oid) {
5114 out.push(oid);
5115 }
5116 continue;
5117 }
5118 for (_, oid) in list_refs(&alt_git_dir, part)? {
5119 if seen.insert(oid) {
5120 out.push(oid);
5121 }
5122 }
5123 }
5124 } else {
5125 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
5126 if seen.insert(oid) {
5127 out.push(oid);
5128 }
5129 }
5130 }
5131 }
5132 Ok(out)
5133}
5134
5135pub fn list_refs_glob_for_repository(
5137 repo: &crate::repo::Repository,
5138 pattern: &str,
5139) -> Result<Vec<(String, ObjectId)>> {
5140 let all = match pattern.find(['*', '?', '[']) {
5144 Some(pos) => list_store_refs_with_name_prefix(repo, &pattern[..pos])?,
5145 None => {
5146 let mut dir = pattern.trim_end_matches('/').to_owned();
5147 if !dir.is_empty() {
5148 dir.push('/');
5149 }
5150 list_refs_for_repository(repo, &dir)?
5151 }
5152 };
5153 Ok(all
5154 .into_iter()
5155 .filter(|(refname, _)| ref_matches_glob(refname, pattern))
5156 .collect())
5157}
5158
5159pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
5166 let glob_pos = pattern.find(['*', '?', '[']);
5167 let prefix_owned: String = match glob_pos {
5168 Some(pos) => pattern[..pos].to_owned(),
5170 None => {
5171 let mut p = pattern.trim_end_matches('/').to_owned();
5172 if !p.is_empty() {
5173 p.push('/');
5174 }
5175 p
5176 }
5177 };
5178 let prefix = prefix_owned.as_str();
5179 let all = list_refs(git_dir, prefix)?;
5180 let mut results = Vec::new();
5181 for (refname, oid) in all {
5182 if ref_matches_glob(&refname, pattern) {
5183 results.push((refname, oid));
5184 }
5185 }
5186 Ok(results)
5187}
5188
5189pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
5193 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
5195 return refname == pattern
5196 || refname.ends_with(&format!("/{pattern}"))
5197 || refname.starts_with(&format!("{pattern}/"));
5198 }
5199 glob_match(pattern, refname)
5200}
5201
5202fn glob_match(pattern: &str, text: &str) -> bool {
5203 let pat = pattern.as_bytes();
5204 let txt = text.as_bytes();
5205 let (mut pi, mut ti) = (0, 0);
5206 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
5207 while ti < txt.len() {
5208 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
5209 pi += 1;
5210 ti += 1;
5211 } else if pi < pat.len() && pat[pi] == b'*' {
5212 star_pi = pi;
5213 star_ti = ti;
5214 pi += 1;
5215 } else if star_pi != usize::MAX {
5216 pi = star_pi + 1;
5217 star_ti += 1;
5218 ti = star_ti;
5219 } else {
5220 return false;
5221 }
5222 }
5223 while pi < pat.len() && pat[pi] == b'*' {
5224 pi += 1;
5225 }
5226 pi == pat.len()
5227}
5228
5229fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
5231 let content = fs::read_to_string(path).ok()?;
5232 let content = content.trim_end_matches('\n').trim();
5233 if ObjectId::is_full_hex(content) {
5234 content.parse().ok()
5235 } else {
5236 None
5237 }
5238}
5239
5240fn collect_loose_refs_into_map(
5241 dir: &Path,
5242 prefix: &str,
5243 filter_prefix: &str,
5244 resolve_git_dir: &Path,
5245 physical_keys: bool,
5246 out: &mut HashMap<String, ObjectId>,
5247) -> Result<()> {
5248 let read = match fs::read_dir(dir) {
5249 Ok(r) => r,
5250 Err(e)
5251 if e.kind() == io::ErrorKind::NotFound
5252 || e.kind() == io::ErrorKind::NotADirectory
5253 || e.raw_os_error() == Some(libc::ENOTDIR) =>
5254 {
5255 return Ok(());
5256 }
5257 Err(e) => return Err(Error::Io(e)),
5258 };
5259
5260 for entry in read {
5261 let entry = entry?;
5262 let name = entry.file_name();
5263 let name_str = name.to_string_lossy();
5264 let refname = format!("{prefix}{name_str}");
5265 let path = entry.path();
5266 let meta = match fs::metadata(&path) {
5267 Ok(m) => m,
5268 Err(_) => continue,
5269 };
5270
5271 if meta.is_dir() {
5272 collect_loose_refs_into_map(
5273 &path,
5274 &format!("{refname}/"),
5275 filter_prefix,
5276 resolve_git_dir,
5277 physical_keys,
5278 out,
5279 )?;
5280 } else if meta.is_file() {
5281 if physical_keys {
5282 if let Some(oid) = loose_ref_file_direct_oid(&path) {
5283 out.insert(refname, oid);
5284 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
5285 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
5286 out.insert(refname, oid);
5287 }
5288 }
5289 } else {
5290 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
5291 .unwrap_or_else(|| refname.clone());
5292 if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
5293 continue;
5294 }
5295 if let Some(oid) = loose_ref_file_direct_oid(&path) {
5296 out.insert(logical, oid);
5297 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
5298 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
5299 out.insert(logical, oid);
5300 }
5301 }
5302 }
5303 }
5304 }
5305 Ok(())
5306}
5307
5308pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
5317 let inner = spec
5319 .strip_prefix("@{-")
5320 .and_then(|s| s.strip_suffix('}'))
5321 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
5322 let n: usize = inner
5323 .parse()
5324 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
5325 if n == 0 {
5326 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
5327 }
5328 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
5329 let mut count = 0usize;
5330 for entry in entries.iter().rev() {
5331 let msg = &entry.message;
5332 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
5333 count += 1;
5334 if count == n {
5335 if let Some(to_pos) = rest.find(" to ") {
5336 return Ok(rest[..to_pos].to_string());
5337 }
5338 }
5339 }
5340 }
5341 Err(Error::InvalidRef(format!(
5342 "{spec}: only {count} checkout(s) in reflog"
5343 )))
5344}
5345
5346fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
5347 let prefix = prefix.trim_end_matches('/');
5348 if prefix.is_empty() {
5349 return true;
5350 }
5351 if refname.starts_with(prefix) {
5352 return true;
5353 }
5354 if refname == prefix {
5355 return true;
5356 }
5357 false
5358}
5359
5360fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
5361 let base = prefix.trim_end_matches('/');
5362 let (head, tail) = base.rsplit_once('/')?;
5363 let mut chars: Vec<char> = tail.chars().collect();
5364 while let Some(last) = chars.pop() {
5365 if last != '\u{10ffff}' {
5366 chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
5367 return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
5368 }
5369 }
5370 None
5371}
5372
5373pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
5376 let trimmed = prefix.trim_end_matches('/');
5377 if trimmed.is_empty() {
5378 return (git_dir.join("refs"), "refs/".to_owned());
5379 }
5380 let direct = git_dir.join(trimmed);
5381 if direct.is_dir() {
5382 return (direct, format!("{trimmed}/"));
5383 }
5384 if let Some(slash) = trimmed.rfind('/') {
5385 let parent = &trimmed[..slash];
5386 return (git_dir.join(parent), format!("{parent}/"));
5387 }
5388 (git_dir.to_path_buf(), String::new())
5389}
5390
5391fn collect_packed_refs_into_map(
5392 git_dir: &Path,
5393 prefix: &str,
5394 physical_keys: bool,
5395 out: &mut HashMap<String, ObjectId>,
5396) -> Result<()> {
5397 let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
5398 for (refname, oid) in map {
5399 let key = if physical_keys {
5400 refname.clone()
5401 } else {
5402 crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
5403 };
5404 out.insert(key, oid);
5405 }
5406 Ok(())
5407}
5408
5409#[cfg(test)]
5410mod update_branch_for_commit_tests {
5411 use super::*;
5412 use crate::reflog::read_reflog;
5413 use crate::repo::init_repository;
5414 use std::sync::{Arc, Condvar, Mutex};
5415 use std::thread;
5416 use tempfile::TempDir;
5417
5418 fn sample_oid(hex: &str) -> ObjectId {
5419 hex.parse().unwrap()
5420 }
5421
5422 #[test]
5423 fn update_branch_for_commit_holds_lock_until_ref_commit() {
5424 let _pause_serial = super::test_pause_injection_serial_guard();
5425 let tmp = TempDir::new().unwrap();
5426 let repo = init_repository(
5427 tmp.path(),
5428 false,
5429 "main",
5430 None,
5431 crate::RefStorageFormat::Files,
5432 )
5433 .unwrap();
5434 let git_dir = repo.git_dir().to_path_buf();
5435 let oid1 = test_commit(&git_dir, "one");
5436 let oid2 = test_commit(&git_dir, "two");
5437 let oid3 = test_commit(&git_dir, "three");
5438 write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
5439
5440 let gate = Arc::new(branch_ref_pause_inject::PauseGate {
5441 wait: Mutex::new(false),
5442 cv: Condvar::new(),
5443 });
5444 set_test_inject_branch_ref_pause_before_commit(Some(gate.clone()));
5445
5446 let git_dir_thread = git_dir.clone();
5447 let handle = thread::spawn(move || {
5448 update_branch_for_commit(
5449 &git_dir_thread,
5450 &BranchCommitRefUpdate {
5451 branch_ref: "refs/heads/main",
5452 expected_old: Some(oid1),
5453 new_oid: oid2,
5454 identity: "T <t@e.com> 1 +0000",
5455 reflog_message: "commit: test",
5456 },
5457 )
5458 });
5459
5460 let mut wait = gate.wait.lock().unwrap();
5461 while !*wait {
5462 wait = gate.cv.wait(wait).unwrap();
5463 }
5464
5465 assert!(
5466 write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
5467 "concurrent branch update must block on the held ref lock"
5468 );
5469
5470 *wait = false;
5471 gate.cv.notify_all();
5472 drop(wait);
5473 handle.join().unwrap().unwrap();
5474 set_test_inject_branch_ref_pause_before_commit(None);
5475
5476 assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
5477 }
5478
5479 #[test]
5480 fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
5481 let tmp = TempDir::new().unwrap();
5482 let repo = init_repository(
5483 tmp.path(),
5484 false,
5485 "main",
5486 None,
5487 crate::RefStorageFormat::Reftable,
5488 )
5489 .unwrap();
5490 let git_dir = repo.git_dir().to_path_buf();
5491 let new_oid = sample_oid("3333333333333333333333333333333333333333");
5492
5493 crate::reftable::set_test_inject_reftable_transaction_fail(true);
5494 let err = update_branch_for_commit(
5495 &git_dir,
5496 &BranchCommitRefUpdate {
5497 branch_ref: "refs/heads/main",
5498 expected_old: None,
5499 new_oid,
5500 identity: "T <t@e.com> 1 +0000",
5501 reflog_message: "commit (initial): test",
5502 },
5503 )
5504 .unwrap_err();
5505 crate::reftable::set_test_inject_reftable_transaction_fail(false);
5506 assert!(!matches!(err, Error::Message(_)));
5507
5508 assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
5509 assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
5510 assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
5511 }
5512}
5513
5514#[cfg(test)]
5515mod refname_available_tests {
5516 use super::*;
5517 use std::collections::{BTreeSet, HashSet};
5518 use tempfile::tempdir;
5519
5520 #[test]
5521 fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
5522 let dir = tempdir().unwrap();
5523 let git_dir = dir.path();
5524 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5525 let items = vec![
5526 RefBatchItem {
5527 name: "refs/heads/c".to_owned(),
5528 new_oid: Some(sample_oid()),
5529 },
5530 RefBatchItem {
5531 name: "refs/heads/c/x".to_owned(),
5532 new_oid: Some(sample_oid()),
5533 },
5534 ];
5535 let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
5536 assert!(matches!(
5537 err,
5538 RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
5539 ));
5540 }
5541
5542 fn sample_oid() -> ObjectId {
5543 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5544 }
5545
5546 #[test]
5547 fn loose_parent_blocks_child_create() {
5548 let dir = tempdir().unwrap();
5549 let git_dir = dir.path();
5550 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5551 fs::write(
5552 git_dir.join("refs/1l/c"),
5553 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5554 )
5555 .unwrap();
5556 assert_eq!(
5557 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
5558 RawRefLookup::Exists
5559 );
5560 let extras = BTreeSet::from([
5561 "refs/1l/b".to_string(),
5562 "refs/1l/c/x".to_string(),
5563 "refs/1l/d".to_string(),
5564 ]);
5565 let skip = HashSet::new();
5566 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
5567 .unwrap_err();
5568 assert!(matches!(
5569 err,
5570 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5571 ));
5572 }
5573
5574 #[test]
5575 fn verify_sees_loose_ref_after_canonical_git_dir() {
5576 let dir = tempdir().unwrap();
5577 let git_dir = dir.path().join(".git");
5578 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5579 fs::write(
5580 git_dir.join("refs/1l/c"),
5581 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5582 )
5583 .unwrap();
5584 let skip = HashSet::new();
5585 let extras = BTreeSet::new();
5586 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
5587 .unwrap_err();
5588 assert!(matches!(
5589 err,
5590 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5591 ));
5592 }
5593
5594 fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
5595 if existing == candidate {
5596 return false;
5597 }
5598 existing
5599 .strip_prefix(candidate)
5600 .is_some_and(|rest| rest.starts_with('/'))
5601 || candidate
5602 .strip_prefix(existing)
5603 .is_some_and(|rest| rest.starts_with('/'))
5604 }
5605
5606 #[test]
5607 fn packed_namespace_index_matches_pairwise_scan() {
5608 let mut sorted: BTreeSet<String> = BTreeSet::new();
5609 for i in 0..500 {
5610 sorted.insert(format!("refs/heads/b{i:05}"));
5611 }
5612 sorted.insert("refs/heads/parent".into());
5613 sorted.insert("refs/heads/a/b".into());
5614 let candidates = [
5615 "refs/heads/b00001",
5616 "refs/heads/parent/child",
5617 "refs/heads/new",
5618 "refs/heads/a",
5619 ];
5620 for candidate in candidates {
5621 let indexed = packed_refname_namespace_conflict(&sorted, candidate);
5622 let linear = sorted
5623 .iter()
5624 .any(|existing| refname_namespace_conflicts(existing, candidate));
5625 assert_eq!(indexed, linear, "candidate {candidate}");
5626 }
5627 }
5628
5629 #[test]
5630 fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
5631 let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
5632 if !git_dir.is_dir() {
5633 return;
5634 }
5635 let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
5636 assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
5637 assert!(listed
5638 .iter()
5639 .all(|(name, _)| name.starts_with("refs/heads/b05")));
5640 }
5641
5642 #[test]
5643 fn list_refs_finds_sibling_under_parent_directory() {
5644 let dir = tempdir().unwrap();
5645 let git_dir = dir.path();
5646 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5647 fs::write(
5648 git_dir.join("refs/ns/p/x"),
5649 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5650 )
5651 .unwrap();
5652 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
5653 assert!(
5654 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
5655 "got {listed:?}"
5656 );
5657 }
5658
5659 #[test]
5660 fn verify_blocks_parent_when_child_ref_exists() {
5661 let dir = tempdir().unwrap();
5662 let git_dir = dir.path();
5663 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5664 fs::write(
5665 git_dir.join("refs/ns/p/x"),
5666 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5667 )
5668 .unwrap();
5669 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
5670 let skip = HashSet::new();
5671 let err =
5672 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
5673 assert!(matches!(
5674 err,
5675 RefnameUnavailable::DescendantExists { ref blocking, .. }
5676 if blocking == "refs/ns/p/x"
5677 ));
5678 }
5679
5680 #[test]
5681 fn verify_blocks_parent_when_only_a_packed_child_exists() {
5682 let dir = tempdir().unwrap();
5685 let git_dir = dir.path();
5686 fs::create_dir_all(git_dir.join("refs")).unwrap();
5687 fs::write(
5688 store::paths::packed_refs_path(git_dir),
5689 "# pack-refs with: peeled fully-peeled\n\
5690 67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
5691 )
5692 .unwrap();
5693 let err = verify_refname_available_for_create(
5694 git_dir,
5695 "refs/ns/q",
5696 &BTreeSet::new(),
5697 &HashSet::new(),
5698 )
5699 .unwrap_err();
5700 assert!(matches!(
5701 err,
5702 RefnameUnavailable::DescendantExists { ref blocking, .. }
5703 if blocking == "refs/ns/q/x"
5704 ));
5705 }
5706
5707 #[test]
5708 fn verify_allows_name_whose_siblings_share_a_prefix() {
5709 let dir = tempdir().unwrap();
5710 let git_dir = dir.path();
5711 fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
5712 for name in ["v1.0.1", "v1.0-rc1"] {
5713 fs::write(
5714 git_dir.join("refs/tags").join(name),
5715 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5716 )
5717 .unwrap();
5718 }
5719 verify_refname_available_for_create(
5720 git_dir,
5721 "refs/tags/v1.0",
5722 &BTreeSet::new(),
5723 &HashSet::new(),
5724 )
5725 .expect("siblings are not descendants");
5726 }
5727
5728 #[test]
5729 fn verify_blocks_parent_git_style_nested_path() {
5730 let dir = tempdir().unwrap();
5731 let git_dir = dir.path();
5732 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
5733 fs::write(
5734 git_dir.join("refs/3l/c/x"),
5735 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5736 )
5737 .unwrap();
5738 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
5739 let skip = HashSet::new();
5740 let err =
5741 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
5742 assert!(matches!(
5743 err,
5744 RefnameUnavailable::DescendantExists { ref blocking, .. }
5745 if blocking == "refs/3l/c/x"
5746 ));
5747 }
5748
5749 #[test]
5750 fn intermediate_directory_does_not_block_nested_create() {
5751 let dir = tempdir().unwrap();
5752 let git_dir = dir.path();
5753 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
5754 fs::write(
5755 git_dir.join("refs/ns/existing"),
5756 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5757 )
5758 .unwrap();
5759 assert_eq!(
5760 read_raw_ref(git_dir, "refs/ns").unwrap(),
5761 RawRefLookup::IsDirectory
5762 );
5763 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
5764 let skip = HashSet::new();
5765 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
5766 }
5767}
5768
5769#[cfg(test)]
5770mod ref_storage_traversal_guard_tests {
5771 use super::*;
5772 use crate::objects::ObjectId;
5773 use tempfile::tempdir;
5774
5775 const TRAVERSAL_REF: &str = "refs/heads/../../config";
5776
5777 fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
5778 let dir = tempdir().unwrap();
5779 let git_dir = dir.path().to_path_buf();
5780 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5781 fs::write(
5782 git_dir.join("config"),
5783 "[core]\nrepositoryformatversion = 0\n",
5784 )
5785 .unwrap();
5786 (dir, git_dir)
5787 }
5788
5789 fn sample_oid() -> ObjectId {
5790 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5791 }
5792
5793 fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
5794 assert!(matches!(result, Err(Error::InvalidRef(_))));
5795 assert!(
5796 git_dir.join("config").is_file(),
5797 "repository config must not be touched"
5798 );
5799 }
5800
5801 #[test]
5802 fn write_ref_rejects_traversal_refname() {
5803 let (_dir, git_dir) = bare_repo_with_config();
5804 let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
5805 assert_invalid_ref_preserves_config(&git_dir, result);
5806 }
5807
5808 #[test]
5809 fn write_ref_cached_rejects_traversal_refname() {
5810 let (_dir, git_dir) = bare_repo_with_config();
5811 let mut packed = PackedRefs::load(&git_dir).unwrap();
5812 let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
5813 assert_invalid_ref_preserves_config(&git_dir, result);
5814 }
5815
5816 #[test]
5817 fn write_symbolic_ref_rejects_traversal_refname() {
5818 let (_dir, git_dir) = bare_repo_with_config();
5819 let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
5820 assert_invalid_ref_preserves_config(&git_dir, result);
5821 }
5822
5823 #[test]
5824 fn delete_ref_rejects_traversal_refname() {
5825 let (_dir, git_dir) = bare_repo_with_config();
5826 let result = delete_ref(&git_dir, TRAVERSAL_REF);
5827 assert_invalid_ref_preserves_config(&git_dir, result);
5828 }
5829
5830 #[test]
5831 fn write_ref_rejects_illegal_branch_refname() {
5832 let (_dir, git_dir) = bare_repo_with_config();
5833 for refname in [
5834 "refs/heads/bad name",
5835 "refs/heads/x..y",
5836 "refs/heads/a~b",
5837 "refs/heads/a.lock",
5838 ] {
5839 let result = write_ref(&git_dir, refname, &sample_oid());
5840 assert_invalid_ref_preserves_config(&git_dir, result);
5841 assert!(
5842 !git_dir
5843 .join(refname.strip_prefix("refs/").unwrap())
5844 .exists(),
5845 "must not write loose ref for {refname}"
5846 );
5847 }
5848 }
5849
5850 #[test]
5851 fn write_ref_accepts_refs_heads_head() {
5852 let (_dir, git_dir) = bare_repo_with_config();
5853 let commit = test_commit(&git_dir, "head-named branch");
5854 write_ref(&git_dir, "refs/heads/HEAD", &commit)
5855 .expect("refs/heads/HEAD is valid ref syntax");
5856 assert!(git_dir.join("refs/heads/HEAD").is_file());
5857 }
5858}
5859
5860#[cfg(test)]
5861mod log_refs_config_tests {
5862 use super::*;
5863 use crate::config::{ConfigFile, ConfigScope};
5864
5865 fn set(text: &str) -> ConfigSet {
5866 let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
5867 let mut set = ConfigSet::new();
5868 set.merge(&file);
5869 set
5870 }
5871
5872 #[test]
5873 fn parse_accepts_modes_and_bool_spellings() {
5874 for (raw, mode) in [
5875 ("always", LogRefsConfig::Always),
5876 (" ALWAYS ", LogRefsConfig::Always),
5877 ("true", LogRefsConfig::Normal),
5878 ("Yes", LogRefsConfig::Normal),
5879 ("on", LogRefsConfig::Normal),
5880 ("1", LogRefsConfig::Normal),
5881 ("false", LogRefsConfig::None),
5882 ("no", LogRefsConfig::None),
5883 ("OFF", LogRefsConfig::None),
5884 ("0", LogRefsConfig::None),
5885 ("never", LogRefsConfig::None),
5886 ] {
5887 assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
5888 }
5889 assert_eq!(LogRefsConfig::parse("sometimes"), None);
5890 }
5891
5892 #[test]
5893 fn effective_mode_applies_bare_default_only_when_unset() {
5894 assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
5895 assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
5896 let bare = set("[core]\n\tbare = true\n");
5897 assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
5898 let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
5899 assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
5900 assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
5901 let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
5902 assert_eq!(
5903 bare_logged.effective_log_refs_config(),
5904 LogRefsConfig::Normal
5905 );
5906 let always = set("[core]\n\tlogAllRefUpdates = always\n");
5907 assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
5908 }
5909
5910 #[test]
5911 fn autocreate_with_config_matches_mode() {
5912 let normal = set("[core]\n\tlogAllRefUpdates = true\n");
5913 let git_dir = Path::new(".git");
5914 assert!(should_autocreate_reflog_with_config(
5915 &normal,
5916 git_dir,
5917 "refs/heads/main"
5918 ));
5919 assert!(!should_autocreate_reflog_with_config(
5920 &normal,
5921 git_dir,
5922 "refs/tags/v1"
5923 ));
5924 let none = set("[core]\n\tlogAllRefUpdates = false\n");
5925 assert!(!should_autocreate_reflog_with_config(
5926 &none, git_dir, "HEAD"
5927 ));
5928 }
5929}
5930
5931#[cfg(test)]
5932mod read_raw_ref_tests {
5933 use super::*;
5934 use tempfile::tempdir;
5935
5936 #[test]
5937 fn loose_ref_file_is_exists() {
5938 let dir = tempdir().unwrap();
5939 let git_dir = dir.path();
5940 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5941 fs::write(
5942 git_dir.join("refs/heads/side"),
5943 "0000000000000000000000000000000000000000\n",
5944 )
5945 .unwrap();
5946 assert_eq!(
5947 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
5948 RawRefLookup::Exists
5949 );
5950 }
5951
5952 #[test]
5953 fn missing_ref_is_not_found() {
5954 let dir = tempdir().unwrap();
5955 let git_dir = dir.path();
5956 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5957 assert_eq!(
5958 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
5959 RawRefLookup::NotFound
5960 );
5961 }
5962
5963 #[test]
5964 fn directory_where_ref_expected_is_is_directory() {
5965 let dir = tempdir().unwrap();
5966 let git_dir = dir.path();
5967 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5968 assert_eq!(
5969 read_raw_ref(git_dir, "refs/heads").unwrap(),
5970 RawRefLookup::IsDirectory
5971 );
5972 }
5973
5974 #[test]
5975 fn packed_ref_name_is_exists() {
5976 let dir = tempdir().unwrap();
5977 let git_dir = dir.path();
5978 fs::write(
5979 store::paths::packed_refs_path(git_dir),
5980 "# pack-refs with: peeled fully-peeled \n\
5981 0000000000000000000000000000000000000000 refs/heads/packed\n",
5982 )
5983 .unwrap();
5984 assert_eq!(
5985 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
5986 RawRefLookup::Exists
5987 );
5988 }
5989
5990 #[test]
5991 fn access_denied_on_a_directory_is_a_missing_file() {
5992 let dir = tempdir().unwrap();
5993 let ref_dir = dir.path().join("refs/heads/rel");
5994 fs::create_dir_all(&ref_dir).unwrap();
5995 let file = dir.path().join("refs/heads/main");
5996 fs::write(&file, "x\n").unwrap();
5997 let denied = || io::Error::from(io::ErrorKind::PermissionDenied);
5998
5999 assert!(io_error_is_missing_file(&denied(), &ref_dir));
6001 assert!(
6002 !io_error_is_missing_file(&denied(), &file),
6003 "an unreadable ref file is a real I/O error"
6004 );
6005 assert!(!io_error_is_missing_file(
6006 &denied(),
6007 &dir.path().join("refs/heads/none")
6008 ));
6009 for kind in [
6010 io::ErrorKind::NotFound,
6011 io::ErrorKind::IsADirectory,
6012 io::ErrorKind::NotADirectory,
6013 ] {
6014 assert!(io_error_is_missing_file(&io::Error::from(kind), &file));
6015 }
6016 assert!(io_error_is_missing_file(
6017 &io::Error::from_raw_os_error(libc::EISDIR),
6018 &file
6019 ));
6020 assert!(!io_error_is_missing_file(
6021 &io::Error::from(io::ErrorKind::InvalidData),
6022 &ref_dir
6023 ));
6024 }
6025
6026 #[test]
6027 fn reading_a_ref_directory_is_not_found() {
6028 let dir = tempdir().unwrap();
6029 let ref_dir = dir.path().join("refs/heads/rel");
6030 fs::create_dir_all(ref_dir.join("1.0")).unwrap();
6031 match read_ref_file(&ref_dir) {
6032 Err(Error::Io(e)) => assert_eq!(e.kind(), io::ErrorKind::NotFound),
6033 other => panic!("expected a missing ref, got {other:?}"),
6034 }
6035 }
6036}
6037
6038#[cfg(test)]
6039mod packed_refs_parse_tests {
6040 use super::*;
6041
6042 use tempfile::tempdir;
6043
6044 #[test]
6045 fn packed_removal_validates_the_whole_file_before_any_change() {
6046 let dir = tempdir().unwrap();
6047 let path = store::paths::packed_refs_path(dir.path());
6048 let record = "1111111111111111111111111111111111111111 refs/heads/keep\n";
6049 let malformed = [
6050 format!("{record}garbage\n").into_bytes(),
6051 format!("{record}\n").into_bytes(),
6052 format!("{record}^bad-id\n").into_bytes(),
6053 format!("{record}^{}\n^{}\n", "1".repeat(40), "2".repeat(40)).into_bytes(),
6054 format!("{record}{} refs/heads/bad..name\n", "2".repeat(40)).into_bytes(),
6055 format!("{record}# comment\r\n").into_bytes(),
6056 record.trim_end().as_bytes().to_vec(),
6057 [record.as_bytes(), b"\xff\n"].concat(),
6058 ];
6059 for content in malformed {
6060 for name in ["refs/heads/missing", "refs/heads/keep"] {
6061 fs::write(&path, &content).unwrap();
6062 assert!(
6063 remove_packed_ref(dir.path(), name).is_err(),
6064 "{name}: {content:?}"
6065 );
6066 assert_eq!(fs::read(&path).unwrap(), content);
6067 assert!(!path.with_extension("lock").exists());
6068 assert!(!dir.path().join("packed-refs.new").exists());
6069 }
6070 }
6071 }
6072
6073 #[test]
6074 fn packed_removal_without_a_match_preserves_valid_file_bytes() {
6075 let dir = tempdir().unwrap();
6076 let path = store::paths::packed_refs_path(dir.path());
6077 for hash_len in [40, 64] {
6078 let body = format!(
6079 "# pack-refs with: peeled\n# keep this comment\n{} refs/tags/tag\n^{}\n{} refs/heads/main\n",
6080 "1".repeat(hash_len), "2".repeat(hash_len), "3".repeat(hash_len)
6081 );
6082 fs::write(&path, &body).unwrap();
6083 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
6084 assert_eq!(fs::read_to_string(&path).unwrap(), body);
6085 }
6086 fs::write(&path, "").unwrap();
6087 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
6088 assert!(fs::read(&path).unwrap().is_empty());
6089 }
6090
6091 #[test]
6092 fn parse_rejects_garbage_line() {
6093 let dir = tempdir().unwrap();
6094 let path = store::paths::packed_refs_path(dir.path());
6095 fs::write(
6096 &path,
6097 "# pack-refs with: peeled\n\
6098 0000000000000000000000000000000000000000 refs/heads/main\n\
6099 not a ref line\n",
6100 )
6101 .unwrap();
6102 let data = fs::read(&path).unwrap();
6103 assert!(matches!(
6104 parse_packed_refs_to_map(&path, &data),
6105 Err(Error::PackedRefsUnexpectedLine { .. })
6106 ));
6107 }
6108
6109 #[test]
6110 fn parse_rejects_carriage_return_bytes() {
6111 let dir = tempdir().unwrap();
6112 let path = store::paths::packed_refs_path(dir.path());
6113 fs::write(
6114 &path,
6115 "# pack-refs with: peeled\n\
6116 0000000000000000000000000000000000000000 refs/heads/main\r\n",
6117 )
6118 .unwrap();
6119 let data = fs::read(&path).unwrap();
6120 assert!(matches!(
6121 parse_packed_refs_to_map(&path, &data),
6122 Err(Error::PackedRefsUnexpectedLine { .. })
6123 ));
6124 }
6125
6126 #[test]
6127 fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
6128 let dir = tempdir().unwrap();
6129 let git_dir = dir.path();
6130 let oid = "1111111111111111111111111111111111111111";
6131 fs::write(
6132 store::paths::packed_refs_path(git_dir),
6133 format!(
6134 "# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
6135 ),
6136 )
6137 .unwrap();
6138 let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
6139 assert_eq!(listed.len(), 1);
6140 assert_eq!(listed[0].0, "refs/heads/aaa");
6141 }
6142
6143 #[test]
6144 fn parse_rejects_blank_line_and_missing_final_newline() {
6145 let dir = tempdir().unwrap();
6146 let path = store::paths::packed_refs_path(dir.path());
6147 fs::write(
6148 &path,
6149 "# pack-refs with: peeled\n\n\
6150 0000000000000000000000000000000000000000 refs/heads/main\n",
6151 )
6152 .unwrap();
6153 let data = fs::read(&path).unwrap();
6154 assert!(matches!(
6155 parse_packed_refs_to_map(&path, &data),
6156 Err(Error::PackedRefsUnexpectedLine { .. })
6157 ));
6158
6159 fs::write(
6160 &path,
6161 "# pack-refs with: peeled\n\
6162 0000000000000000000000000000000000000000 refs/heads/main",
6163 )
6164 .unwrap();
6165 let data = fs::read(&path).unwrap();
6166 assert!(matches!(
6167 parse_packed_refs_to_map(&path, &data),
6168 Err(Error::PackedRefsUnexpectedLine { .. })
6169 ));
6170 }
6171
6172 #[test]
6173 fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
6174 let dir = tempdir().unwrap();
6175 let path = store::paths::packed_refs_path(dir.path());
6176 let a = "1111111111111111111111111111111111111111";
6177 let b = "2222222222222222222222222222222222222222";
6178 let peel = "3333333333333333333333333333333333333333";
6179 let body = |header: &str| {
6180 format!(
6181 "{header}{a} refs/heads/main\n\
6182 {b} refs/heads/tagged\n\
6183 ^{peel}\n\
6184 {a} refs/tags/light\n\
6185 {b} refs/tags/v1\n\
6186 ^{peel}\n"
6187 )
6188 };
6189 let a: ObjectId = a.parse().unwrap();
6190 let b: ObjectId = b.parse().unwrap();
6191 let peel: ObjectId = peel.parse().unwrap();
6192 let cases = [
6193 (
6194 "# pack-refs with: peeled fully-peeled sorted \n",
6195 [
6196 PackedPeel::NotTag,
6197 PackedPeel::Peeled(peel),
6198 PackedPeel::NotTag,
6199 PackedPeel::Peeled(peel),
6200 ],
6201 ),
6202 (
6203 "# pack-refs with: peeled sorted \n",
6204 [
6205 PackedPeel::Unknown,
6206 PackedPeel::Unknown,
6207 PackedPeel::NotTag,
6208 PackedPeel::Peeled(peel),
6209 ],
6210 ),
6211 ("", [PackedPeel::Unknown; 4]),
6212 ];
6213 for (header, want) in cases {
6214 let data = body(header).into_bytes();
6215 let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
6216 let seen = std::cell::RefCell::new(Vec::new());
6217 let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
6218 seen.borrow_mut().push((name.to_owned(), *oid, peel));
6219 name.starts_with("refs/tags/")
6220 };
6221 let kept = view
6222 .map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
6223 .unwrap();
6224 assert_eq!(
6225 seen.into_inner(),
6226 vec![
6227 ("refs/heads/main".to_owned(), a, want[0]),
6228 ("refs/heads/tagged".to_owned(), b, want[1]),
6229 ("refs/tags/light".to_owned(), a, want[2]),
6230 ("refs/tags/v1".to_owned(), b, want[3]),
6231 ],
6232 "header {header:?}"
6233 );
6234 let mut kept: Vec<_> = kept.into_iter().collect();
6235 kept.sort();
6236 assert_eq!(
6237 kept,
6238 vec![
6239 ("refs/tags/light".to_owned(), a),
6240 ("refs/tags/v1".to_owned(), b)
6241 ]
6242 );
6243 assert_eq!(
6244 view.map_for_prefix_filtered(&path, Some("refs"), None)
6245 .unwrap(),
6246 view.map_for_prefix(&path, Some("refs")).unwrap()
6247 );
6248 }
6249
6250 let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
6252 let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
6253 assert!(matches!(
6254 view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
6255 Err(Error::PackedRefsUnexpectedLine { .. })
6256 ));
6257 }
6258
6259 #[test]
6260 fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
6261 let dir = tempdir().unwrap();
6262 let path = store::paths::packed_refs_path(dir.path());
6263 let oid = "1111111111111111111111111111111111111111";
6264 let peel = "2222222222222222222222222222222222222222";
6265 let sorted_body = format!(
6266 "# pack-refs with: peeled fully-peeled sorted\n\
6267 {oid} refs/heads/aaa\n\
6268 {oid} refs/heads/main\n\
6269 {oid} refs/heads/zzz\n\
6270 {oid} refs/tags/v1\n\
6271 ^{peel}\n"
6272 );
6273 fs::write(&path, &sorted_body).unwrap();
6274 let data = fs::read(&path).unwrap();
6275 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6276 let full = parse_packed_refs_to_map(&path, &data).unwrap();
6277 for (name, want) in &full {
6278 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
6279 }
6280 assert_eq!(
6281 lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
6282 .unwrap()
6283 .map(|(o, p)| (o, p.unwrap())),
6284 Some((oid.parse().unwrap(), peel.parse().unwrap()))
6285 );
6286
6287 let unsorted_body = format!(
6288 "# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
6289 );
6290 fs::write(&path, unsorted_body).unwrap();
6291 let data = fs::read(&path).unwrap();
6292 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6293 let full = parse_packed_refs_to_map(&path, &data).unwrap();
6294 for (name, want) in &full {
6295 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
6296 }
6297 assert_eq!(
6298 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6299 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6300 );
6301 }
6302
6303 #[test]
6304 fn sorted_prefix_without_upper_bound_filters_names() {
6305 let dir = tempdir().unwrap();
6306 let path = store::paths::packed_refs_path(dir.path());
6307 let oid = "1111111111111111111111111111111111111111";
6308 let sorted_body = format!(
6309 "# pack-refs with: peeled fully-peeled sorted\n\
6310 {oid} refs/heads/aaa\n\
6311 {oid} refs/heads/zzz\n\
6312 {oid} refs/tags/v1\n"
6313 );
6314 fs::write(&path, sorted_body).unwrap();
6315 let data = fs::read(&path).unwrap();
6316 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6317 assert_eq!(
6318 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6319 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6320 );
6321 assert!(
6322 !view
6323 .map_for_prefix(&path, Some("refs/heads/a"))
6324 .unwrap()
6325 .contains_key("refs/heads/zzz"),
6326 "prefix refs/heads/a must not include refs/heads/zzz"
6327 );
6328 assert_eq!(
6329 view.map_for_prefix(&path, Some("refs")).unwrap(),
6330 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
6331 );
6332 }
6333
6334 #[test]
6335 fn sorted_prefix_scan_validates_only_its_range() {
6336 let dir = tempdir().unwrap();
6337 let path = store::paths::packed_refs_path(dir.path());
6338 let oid = "1111111111111111111111111111111111111111";
6339 let view_of =
6340 |body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
6341
6342 let bad_peel_on_branch = view_of(format!(
6343 "# pack-refs with: peeled fully-peeled sorted\n\
6344 {oid} refs/heads/main\n\
6345 ^not-hex\n\
6346 {oid} refs/heads/topic\n\
6347 {oid} refs/tags/v1\n"
6348 ))
6349 .unwrap();
6350 assert!(matches!(
6351 bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
6352 Err(Error::PackedRefsUnexpectedLine { .. })
6353 ));
6354 let tags = bad_peel_on_branch
6355 .map_for_prefix(&path, Some("refs/tags"))
6356 .unwrap();
6357 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
6358 assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
6359
6360 let blank_among_tags = view_of(format!(
6361 "# pack-refs with: peeled fully-peeled sorted\n\
6362 {oid} refs/heads/main\n\
6363 {oid} refs/tags/v1\n\
6364 \n\
6365 {oid} refs/tags/v2\n"
6366 ))
6367 .unwrap();
6368 assert_eq!(
6369 blank_among_tags
6370 .map_for_prefix(&path, Some("refs/heads"))
6371 .unwrap()
6372 .len(),
6373 1
6374 );
6375 assert!(blank_among_tags
6376 .map_for_prefix(&path, Some("refs/tags"))
6377 .is_err());
6378
6379 let no_match = blank_among_tags
6380 .map_for_prefix(&path, Some("refs/notes"))
6381 .unwrap();
6382 assert!(no_match.is_empty());
6383 }
6384
6385 #[test]
6386 fn non_utf8_record_only_breaks_reads_that_cover_it() {
6387 let dir = tempdir().unwrap();
6388 let path = store::paths::packed_refs_path(dir.path());
6389 let oid = "1111111111111111111111111111111111111111";
6390 let mut body =
6391 format!("# pack-refs with: peeled fully-peeled sorted \n{oid} refs/heads/main\n")
6392 .into_bytes();
6393 body.extend_from_slice(format!("{oid} refs/tags/v1\n{oid} refs/tags/").as_bytes());
6394 body.extend_from_slice(b"\xff\n");
6395 let view = PackedRefsFileView::from_bytes(&path, body, HashAlgo::Sha1).unwrap();
6396 assert!(view.sorted);
6397 for name in ["refs/heads/main", "refs/tags/v1"] {
6398 assert_eq!(
6399 view.lookup_oid(&path, name).unwrap(),
6400 Some(oid.parse().unwrap())
6401 );
6402 }
6403 assert_eq!(view.lookup_oid(&path, "refs/tags/v2").unwrap(), None);
6404 let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
6405 assert_eq!(heads.keys().collect::<Vec<_>>(), ["refs/heads/main"]);
6406 assert!(matches!(
6407 view.map_for_prefix(&path, Some("refs/tags")),
6408 Err(Error::PackedRefsUnexpectedLine { .. })
6409 ));
6410 assert!(view.map_for_prefix(&path, None).is_err());
6411
6412 let mut bad_header = b"# pack-refs with: \xff sorted \n".to_vec();
6413 bad_header.extend_from_slice(format!("{oid} refs/heads/main\n").as_bytes());
6414 assert!(matches!(
6415 PackedRefsFileView::from_bytes(&path, bad_header, HashAlgo::Sha1),
6416 Err(Error::PackedRefsUnexpectedLine { .. })
6417 ));
6418 }
6419
6420 #[test]
6421 fn scan_of_file_without_ref_lines_still_validates_it() {
6422 let dir = tempdir().unwrap();
6423 let path = store::paths::packed_refs_path(dir.path());
6424 let view = PackedRefsFileView::from_bytes(
6425 &path,
6426 b"# pack-refs with: peeled fully-peeled sorted\n\
6427 ^1111111111111111111111111111111111111111\n"
6428 .to_vec(),
6429 HashAlgo::Sha1,
6430 )
6431 .unwrap();
6432 for prefix in [None, Some("refs/heads")] {
6433 assert!(matches!(
6434 view.map_for_prefix(&path, prefix),
6435 Err(Error::PackedRefsUnexpectedLine { .. })
6436 ));
6437 }
6438 let header_only = PackedRefsFileView::from_bytes(
6439 &path,
6440 b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
6441 HashAlgo::Sha1,
6442 )
6443 .unwrap();
6444 assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
6445 }
6446
6447 #[test]
6448 fn view_checks_last_line_length_against_the_repository_hash() {
6449 let dir = tempdir().unwrap();
6450 let path = store::paths::packed_refs_path(dir.path());
6451 let sha1 = "1111111111111111111111111111111111111111";
6452 let sha256 = "2".repeat(64);
6453 let view =
6454 |body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
6455 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6456 let fifty_bytes = "x".repeat(49);
6457
6458 for (body, algo) in [
6459 (
6460 format!("{header}{sha1} refs/heads/main\ngarbage\n"),
6461 HashAlgo::Sha1,
6462 ),
6463 (
6464 format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
6465 HashAlgo::Sha1,
6466 ),
6467 (
6468 format!("{header}{sha1} refs/heads/main\n#\n"),
6469 HashAlgo::Sha1,
6470 ),
6471 (
6472 format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
6473 HashAlgo::Sha256,
6474 ),
6475 (format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
6476 ] {
6477 assert!(
6478 matches!(
6479 view(body.clone(), algo),
6480 Err(Error::PackedRefsUnexpectedLine { .. })
6481 ),
6482 "{algo:?}: {body:?}"
6483 );
6484 }
6485 for (body, algo) in [
6486 (header.to_owned(), HashAlgo::Sha1),
6487 (header.to_owned(), HashAlgo::Sha256),
6488 ("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
6489 (
6490 format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
6491 HashAlgo::Sha1,
6492 ),
6493 (
6494 format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
6495 HashAlgo::Sha1,
6496 ),
6497 (
6498 format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
6499 HashAlgo::Sha256,
6500 ),
6501 ] {
6502 assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
6503 }
6504 }
6505
6506 #[test]
6507 fn sha256_short_last_record_fails_branch_listing() {
6508 let dir = tempdir().unwrap();
6509 let path = store::paths::packed_refs_path(dir.path());
6510 let sha256 = "2".repeat(64);
6511 let body = format!(
6512 "# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
6513 "x".repeat(49)
6514 );
6515 assert!(
6516 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
6517 );
6518 }
6519
6520 #[test]
6521 fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
6522 let dir = tempdir().unwrap();
6523 let path = store::paths::packed_refs_path(dir.path());
6524 let oid = "1111111111111111111111111111111111111111";
6525 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6526 let view = |body: String| {
6527 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6528 };
6529
6530 let garbage_after_first_branch = view(format!(
6531 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
6532 {oid} refs/tags/t1\n"
6533 ));
6534 assert!(matches!(
6535 garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
6536 Err(Error::PackedRefsUnexpectedLine { .. })
6537 ));
6538 let tags = garbage_after_first_branch
6539 .map_for_prefix(&path, Some("refs/tags"))
6540 .unwrap();
6541 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
6542
6543 let garbage_before_range = view(format!(
6544 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
6545 ));
6546 for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
6547 assert!(
6548 garbage_before_range
6549 .map_for_prefix(&path, Some(prefix))
6550 .is_err(),
6551 "{prefix}"
6552 );
6553 }
6554 assert_eq!(
6555 garbage_before_range
6556 .map_for_prefix(&path, Some("refs/zz"))
6557 .unwrap()
6558 .len(),
6559 0
6560 );
6561
6562 let peeled_tag_before_range = view(format!(
6563 "{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
6564 ));
6565 let orphan_peel_before_range = view(format!(
6566 "{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
6567 ));
6568 assert!(orphan_peel_before_range
6569 .map_for_prefix(&path, Some("refs/tags"))
6570 .is_err());
6571 assert_eq!(
6572 peeled_tag_before_range
6573 .map_for_prefix(&path, Some("refs/tags"))
6574 .unwrap()
6575 .len(),
6576 1
6577 );
6578 }
6579
6580 fn lying_and_unsorted_views(path: &Path) -> (PackedRefsFileView, PackedRefsFileView) {
6582 let oid = "1111111111111111111111111111111111111111";
6583 let records = format!("{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n");
6584 let view_of = |header: &str| {
6585 PackedRefsFileView::from_bytes(
6586 path,
6587 format!("{header}{records}").into_bytes(),
6588 HashAlgo::Sha1,
6589 )
6590 .unwrap()
6591 };
6592 (
6593 view_of("# pack-refs with: peeled fully-peeled sorted\n"),
6594 view_of("# pack-refs with: peeled fully-peeled\n"),
6595 )
6596 }
6597
6598 fn sorted_names(map: HashMap<String, ObjectId>) -> Vec<String> {
6599 let mut names: Vec<String> = map.into_keys().collect();
6600 names.sort();
6601 names
6602 }
6603
6604 #[test]
6605 fn full_scans_of_a_lying_sorted_file_return_every_ref() {
6606 let dir = tempdir().unwrap();
6607 let path = store::paths::packed_refs_path(dir.path());
6608 let (lying, unsorted) = lying_and_unsorted_views(&path);
6609 assert!(lying.sorted);
6610 let every = ["refs/heads/a", "refs/heads/b", "refs/tags/t"];
6611 for view in [&lying, &unsorted] {
6612 for prefix in [None, Some("refs"), Some("refs/")] {
6613 assert_eq!(
6614 sorted_names(view.map_for_prefix(&path, prefix).unwrap()),
6615 every,
6616 "{prefix:?}"
6617 );
6618 }
6619 }
6620 fs::write(&path, lying.data.as_ref() as &[u8]).unwrap();
6621 let loaded = PackedRefs::load(dir.path()).unwrap();
6622 for name in every {
6623 assert!(loaded.get(name).is_some(), "{name}");
6624 }
6625 }
6626
6627 #[test]
6628 fn garbage_between_sorted_records_rejects_listing() {
6629 let dir = tempdir().unwrap();
6630 let git_dir = dir.path();
6631 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
6632 let oid = "1111111111111111111111111111111111111111";
6633 fs::write(
6634 store::paths::packed_refs_path(git_dir),
6635 format!(
6636 "# pack-refs with: sorted\n{oid} refs/heads/a\nthis is not a valid packed-refs line\n{oid} refs/heads/b\n"
6637 ),
6638 )
6639 .unwrap();
6640 assert!(matches!(
6641 list_refs(git_dir, "refs/"),
6642 Err(Error::PackedRefsUnexpectedLine { .. })
6643 ));
6644 }
6645
6646 #[test]
6647 fn for_each_record_streaming_matches_map_and_stops_early() {
6648 let dir = tempdir().unwrap();
6649 let path = store::paths::packed_refs_path(dir.path());
6650 let oid = "1111111111111111111111111111111111111111";
6651 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6652 let body = format!("{header}{oid} refs/heads/a\n{oid} refs/heads/b\n{oid} refs/tags/t\n");
6653 let view =
6654 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6655 let mut streamed = Vec::new();
6656 let _ = view
6657 .for_each_record(&path, Some("refs/heads"), None, |name, _, _| {
6658 streamed.push(name.to_owned());
6659 ControlFlow::Continue(())
6660 })
6661 .unwrap();
6662 assert_eq!(
6663 streamed,
6664 sorted_names(view.map_for_prefix(&path, Some("refs/heads")).unwrap())
6665 );
6666 let mut count = 0;
6667 assert!(view
6668 .for_each_record(&path, Some("refs/heads"), None, |_, _, _| {
6669 count += 1;
6670 ControlFlow::Break(())
6671 })
6672 .unwrap()
6673 .is_break());
6674 assert_eq!(count, 1);
6675 }
6676
6677 #[test]
6678 fn for_each_record_break_before_corrupt_tail_ok_while_full_scan_errors() {
6679 let dir = tempdir().unwrap();
6680 let path = store::paths::packed_refs_path(dir.path());
6681 let oid = "1111111111111111111111111111111111111111";
6682 let good_only = format!(
6683 "# pack-refs with: peeled fully-peeled sorted\n{oid} refs/heads/good\n{oid} refs/heads/good2\n"
6684 );
6685 let with_corrupt_tail = format!(
6686 "# pack-refs with: peeled fully-peeled sorted\n{oid} refs/heads/good\n{oid} refs/heads/good2\n{oid} refs/heads/bad..name\n"
6687 );
6688 let view =
6689 PackedRefsFileView::from_bytes(&path, good_only.into_bytes(), HashAlgo::Sha1).unwrap();
6690 assert!(view
6691 .for_each_record(&path, None, None, |_, _, _| ControlFlow::Break(()))
6692 .unwrap()
6693 .is_break());
6694 let view =
6695 PackedRefsFileView::from_bytes(&path, with_corrupt_tail.into_bytes(), HashAlgo::Sha1)
6696 .unwrap();
6697 assert!(view
6698 .for_each_record(&path, None, None, |_, _, _| ControlFlow::Continue(()))
6699 .is_err());
6700 }
6701
6702 fn collect_records(
6703 view: &PackedRefsFileView,
6704 path: &Path,
6705 keep: Option<PackedRecordFilter<'_>>,
6706 ) -> Result<Vec<(String, ObjectId, PackedPeel)>> {
6707 let mut out = Vec::new();
6708 view.for_each_record(path, None, keep, |name, oid, peel| {
6709 out.push((name.to_owned(), oid, peel));
6710 ControlFlow::Continue(())
6711 })?;
6712 Ok(out)
6713 }
6714
6715 #[test]
6716 fn filtered_record_walk_emits_the_unfiltered_walk_minus_rejected_records() {
6717 let dir = tempdir().unwrap();
6718 let path = store::paths::packed_refs_path(dir.path());
6719 let one = "1111111111111111111111111111111111111111";
6720 let two = "2222222222222222222222222222222222222222";
6721 let tag = "3333333333333333333333333333333333333333";
6722 let body = format!(
6723 "# pack-refs with: peeled fully-peeled sorted\n\
6724 {one} refs/heads/a\n{two} refs/heads/b\n{tag} refs/tags/v1\n^{one}\n{two} refs/tags/v2\n"
6725 );
6726 let view =
6727 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6728 let target: ObjectId = one.parse().unwrap();
6729 let keep = |_: &str, oid: &ObjectId, peel: PackedPeel| match peel {
6730 PackedPeel::Peeled(peeled) => peeled == target,
6731 PackedPeel::NotTag | PackedPeel::Unknown => *oid == target,
6732 };
6733
6734 let all = collect_records(&view, &path, None).unwrap();
6735 let expected: Vec<_> = all
6736 .into_iter()
6737 .filter(|(name, oid, peel)| keep(name, oid, *peel))
6738 .collect();
6739 let kept = collect_records(&view, &path, Some(&keep)).unwrap();
6740 assert_eq!(kept, expected);
6741 let names: Vec<&str> = kept.iter().map(|(name, _, _)| name.as_str()).collect();
6742 assert_eq!(names, ["refs/heads/a", "refs/tags/v1"]);
6743 assert_eq!(kept[1].2, PackedPeel::Peeled(target));
6744 }
6745
6746 #[test]
6747 fn filtered_record_walk_emits_nothing_before_a_corrupt_line() {
6748 let dir = tempdir().unwrap();
6749 let path = store::paths::packed_refs_path(dir.path());
6750 let oid = "1111111111111111111111111111111111111111";
6751 let body = format!(
6752 "# pack-refs with: peeled fully-peeled sorted\n\
6753 {oid} refs/heads/good\n{oid} refs/heads/good2\n{oid} refs/heads/bad..name\n"
6754 );
6755 let view =
6756 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6757 let keep_all = |_: &str, _: &ObjectId, _: PackedPeel| true;
6758 let mut emitted = 0;
6759 let result = view.for_each_record(&path, None, Some(&keep_all), |_, _, _| {
6760 emitted += 1;
6761 ControlFlow::Continue(())
6762 });
6763 assert!(matches!(
6764 result,
6765 Err(Error::PackedRefsUnexpectedLine { .. })
6766 ));
6767 assert_eq!(emitted, 0);
6768 }
6769
6770 #[test]
6771 fn filtered_record_walk_stops_when_the_visitor_breaks() {
6772 let dir = tempdir().unwrap();
6773 let path = store::paths::packed_refs_path(dir.path());
6774 let oid = "1111111111111111111111111111111111111111";
6775 let body = format!(
6776 "# pack-refs with: peeled fully-peeled sorted\n\
6777 {oid} refs/heads/a\n{oid} refs/heads/b\n{oid} refs/heads/c\n"
6778 );
6779 let view =
6780 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6781 let keep_all = |_: &str, _: &ObjectId, _: PackedPeel| true;
6782 let mut seen = Vec::new();
6783 let flow = view
6784 .for_each_record(&path, None, Some(&keep_all), |name, _, _| {
6785 seen.push(name.to_owned());
6786 if seen.len() == 2 {
6787 ControlFlow::Break(())
6788 } else {
6789 ControlFlow::Continue(())
6790 }
6791 })
6792 .unwrap();
6793 assert!(flow.is_break());
6794 assert_eq!(seen, ["refs/heads/a", "refs/heads/b"]);
6795 }
6796
6797 #[test]
6798 fn filtered_record_walk_of_a_lying_sorted_file_returns_every_kept_ref() {
6799 let dir = tempdir().unwrap();
6800 let path = store::paths::packed_refs_path(dir.path());
6801 let (lying, _) = lying_and_unsorted_views(&path);
6802 let keep_all = |_: &str, _: &ObjectId, _: PackedPeel| true;
6803 let names: Vec<String> = collect_records(&lying, &path, Some(&keep_all))
6804 .unwrap()
6805 .into_iter()
6806 .map(|(name, _, _)| name)
6807 .collect();
6808 assert_eq!(names, ["refs/heads/a", "refs/heads/b", "refs/tags/t"]);
6809 }
6810
6811 #[test]
6812 fn prefix_scan_over_an_out_of_order_range_falls_back_to_the_whole_file() {
6813 let dir = tempdir().unwrap();
6814 let path = store::paths::packed_refs_path(dir.path());
6815 let (lying, _) = lying_and_unsorted_views(&path);
6816 let heads = lying.map_for_prefix(&path, Some("refs/heads")).unwrap();
6817 assert_eq!(sorted_names(heads), ["refs/heads/a", "refs/heads/b"]);
6818
6819 let oid = "1111111111111111111111111111111111111111";
6821 let view_of = |names: &[&str]| {
6822 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
6823 for name in names {
6824 body.push_str(&format!("{oid} {name}\n"));
6825 }
6826 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6827 };
6828 let interleaved = view_of(&["refs/heads/a", "refs/tags/t", "refs/heads/b", "refs/tags/u"]);
6829 let (begin, end) = interleaved.prefix_region("refs/heads").unwrap();
6830 let (_, in_order) = interleaved
6831 .parse_region::<HashMap<_, _>>(&path, begin, end, Some("refs/heads"), None)
6832 .unwrap();
6833 assert!(!in_order);
6834 assert_eq!(
6835 sorted_names(
6836 interleaved
6837 .map_for_prefix(&path, Some("refs/heads"))
6838 .unwrap()
6839 ),
6840 ["refs/heads/a", "refs/heads/b"]
6841 );
6842
6843 let bad_neighbour = view_of(&["refs/heads/x", "refs/heads/a"]);
6846 assert_eq!(bad_neighbour.prefix_region("refs/heads/a"), None);
6847 assert_eq!(
6848 sorted_names(
6849 bad_neighbour
6850 .map_for_prefix(&path, Some("refs/heads/a"))
6851 .unwrap()
6852 ),
6853 ["refs/heads/a"]
6854 );
6855
6856 let corrupt = PackedRefsFileView::from_bytes(
6858 &path,
6859 format!(
6860 "# pack-refs with: peeled fully-peeled sorted\n\
6861 {oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/bad..name\n"
6862 )
6863 .into_bytes(),
6864 HashAlgo::Sha1,
6865 )
6866 .unwrap();
6867 assert!(matches!(
6868 corrupt.map_for_prefix(&path, Some("refs/heads")),
6869 Err(Error::PackedRefsUnexpectedLine { .. })
6870 ));
6871 }
6872
6873 #[test]
6874 fn full_map_keeps_every_name_and_the_last_duplicate_value() {
6875 let dir = tempdir().unwrap();
6876 let path = store::paths::packed_refs_path(dir.path());
6877 let first = "1111111111111111111111111111111111111111";
6878 let last = "2222222222222222222222222222222222222222";
6879 for header in ["# pack-refs with: sorted\n", "# pack-refs with: peeled\n"] {
6880 for names in [
6881 ["refs/heads/a", "refs/heads/b", "refs/heads/b"],
6882 ["refs/heads/b", "refs/heads/a", "refs/heads/b"],
6883 ] {
6884 let body = format!(
6885 "{header}{first} {}\n{first} {}\n{last} {}\n",
6886 names[0], names[1], names[2]
6887 );
6888 let view = PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1)
6889 .unwrap();
6890 let full = view.full_map(&path).unwrap();
6891 assert_eq!(full.len(), 2);
6892 assert_eq!(full["refs/heads/b"], last.parse().unwrap());
6893 assert_eq!(
6894 full.into_iter().collect::<HashMap<_, _>>(),
6895 view.map_for_prefix(&path, None).unwrap()
6896 );
6897 }
6898 }
6899 }
6900
6901 #[test]
6902 fn prefix_scan_validates_both_neighbouring_records_and_their_peels() {
6903 let dir = tempdir().unwrap();
6904 let path = store::paths::packed_refs_path(dir.path());
6905 let oid = "1111111111111111111111111111111111111111";
6906 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6907 for neighbour in ["refs/heads/a", "refs/tags/z"] {
6908 for corrupt in [
6909 format!("{oid} {neighbour}..bad\n"),
6910 format!("{} {neighbour}\n", "g".repeat(40)),
6911 format!("{oid} {neighbour}\n^{}\n", "g".repeat(40)),
6912 ] {
6913 let records = if neighbour.starts_with("refs/heads/") {
6914 format!("{corrupt}{oid} refs/heads/m\n{oid} refs/tags/z\n")
6915 } else {
6916 format!("{oid} refs/heads/a\n{oid} refs/heads/m\n{corrupt}")
6917 };
6918 let view = PackedRefsFileView::from_bytes(
6919 &path,
6920 format!("{header}{records}").into_bytes(),
6921 HashAlgo::Sha1,
6922 )
6923 .unwrap();
6924 assert!(
6925 matches!(
6926 view.map_for_prefix(&path, Some("refs/heads/m")),
6927 Err(Error::PackedRefsUnexpectedLine { .. })
6928 ),
6929 "{records}"
6930 );
6931 }
6932 }
6933 }
6934
6935 #[test]
6938 fn point_lookup_in_a_lying_sorted_file_behaves_like_git() {
6939 let dir = tempdir().unwrap();
6940 let path = store::paths::packed_refs_path(dir.path());
6941 let (lying, unsorted) = lying_and_unsorted_views(&path);
6942 assert!(lying.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
6944 assert_eq!(lying.lookup_oid(&path, "refs/heads/b").unwrap(), None);
6945 assert!(lying.lookup_oid(&path, "refs/tags/t").unwrap().is_some());
6946
6947 assert!(!unsorted.sorted);
6948 for name in ["refs/heads/a", "refs/heads/b", "refs/tags/t"] {
6949 assert!(
6950 unsorted.lookup_oid(&path, name).unwrap().is_some(),
6951 "{name}"
6952 );
6953 }
6954 }
6955
6956 #[test]
6957 fn records_are_only_record_shaped_lines() {
6958 let dir = tempdir().unwrap();
6959 let path = store::paths::packed_refs_path(dir.path());
6960 let view = |data: &[u8]| {
6961 PackedRefsFileView::from_bytes(&path, data.to_vec(), HashAlgo::Sha1).unwrap()
6962 };
6963 let hex = "1".repeat(40);
6964 let data = format!("# header\n{hex} x\n^peel\n\n{hex} y\n#c\nshort\n{hex}z\n{hex} z\n");
6965 let starts: Vec<usize> = view(data.as_bytes()).records().collect();
6966 assert_eq!(starts, [9, 59, 153]);
6967 assert_eq!(view(b"").records().count(), 0);
6968 let out_of_order = format!("{hex} b\n{hex} a\n");
6969 assert_eq!(
6970 view(out_of_order.as_bytes()).records().collect::<Vec<_>>(),
6971 [0, 43]
6972 );
6973 }
6974
6975 #[test]
6976 fn parse_rejects_orphan_peel_and_double_space() {
6977 let dir = tempdir().unwrap();
6978 let path = store::paths::packed_refs_path(dir.path());
6979 fs::write(
6980 &path,
6981 "# pack-refs with: peeled\n\
6982 ^0000000000000000000000000000000000000000\n\
6983 0000000000000000000000000000000000000000 refs/heads/main\n",
6984 )
6985 .unwrap();
6986 let data = fs::read(&path).unwrap();
6987 assert!(matches!(
6988 parse_packed_refs_to_map(&path, &data),
6989 Err(Error::PackedRefsUnexpectedLine { .. })
6990 ));
6991
6992 fs::write(
6993 &path,
6994 "# pack-refs with: peeled\n\
6995 0000000000000000000000000000000000000000 refs/heads/main\n",
6996 )
6997 .unwrap();
6998 let data = fs::read(&path).unwrap();
6999 assert!(matches!(
7000 parse_packed_refs_to_map(&path, &data),
7001 Err(Error::PackedRefsUnexpectedLine { .. })
7002 ));
7003 }
7004}
7005
7006#[cfg(test)]
7007mod pack_refs_tests {
7008 use super::*;
7009 use crate::objects::{serialize_commit, CommitData, ObjectKind};
7010 use crate::repo::{init_repository, Repository};
7011 use std::fs;
7012 use std::sync::Arc;
7013
7014 fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
7015 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
7016 let raw = serialize_commit(&CommitData {
7017 tree,
7018 parents,
7019 author: "T <t@example.com> 1 +0000".into(),
7020 committer: "T <t@example.com> 1 +0000".into(),
7021 author_raw: Vec::new(),
7022 committer_raw: Vec::new(),
7023 encoding: None,
7024 message: "m\n".into(),
7025 raw_message: None,
7026 extra_headers: Vec::new(),
7027 });
7028 repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
7029 }
7030
7031 #[test]
7032 fn pack_refs_skips_concurrent_loose_update() {
7033 let dir = tempfile::tempdir().expect("tempdir");
7034 let repo = init_repository(
7035 dir.path(),
7036 false,
7037 "main",
7038 None,
7039 crate::RefStorageFormat::Files,
7040 )
7041 .expect("init");
7042 let git_dir = repo.git_dir().to_path_buf();
7043 let c1 = write_commit(&repo, vec![]);
7044 let c2 = write_commit(&repo, vec![c1]);
7045 write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
7046
7047 let git_dir_hook = git_dir.clone();
7048 let c2 = Arc::new(c2);
7049 let c2_hook = Arc::clone(&c2);
7050 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
7051 write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
7052 })));
7053
7054 let (_path, packed, pruned) =
7055 pack_refs_files(&git_dir, repo.odb(), None, true, false).expect("pack");
7056 pack_refs_test_hooks::set_before_prune(None);
7057 assert!(packed >= 1);
7058 assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
7059
7060 let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
7061 assert_eq!(resolved, *c2);
7062 let packed_body =
7063 fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
7064 assert!(
7065 !packed_body.contains("refs/heads/race"),
7066 "packed entry for concurrently updated ref must be removed"
7067 );
7068 }
7069
7070 #[test]
7071 fn clone_packing_retains_symbolic_refs_and_concurrent_tag_updates() {
7072 let dir = tempfile::tempdir().expect("tempdir");
7073 let repo = init_repository(
7074 dir.path(),
7075 false,
7076 "main",
7077 None,
7078 crate::RefStorageFormat::Files,
7079 )
7080 .expect("init");
7081 let git_dir = repo.git_dir().to_path_buf();
7082 let first = write_commit(&repo, vec![]);
7083 let second = write_commit(&repo, vec![first]);
7084 write_ref(&git_dir, "refs/tags/race", &first).expect("tag");
7085 write_ref(&git_dir, "refs/heads/main", &first).expect("branch");
7086 write_ref(&git_dir, "refs/remotes/origin/main", &first).expect("remote");
7087 write_symbolic_ref(
7088 &git_dir,
7089 "refs/remotes/origin/HEAD",
7090 "refs/remotes/origin/main",
7091 )
7092 .expect("symbolic");
7093 let tag = repo
7094 .odb()
7095 .write(
7096 ObjectKind::Tag,
7097 format!("object {first}\ntype commit\n\nremote tag\n").as_bytes(),
7098 )
7099 .expect("tag object");
7100 write_ref(&git_dir, "refs/remotes/origin/tag", &tag).expect("tagged remote");
7101 let hook_dir = git_dir.clone();
7102 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
7103 write_ref(&hook_dir, "refs/tags/race", &second).expect("concurrent update");
7104 })));
7105 pack_remote_tracking_refs_for_clone(&git_dir, "origin").expect("pack");
7106 assert_eq!(
7107 resolve_ref(&git_dir, "refs/tags/race").expect("resolve"),
7108 second
7109 );
7110 assert!(git_dir.join("refs/heads/main").exists());
7111 assert!(git_dir.join("refs/remotes/origin/HEAD").exists());
7112 assert!(!git_dir.join("refs/remotes/origin/main").exists());
7113 let body = fs::read_to_string(store::paths::packed_refs_path(&git_dir)).expect("packed");
7114 assert!(!body.contains("refs/tags/race"));
7115 assert!(!body.contains("refs/remotes/origin/HEAD"));
7116 assert!(
7117 body.contains(&format!("{tag} refs/remotes/origin/tag\n^{first}\n")),
7118 "{body}"
7119 );
7120 }
7121
7122 #[test]
7123 fn clone_packing_routes_reftable_without_creating_packed_refs() {
7124 let dir = tempfile::tempdir().expect("tempdir");
7125 let repo = init_repository(
7126 dir.path(),
7127 false,
7128 "main",
7129 None,
7130 crate::RefStorageFormat::Reftable,
7131 )
7132 .expect("init");
7133 let commit = write_commit(&repo, vec![]);
7134 write_ref(repo.git_dir(), "refs/tags/tip", &commit).expect("tag");
7135 pack_remote_tracking_refs_for_clone(repo.git_dir(), "origin").expect("pack");
7136 assert_eq!(
7137 resolve_ref(repo.git_dir(), "refs/tags/tip").expect("resolve"),
7138 commit
7139 );
7140 assert!(!store::paths::packed_refs_path(repo.git_dir()).exists());
7141 }
7142
7143 #[test]
7144 fn pack_refs_nested_tag_peel_line_points_at_commit() {
7145 let dir = tempfile::tempdir().expect("tempdir");
7146 let repo = init_repository(
7147 dir.path(),
7148 false,
7149 "main",
7150 None,
7151 crate::RefStorageFormat::Files,
7152 )
7153 .expect("init");
7154 let commit = write_commit(&repo, vec![]);
7155 let inner_tag_body = format!(
7156 "object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
7157 commit.to_hex()
7158 );
7159 let inner = repo
7160 .odb()
7161 .write(ObjectKind::Tag, inner_tag_body.as_bytes())
7162 .expect("inner tag");
7163 let outer_tag_body = format!(
7164 "object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
7165 inner.to_hex()
7166 );
7167 let outer = repo
7168 .odb()
7169 .write(ObjectKind::Tag, outer_tag_body.as_bytes())
7170 .expect("outer tag");
7171 write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
7172
7173 pack_refs_files(repo.git_dir(), repo.odb(), None, false, false).expect("pack");
7174 let body =
7175 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
7176 let mut lines = body.lines();
7177 let tag_line = lines
7178 .find(|l| l.contains("refs/tags/outer"))
7179 .expect("tag line");
7180 assert!(tag_line.starts_with(&outer.to_hex()));
7181 let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
7182 assert!(
7183 peel_line.contains(&commit.to_hex()),
7184 "peel must reach commit through nested tags, got {peel_line}"
7185 );
7186 }
7187
7188 #[test]
7189 fn pack_refs_tree_target_tag_has_peel_line() {
7190 let dir = tempfile::tempdir().expect("tempdir");
7191 let repo = init_repository(
7192 dir.path(),
7193 false,
7194 "main",
7195 None,
7196 crate::RefStorageFormat::Files,
7197 )
7198 .expect("init");
7199 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
7200 let tag_body = format!(
7201 "object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
7202 tree.to_hex()
7203 );
7204 let tag = repo
7205 .odb()
7206 .write(ObjectKind::Tag, tag_body.as_bytes())
7207 .expect("tag");
7208 write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
7209
7210 pack_refs_files(repo.git_dir(), repo.odb(), None, true, false).expect("pack");
7211 let body =
7212 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
7213 assert!(body.contains("refs/tags/tree-tag"));
7214 assert!(
7215 body.contains(&format!("^{}", tree.to_hex())),
7216 "expected peel to tree: {body}"
7217 );
7218 }
7219
7220 fn packed_entry<'a>(name: &'a str, hex: &str, peel: Option<&str>) -> PackedEntry<'a> {
7221 PackedEntry {
7222 name,
7223 oid: ObjectId::from_hex(hex).expect("oid"),
7224 peel: peel.map(|p| ObjectId::from_hex(p).expect("peel")),
7225 }
7226 }
7227
7228 const OID_A: &str = "1111111111111111111111111111111111111111";
7229 const OID_B: &str = "2222222222222222222222222222222222222222";
7230 const OID_C: &str = "3333333333333333333333333333333333333333";
7231
7232 #[test]
7233 fn merged_packed_refs_body_interleaves_loose_and_replaces_by_name() {
7234 let existing = vec![
7235 packed_entry("refs/tags/b", OID_A, Some(OID_B)),
7236 packed_entry("refs/tags/d", OID_A, None),
7237 ];
7238 let loose = vec![
7239 (
7240 "refs/tags/a".to_owned(),
7241 ObjectId::from_hex(OID_C).unwrap(),
7242 None,
7243 ),
7244 (
7245 "refs/tags/b".to_owned(),
7246 ObjectId::from_hex(OID_C).unwrap(),
7247 None,
7248 ),
7249 (
7250 "refs/tags/c".to_owned(),
7251 ObjectId::from_hex(OID_B).unwrap(),
7252 Some(ObjectId::from_hex(OID_A).unwrap()),
7253 ),
7254 (
7255 "refs/tags/e".to_owned(),
7256 ObjectId::from_hex(OID_A).unwrap(),
7257 None,
7258 ),
7259 ];
7260 let body = merged_packed_refs_body(existing, &loose);
7261 assert_eq!(
7262 body,
7263 format!(
7264 "{PACKED_REFS_HEADER}\
7265 {OID_C} refs/tags/a\n\
7266 {OID_C} refs/tags/b\n\
7267 {OID_B} refs/tags/c\n^{OID_A}\n\
7268 {OID_A} refs/tags/d\n\
7269 {OID_A} refs/tags/e\n"
7270 ),
7271 "a loose ref replaces the packed record and its old peel line"
7272 );
7273 }
7274
7275 #[test]
7276 fn merged_packed_refs_body_sorts_unsorted_input_and_keeps_last_duplicate() {
7277 let existing = vec![
7278 packed_entry("refs/tags/z", OID_A, None),
7279 packed_entry("refs/tags/m", OID_A, Some(OID_C)),
7280 packed_entry("refs/tags/m", OID_B, None),
7281 ];
7282 let loose = vec![(
7283 "refs/tags/n".to_owned(),
7284 ObjectId::from_hex(OID_C).unwrap(),
7285 None,
7286 )];
7287 let body = merged_packed_refs_body(existing, &loose);
7288 assert_eq!(
7289 body,
7290 format!(
7291 "{PACKED_REFS_HEADER}\
7292 {OID_B} refs/tags/m\n\
7293 {OID_C} refs/tags/n\n\
7294 {OID_A} refs/tags/z\n"
7295 )
7296 );
7297 }
7298
7299 #[test]
7300 fn packed_entries_with_peel_attaches_peels_and_rejects_orphans() {
7301 let path = Path::new("packed-refs");
7302 let upper = OID_A.to_ascii_uppercase();
7303 let data = format!(
7304 "# pack-refs with: peeled fully-peeled sorted \n\
7305 {upper} refs/tags/a\n^{OID_B}\n{OID_C} refs/tags/b\n"
7306 );
7307 let entries = packed_entries_with_peel(path, data.as_bytes()).expect("parse");
7308 let got: Vec<_> = entries.iter().map(|e| (e.name, e.oid, e.peel)).collect();
7309 assert_eq!(
7310 got,
7311 vec![
7312 (
7313 "refs/tags/a",
7314 ObjectId::from_hex(OID_A).unwrap(),
7315 Some(ObjectId::from_hex(OID_B).unwrap())
7316 ),
7317 ("refs/tags/b", ObjectId::from_hex(OID_C).unwrap(), None),
7318 ]
7319 );
7320 let body = merged_packed_refs_body(entries, &[]);
7321 assert!(
7322 body.contains(&format!("{OID_A} refs/tags/a\n")),
7323 "ids are rewritten in lowercase: {body}"
7324 );
7325
7326 let orphan = format!("^{OID_B}\n{OID_C} refs/tags/b\n");
7327 assert!(matches!(
7328 packed_entries_with_peel(path, orphan.as_bytes()),
7329 Err(Error::PackedRefsUnexpectedLine { .. })
7330 ));
7331 }
7332
7333 fn files_repo() -> (tempfile::TempDir, crate::repo::Repository) {
7334 let dir = tempfile::tempdir().expect("tempdir");
7335 let repo = init_repository(
7336 dir.path(),
7337 false,
7338 "main",
7339 None,
7340 crate::RefStorageFormat::Files,
7341 )
7342 .expect("init");
7343 (dir, repo)
7344 }
7345
7346 #[test]
7347 fn pack_refs_merges_loose_tags_into_existing_packed_refs() {
7348 let (_dir, repo) = files_repo();
7349 let git_dir = repo.git_dir().to_path_buf();
7350 let c1 = write_commit(&repo, vec![]);
7351 let c2 = write_commit(&repo, vec![c1]);
7352 let tag = repo
7353 .odb()
7354 .write(
7355 ObjectKind::Tag,
7356 format!("object {c1}\ntype commit\ntag v2\n\nv2\n").as_bytes(),
7357 )
7358 .expect("tag object");
7359 fs::write(
7360 store::paths::packed_refs_path(&git_dir),
7361 format!(
7362 "{PACKED_REFS_HEADER}{c1} refs/heads/old\n{c1} refs/tags/v1\n{c1} refs/tags/v3\n"
7363 ),
7364 )
7365 .expect("seed packed-refs");
7366 write_ref(&git_dir, "refs/tags/v1", &c2).expect("replace v1");
7367 write_ref(&git_dir, "refs/tags/v2", &tag).expect("v2");
7368 write_ref(&git_dir, "refs/heads/main", &c2).expect("branch");
7369
7370 let (path, packed, pruned) =
7371 pack_refs_files(&git_dir, repo.odb(), None, false, false).expect("pack");
7372 assert_eq!((packed, pruned), (2, 2));
7373 assert_eq!(
7374 fs::read_to_string(path).expect("packed-refs"),
7375 format!(
7376 "{PACKED_REFS_HEADER}{c1} refs/heads/old\n{c2} refs/tags/v1\n\
7377 {tag} refs/tags/v2\n^{c1}\n{c1} refs/tags/v3\n"
7378 )
7379 );
7380 assert!(!git_dir.join("refs/tags/v1").exists());
7381 assert!(!git_dir.join("refs/tags/v2").exists());
7382 assert!(git_dir.join("refs/heads/main").exists(), "tags-only pack");
7383 assert_eq!(resolve_ref(&git_dir, "refs/tags/v1").expect("v1"), c2);
7384 }
7385
7386 #[test]
7387 fn pack_refs_dry_run_and_noop_leave_packed_refs_untouched() {
7388 let (_dir, repo) = files_repo();
7389 let git_dir = repo.git_dir().to_path_buf();
7390 let c1 = write_commit(&repo, vec![]);
7391 let packed_path = store::paths::packed_refs_path(&git_dir);
7392 let seed = format!("# hand written\n{c1} refs/tags/v1\n");
7393 fs::write(&packed_path, &seed).expect("seed");
7394
7395 assert_eq!(
7396 pack_refs_files(&git_dir, repo.odb(), None, false, false)
7397 .expect("noop")
7398 .1,
7399 0
7400 );
7401 write_ref(&git_dir, "refs/tags/v2", &c1).expect("v2");
7402 let (_, packed, pruned) =
7403 pack_refs_files(&git_dir, repo.odb(), None, false, true).expect("dry run");
7404 assert_eq!((packed, pruned), (1, 1));
7405 assert_eq!(fs::read_to_string(&packed_path).expect("read"), seed);
7406 assert!(git_dir.join("refs/tags/v2").exists());
7407
7408 fs::write(&packed_path, format!("^{c1}\n")).expect("corrupt");
7409 assert!(matches!(
7410 pack_refs_files(&git_dir, repo.odb(), None, false, true),
7411 Err(Error::PackedRefsUnexpectedLine { .. })
7412 ));
7413 }
7414
7415 #[test]
7416 fn pack_refs_reports_held_packed_refs_lock_and_keeps_loose_refs() {
7417 let (_dir, repo) = files_repo();
7418 let git_dir = repo.git_dir().to_path_buf();
7419 let c1 = write_commit(&repo, vec![]);
7420 write_ref(&git_dir, "refs/tags/v1", &c1).expect("v1");
7421 fs::write(
7422 git_dir.join("config"),
7423 "[core]\n\trepositoryformatversion = 0\n\tpackedRefsTimeout = 0\n",
7424 )
7425 .expect("config");
7426 let lock = git_dir.join("packed-refs.lock");
7427 fs::write(&lock, b"").expect("foreign lock");
7428 let config = crate::config::ConfigSet::load(
7429 &crate::environment::Environment::empty(),
7430 Some(&git_dir),
7431 true,
7432 )
7433 .expect("config");
7434
7435 let err = pack_refs_files(&git_dir, repo.odb(), Some(&config), false, false)
7436 .expect_err("lock held");
7437 assert!(
7438 matches!(
7439 &err,
7440 Error::RefLock(RefLockError::PackedRefsLockHeld { lock_path })
7441 if lock_path.ends_with("packed-refs.lock")
7442 ),
7443 "{err:?}"
7444 );
7445 assert!(lock.exists(), "a foreign lock is never removed");
7446 assert!(git_dir.join("refs/tags/v1").exists());
7447 assert!(!store::paths::packed_refs_path(&git_dir).exists());
7448 }
7449
7450 #[test]
7451 fn write_packed_refs_file_survives_publish_failure() {
7452 let dir = tempfile::tempdir().expect("tempdir");
7453 let git_dir = dir.path();
7454 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
7455 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7456 fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
7457 fs::create_dir_all(store::paths::packed_refs_path(git_dir))
7458 .expect("block packed-refs file");
7459 let err = write_packed_refs_file(
7460 git_dir,
7461 &[PackedRefRecord {
7462 refname: "refs/heads/keep".into(),
7463 oid,
7464 peeled: None,
7465 }],
7466 )
7467 .expect_err("publish should fail");
7468 assert!(matches!(err, Error::Io(_)));
7469 assert!(git_dir.join("refs/heads/keep").is_file());
7470 }
7471
7472 #[test]
7473 fn write_packed_refs_file_replaces_loose_and_sorts() {
7474 let dir = tempfile::tempdir().expect("tempdir");
7475 let git_dir = dir.path();
7476 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
7477 let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
7478 let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
7479 fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
7480 write_packed_refs_file(
7481 git_dir,
7482 &[
7483 PackedRefRecord {
7484 refname: "refs/heads/z-last".into(),
7485 oid: oid_b,
7486 peeled: None,
7487 },
7488 PackedRefRecord {
7489 refname: "refs/heads/a-first".into(),
7490 oid: oid_a,
7491 peeled: None,
7492 },
7493 ],
7494 )
7495 .expect("write packed-refs");
7496 assert!(!git_dir.join("refs/heads/a-first").exists());
7497 let body =
7498 fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
7499 assert!(body.contains("refs/heads/a-first"));
7500 assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
7501 }
7502
7503 #[test]
7504 fn write_packed_refs_file_keeps_refs_dir_for_top_level_ref() {
7505 let dir = tempfile::tempdir().expect("tempdir");
7506 let git_dir = dir.path();
7507 fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
7508 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7509 fs::write(git_dir.join("refs/stash"), format!("{oid}\n")).expect("loose stash");
7510 write_packed_refs_file(
7511 git_dir,
7512 &[PackedRefRecord {
7513 refname: "refs/stash".into(),
7514 oid,
7515 peeled: None,
7516 }],
7517 )
7518 .expect("write packed-refs");
7519 assert!(!git_dir.join("refs/stash").exists());
7520 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
7521 assert!(git_dir.join("refs/tags").is_dir());
7522 }
7523
7524 #[test]
7525 fn write_packed_refs_file_prunes_emptied_ref_dirs_below_refs() {
7526 let dir = tempfile::tempdir().expect("tempdir");
7527 let git_dir = dir.path();
7528 fs::create_dir_all(git_dir.join("refs/heads/topic/empty")).expect("refs/heads");
7529 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7530 fs::write(git_dir.join("refs/heads/topic/a"), format!("{oid}\n")).expect("loose ref");
7531 write_packed_refs_file(
7532 git_dir,
7533 &[PackedRefRecord {
7534 refname: "refs/heads/topic/a".into(),
7535 oid,
7536 peeled: None,
7537 }],
7538 )
7539 .expect("write packed-refs");
7540 assert!(!git_dir.join("refs/heads/topic/a").exists());
7541 assert!(!git_dir.join("refs/heads/topic").exists());
7542 assert!(git_dir.join("refs/heads").is_dir(), "refs/heads/ kept");
7543 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
7544 }
7545
7546 #[test]
7547 fn pack_refs_files_keeps_refs_dir_after_packing_stash() {
7548 let dir = tempfile::tempdir().expect("tempdir");
7549 let repo = init_repository(
7550 dir.path(),
7551 false,
7552 "main",
7553 None,
7554 crate::RefStorageFormat::Files,
7555 )
7556 .expect("init");
7557 let blob = repo.odb().write(ObjectKind::Blob, b"x").expect("blob");
7558 write_ref(repo.git_dir(), "refs/stash", &blob).expect("stash");
7559 let refs = repo.git_dir().join("refs");
7560 pack_refs_files(repo.git_dir(), repo.odb(), None, true, false).expect("pack");
7561 assert!(!refs.join("stash").exists());
7562 assert!(refs.is_dir(), "refs/ must remain");
7563 }
7564}
7565
7566#[cfg(test)]
7567mod packed_ref_peels_tests {
7568 use super::*;
7569
7570 fn oid(n: u8) -> ObjectId {
7571 ObjectId::from_bytes(&[n; 20]).expect("oid")
7572 }
7573
7574 fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
7575 let dir = tempfile::tempdir().expect("tempdir");
7576 fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
7577 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7578 (dir, peels)
7579 }
7580
7581 fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
7582 let mut out = String::from(header);
7583 for (name, oid, peel) in records {
7584 out.push_str(&format!("{oid} {name}\n"));
7585 if let Some(peel) = peel {
7586 out.push_str(&format!("^{peel}\n"));
7587 }
7588 }
7589 out
7590 }
7591
7592 #[test]
7593 fn fully_peeled_file_answers_for_every_ref() {
7594 for header in [
7595 "# pack-refs with: peeled fully-peeled sorted \n",
7596 "# pack-refs with: peeled fully-peeled \n",
7597 ] {
7598 let records = [
7599 ("refs/heads/main", oid(1), None),
7600 ("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
7601 ("refs/tags/v1", oid(4), Some(oid(1))),
7602 ("refs/tags/light", oid(1), None),
7603 ];
7604 let mut ordered = records;
7606 if header.contains(" sorted") {
7607 ordered.sort_by_key(|(name, _, _)| *name);
7608 } else {
7609 ordered.reverse();
7610 }
7611 let (_dir, peels) = peels_for(&body(header, &ordered));
7612 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
7613 assert_eq!(
7614 peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
7615 Some(Some(oid(3)))
7616 );
7617 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
7618 assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
7619 assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
7621 assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
7622 }
7623 }
7624
7625 #[test]
7626 fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
7627 let records = [
7628 ("refs/heads/main", oid(1), None),
7629 ("refs/tags/v1", oid(4), Some(oid(1))),
7630 ];
7631 let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
7632 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7633 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
7634
7635 for header in [
7636 "",
7637 "# pack-refs with: sorted\n",
7638 "# some comment\n",
7639 "#pack-refs with: peeled fully-peeled\n",
7640 "# pack-refs with: peeled fully-peeled\n",
7641 ] {
7642 let (_dir, peels) = peels_for(&body(header, &records));
7643 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
7644 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
7645 }
7646 }
7647
7648 #[test]
7649 fn missing_file_and_other_backends_know_nothing() {
7650 let dir = tempfile::tempdir().expect("tempdir");
7651 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7652 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7653
7654 let records = [("refs/heads/main", oid(1), None)];
7655 fs::write(
7656 store::paths::packed_refs_path(dir.path()),
7657 body("# pack-refs with: peeled fully-peeled sorted\n", &records),
7658 )
7659 .expect("packed-refs");
7660 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
7661 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7662 }
7663
7664 #[test]
7665 fn lookups_in_any_order_agree_with_a_fresh_snapshot() {
7666 let records = [
7667 ("refs/heads/main", oid(1), None),
7668 ("refs/tags/a", oid(2), Some(oid(1))),
7669 ("refs/tags/b", oid(1), None),
7670 ("refs/tags/c", oid(3), Some(oid(4))),
7671 ("refs/tags/d", oid(5), None),
7672 ];
7673 let text = body("# pack-refs with: peeled fully-peeled sorted \n", &records);
7674 let (_dir, peels) = peels_for(&text);
7675 let fresh = |name: &str, value: &ObjectId| peels_for(&text).1.peeled(name, value);
7676 let queries = [
7677 ("refs/tags/a", oid(2)),
7678 ("refs/tags/b", oid(1)),
7679 ("refs/tags/b", oid(1)),
7680 ("refs/tags/bb", oid(1)),
7681 ("refs/tags/c", oid(3)),
7682 ("refs/tags/d", oid(9)),
7683 ("refs/tags/d", oid(5)),
7684 ("refs/heads/main", oid(1)),
7685 ("refs/tags/c", oid(3)),
7686 ("refs/tags/a", oid(2)),
7687 ("refs/tags/zzz", oid(1)),
7688 ("refs/tags/a", oid(2)),
7689 ("refs/tags/b", oid(1)),
7690 ];
7691 for (name, value) in queries {
7692 assert_eq!(
7693 peels.peeled(name, &value),
7694 fresh(name, &value),
7695 "{name} {value}"
7696 );
7697 }
7698 assert_eq!(peels.peeled("refs/tags/c", &oid(3)), Some(Some(oid(4))));
7699 assert_eq!(peels.peeled("refs/tags/d", &oid(5)), Some(None));
7700 }
7701
7702 #[test]
7703 fn store_snapshot_reuses_the_files_store_view_and_follows_rewrites() {
7704 let dir = tempfile::tempdir().expect("tempdir");
7705 let path = store::paths::packed_refs_path(dir.path());
7706 let header = "# pack-refs with: peeled fully-peeled sorted \n";
7707 let records = [
7708 ("refs/heads/main", oid(1), None),
7709 ("refs/tags/v1", oid(2), Some(oid(1))),
7710 ("refs/tags/v2", oid(3), None),
7711 ];
7712 fs::write(&path, body(header, &records)).expect("packed-refs");
7713 let files = store::FilesRefStore::from_git_dir(dir.path()).expect("files store");
7714
7715 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7716 let loaded = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7717 for (name, value, _) in records {
7718 assert_eq!(
7719 peels.peeled(name, &value),
7720 loaded.peeled(name, &value),
7721 "{name}"
7722 );
7723 }
7724 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), Some(Some(oid(1))));
7725 let cached = files
7726 .packed_refs_view()
7727 .expect("view")
7728 .expect("packed-refs");
7729 let shared = peels.view.as_ref().expect("snapshot view");
7730 assert!(std::sync::Arc::ptr_eq(&cached.data, &shared.data));
7731
7732 let rewritten = [
7733 ("refs/heads/main", oid(1), None),
7734 ("refs/tags/v1", oid(4), Some(oid(5))),
7735 ("refs/tags/v2", oid(3), None),
7736 ("refs/tags/v3", oid(6), Some(oid(1))),
7737 ];
7738 fs::write(&path, body(header, &rewritten)).expect("rewrite packed-refs");
7739 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7740 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), None);
7741 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(5))));
7742 assert_eq!(peels.peeled("refs/tags/v3", &oid(6)), Some(Some(oid(1))));
7743
7744 let memory = store::MemoryRefStore::new();
7745 let fallback = PackedRefPeels::for_store(&memory, dir.path()).expect("fallback");
7746 let direct = PackedRefPeels::load(dir.path(), memory.format()).expect("load");
7747 for (name, value, _) in rewritten {
7748 assert_eq!(
7749 fallback.peeled(name, &value),
7750 direct.peeled(name, &value),
7751 "{name}"
7752 );
7753 }
7754 }
7755
7756 fn raw_view(data: &[u8], hex_len: usize) -> PackedRefsFileView {
7758 PackedRefsFileView {
7759 data: crate::pack_map::PackData::from_owned(data.to_vec()),
7760 sorted: true,
7761 hex_len,
7762 body_start: 0,
7763 peel_trait: PackedPeelTrait::None,
7764 }
7765 }
7766
7767 fn naive_record_starts(data: &[u8], hex_len: usize) -> Vec<usize> {
7769 let mut out = Vec::new();
7770 let mut start = 0;
7771 for line in data.split(|&b| b == b'\n') {
7772 if start >= data.len() {
7773 break;
7774 }
7775 if packed_line_is_record(line, hex_len) {
7776 out.push(start);
7777 }
7778 start += line.len() + 1;
7779 }
7780 out
7781 }
7782
7783 fn assert_record_walks_match(data: &[u8], hex_len: usize) {
7784 let view = raw_view(data, hex_len);
7785 let want = naive_record_starts(data, hex_len);
7786 assert_eq!(
7787 view.records().collect::<Vec<_>>(),
7788 want,
7789 "{:?} hex_len={hex_len}",
7790 String::from_utf8_lossy(data)
7791 );
7792 let mut line_starts: Vec<usize> = memchr::memchr_iter(b'\n', data)
7793 .map(|nl| nl + 1)
7794 .filter(|&s| s < data.len())
7795 .collect();
7796 line_starts.insert(0, 0);
7797 if data.ends_with(b"\n") {
7799 line_starts.push(data.len());
7800 }
7801 for before in line_starts {
7802 let prev = want.iter().copied().filter(|&s| s < before).last();
7803 assert_eq!(view.prev_record(before), prev, "{data:?} before={before}");
7804 }
7805 }
7806
7807 fn xorshift(state: &mut u64) -> u64 {
7808 *state ^= *state << 13;
7809 *state ^= *state >> 7;
7810 *state ^= *state << 17;
7811 *state
7812 }
7813
7814 #[test]
7815 fn record_walks_match_a_line_by_line_scan() {
7816 let hex = oid(1).to_hex();
7817 let inputs = [
7818 String::new(),
7819 "\n".to_owned(),
7820 format!("# pack-refs with: peeled \n{hex} refs/heads/a\n^{hex}\n{hex} refs/tags/b\n"),
7821 format!("{hex} refs/heads/z\n{hex} refs/heads/a\n"),
7822 format!("{hex} refs/heads/a\n\n{hex} refs/heads/b"),
7823 format!("{hex}\n{hex} \n{hex} x\nshort\n#{hex} refs/heads/c\n"),
7824 format!("{hex} refs/tags/a\n^{hex}\n^{hex}\n{hex} refs/tags/b\n^{hex}"),
7825 format!("^ab\n{}\n{hex} refs/heads/a\n", "x".repeat(37)),
7826 format!("^\n{hex} refs/heads/a\n^{hex}x\n{hex} refs/heads/b\n"),
7827 format!("^{hex}\n"),
7828 format!("^{}", &hex[1..]),
7829 ];
7830 for input in &inputs {
7831 assert_record_walks_match(input.as_bytes(), 40);
7832 }
7833 }
7834
7835 #[test]
7836 fn record_walks_match_a_line_by_line_scan_on_random_bytes() {
7837 let alphabet = b"^\n #ab";
7838 let mut state = 0x2545_f491_4f6c_dd1du64;
7839 for len in 0..400 {
7840 let input: Vec<u8> = (0..len % 97)
7841 .map(|_| alphabet[(xorshift(&mut state) % alphabet.len() as u64) as usize])
7842 .collect();
7843 for hex_len in [1, 2, 3] {
7844 assert_record_walks_match(&input, hex_len);
7845 }
7846 }
7847 }
7848
7849 #[test]
7852 fn search_matches_a_linear_scan_on_sorted_records() {
7853 let hex_len = 2;
7854 let mut state = 0x9e37_79b9_7f4a_7c15u64;
7855 for _ in 0..300 {
7856 let mut names: Vec<String> = (0..xorshift(&mut state) % 12)
7857 .map(|_| format!("n{}", xorshift(&mut state) % 40))
7858 .collect();
7859 names.sort();
7860 names.dedup();
7861 let mut data = b"# h\n".to_vec();
7862 for name in &names {
7863 data.extend_from_slice(format!("ab {name}\n").as_bytes());
7864 match xorshift(&mut state) % 4 {
7865 0 => data.extend_from_slice(b"^ab\n"),
7866 1 => data.extend_from_slice(b"junk\n"),
7867 _ => {}
7868 }
7869 }
7870 let mut view = raw_view(&data, hex_len);
7871 view.body_start = 4;
7872 let records: Vec<usize> = view.records().collect();
7873 for probe in 0..42 {
7874 let target = format!("n{probe}");
7875 let want = records
7876 .iter()
7877 .copied()
7878 .find(|&s| view.record_cmp(s, &target).is_ge())
7879 .unwrap_or(data.len());
7880 assert_eq!(view.lower_bound(&target), want, "{target} in {data:?}");
7881 let hit = view.search(&target).ok();
7882 assert_eq!(
7883 hit,
7884 names.contains(&target).then_some(want),
7885 "{target} in {data:?}"
7886 );
7887 }
7888 }
7889 }
7890}