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 let mut prove = |_: &str, _: ObjectId, _: PackedPeel| ControlFlow::Continue(());
1641 match self.walk_records(path, prefix, keep, false, &mut prove) {
1642 Err(StreamRecordsError::OutOfOrder) => {
1643 return self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f);
1644 }
1645 Err(StreamRecordsError::Packed(e)) => return Err(e),
1646 Ok(_) => {}
1647 }
1648 match self.walk_records(path, prefix, keep, true, &mut f) {
1649 Ok(flow) => Ok(flow),
1650 Err(StreamRecordsError::OutOfOrder) => {
1651 self.for_each_record_fallback(path, prefix, keep, &BTreeSet::new(), &mut f)
1652 }
1653 Err(StreamRecordsError::Packed(e)) => Err(e),
1654 }
1655 }
1656
1657 fn for_each_record_fallback(
1658 &self,
1659 path: &Path,
1660 prefix: Option<&str>,
1661 keep: Option<PackedRecordFilter<'_>>,
1662 already_emitted: &BTreeSet<String>,
1663 f: &mut impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1664 ) -> Result<ControlFlow<()>> {
1665 let map = self.map_for_prefix_filtered(path, prefix, keep)?;
1666 let mut names: Vec<String> = map.keys().cloned().collect();
1667 names.sort();
1668 for name in names {
1669 if already_emitted.contains(&name) {
1670 continue;
1671 }
1672 let oid = map[&name];
1673 if f(&name, oid, PackedPeel::Unknown).is_break() {
1674 return Ok(ControlFlow::Break(()));
1675 }
1676 }
1677 Ok(ControlFlow::Continue(()))
1678 }
1679
1680 fn walk_records(
1681 &self,
1682 path: &Path,
1683 prefix: Option<&str>,
1684 keep: Option<PackedRecordFilter<'_>>,
1685 emit: bool,
1686 f: &mut impl FnMut(&str, ObjectId, PackedPeel) -> ControlFlow<()>,
1687 ) -> std::result::Result<ControlFlow<()>, StreamRecordsError> {
1688 let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
1689 let prefix_upper = if self.sorted {
1690 prefix_trim.and_then(list_prefix_upper_bound)
1691 } else {
1692 None
1693 };
1694 let ref_opts = RefNameOptions {
1695 allow_onelevel: false,
1696 refspec_pattern: false,
1697 normalize: false,
1698 };
1699 let (mut pos, end) = self.record_scan_range(prefix_trim, prefix_upper.as_deref());
1700 let mut prev_name = "";
1701 let mut expect_peel = false;
1702 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
1703 let mut flush = |pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
1704 let Some((refname, oid, peeled)) = pending.take() else {
1705 return Ok(ControlFlow::Continue(()));
1706 };
1707 let peel = if self.peel_trait.covers(refname) {
1708 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1709 } else {
1710 PackedPeel::Unknown
1711 };
1712 if keep.is_some_and(|keep| !keep(refname, &oid, peel)) {
1713 return Ok(ControlFlow::Continue(()));
1714 }
1715 if !emit {
1716 return Ok(ControlFlow::Continue(()));
1717 }
1718 if f(refname, oid, peel).is_break() {
1719 Ok(ControlFlow::Break(()))
1720 } else {
1721 Ok(ControlFlow::Continue(()))
1722 }
1723 };
1724
1725 while pos < end {
1726 let line_end = packed_line_end(&self.data, pos).min(end);
1727 if self.data[pos..line_end].contains(&b'\r') {
1728 return Err(StreamRecordsError::Packed(packed_refs_unexpected_line(
1729 path,
1730 "<contains carriage return>",
1731 )));
1732 }
1733 let line = std::str::from_utf8(&self.data[pos..line_end]).map_err(|_| {
1734 StreamRecordsError::Packed(packed_refs_unexpected_line(path, "<invalid utf-8>"))
1735 })?;
1736 if line.is_empty() {
1737 if pos + 1 < end {
1738 return Err(StreamRecordsError::Packed(packed_refs_unexpected_line(
1739 path, line,
1740 )));
1741 }
1742 break;
1743 }
1744 if line.starts_with('#') {
1745 if flush(&mut pending)?.is_break() {
1746 return Ok(ControlFlow::Break(()));
1747 }
1748 expect_peel = false;
1749 pos = line_end + 1;
1750 continue;
1751 }
1752 if line.starts_with('^') {
1753 let peel = parse_packed_ref_peel_line(path, line, expect_peel)
1754 .map_err(StreamRecordsError::Packed)?;
1755 if let Some((_, _, stored)) = pending.as_mut() {
1756 *stored = Some(peel);
1757 }
1758 expect_peel = false;
1759 pos = line_end + 1;
1760 continue;
1761 }
1762 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)
1763 .map_err(StreamRecordsError::Packed)?;
1764 if self.sorted && !prev_name.is_empty() && refname < prev_name {
1765 return Err(StreamRecordsError::OutOfOrder);
1766 }
1767 if flush(&mut pending)?.is_break() {
1768 return Ok(ControlFlow::Break(()));
1769 }
1770 prev_name = refname;
1771 let in_prefix = prefix_trim.is_none_or(|p| ref_name_matches_list_prefix(refname, p));
1772 if in_prefix {
1773 pending = Some((refname, oid, None));
1774 } else if self.sorted {
1775 if let Some(upper) = prefix_upper.as_deref() {
1776 if refname >= upper {
1777 break;
1778 }
1779 }
1780 }
1781 expect_peel = true;
1782 pos = line_end + 1;
1783 }
1784 if flush(&mut pending)?.is_break() {
1785 return Ok(ControlFlow::Break(()));
1786 }
1787 Ok(ControlFlow::Continue(()))
1788 }
1789
1790 fn record_scan_range(
1791 &self,
1792 prefix_trim: Option<&str>,
1793 prefix_upper: Option<&str>,
1794 ) -> (usize, usize) {
1795 let len = self.data.len();
1796 if self.sorted {
1797 if let Some(p) = prefix_trim.filter(|p| !p.is_empty() && *p != "refs") {
1798 let first = self.lower_bound(p);
1799 let end = prefix_upper.map_or(len, |upper| self.lower_bound(upper));
1800 if end < first {
1801 return (len, len);
1802 }
1803 return (first, end);
1804 }
1805 }
1806 (self.body_start, len)
1807 }
1808
1809 fn prefix_region(&self, prefix: &str) -> Option<(usize, usize)> {
1817 let len = self.data.len();
1818 let first = self.lower_bound(prefix);
1819 let upper = list_prefix_upper_bound(prefix);
1820 let end = upper
1821 .as_deref()
1822 .map_or(len, |upper| self.lower_bound(upper));
1823 if end < first || (first < end && self.record_cmp(first, prefix).is_lt()) {
1824 return None;
1825 }
1826 if let Some(upper) = upper.as_deref() {
1827 if end < len && self.record_cmp(end, upper).is_lt() {
1828 return None;
1829 }
1830 }
1831 let begin = match self.prev_record(first) {
1832 Some(prev) => {
1833 if !self.record_cmp(prev, prefix).is_lt() {
1834 return None;
1835 }
1836 prev
1837 }
1838 None => 0,
1839 };
1840 let end = if end < len {
1841 let after = packed_line_end(&self.data, end) + 1;
1842 if self.data.get(after) == Some(&b'^') {
1843 packed_line_end(&self.data, after) + 1
1844 } else {
1845 after
1846 }
1847 } else {
1848 end
1849 };
1850 Some((begin, end))
1851 }
1852
1853 fn parse_region<C: Default + Extend<(String, ObjectId)>>(
1857 &self,
1858 path: &Path,
1859 begin: usize,
1860 end: usize,
1861 prefix: Option<&str>,
1862 keep: Option<PackedRecordFilter<'_>>,
1863 ) -> Result<(C, bool)> {
1864 let bytes = &self.data[begin..end];
1865 if memchr::memchr(b'\r', bytes).is_some() {
1866 return Err(packed_refs_unexpected_line(
1867 path,
1868 "<contains carriage return>",
1869 ));
1870 }
1871 let region = std::str::from_utf8(bytes)
1872 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
1873 parse_packed_refs_region(path, region, prefix, keep, self.peel_trait)
1874 }
1875}
1876
1877#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1880pub(crate) enum PackedPeel {
1881 Peeled(ObjectId),
1883 NotTag,
1885 Unknown,
1887}
1888
1889pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
1892
1893fn parse_packed_refs_region<C: Default + Extend<(String, ObjectId)>>(
1899 path: &Path,
1900 region: &str,
1901 prefix: Option<&str>,
1902 keep: Option<PackedRecordFilter<'_>>,
1903 peel_trait: PackedPeelTrait,
1904) -> Result<(C, bool)> {
1905 let ref_opts = RefNameOptions {
1906 allow_onelevel: false,
1907 refspec_pattern: false,
1908 normalize: false,
1909 };
1910 let mut map = C::default();
1911 let mut in_order = true;
1912 let mut prev_name = "";
1913 let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
1915 let flush = |map: &mut C, pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
1916 let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
1917 return;
1918 };
1919 let peel = if peel_trait.covers(refname) {
1920 peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
1921 } else {
1922 PackedPeel::Unknown
1923 };
1924 if keep(refname, &oid, peel) {
1925 map.extend(Some((refname.to_owned(), oid)));
1926 }
1927 };
1928 let mut expect_peel = false;
1929 let mut lines = region.split('\n').peekable();
1930 while let Some(line) = lines.next() {
1931 if line.is_empty() {
1932 if lines.peek().is_some() {
1933 return Err(packed_refs_unexpected_line(path, line));
1934 }
1935 break;
1936 }
1937 if line.starts_with('#') {
1938 flush(&mut map, &mut pending);
1939 expect_peel = false;
1940 continue;
1941 }
1942 if line.starts_with('^') {
1943 let peel = parse_packed_ref_peel_line(path, line, expect_peel)?;
1944 if let Some((_, _, peeled)) = pending.as_mut() {
1945 *peeled = Some(peel);
1946 }
1947 expect_peel = false;
1948 continue;
1949 }
1950 flush(&mut map, &mut pending);
1951 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
1952 in_order &= prev_name <= refname;
1953 prev_name = refname;
1954 if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
1955 if keep.is_some() {
1956 pending = Some((refname, oid, None));
1957 } else {
1958 map.extend(Some((refname.to_owned(), oid)));
1959 }
1960 }
1961 expect_peel = true;
1962 }
1963 flush(&mut map, &mut pending);
1964 Ok((map, in_order))
1965}
1966
1967fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
1968 let packed_path = store::paths::packed_refs_path(store);
1969 let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
1972 Ok(d) => d,
1973 Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
1974 Err(e) => return Err(e),
1975 };
1976 let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
1977 .map(|format| format.hash_algo())
1978 .unwrap_or(crate::objects::HashAlgo::Sha1);
1979 Ok(Some(PackedRefsFileView::from_data(
1980 &packed_path,
1981 data,
1982 hash_algo,
1983 )?))
1984}
1985
1986fn packed_line_is_record(line: &[u8], hex_len: usize) -> bool {
1989 line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^')
1990}
1991
1992fn packed_line_end(data: &[u8], line_start: usize) -> usize {
1993 memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
1994}
1995
1996fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
1997 std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
1998}
1999
2000fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
2001 let line_end = packed_line_end(data, line_start);
2002 if line_end >= data.len() || data[line_end] != b'\n' {
2003 return None;
2004 }
2005 let peel_start = line_end + 1;
2006 if peel_start >= data.len() || data[peel_start] != b'^' {
2007 return None;
2008 }
2009 let peel_end = packed_line_end(data, peel_start);
2010 let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
2011 ObjectId::from_hex(inner.trim()).ok()
2012}
2013
2014fn parse_packed_line_at(
2015 path: &Path,
2016 data: &[u8],
2017 line_start: usize,
2018 ref_opts: &RefNameOptions,
2019) -> Result<(String, ObjectId)> {
2020 let line = packed_line_as_str(data, line_start);
2021 parse_packed_ref_data_line(path, line, ref_opts)
2022}
2023
2024pub(crate) fn parse_packed_refs_to_map(
2025 path: &Path,
2026 data: &[u8],
2027) -> Result<HashMap<String, ObjectId>> {
2028 parse_packed_refs_to_map_with_prefix(path, data, None)
2029}
2030
2031fn packed_refs_leading_comments_end(data: &[u8]) -> usize {
2033 let mut end = 0;
2034 while data.get(end) == Some(&b'#') {
2035 match memchr::memchr(b'\n', &data[end..]) {
2036 Some(nl) => end += nl + 1,
2037 None => return data.len(),
2038 }
2039 }
2040 end
2041}
2042
2043fn packed_refs_file_declares_sorted(text: &str) -> bool {
2045 for line in text.split('\n') {
2046 if line.is_empty() {
2047 break;
2048 }
2049 if !line.starts_with('#') {
2050 break;
2051 }
2052 let body = line[1..].trim();
2053 let Some(traits) = body.strip_prefix("pack-refs with:") else {
2054 continue;
2055 };
2056 return traits.split_whitespace().any(|t| t == "sorted");
2057 }
2058 false
2059}
2060
2061fn parse_packed_refs_to_map_with_prefix(
2062 path: &Path,
2063 data: &[u8],
2064 prefix_filter: Option<&str>,
2065) -> Result<HashMap<String, ObjectId>> {
2066 let mut map = HashMap::new();
2067 visit_packed_refs_with_prefix(path, data, prefix_filter, |refname, oid| {
2068 map.insert(refname.to_owned(), oid);
2069 })?;
2070 Ok(map)
2071}
2072
2073fn visit_packed_refs_with_prefix(
2075 path: &Path,
2076 data: &[u8],
2077 prefix_filter: Option<&str>,
2078 mut visit: impl FnMut(&str, ObjectId),
2079) -> Result<()> {
2080 if data.is_empty() {
2081 return Ok(());
2082 }
2083 if data.contains(&b'\r') {
2084 return Err(packed_refs_unexpected_line(
2085 path,
2086 "<contains carriage return>",
2087 ));
2088 }
2089 if !data.ends_with(b"\n") {
2090 let tail = std::str::from_utf8(data)
2091 .map(|s| s.rsplit('\n').next().unwrap_or(s))
2092 .unwrap_or("<invalid utf-8>");
2093 return Err(packed_refs_unterminated_line(path, tail));
2094 }
2095 let text = std::str::from_utf8(data)
2096 .map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
2097
2098 let ref_opts = RefNameOptions {
2099 allow_onelevel: false,
2100 refspec_pattern: false,
2101 normalize: false,
2102 };
2103
2104 let mut expect_peel = false;
2105 let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
2106 let packed_sorted = packed_refs_file_declares_sorted(text);
2107 let prefix_upper = if packed_sorted {
2108 prefix_trim.and_then(list_prefix_upper_bound)
2109 } else {
2110 None
2111 };
2112 let mut lines = text.split('\n').peekable();
2113 while let Some(line) = lines.next() {
2114 if line.is_empty() {
2115 if lines.peek().is_some() {
2116 return Err(packed_refs_unexpected_line(path, line));
2117 }
2118 break;
2119 }
2120 if line.starts_with('#') {
2121 expect_peel = false;
2122 continue;
2123 }
2124 if line.starts_with('^') {
2125 parse_packed_ref_peel_line(path, line, expect_peel)?;
2126 expect_peel = false;
2127 continue;
2128 }
2129
2130 if let Some(prefix) = prefix_trim {
2131 let Some(refname) = packed_ref_data_line_refname(line) else {
2132 return Err(packed_refs_unexpected_line(path, line));
2133 };
2134 if packed_sorted {
2135 if refname < prefix {
2136 expect_peel = true;
2137 continue;
2138 }
2139 if let Some(upper) = &prefix_upper {
2140 if refname >= upper.as_str() {
2141 break;
2142 }
2143 }
2144 }
2145 if !ref_name_matches_list_prefix(refname, prefix) {
2146 expect_peel = true;
2147 continue;
2148 }
2149 }
2150 let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
2151 visit(refname, oid);
2152 expect_peel = true;
2153 }
2154 Ok(())
2155}
2156
2157pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
2158 read_packed_refs_map_for_prefix(store, "")
2159}
2160
2161pub(crate) fn read_packed_refs_map_for_prefix(
2162 store: &Path,
2163 prefix: &str,
2164) -> Result<HashMap<String, ObjectId>> {
2165 read_packed_refs_map_for_prefix_filtered(store, prefix, None)
2166}
2167
2168pub(crate) fn read_packed_refs_map_for_prefix_filtered(
2170 store: &Path,
2171 prefix: &str,
2172 keep: Option<PackedRecordFilter<'_>>,
2173) -> Result<HashMap<String, ObjectId>> {
2174 let Some(view) = read_packed_refs_file_view(store)? else {
2175 return Ok(HashMap::new());
2176 };
2177 let packed_path = store::paths::packed_refs_path(store);
2178 let filter = if prefix.is_empty() {
2179 None
2180 } else {
2181 Some(prefix)
2182 };
2183 view.map_for_prefix_filtered(&packed_path, filter, keep)
2184}
2185
2186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2188enum PackedPeelTrait {
2189 None,
2191 Tags,
2193 All,
2195}
2196
2197impl PackedPeelTrait {
2198 fn covers(self, refname: &str) -> bool {
2200 match self {
2201 Self::None => false,
2202 Self::Tags => refname.starts_with("refs/tags/"),
2203 Self::All => true,
2204 }
2205 }
2206}
2207
2208pub(crate) struct PackedRefPeels {
2218 view: Option<PackedRefsFileView>,
2219 unsorted: Option<HashMap<Box<[u8]>, usize>>,
2221 peel_trait: PackedPeelTrait,
2222 next: std::cell::Cell<usize>,
2225}
2226
2227impl PackedRefPeels {
2228 pub(crate) fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
2238 if format != RefStorageFormat::Files {
2239 return Ok(Self::from_view(None));
2240 }
2241 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2242 Ok(Self::from_view(read_packed_refs_file_view(&store)?))
2243 }
2244
2245 pub(crate) fn for_store(store: &dyn store::RefStore, git_dir: &Path) -> Result<Self> {
2254 match store.as_files() {
2255 Some(files) => Ok(Self::from_view(files.packed_refs_view()?)),
2256 None => Self::load(git_dir, store.format()),
2257 }
2258 }
2259
2260 fn from_view(view: Option<PackedRefsFileView>) -> Self {
2261 let empty = Self {
2262 view: None,
2263 unsorted: None,
2264 peel_trait: PackedPeelTrait::None,
2265 next: std::cell::Cell::new(0),
2266 };
2267 let Some(view) = view else {
2268 return empty;
2269 };
2270 let peel_trait = view.peel_trait;
2271 let unsorted = (!view.sorted).then(|| {
2272 view.records()
2273 .map(|start| (Box::<[u8]>::from(view.record_name(start)), start))
2274 .collect()
2275 });
2276 let first = view
2277 .next_record(view.body_start, view.data.len())
2278 .unwrap_or(view.data.len());
2279 Self {
2280 view: Some(view),
2281 unsorted,
2282 peel_trait,
2283 next: std::cell::Cell::new(first),
2284 }
2285 }
2286
2287 pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
2299 if !self.peel_trait.covers(refname) {
2300 return None;
2301 }
2302 self.record(refname, oid)?.peeled
2303 }
2304
2305 pub(crate) fn record(&self, refname: &str, oid: &ObjectId) -> Option<PackedRecord> {
2311 let view = self.view.as_ref()?;
2312 let start = match &self.unsorted {
2313 Some(map) => *map.get(refname.as_bytes())?,
2314 None => {
2315 let len = view.data.len();
2316 let hint = self.next.get();
2317 let start = if hint < len && view.record_cmp(hint, refname).is_eq() {
2318 hint
2319 } else {
2320 view.search(refname).ok()?
2321 };
2322 let after = packed_line_end(&view.data, start) + 1;
2323 self.next.set(view.next_record(after, len).unwrap_or(len));
2324 start
2325 }
2326 };
2327 let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
2328 let packed_oid = ObjectId::from_hex(hex).ok()?;
2329 (packed_oid == *oid).then(|| PackedRecord {
2330 peeled: self
2331 .peel_trait
2332 .covers(refname)
2333 .then(|| peel_oid_following_line(&view.data, start)),
2334 })
2335 }
2336}
2337
2338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2340pub(crate) struct PackedRecord {
2341 pub(crate) peeled: Option<Option<ObjectId>>,
2343}
2344
2345fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
2347 let Some(header) = text.strip_prefix("# pack-refs with: ") else {
2348 return PackedPeelTrait::None;
2349 };
2350 let traits = header.split('\n').next().unwrap_or_default();
2351 let mut peel_trait = PackedPeelTrait::None;
2352 for t in traits.split_whitespace() {
2353 match t {
2354 "fully-peeled" => return PackedPeelTrait::All,
2355 "peeled" => peel_trait = PackedPeelTrait::Tags,
2356 _ => {}
2357 }
2358 }
2359 peel_trait
2360}
2361
2362fn acquire_packed_refs_lock(
2363 git_dir: &Path,
2364 lock: &Path,
2365 lock_display: &Path,
2366) -> Result<crate::lockfile::LockFile> {
2367 use crate::lockfile::{LockError, LockFile, LockWait};
2368 let wait = LockWait::packed_refs_for_git_dir(git_dir);
2369 LockFile::acquire(lock, wait).map_err(|err| match err {
2370 LockError::Held { .. } => Error::from(RefLockError::PackedRefsLockHeld {
2371 lock_path: lock_display.display().to_string(),
2372 }),
2373 LockError::Io { source, .. } => Error::Io(source),
2374 })
2375}
2376
2377pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
2379 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2380 return Ok(None);
2381 };
2382 let packed_path = store::paths::packed_refs_path(git_dir);
2383 view.lookup_oid(&packed_path, refname)
2384}
2385
2386#[cfg(test)]
2388pub(crate) fn lookup_packed_ref_with_peel(
2389 git_dir: &Path,
2390 refname: &str,
2391) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
2392 let Some(view) = read_packed_refs_file_view(git_dir)? else {
2393 return Ok(None);
2394 };
2395 let packed_path = store::paths::packed_refs_path(git_dir);
2396 view.lookup_entry(&packed_path, refname)
2397}
2398
2399#[cfg(test)]
2402pub(crate) fn test_commit(git_dir: &Path, message: &str) -> ObjectId {
2403 let common = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2404 let body = format!(
2405 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
2406 author T <t@example.com> 0 +0000\n\
2407 committer T <t@example.com> 0 +0000\n\n{message}\n"
2408 );
2409 crate::odb::Odb::new(&common.join("objects"))
2410 .write(crate::objects::ObjectKind::Commit, body.as_bytes())
2411 .unwrap_or_else(|err| panic!("store test commit {message:?}: {err}"))
2412}
2413
2414pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
2429 store_write_value(
2430 git_dir,
2431 refname,
2432 Some(StoreRawRef::Symbolic(target.to_owned())),
2433 Expected::Any,
2434 None,
2435 )
2436}
2437
2438pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
2439 if let Some(rest) = refname.strip_prefix("refs/") {
2440 if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
2441 return Err(Error::InvalidRef(format!(
2442 "refusing to update ref with bad name '{refname}'"
2443 )));
2444 }
2445 return Ok(());
2446 }
2447 if crate::check_ref_format::refname_is_safe(refname) {
2448 Ok(())
2449 } else {
2450 Err(Error::InvalidRef(format!(
2451 "refusing to update ref with bad name '{refname}'"
2452 )))
2453 }
2454}
2455
2456pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
2479 store_write_value(
2480 git_dir,
2481 refname,
2482 Some(StoreRawRef::Direct(*oid)),
2483 Expected::Any,
2484 None,
2485 )
2486}
2487
2488#[must_use]
2490pub fn ref_update_flags(refname: &str) -> RefUpdateFlags {
2491 legacy_ref_update_flags(refname)
2492}
2493
2494pub(crate) fn create_ref_with_reflog_on_store(
2498 store: &dyn RefStore,
2499 git_dir: &Path,
2500 config: &ConfigSet,
2501 refname: &str,
2502 oid: &ObjectId,
2503 identity: &str,
2504 message: &str,
2505) -> Result<()> {
2506 let reflog =
2507 should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
2508 identity: identity.to_owned(),
2509 message: message.to_owned(),
2510 time: time::OffsetDateTime::now_utc(),
2511 old_oid: None,
2512 new_oid: None,
2513 });
2514 store_write_value_on_store(
2515 store,
2516 git_dir,
2517 refname,
2518 Some(StoreRawRef::Direct(*oid)),
2519 Expected::Missing,
2520 reflog,
2521 )
2522}
2523
2524fn write_ref_at_storage(
2527 git_dir: &Path,
2528 storage_dir: &Path,
2529 refname: &str,
2530 oid: &ObjectId,
2531) -> Result<()> {
2532 let stor = crate::ref_namespace::storage_ref_name(refname);
2533 let path = storage_dir.join(stor);
2534 remove_empty_ref_directory(&path);
2541 if fs::symlink_metadata(&path)
2544 .map(|m| m.file_type().is_dir())
2545 .unwrap_or(false)
2546 {
2547 return Err(directory_in_the_way(git_dir, refname, &path));
2548 }
2549 if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
2550 return Err(Error::InvalidRef(format!(
2551 "cannot update ref '{refname}': reference broken"
2552 )));
2553 }
2554 if let Some(parent) = path.parent() {
2555 crate::git_dir::io_error::io_with_git_path(
2556 git_dir,
2557 parent,
2558 crate::git_dir::GitDirPathOperation::CreateDir,
2559 fs::create_dir_all(parent),
2560 )?;
2561 }
2562 let content = format!("{oid}\n");
2563 let lock_path = lock_path_for_ref(&path);
2566 use crate::lockfile::{LockError, LockFile, LockWait};
2567 use std::io::Write as _;
2568 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
2569 let mut lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
2570 held @ LockError::Held { .. } => Error::from(held),
2571 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2572 git_dir,
2573 &lock_path,
2574 crate::git_dir::GitDirPathOperation::UpdateRef,
2575 source,
2576 ),
2577 })?;
2578 crate::git_dir::io_error::io_with_git_path(
2579 git_dir,
2580 &path,
2581 crate::git_dir::GitDirPathOperation::UpdateRef,
2582 lock.write_all(content.as_bytes()),
2583 )?;
2584 lock.commit_to(&path).map_err(|err| match err {
2585 LockError::Io { source, .. } => crate::git_dir::io_error::map_git_dir_io(
2586 git_dir,
2587 &path,
2588 crate::git_dir::GitDirPathOperation::UpdateRef,
2589 source,
2590 ),
2591 held @ LockError::Held { .. } => Error::from(held),
2592 })?;
2593 Ok(())
2594}
2595
2596pub fn write_ref_cas(
2610 git_dir: &Path,
2611 refname: &str,
2612 oid: &ObjectId,
2613 expected_old: ObjectId,
2614) -> Result<()> {
2615 store_write_value(
2616 git_dir,
2617 refname,
2618 Some(StoreRawRef::Direct(*oid)),
2619 Expected::Oid(expected_old),
2620 None,
2621 )
2622}
2623
2624pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
2637 store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
2638}
2639
2640fn prune_loose_ref_cas(
2650 git_dir: &Path,
2651 refname: &str,
2652 expected: ObjectId,
2653 wait: crate::lockfile::LockWait,
2654) -> Result<bool> {
2655 ensure_refname_safe_for_storage(refname)?;
2656 let storage_dir = ref_storage_dir(git_dir, refname);
2657 let stor = crate::ref_namespace::storage_ref_name(refname);
2658 let path = storage_dir.join(&stor);
2659 let lock_path = lock_path_for_ref(&path);
2660 use crate::lockfile::LockFile;
2661 let _lock = LockFile::acquire(&lock_path, wait)?;
2662 if !path.is_file() {
2663 return Ok(true);
2664 }
2665 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
2666 return Ok(false);
2667 };
2668 if oid != expected {
2669 return Ok(false);
2670 }
2671 fs::remove_file(&path).map_err(Error::Io)?;
2672 Ok(true)
2673}
2674
2675pub struct PackedRefs {
2691 map: HashMap<String, ObjectId>,
2692 sorted_names: BTreeSet<String>,
2693}
2694
2695fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
2696 let sorted_names = map.keys().cloned().collect();
2697 PackedRefs { map, sorted_names }
2698}
2699
2700impl PackedRefs {
2701 pub fn load(git_dir: &Path) -> Result<Self> {
2710 let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2711 let map = read_packed_refs_map(&store)?;
2712 Ok(packed_refs_from_map(map))
2713 }
2714
2715 #[must_use]
2717 pub fn get(&self, refname: &str) -> Option<ObjectId> {
2718 self.map.get(refname).copied()
2719 }
2720
2721 #[must_use]
2725 pub fn has_namespace_conflict(&self, refname: &str) -> bool {
2726 packed_refname_namespace_conflict(&self.sorted_names, refname)
2727 }
2728
2729 pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
2732 self.map.insert(refname.to_owned(), oid);
2733 self.sorted_names.insert(refname.to_owned());
2734 }
2735}
2736
2737pub fn resolve_ref_cached(
2747 git_dir: &Path,
2748 refname: &str,
2749 packed: &PackedRefs,
2750) -> Result<Option<ObjectId>> {
2751 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2752 return Ok(resolve_ref(git_dir, refname).ok());
2753 }
2754 resolve_files_ref_cached(git_dir, refname, packed)
2755}
2756
2757fn resolve_files_ref_cached(
2758 git_dir: &Path,
2759 refname: &str,
2760 packed: &PackedRefs,
2761) -> Result<Option<ObjectId>> {
2762 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
2763 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
2764 let try_names: Vec<&str> = if storage_owned != stor_name {
2765 vec![storage_owned.as_str(), stor_name.as_str()]
2766 } else {
2767 vec![stor_name.as_str()]
2768 };
2769
2770 for name in &try_names {
2771 let path = store.join(name);
2772 match read_ref_file(&path) {
2773 Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
2774 Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
2775 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
2776 Err(e) => return Err(e),
2777 }
2778 }
2779
2780 for name in &try_names {
2781 if let Some(oid) = packed.get(name) {
2782 return Ok(Some(oid));
2783 }
2784 }
2785 Ok(None)
2786}
2787
2788pub fn write_ref_cached(
2797 git_dir: &Path,
2798 refname: &str,
2799 oid: &ObjectId,
2800 packed: &mut PackedRefs,
2801) -> Result<()> {
2802 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2803 return write_ref(git_dir, refname, oid);
2804 }
2805 ensure_refname_safe_for_storage(refname)?;
2806 if packed.has_namespace_conflict(refname) {
2807 return Err(Error::InvalidRef(format!(
2808 "cannot update ref '{refname}': reference namespace conflict"
2809 )));
2810 }
2811 let storage_dir = ref_storage_dir(git_dir, refname);
2812 write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
2813 packed.record_loose_write(refname, *oid);
2814 Ok(())
2815}
2816
2817fn ref_path_for_display(path: &Path) -> String {
2822 let cwd = crate::environment::Environment::empty().cwd.clone();
2823 if let Ok(rel) = path.strip_prefix(&cwd) {
2824 return rel.to_string_lossy().into_owned();
2825 }
2826 if let (Ok(cwd_c), Ok(path_c)) = (
2830 cwd.canonicalize_simplified(),
2831 path.canonicalize_simplified(),
2832 ) {
2833 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
2834 return rel.to_string_lossy().into_owned();
2835 }
2836 }
2837 path.to_string_lossy().into_owned()
2838}
2839
2840pub(crate) fn remove_empty_ref_directory(path: &Path) {
2846 match fs::symlink_metadata(path) {
2847 Ok(meta) if meta.file_type().is_dir() => {}
2848 _ => return,
2849 }
2850 if dir_tree_has_files(path) {
2853 return;
2854 }
2855 let _ = remove_dir_tree(path);
2856}
2857
2858pub(crate) fn dir_tree_has_files(dir: &Path) -> bool {
2860 let Ok(entries) = fs::read_dir(dir) else {
2861 return true;
2863 };
2864 for entry in entries.flatten() {
2865 match entry.file_type() {
2866 Ok(ft) if ft.is_dir() => {
2867 if dir_tree_has_files(&entry.path()) {
2868 return true;
2869 }
2870 }
2871 Ok(_) => return true, Err(_) => return true,
2873 }
2874 }
2875 false
2876}
2877
2878pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
2880 if let Some(dir) = ref_path.parent() {
2881 prune_empty_ref_dirs(storage_dir, dir);
2882 }
2883}
2884
2885pub(crate) fn prune_empty_ref_dirs(storage_dir: &Path, dir: &Path) {
2890 let refs_root = storage_dir.join("refs");
2891 let mut current = Some(dir);
2892 while let Some(dir) = current {
2893 if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
2894 break;
2895 }
2896 if dir
2897 .parent()
2898 .is_some_and(|parent| parent == refs_root.as_path())
2899 {
2900 break;
2901 }
2902 if dir_tree_has_files(dir) {
2903 break;
2904 }
2905 if remove_dir_tree(dir).is_err() {
2906 break;
2907 }
2908 current = dir.parent();
2909 }
2910}
2911
2912fn remove_dir_tree(dir: &Path) -> io::Result<()> {
2914 for entry in fs::read_dir(dir)? {
2915 let entry = entry?;
2916 if entry.file_type()?.is_dir() {
2917 remove_dir_tree(&entry.path())?;
2918 }
2919 }
2920 fs::remove_dir(dir)
2921}
2922
2923pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
2939 if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
2940 return Err(store::RefStoreError::not_found(refname).into());
2941 }
2942 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
2943 let store = open_ref_store(git_dir)?;
2944 let mut flags = legacy_ref_update_flags(refname);
2945 if refname != "HEAD" {
2946 if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
2947 flags.no_deref = true;
2948 }
2949 }
2950 return commit_store_update(
2951 git_dir,
2952 RefUpdate {
2953 name: refname.to_owned(),
2954 new_value: None,
2955 expected: Expected::Exists,
2956 reflog: None,
2957 flags,
2958 },
2959 )
2960 .map_err(|e| map_public_ref_store_error(e, refname));
2961 }
2962 ensure_refname_safe_for_storage(refname)?;
2963 let storage_dir = ref_storage_dir(git_dir, refname);
2964 let stor = crate::ref_namespace::storage_ref_name(refname);
2965 let path = storage_dir.join(&stor);
2966 let lock = lock_path_for_ref(&path);
2967 if fs::symlink_metadata(&lock).is_ok() {
2968 return Err(Error::from(crate::lockfile::LockError::Held { path: lock }));
2969 }
2970
2971 remove_packed_ref(&storage_dir, &stor)?;
2977
2978 remove_empty_ref_directory(&path);
2982 match fs::remove_file(&path) {
2983 Ok(()) => {}
2984 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
2985 Err(e)
2986 if e.kind() == io::ErrorKind::NotADirectory
2987 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
2988 Err(e)
2991 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
2992 Err(e) => return Err(Error::Io(e)),
2993 }
2994
2995 prune_empty_loose_ref_parents(&storage_dir, &path);
2996
2997 let log_path = storage_dir.join("logs").join(&stor);
2998
2999 let _ = fs::remove_file(&log_path);
3005
3006 let logs_root = storage_dir.join("logs");
3007 let mut parent = log_path.parent();
3008 while let Some(p) = parent {
3009 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
3010 break;
3011 }
3012 if fs::remove_dir(p).is_err() {
3013 break; }
3015 parent = p.parent();
3016 }
3017
3018 Ok(())
3019}
3020
3021pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
3023 remove_packed_refs_inner(git_dir, &|name| name == refname, false, false).map(drop)
3024}
3025
3026pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
3027 remove_packed_refs_inner(git_dir, &|name| name == refname, false, true).map(drop)
3028}
3029
3030pub(crate) fn remove_packed_refs_under_lock(
3040 git_dir: &Path,
3041 refnames: &HashSet<String>,
3042) -> Result<Vec<String>> {
3043 if refnames.is_empty() {
3044 return Ok(Vec::new());
3045 }
3046 remove_packed_refs_inner(git_dir, &|name| refnames.contains(name), true, true)
3047}
3048
3049pub(crate) fn packed_record_name(record: &str) -> &str {
3051 let line = record.lines().next().unwrap_or("");
3052 line.split_once(' ').map_or("", |(_, name)| name.trim())
3053}
3054
3055pub(crate) fn packed_record_oid(record: &str) -> Option<ObjectId> {
3057 let line = record.lines().next()?;
3058 ObjectId::from_hex(line.split_once(' ')?.0).ok()
3059}
3060
3061pub(crate) fn restore_packed_records_under_lock(git_dir: &Path, records: &[&str]) -> Result<()> {
3068 if records.is_empty() {
3069 return Ok(());
3070 }
3071 let packed_path = store::paths::packed_refs_path(git_dir);
3072 let content = match fs::read(&packed_path) {
3073 Ok(c) => c,
3074 Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
3075 Err(e) => return Err(Error::Io(e)),
3076 };
3077 parse_packed_refs_to_map(&packed_path, &content)?;
3078 let content = String::from_utf8(content)
3079 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3080
3081 let mut blocks: Vec<String> = Vec::new();
3082 for line in content.lines() {
3083 if line.starts_with('#') {
3084 continue;
3085 }
3086 match blocks.last_mut() {
3087 Some(block) if line.starts_with('^') => block.push_str(line),
3088 _ => blocks.push(line.to_owned()),
3089 }
3090 if let Some(block) = blocks.last_mut() {
3091 block.push('\n');
3092 }
3093 }
3094 blocks.extend(records.iter().map(|r| (*r).to_owned()));
3095 blocks.sort_by(|a, b| packed_record_name(a).cmp(packed_record_name(b)));
3096
3097 let mut out =
3098 String::with_capacity(content.len() + records.iter().map(|r| r.len()).sum::<usize>());
3099 out.push_str(PACKED_REFS_HEADER);
3100 for block in &blocks {
3101 out.push_str(block);
3102 }
3103 replace_packed_refs_file(git_dir, &packed_path, &out, true)
3104}
3105
3106const PACKED_REFS_HEADER: &str = "# pack-refs with: peeled fully-peeled sorted\n";
3107
3108fn remove_packed_refs_inner(
3109 git_dir: &Path,
3110 remove: &dyn Fn(&str) -> bool,
3111 keep_removed: bool,
3112 lock_already_held: bool,
3113) -> Result<Vec<String>> {
3114 let mut removed: Vec<String> = Vec::new();
3115 let packed_path = store::paths::packed_refs_path(git_dir);
3116 let content = match fs::read(&packed_path) {
3117 Ok(c) => c,
3118 Err(e)
3119 if e.kind() == io::ErrorKind::NotFound
3120 || e.kind() == io::ErrorKind::NotADirectory
3121 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3122 {
3123 return Ok(removed);
3124 }
3125 Err(e) => return Err(Error::Io(e)),
3126 };
3127 let mut has_removed_record = false;
3130 visit_packed_refs_with_prefix(&packed_path, &content, None, |name, _| {
3131 has_removed_record |= remove(name);
3132 })?;
3133 if !has_removed_record {
3134 return Ok(removed);
3135 }
3136
3137 let content = String::from_utf8(content)
3138 .map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
3139
3140 let mut out = String::with_capacity(content.len());
3141 let mut skip_peeled = false;
3142 let mut changed = false;
3143 let mut header_written = false;
3146
3147 for line in content.lines() {
3148 if skip_peeled {
3149 if line.starts_with('^') {
3150 if let Some(record) = removed.last_mut().filter(|_| keep_removed) {
3151 record.push_str(line);
3152 record.push('\n');
3153 }
3154 changed = true;
3155 continue;
3156 }
3157 skip_peeled = false;
3158 }
3159
3160 if line.starts_with('#') {
3161 continue;
3163 }
3164 if line.starts_with('^') {
3165 out.push_str(line);
3166 out.push('\n');
3167 continue;
3168 }
3169
3170 if !header_written {
3172 out.insert_str(0, PACKED_REFS_HEADER);
3173 header_written = true;
3174 }
3175
3176 let name = line.split_once(' ').map_or("", |(_, name)| name.trim());
3177 if remove(name) {
3178 if keep_removed {
3179 removed.push(format!("{line}\n"));
3180 }
3181 changed = true;
3182 skip_peeled = true;
3183 continue;
3184 }
3185
3186 out.push_str(line);
3187 out.push('\n');
3188 }
3189
3190 if changed {
3191 replace_packed_refs_file(git_dir, &packed_path, &out, lock_already_held)?;
3192 }
3193
3194 Ok(removed)
3195}
3196
3197fn replace_packed_refs_file(
3199 git_dir: &Path,
3200 packed_path: &Path,
3201 body: &str,
3202 lock_already_held: bool,
3203) -> Result<()> {
3204 let lock = lock_path_for_ref(packed_path); let _packed_lock = if !lock_already_held {
3212 let abs_lock = git_dir
3213 .canonicalize_simplified()
3214 .map(|d| d.join("packed-refs.lock"))
3215 .unwrap_or_else(|_| lock.clone());
3216 Some(acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?)
3217 } else {
3218 None
3219 };
3220
3221 let tmp = packed_path.with_extension("new");
3222 let mut created_tmp = false;
3223 let write_result = (|| -> Result<()> {
3224 let mut file = std::fs::OpenOptions::new()
3226 .write(true)
3227 .create_new(true)
3228 .open(&tmp)
3229 .map_err(Error::Io)?;
3230 created_tmp = true;
3231 use std::io::Write as _;
3232 file.write_all(body.as_bytes()).map_err(Error::Io)?;
3233 drop(file);
3234 fs::rename(&tmp, packed_path).map_err(Error::Io)?;
3235 created_tmp = false; Ok(())
3237 })();
3238
3239 if write_result.is_err() && created_tmp {
3240 let _ = fs::remove_file(&tmp);
3241 }
3242 write_result
3243}
3244
3245pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
3258 read_symbolic_ref(git_dir, "HEAD")
3259}
3260
3261pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
3273 match open_ref_store(git_dir)?.read_raw(refname)? {
3274 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3275 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3276 }
3277}
3278
3279pub fn read_symbolic_ref_for_repository(
3281 repo: &crate::repo::Repository,
3282 refname: &str,
3283) -> Result<Option<String>> {
3284 match repo.refs().read_raw(refname)? {
3285 Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
3286 Some(StoreRawRef::Direct(_)) | None => Ok(None),
3287 }
3288}
3289
3290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3292pub enum LogRefsConfig {
3293 Unset,
3295 None,
3297 Normal,
3299 Always,
3301}
3302
3303impl LogRefsConfig {
3304 pub(crate) fn parse(raw: &str) -> Option<Self> {
3306 match raw.trim().to_ascii_lowercase().as_str() {
3307 "always" => Some(Self::Always),
3308 "1" | "true" | "yes" | "on" => Some(Self::Normal),
3309 "0" | "false" | "no" | "off" | "never" => Some(Self::None),
3310 _ => None,
3311 }
3312 }
3313}
3314
3315pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3319 ConfigSet::load(
3320 &crate::environment::Environment::empty(),
3321 Some(git_dir),
3322 true,
3323 )
3324 .map(|cfg| cfg.log_refs_config())
3325 .unwrap_or(LogRefsConfig::Unset)
3326}
3327
3328pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
3332 ConfigSet::load(
3333 &crate::environment::Environment::empty(),
3334 Some(git_dir),
3335 true,
3336 )
3337 .map(|cfg| cfg.effective_log_refs_config())
3338 .unwrap_or(LogRefsConfig::Normal)
3339}
3340
3341#[must_use]
3344pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
3345 match mode {
3346 LogRefsConfig::Always => true,
3347 LogRefsConfig::Normal => {
3348 refname == "HEAD"
3349 || refname.starts_with("refs/heads/")
3350 || refname.starts_with("refs/remotes/")
3351 || refname.starts_with("refs/notes/")
3352 }
3353 LogRefsConfig::None | LogRefsConfig::Unset => false,
3354 }
3355}
3356
3357#[must_use]
3359pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
3360 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
3361}
3362
3363#[must_use]
3365pub fn should_autocreate_reflog_with_config(
3366 config: &ConfigSet,
3367 _git_dir: &Path,
3368 refname: &str,
3369) -> bool {
3370 should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
3371}
3372
3373fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
3399 let Ok(rel) = target.strip_prefix(logs_root) else {
3400 return;
3401 };
3402 let mut cur = logs_root.to_path_buf();
3403 for component in rel.components() {
3404 cur.push(component);
3405 match fs::symlink_metadata(&cur) {
3406 Ok(meta) if meta.file_type().is_dir() => {}
3407 Ok(_) => {
3408 let _ = fs::remove_file(&cur);
3411 }
3412 Err(_) => break, }
3414 }
3415}
3416
3417pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
3420 let width = if !old.is_zero() {
3421 old.algo().hex_len()
3422 } else if !new.is_zero() {
3423 new.algo().hex_len()
3424 } else {
3425 old.algo().hex_len()
3426 };
3427 let render = |oid: &ObjectId| {
3428 if oid.is_zero() {
3429 "0".repeat(width)
3430 } else {
3431 oid.to_hex()
3432 }
3433 };
3434 (render(old), render(new))
3435}
3436
3437fn map_reflog_store_validation(err: store::RefStoreError) -> Error {
3438 match err {
3439 store::RefStoreError::InvalidReflog(sig) => Error::InvalidSignature(sig),
3440 other => Error::from(other),
3441 }
3442}
3443
3444fn validate_reflog_message(message: &str) -> Result<()> {
3445 store::validate_reflog_message(message).map_err(map_reflog_store_validation)
3446}
3447
3448fn validate_reflog_identity_string(identity: &str) -> Result<()> {
3449 store::validate_reflog_identity_string(identity).map_err(map_reflog_store_validation)
3450}
3451
3452fn validate_reflog_update(log: &ReflogUpdate) -> Result<()> {
3454 store::validate_reflog_update(log).map_err(map_reflog_store_validation)
3455}
3456
3457pub fn append_reflog_with_signature(
3463 git_dir: &Path,
3464 refname: &str,
3465 old_oid: &ObjectId,
3466 new_oid: &ObjectId,
3467 identity: &Signature,
3468 message: &str,
3469 force_create: bool,
3470) -> Result<()> {
3471 identity.validate().map_err(Error::InvalidSignature)?;
3472 let formatted = format_reflog_identity(&identity.reflog_name_email(), identity.time);
3473 append_reflog_with_config(
3474 git_dir,
3475 refname,
3476 old_oid,
3477 new_oid,
3478 &formatted,
3479 message,
3480 force_create,
3481 None,
3482 )
3483}
3484
3485pub fn append_reflog(
3494 git_dir: &Path,
3495 refname: &str,
3496 old_oid: &ObjectId,
3497 new_oid: &ObjectId,
3498 identity: &str,
3499 message: &str,
3500 force_create: bool,
3501) -> Result<()> {
3502 append_reflog_with_config(
3503 git_dir,
3504 refname,
3505 old_oid,
3506 new_oid,
3507 identity,
3508 message,
3509 force_create,
3510 None,
3511 )
3512}
3513
3514#[allow(clippy::too_many_arguments)]
3516pub fn append_reflog_with_config(
3517 git_dir: &Path,
3518 refname: &str,
3519 old_oid: &ObjectId,
3520 new_oid: &ObjectId,
3521 identity: &str,
3522 message: &str,
3523 force_create: bool,
3524 config: Option<&ConfigSet>,
3525) -> Result<()> {
3526 validate_reflog_identity_string(identity)?;
3527 validate_reflog_message(message)?;
3528 test_inject_reflog_fail(refname)?;
3529 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3530 return crate::reftable::reftable_append_reflog(
3531 git_dir,
3532 refname,
3533 old_oid,
3534 new_oid,
3535 identity,
3536 message,
3537 force_create,
3538 );
3539 }
3540 let storage_dir = ref_storage_dir(git_dir, refname);
3541 let stor = crate::ref_namespace::storage_ref_name(refname);
3542 let log_path = storage_dir.join("logs").join(&stor);
3543 let may_create = force_create
3544 || match config {
3545 Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
3546 None => should_autocreate_reflog(git_dir, refname),
3547 };
3548 if !may_create && !log_path.exists() {
3549 return Ok(());
3550 }
3551 if let Some(parent) = log_path.parent() {
3552 let logs_root = storage_dir.join("logs");
3557 clear_conflicting_reflog_files(&logs_root, parent);
3558 crate::git_dir::io_error::io_with_git_path(
3559 git_dir,
3560 parent,
3561 crate::git_dir::GitDirPathOperation::CreateDir,
3562 fs::create_dir_all(parent),
3563 )?;
3564 }
3565 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
3570 let line = if message.is_empty() {
3571 format!("{old_hex} {new_hex} {identity}\n")
3572 } else {
3573 format!("{old_hex} {new_hex} {identity}\t{message}\n")
3574 };
3575 let mut file = fs::OpenOptions::new()
3576 .create(true)
3577 .append(true)
3578 .open(&log_path)
3579 .map_err(|source| {
3580 crate::git_dir::io_error::map_git_dir_io(
3581 git_dir,
3582 &log_path,
3583 crate::git_dir::GitDirPathOperation::AppendReflog,
3584 source,
3585 )
3586 })?;
3587 use io::Write;
3588 crate::git_dir::io_error::io_with_git_path(
3589 git_dir,
3590 &log_path,
3591 crate::git_dir::GitDirPathOperation::AppendReflog,
3592 file.write_all(line.as_bytes()),
3593 )?;
3594 Ok(())
3595}
3596
3597fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
3598 if all {
3599 b"*"
3600 } else {
3601 b"refs/tags/*"
3602 }
3603}
3604
3605fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
3606 crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
3607}
3608
3609fn collect_loose_ref_files(
3610 dir: &Path,
3611 prefix: &str,
3612 out: &mut Vec<(String, PathBuf)>,
3613) -> Result<()> {
3614 let read = match fs::read_dir(dir) {
3615 Ok(r) => r,
3616 Err(e)
3617 if e.kind() == io::ErrorKind::NotFound
3618 || e.kind() == io::ErrorKind::NotADirectory
3619 || e.raw_os_error() == Some(libc::ENOTDIR) =>
3620 {
3621 return Ok(());
3622 }
3623 Err(e) => return Err(Error::Io(e)),
3624 };
3625 for entry in read {
3626 let entry = entry?;
3627 let name = entry.file_name().to_string_lossy().to_string();
3628 let refname = format!("{prefix}{name}");
3629 let path = entry.path();
3630 if path.is_dir() {
3631 collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
3632 } else if path.is_file() {
3633 out.push((refname, path));
3634 }
3635 }
3636 Ok(())
3637}
3638
3639fn load_packed_refs_with_peel(
3640 packed_path: &Path,
3641 merged: &mut HashMap<String, (ObjectId, Option<ObjectId>)>,
3642) -> Result<()> {
3643 if !packed_path.is_file() {
3644 return Ok(());
3645 }
3646 let data = fs::read(packed_path)?;
3647 parse_packed_refs_to_map(packed_path, &data)?;
3648 let content = String::from_utf8(data)
3649 .map_err(|_| packed_refs_unexpected_line(packed_path, "<invalid utf-8>"))?;
3650 let mut last_name: Option<String> = None;
3651 for line in content.lines() {
3652 if line.starts_with('#') {
3653 continue;
3654 }
3655 if let Some(hex) = line.strip_prefix('^') {
3656 let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
3657 continue;
3658 };
3659 if let Some(name) = &last_name {
3660 if let Some(entry) = merged.get_mut(name) {
3661 entry.1 = Some(peel_oid);
3662 }
3663 }
3664 continue;
3665 }
3666 let mut parts = line.splitn(2, ' ');
3667 let Some(hex) = parts.next() else {
3668 continue;
3669 };
3670 let Some(name) = parts.next() else {
3671 continue;
3672 };
3673 let name = name.trim();
3674 let Ok(oid) = ObjectId::from_hex(hex) else {
3675 continue;
3676 };
3677 merged.insert(name.to_owned(), (oid, None));
3678 last_name = Some(name.to_owned());
3679 }
3680 Ok(())
3681}
3682
3683pub(crate) fn pack_refs_for_repository(
3685 repo: &crate::repo::Repository,
3686 all: bool,
3687 dry_run: bool,
3688) -> Result<(PathBuf, usize, usize)> {
3689 let git_dir = repo.git_dir();
3690 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
3691 return pack_refs_reftable(git_dir, dry_run);
3692 }
3693 pack_refs_files(git_dir, repo.odb(), all, dry_run)
3694}
3695
3696fn pack_refs_reftable(git_dir: &Path, dry_run: bool) -> Result<(PathBuf, usize, usize)> {
3697 use crate::reftable::ReftableStack;
3698 let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3699 let rt_dir = store_git_dir.join("reftable");
3700 let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
3701 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3702 .unwrap_or(0);
3703 if dry_run {
3704 let tables_pruned = tables_before.saturating_sub(1);
3705 return Ok((rt_dir, 0, tables_pruned));
3706 }
3707 let mut stack = ReftableStack::open(&store_git_dir)?;
3708 stack.compact()?;
3709 let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
3710 .map(|s| s.lines().filter(|l| !l.is_empty()).count())
3711 .unwrap_or(0);
3712 Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
3713}
3714
3715fn pack_refs_files(
3716 git_dir: &Path,
3717 odb: &crate::odb::Odb,
3718 all: bool,
3719 dry_run: bool,
3720) -> Result<(PathBuf, usize, usize)> {
3721 pack_refs_files_matching(
3722 git_dir,
3723 odb,
3724 |name| ref_matches_pack_include(name, all),
3725 dry_run,
3726 false,
3727 )
3728}
3729
3730fn pack_refs_files_matching(
3731 git_dir: &Path,
3732 odb: &crate::odb::Odb,
3733 include: impl Fn(&str) -> bool,
3734 dry_run: bool,
3735 allow_missing: bool,
3736) -> Result<(PathBuf, usize, usize)> {
3737 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
3738 let packed_path = store::paths::packed_refs_path(&storage_git_dir);
3739
3740 let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
3741 load_packed_refs_with_peel(&packed_path, &mut merged)?;
3742
3743 let mut loose_files = Vec::new();
3744 collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
3745
3746 let mut packed_loose = 0usize;
3747 let mut to_prune: Vec<(String, ObjectId)> = Vec::new();
3748
3749 for (refname, path) in loose_files {
3750 if crate::worktree_ref::is_per_worktree_ref(&refname) {
3751 continue;
3752 }
3753 if !include(&refname) {
3754 continue;
3755 }
3756 let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
3757 continue;
3758 };
3759 let object = odb.try_read_once(&oid).ok().flatten();
3760 if object.is_none() && !allow_missing {
3761 continue;
3762 }
3763 let peel = if object.is_some_and(|object| object.kind == crate::objects::ObjectKind::Tag) {
3764 packed_ref_peel(odb, oid)
3765 } else {
3766 None
3767 };
3768 merged.insert(refname.clone(), (oid, peel));
3769 to_prune.push((refname, oid));
3770 let _ = path;
3771 packed_loose += 1;
3772 }
3773
3774 if packed_loose == 0 {
3775 return Ok((packed_path, 0, 0));
3776 }
3777
3778 if dry_run {
3779 return Ok((packed_path, packed_loose, to_prune.len()));
3780 }
3781
3782 let mut names: Vec<String> = merged.keys().cloned().collect();
3783 names.sort();
3784
3785 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
3786 for name in &names {
3787 let Some((oid, peel)) = merged.get(name) else {
3788 continue;
3789 };
3790 body.push_str(&oid.to_hex());
3791 body.push(' ');
3792 body.push_str(name);
3793 body.push('\n');
3794 if let Some(p) = peel {
3795 body.push('^');
3796 body.push_str(&p.to_hex());
3797 body.push('\n');
3798 }
3799 }
3800
3801 atomic_rewrite_packed_refs(&storage_git_dir, &body)?;
3802
3803 #[cfg(test)]
3804 pack_refs_test_hooks::run_before_prune();
3805
3806 let mut pruned = 0usize;
3807 let mut parents = HashSet::new();
3808 let wait = crate::lockfile::LockWait::files_ref_lock_for_git_dir(git_dir);
3809 for (refname, expected) in &to_prune {
3810 match prune_loose_ref_cas(git_dir, refname, *expected, wait) {
3811 Ok(true) => {
3812 pruned += 1;
3813 let path = storage_git_dir.join(crate::ref_namespace::storage_ref_name(refname));
3814 if let Some(parent) = path.parent() {
3815 parents.insert(parent.to_path_buf());
3816 }
3817 }
3818 Ok(false) => {
3819 remove_packed_ref(git_dir, refname)?;
3820 }
3821 Err(e) => return Err(e),
3822 }
3823 }
3824
3825 for parent in &parents {
3828 prune_empty_ref_dirs(&storage_git_dir, parent);
3829 }
3830
3831 Ok((packed_path, packed_loose, pruned))
3832}
3833
3834#[cfg(test)]
3836mod pack_refs_test_hooks {
3837 use std::cell::RefCell;
3838
3839 thread_local! {
3840 static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
3841 }
3842
3843 pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
3844 BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
3845 }
3846
3847 pub fn run_before_prune() {
3848 BEFORE_PRUNE.with(|c| {
3849 if let Some(f) = c.borrow_mut().take() {
3850 f();
3851 }
3852 });
3853 }
3854}
3855
3856#[cfg(test)]
3860mod reflog_fail_inject {
3861 use std::cell::RefCell;
3862
3863 thread_local! {
3865 pub static INJECT: RefCell<Option<String>> = const { RefCell::new(None) };
3866 }
3867}
3868
3869#[cfg(test)]
3871pub fn set_test_inject_reflog_fail(refname: Option<&str>) {
3872 reflog_fail_inject::INJECT.with(|c| {
3873 *c.borrow_mut() = refname.map(str::to_owned);
3874 });
3875}
3876
3877#[cfg(test)]
3878fn test_inject_reflog_fail(refname: &str) -> Result<()> {
3879 reflog_fail_inject::INJECT.with(|c| {
3880 if c.borrow().as_deref() == Some(refname) {
3881 return Err(Error::Io(std::io::Error::other("injected reflog failure")));
3882 }
3883 Ok(())
3884 })
3885}
3886
3887#[cfg(not(test))]
3888fn test_inject_reflog_fail(_refname: &str) -> Result<()> {
3889 Ok(())
3890}
3891
3892pub struct BranchCommitRefUpdate<'a> {
3905 pub branch_ref: &'a str,
3907 pub expected_old: Option<ObjectId>,
3909 pub new_oid: ObjectId,
3911 pub identity: &'a str,
3913 pub reflog_message: &'a str,
3915}
3916
3917pub fn verify_branch_commit_cas(
3927 expected: Option<ObjectId>,
3928 current: Option<ObjectId>,
3929 refname: &str,
3930) -> Result<()> {
3931 verify_branch_cas(expected, current, refname)
3932}
3933
3934fn verify_branch_cas(
3935 expected: Option<ObjectId>,
3936 current: Option<ObjectId>,
3937 refname: &str,
3938) -> Result<()> {
3939 if expected == current {
3940 return Ok(());
3941 }
3942 Err(store::RefStoreError::ExpectedMismatch {
3943 name: refname.to_owned(),
3944 expected: expected.map_or(Expected::Missing, Expected::Oid),
3945 actual: current.map(StoreRawRef::Direct),
3946 }
3947 .into())
3948}
3949
3950fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
3951 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
3952 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
3953 let try_names: Vec<&str> = if storage_owned != stor_name {
3954 vec![storage_owned.as_str(), stor_name.as_str()]
3955 } else {
3956 vec![stor_name.as_str()]
3957 };
3958 for name in try_names {
3959 let path = store.join(name);
3960 if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
3961 return Ok(Some(oid));
3962 }
3963 if let Some(oid) = lookup_packed_ref(&store, name)? {
3964 return Ok(Some(oid));
3965 }
3966 }
3967 Ok(None)
3968}
3969
3970#[cfg(test)]
3971mod test_pause_inject_serial {
3972 use std::sync::{Mutex, OnceLock};
3973
3974 static SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
3976
3977 pub fn lock() -> std::sync::MutexGuard<'static, ()> {
3978 SERIAL
3979 .get_or_init(|| Mutex::new(()))
3980 .lock()
3981 .unwrap_or_else(|e| e.into_inner())
3982 }
3983}
3984
3985#[cfg(test)]
3987pub fn test_pause_injection_serial_guard() -> std::sync::MutexGuard<'static, ()> {
3988 test_pause_inject_serial::lock()
3989}
3990
3991#[cfg(test)]
3992mod branch_ref_pause_inject {
3993 use std::sync::{Condvar, Mutex, OnceLock};
3994
3995 pub struct PauseGate {
3996 pub wait: Mutex<bool>,
3997 pub cv: Condvar,
3998 }
3999
4000 static PAUSE_BEFORE_COMMIT: OnceLock<Mutex<Option<std::sync::Arc<PauseGate>>>> =
4002 OnceLock::new();
4003
4004 pub fn pause_slot() -> &'static Mutex<Option<std::sync::Arc<PauseGate>>> {
4005 PAUSE_BEFORE_COMMIT.get_or_init(|| Mutex::new(None))
4006 }
4007}
4008
4009#[cfg(test)]
4011pub type BranchRefPauseGate = std::sync::Arc<branch_ref_pause_inject::PauseGate>;
4012
4013#[cfg(test)]
4015pub fn set_test_inject_branch_ref_pause_before_commit(gate: Option<BranchRefPauseGate>) {
4016 *branch_ref_pause_inject::pause_slot()
4017 .lock()
4018 .unwrap_or_else(|e| e.into_inner()) = gate;
4019}
4020
4021#[cfg(test)]
4022fn test_branch_ref_pause_before_commit() {
4023 let gate = branch_ref_pause_inject::pause_slot()
4024 .lock()
4025 .unwrap_or_else(|e| e.into_inner())
4026 .clone();
4027 if let Some(gate) = gate {
4028 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
4029 *wait = true;
4030 gate.cv.notify_all();
4031 while *wait {
4032 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
4033 }
4034 }
4035}
4036
4037#[cfg(not(test))]
4038fn test_branch_ref_pause_before_commit() {}
4039
4040#[cfg(test)]
4041mod ref_store_batch_pause_inject {
4042 use std::sync::{Condvar, Mutex, OnceLock};
4043
4044 pub struct PauseGate {
4045 pub wait: Mutex<bool>,
4046 pub cv: Condvar,
4047 }
4048
4049 pub type PauseSlot = Option<(usize, std::path::PathBuf, std::sync::Arc<PauseGate>)>;
4052
4053 static PAUSE_AFTER_REF_WRITE: OnceLock<Mutex<PauseSlot>> = OnceLock::new();
4054
4055 pub fn pause_slot() -> &'static Mutex<PauseSlot> {
4056 PAUSE_AFTER_REF_WRITE.get_or_init(|| Mutex::new(None))
4057 }
4058}
4059
4060#[cfg(test)]
4062pub type RefStoreBatchPauseGate = std::sync::Arc<ref_store_batch_pause_inject::PauseGate>;
4063
4064#[cfg(test)]
4066pub fn new_ref_store_batch_pause_gate() -> RefStoreBatchPauseGate {
4067 std::sync::Arc::new(ref_store_batch_pause_inject::PauseGate {
4068 wait: std::sync::Mutex::new(false),
4069 cv: std::sync::Condvar::new(),
4070 })
4071}
4072
4073#[cfg(test)]
4078pub fn set_test_inject_ref_store_batch_pause_after_apply(
4079 apply_index: usize,
4080 git_dir: &Path,
4081 gate: Option<RefStoreBatchPauseGate>,
4082) {
4083 *ref_store_batch_pause_inject::pause_slot()
4084 .lock()
4085 .unwrap_or_else(|e| e.into_inner()) = gate.map(|g| (apply_index, git_dir.to_path_buf(), g));
4086}
4087
4088#[cfg(test)]
4089struct RefStorePauseTestSerial {
4090 holder: Option<std::thread::ThreadId>,
4092}
4093
4094#[cfg(test)]
4095static REF_STORE_PAUSE_TEST_SERIAL: std::sync::Mutex<RefStorePauseTestSerial> =
4096 std::sync::Mutex::new(RefStorePauseTestSerial { holder: None });
4097
4098#[cfg(test)]
4100pub(crate) struct RefStoreBatchPauseTestScope {
4101 _serial: std::sync::MutexGuard<'static, RefStorePauseTestSerial>,
4102}
4103
4104#[cfg(test)]
4105pub(crate) fn ref_store_batch_pause_test_scope() -> RefStoreBatchPauseTestScope {
4106 let mut serial = REF_STORE_PAUSE_TEST_SERIAL
4107 .lock()
4108 .unwrap_or_else(|e| e.into_inner());
4109 assert!(
4110 serial.holder.is_none(),
4111 "ref_store_batch_pause_test_scope must not nest"
4112 );
4113 serial.holder = Some(std::thread::current().id());
4114 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
4115 RefStoreBatchPauseTestScope { _serial: serial }
4116}
4117
4118#[cfg(test)]
4119impl Drop for RefStoreBatchPauseTestScope {
4120 fn drop(&mut self) {
4121 set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
4122 self._serial.holder = None;
4123 }
4124}
4125
4126#[cfg(test)]
4127pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
4128 let slot = ref_store_batch_pause_inject::pause_slot()
4129 .lock()
4130 .unwrap_or_else(|e| e.into_inner())
4131 .clone();
4132 if let Some((target, target_dir, gate)) = slot {
4133 let same_repo = |a: &Path, b: &Path| {
4134 a == b
4135 || matches!((a.canonicalize_simplified(), b.canonicalize_simplified()), (Ok(a), Ok(b)) if a == b)
4136 };
4137 if idx != target || !same_repo(git_dir, &target_dir) {
4138 return;
4139 }
4140 let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
4141 *wait = true;
4142 gate.cv.notify_all();
4143 while *wait {
4144 wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
4145 }
4146 }
4147}
4148
4149#[cfg(not(test))]
4150pub(crate) fn test_ref_store_batch_pause_after_ref_write(_idx: usize, _git_dir: &Path) {}
4151
4152pub struct BranchCommitRefLock {
4156 lock: Option<crate::lockfile::LockFile>,
4157 disarmed: bool,
4158}
4159
4160impl BranchCommitRefLock {
4161 pub fn acquire(
4167 git_dir: &Path,
4168 branch_ref: &str,
4169 expected_old: Option<ObjectId>,
4170 ) -> Result<Self> {
4171 ensure_refname_safe_for_storage(branch_ref)?;
4172 let storage_dir = ref_storage_dir(git_dir, branch_ref);
4173 if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
4174 return Err(Error::InvalidRef(format!(
4175 "cannot update ref '{}': reference namespace conflict",
4176 branch_ref
4177 )));
4178 }
4179 let stor = crate::ref_namespace::storage_ref_name(branch_ref);
4180 let path = storage_dir.join(&stor);
4181 remove_empty_ref_directory(&path);
4182 if fs::symlink_metadata(&path)
4183 .map(|m| m.file_type().is_dir())
4184 .unwrap_or(false)
4185 {
4186 return Err(directory_in_the_way(git_dir, branch_ref, &path));
4187 }
4188 if let Some(parent) = path.parent() {
4189 crate::git_dir::io_error::io_with_git_path(
4190 git_dir,
4191 parent,
4192 crate::git_dir::GitDirPathOperation::CreateDir,
4193 fs::create_dir_all(parent),
4194 )?;
4195 }
4196
4197 let lock_path = lock_path_for_ref(&path);
4198 use crate::lockfile::{LockError, LockFile, LockWait};
4199 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4200 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4201 held @ LockError::Held { .. } => Error::from(held),
4202 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4203 git_dir,
4204 path.as_path(),
4205 crate::git_dir::GitDirPathOperation::UpdateRef,
4206 source,
4207 ),
4208 })?;
4209
4210 let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
4211 verify_branch_cas(expected_old, current, branch_ref)?;
4212
4213 Ok(Self {
4214 lock: Some(lock),
4215 disarmed: false,
4216 })
4217 }
4218
4219 pub fn write_reflogs(
4229 &mut self,
4230 git_dir: &Path,
4231 update: &BranchCommitRefUpdate<'_>,
4232 reflog_old: &ObjectId,
4233 config: &ConfigSet,
4234 ) -> Result<()> {
4235 write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
4236 }
4237
4238 pub fn finish_ref_commit(
4246 mut self,
4247 git_dir: &Path,
4248 update: &BranchCommitRefUpdate<'_>,
4249 ) -> Result<()> {
4250 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4251 self.disarmed = true;
4252 Ok(())
4253 }
4254
4255 pub fn commit(
4261 mut self,
4262 git_dir: &Path,
4263 update: &BranchCommitRefUpdate<'_>,
4264 reflog_old: &ObjectId,
4265 config: &ConfigSet,
4266 ) -> Result<()> {
4267 write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
4268 finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
4269 self.disarmed = true;
4270 Ok(())
4271 }
4272}
4273
4274impl Drop for BranchCommitRefLock {
4275 fn drop(&mut self) {
4276 drop(self.lock.take());
4277 }
4278}
4279
4280fn write_branch_commit_reflogs(
4281 git_dir: &Path,
4282 lock: &mut BranchCommitRefLock,
4283 update: &BranchCommitRefUpdate<'_>,
4284 reflog_old: &ObjectId,
4285 config: &ConfigSet,
4286) -> Result<()> {
4287 let mut branch_reflog_written = false;
4288 if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
4289 match append_reflog_with_config(
4290 git_dir,
4291 update.branch_ref,
4292 reflog_old,
4293 &update.new_oid,
4294 update.identity,
4295 update.reflog_message,
4296 false,
4297 Some(config),
4298 ) {
4299 Ok(()) => branch_reflog_written = true,
4300 Err(err) => {
4301 drop(lock.lock.take());
4302 return Err(err);
4303 }
4304 }
4305 }
4306
4307 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4308 if let Err(err) = append_reflog_with_config(
4309 git_dir,
4310 "HEAD",
4311 reflog_old,
4312 &update.new_oid,
4313 update.identity,
4314 update.reflog_message,
4315 false,
4316 Some(config),
4317 ) {
4318 if branch_reflog_written {
4319 let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
4320 }
4321 drop(lock.lock.take());
4322 return Err(err);
4323 }
4324 }
4325
4326 Ok(())
4327}
4328
4329fn finish_branch_commit_ref_lock_files(
4330 git_dir: &Path,
4331 lock: &mut BranchCommitRefLock,
4332 update: &BranchCommitRefUpdate<'_>,
4333) -> Result<()> {
4334 test_branch_ref_pause_before_commit();
4335
4336 let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
4337 let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
4338 let path = storage_dir.join(&stor);
4339 let content = format!("{}\n", update.new_oid);
4340 use crate::lockfile::LockError;
4341 use std::io::Write as _;
4342 let mut file_lock = lock
4343 .lock
4344 .take()
4345 .ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
4346 crate::git_dir::io_error::io_with_git_path(
4347 git_dir,
4348 &path,
4349 crate::git_dir::GitDirPathOperation::UpdateRef,
4350 file_lock.write_all(content.as_bytes()),
4351 )?;
4352 file_lock.commit_to(&path).map_err(|err| match err {
4353 LockError::Io {
4354 source,
4355 path: lock_path,
4356 } => crate::git_dir::io_error::map_git_dir_io(
4357 git_dir,
4358 lock_path.as_path(),
4359 crate::git_dir::GitDirPathOperation::UpdateRef,
4360 source,
4361 ),
4362 held @ LockError::Held { .. } => Error::from(held),
4363 })?;
4364 Ok(())
4365}
4366
4367pub(crate) fn update_branch_for_commit_files(
4368 git_dir: &Path,
4369 update: &BranchCommitRefUpdate<'_>,
4370 reflog_old: &ObjectId,
4371 config: &ConfigSet,
4372) -> Result<()> {
4373 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4374 lock.commit(git_dir, update, reflog_old, config)
4375}
4376
4377pub(crate) fn update_branch_for_commit_files_with_reference_transaction_hooks(
4380 git_dir: &Path,
4381 update: &BranchCommitRefUpdate<'_>,
4382 reflog_old: &ObjectId,
4383 config: &ConfigSet,
4384 ctx: &crate::hooks::RefHookContext,
4385 lines: &[store::reference_transaction::ReferenceTransactionLine],
4386) -> Result<()> {
4387 use crate::hooks::ReferenceTransactionPhase;
4388 use store::reference_transaction::stdin_from_lines;
4389
4390 let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
4391 if lines.is_empty() {
4392 return lock.commit(git_dir, update, reflog_old, config);
4393 }
4394 let stdin = stdin_from_lines(lines);
4395 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4396 .map_err(Error::Hook)?;
4397 match lock.commit(git_dir, update, reflog_old, config) {
4398 Ok(()) => {
4399 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4400 Ok(())
4401 }
4402 Err(err) => {
4403 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4404 Err(err)
4405 }
4406 }
4407}
4408
4409pub(crate) struct DetachedHeadRefLock {
4411 lock: Option<crate::lockfile::LockFile>,
4412 disarmed: bool,
4413}
4414
4415impl DetachedHeadRefLock {
4416 fn acquire(git_dir: &Path, expected_old: Option<ObjectId>) -> Result<Self> {
4417 let current = resolve_ref(git_dir, "HEAD").ok();
4418 verify_branch_cas(expected_old, current, "HEAD")?;
4419 let path = git_dir.join("HEAD");
4420 let lock_path = lock_path_for_ref(&path);
4421 use crate::lockfile::{LockError, LockFile, LockWait};
4422 let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
4423 let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
4424 held @ LockError::Held { .. } => Error::from(held),
4425 LockError::Io { source, path } => crate::git_dir::io_error::map_git_dir_io(
4426 git_dir,
4427 path.as_path(),
4428 crate::git_dir::GitDirPathOperation::UpdateRef,
4429 source,
4430 ),
4431 })?;
4432 Ok(Self {
4433 lock: Some(lock),
4434 disarmed: false,
4435 })
4436 }
4437
4438 fn commit(
4439 mut self,
4440 git_dir: &Path,
4441 new_oid: ObjectId,
4442 reflog_old: &ObjectId,
4443 identity: &str,
4444 reflog_message: &str,
4445 config: &ConfigSet,
4446 ) -> Result<()> {
4447 let path = git_dir.join("HEAD");
4448 let content = format!("{new_oid}\n");
4449 use crate::lockfile::LockError;
4450 use std::io::Write as _;
4451 let mut file_lock = self
4452 .lock
4453 .take()
4454 .ok_or_else(|| Error::Message("HEAD ref lock already released".into()))?;
4455 crate::git_dir::io_error::io_with_git_path(
4456 git_dir,
4457 &path,
4458 crate::git_dir::GitDirPathOperation::UpdateRef,
4459 file_lock.write_all(content.as_bytes()),
4460 )?;
4461 file_lock.commit_to(&path).map_err(|err| match err {
4462 LockError::Io {
4463 source,
4464 path: lock_path,
4465 } => crate::git_dir::io_error::map_git_dir_io(
4466 git_dir,
4467 lock_path.as_path(),
4468 crate::git_dir::GitDirPathOperation::UpdateRef,
4469 source,
4470 ),
4471 held @ LockError::Held { .. } => Error::from(held),
4472 })?;
4473 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4474 if let Err(err) = append_reflog_with_config(
4475 git_dir,
4476 "HEAD",
4477 reflog_old,
4478 &new_oid,
4479 identity,
4480 reflog_message,
4481 false,
4482 Some(config),
4483 ) {
4484 let _ = write_ref_cas(git_dir, "HEAD", reflog_old, new_oid);
4485 return Err(err);
4486 }
4487 }
4488 self.disarmed = true;
4489 Ok(())
4490 }
4491}
4492
4493impl Drop for DetachedHeadRefLock {
4494 fn drop(&mut self) {
4495 drop(self.lock.take());
4496 }
4497}
4498
4499#[allow(clippy::too_many_arguments)]
4500pub(crate) fn update_detached_head_for_commit_files_with_reference_transaction_hooks(
4501 git_dir: &Path,
4502 expected_old: Option<ObjectId>,
4503 new_oid: ObjectId,
4504 identity: &str,
4505 reflog_message: &str,
4506 config: &ConfigSet,
4507 ctx: &crate::hooks::RefHookContext,
4508 lines: &[store::reference_transaction::ReferenceTransactionLine],
4509) -> Result<()> {
4510 use crate::hooks::ReferenceTransactionPhase;
4511 use store::reference_transaction::stdin_from_lines;
4512
4513 let lock = DetachedHeadRefLock::acquire(git_dir, expected_old)?;
4514 if lines.is_empty() {
4515 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4516 return lock.commit(
4517 git_dir,
4518 new_oid,
4519 &reflog_old,
4520 identity,
4521 reflog_message,
4522 config,
4523 );
4524 }
4525 let stdin = stdin_from_lines(lines);
4526 ctx.run_reference_transaction(ReferenceTransactionPhase::Prepared, &stdin)
4527 .map_err(Error::Hook)?;
4528 let reflog_old = expected_old.unwrap_or_else(|| ctx.null_oid());
4529 match lock.commit(
4530 git_dir,
4531 new_oid,
4532 &reflog_old,
4533 identity,
4534 reflog_message,
4535 config,
4536 ) {
4537 Ok(()) => {
4538 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Committed, &stdin);
4539 Ok(())
4540 }
4541 Err(err) => {
4542 let _ = ctx.run_reference_transaction(ReferenceTransactionPhase::Aborted, &stdin);
4543 Err(err)
4544 }
4545 }
4546}
4547
4548pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
4556 let config = ConfigSet::load(
4557 &crate::environment::Environment::empty(),
4558 Some(git_dir),
4559 true,
4560 )
4561 .unwrap_or_default();
4562 update_branch_for_commit_with_config(git_dir, update, &config)
4563}
4564
4565pub fn update_branch_for_commit_with_config(
4574 git_dir: &Path,
4575 update: &BranchCommitRefUpdate<'_>,
4576 config: &ConfigSet,
4577) -> Result<()> {
4578 update_branch_for_commit_on_store(&store::open_checked_ref_store(git_dir)?, update, config)
4579}
4580
4581pub(crate) fn update_detached_head_for_commit_files(
4584 git_dir: &Path,
4585 expected_old: Option<ObjectId>,
4586 new_oid: ObjectId,
4587 identity: &str,
4588 reflog_message: &str,
4589 config: &ConfigSet,
4590) -> Result<()> {
4591 let current = resolve_ref(git_dir, "HEAD").ok();
4592 verify_branch_cas(expected_old, current, "HEAD")?;
4593 let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
4594
4595 write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
4596 if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
4597 if let Err(err) = append_reflog_with_config(
4598 git_dir,
4599 "HEAD",
4600 &reflog_old,
4601 &new_oid,
4602 identity,
4603 reflog_message,
4604 false,
4605 Some(config),
4606 ) {
4607 let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
4608 return Err(err);
4609 }
4610 }
4611 Ok(())
4612}
4613
4614#[must_use]
4620pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
4621 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
4622 store.join("logs").join(stor_name)
4623}
4624
4625fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
4626 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
4627}
4628
4629fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
4638 if prefix.is_empty() {
4639 return String::new();
4640 }
4641 if prefix.ends_with('/') {
4642 return prefix.to_string();
4643 }
4644 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
4645 if candidate.is_file() {
4646 prefix.to_string()
4647 } else {
4648 format!("{prefix}/")
4649 }
4650}
4651
4652pub(crate) fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
4655 odb.peel_stored_tag_target(oid).ok().flatten()
4656}
4657
4658pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
4669 pack_imported_refs_for_clone(git_dir, Some(remote))
4670}
4671
4672pub(crate) fn pack_imported_refs_for_clone(git_dir: &Path, remote: Option<&str>) -> Result<()> {
4674 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4675 return Ok(());
4676 }
4677 let prefix = remote.map(|name| format!("refs/remotes/{name}/"));
4678 let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
4679 let odb = crate::odb::Odb::new(&storage_git_dir.join("objects"))
4680 .with_config_git_dir(git_dir.to_path_buf());
4681 odb.with_pack_read_context(|| {
4682 pack_refs_files_matching(
4683 git_dir,
4684 &odb,
4685 |name| {
4686 name.starts_with("refs/tags/")
4687 || prefix
4688 .as_ref()
4689 .is_some_and(|prefix| name.starts_with(prefix))
4690 },
4691 false,
4692 true,
4694 )
4695 })?;
4696 Ok(())
4697}
4698
4699fn atomic_rewrite_packed_refs(git_dir: &Path, body: &str) -> Result<()> {
4701 let packed_path = store::paths::packed_refs_path(git_dir);
4702 let lock = lock_path_for_ref(&packed_path);
4703 let abs_lock = git_dir
4704 .canonicalize_simplified()
4705 .map(|d| d.join("packed-refs.lock"))
4706 .unwrap_or_else(|_| lock.clone());
4707 let _packed_lock = acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?;
4708
4709 let tmp = packed_path.with_extension("new");
4710 let mut created_tmp = false;
4711 let write_result = (|| -> Result<()> {
4712 let mut file = std::fs::OpenOptions::new()
4714 .write(true)
4715 .create_new(true)
4716 .open(&tmp)
4717 .map_err(Error::Io)?;
4718 created_tmp = true;
4719 use std::io::Write as _;
4720 file.write_all(body.as_bytes()).map_err(Error::Io)?;
4721 drop(file);
4722 fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
4723 created_tmp = false;
4724 Ok(())
4725 })();
4726 if write_result.is_err() && created_tmp {
4727 let _ = fs::remove_file(&tmp);
4728 }
4729 write_result
4730}
4731
4732#[derive(Debug, Clone, PartialEq, Eq)]
4734pub struct PackedRefRecord {
4735 pub refname: String,
4737 pub oid: ObjectId,
4739 pub peeled: Option<ObjectId>,
4741}
4742
4743pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
4753 let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
4754 sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
4755 for rec in &sorted {
4756 ensure_refname_safe_for_storage(&rec.refname)?;
4757 }
4758 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
4759 for rec in &sorted {
4760 body.push_str(&rec.oid.to_hex());
4761 body.push(' ');
4762 body.push_str(&rec.refname);
4763 body.push('\n');
4764 if let Some(peeled) = rec.peeled {
4765 body.push('^');
4766 body.push_str(&peeled.to_hex());
4767 body.push('\n');
4768 }
4769 }
4770 atomic_rewrite_packed_refs(git_dir, &body)?;
4771 for rec in &sorted {
4772 let storage_dir = ref_storage_dir(git_dir, &rec.refname);
4773 let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
4774 let loose = storage_dir.join(stor);
4775 if loose.is_file() {
4776 fs::remove_file(&loose).map_err(Error::Io)?;
4777 }
4778 if let Some(parent) = loose.parent() {
4779 prune_empty_ref_dirs(&storage_dir, parent);
4780 }
4781 }
4782 Ok(())
4783}
4784
4785pub fn list_refs_for_repository(
4796 repo: &crate::repo::Repository,
4797 prefix: &str,
4798) -> Result<Vec<(String, ObjectId)>> {
4799 let prefix_norm = normalize_list_refs_prefix(repo.git_dir(), prefix);
4800 list_store_refs_with_name_prefix(repo, &prefix_norm)
4801}
4802
4803fn list_store_refs_with_name_prefix(
4805 repo: &crate::repo::Repository,
4806 prefix: &str,
4807) -> Result<Vec<(String, ObjectId)>> {
4808 let git_dir = repo.git_dir();
4809 let mut results = collect_resolved_refs(repo.refs(), prefix)?;
4810 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4811 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4812 }
4813 results.sort_by(|a, b| a.0.cmp(&b.0));
4814 Ok(results)
4815}
4816
4817pub(crate) fn list_refs_in(
4828 repo: &crate::repo::Repository,
4829 prefix: &str,
4830) -> Result<Vec<(String, ObjectId)>> {
4831 if repo.refs_on_disk() {
4832 list_refs(repo.git_dir(), prefix)
4833 } else {
4834 list_refs_for_repository(repo, prefix)
4835 }
4836}
4837
4838pub(crate) fn resolve_ref_in(repo: &crate::repo::Repository, refname: &str) -> Result<ObjectId> {
4845 if repo.refs_on_disk() {
4846 resolve_ref(repo.git_dir(), refname)
4847 } else {
4848 repo.resolve_ref_oid(refname)
4849 }
4850}
4851
4852pub(crate) fn list_refs_glob_in(
4859 repo: &crate::repo::Repository,
4860 pattern: &str,
4861) -> Result<Vec<(String, ObjectId)>> {
4862 if repo.refs_on_disk() {
4863 list_refs_glob(repo.git_dir(), pattern)
4864 } else {
4865 list_refs_glob_for_repository(repo, pattern)
4866 }
4867}
4868
4869pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4889 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4890 let prefix = prefix_norm.as_str();
4891 let store = open_ref_store(git_dir)?;
4892 if store.format() != RefStorageFormat::Files {
4893 return crate::reftable::reftable_list_refs(git_dir, prefix);
4894 }
4895 let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
4896 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4897 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4898 }
4899 results.sort_by(|a, b| a.0.cmp(&b.0));
4900 Ok(results)
4901}
4902
4903pub(crate) fn list_refs_filtering_packed(
4914 git_dir: &Path,
4915 prefix: &str,
4916 keep: PackedRecordFilter<'_>,
4917) -> Result<Vec<(String, ObjectId)>> {
4918 let Some(store) = store::open_files_ref_store(git_dir)? else {
4919 return list_refs(git_dir, prefix);
4920 };
4921 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
4922 let prefix = prefix_norm.as_str();
4923 let mut results = collect_resolved_ref_entries(&store, |f| {
4924 store.for_each_ref_filtered(prefix, Some(keep), f)
4925 })?;
4926 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
4927 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
4928 }
4929 results.sort_by(|a, b| a.0.cmp(&b.0));
4930 Ok(results)
4931}
4932
4933pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
4941 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
4942}
4943
4944pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
4954 if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
4955 return crate::reftable::reftable_list_refs(git_dir, prefix);
4956 }
4957 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
4958 let stored_prefix = prefix.to_owned();
4959
4960 if let Some(cdir) = common_dir(git_dir) {
4961 if cdir != git_dir {
4962 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
4963 let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
4964 collect_loose_refs_into_map(
4965 &loose_root,
4966 &loose_prefix,
4967 &stored_prefix,
4968 &cdir,
4969 true,
4970 &mut by_name,
4971 )?;
4972 }
4973 }
4974
4975 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
4976 let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
4977 collect_loose_refs_into_map(
4978 &loose_root,
4979 &loose_prefix,
4980 &stored_prefix,
4981 git_dir,
4982 true,
4983 &mut by_name,
4984 )?;
4985
4986 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
4987 results.sort_by(|a, b| a.0.cmp(&b.0));
4988 Ok(results)
4989}
4990
4991pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
5004 let config = ConfigSet::load(
5005 &crate::environment::Environment::empty(),
5006 Some(receiving_git_dir),
5007 true,
5008 )?;
5009 let objects_dir = receiving_git_dir.join("objects");
5010 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
5011 let mut out = Vec::new();
5012 let mut seen = std::collections::HashSet::new();
5013 for alt_objects in alternates {
5014 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
5015 continue;
5016 };
5017 if !alt_git_dir.join("refs").is_dir() {
5018 continue;
5019 }
5020 if let Some(prefixes) = config
5021 .get("core.alternaterefsprefixes")
5022 .or_else(|| config.get("core.alternateRefsPrefixes"))
5023 {
5024 for part in prefixes.split_whitespace() {
5025 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
5026 if seen.insert(oid) {
5027 out.push(oid);
5028 }
5029 continue;
5030 }
5031 for (_, oid) in list_refs(&alt_git_dir, part)? {
5032 if seen.insert(oid) {
5033 out.push(oid);
5034 }
5035 }
5036 }
5037 } else {
5038 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
5039 if seen.insert(oid) {
5040 out.push(oid);
5041 }
5042 }
5043 }
5044 }
5045 Ok(out)
5046}
5047
5048pub fn list_refs_glob_for_repository(
5050 repo: &crate::repo::Repository,
5051 pattern: &str,
5052) -> Result<Vec<(String, ObjectId)>> {
5053 let all = match pattern.find(['*', '?', '[']) {
5057 Some(pos) => list_store_refs_with_name_prefix(repo, &pattern[..pos])?,
5058 None => {
5059 let mut dir = pattern.trim_end_matches('/').to_owned();
5060 if !dir.is_empty() {
5061 dir.push('/');
5062 }
5063 list_refs_for_repository(repo, &dir)?
5064 }
5065 };
5066 Ok(all
5067 .into_iter()
5068 .filter(|(refname, _)| ref_matches_glob(refname, pattern))
5069 .collect())
5070}
5071
5072pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
5079 let glob_pos = pattern.find(['*', '?', '[']);
5080 let prefix_owned: String = match glob_pos {
5081 Some(pos) => pattern[..pos].to_owned(),
5083 None => {
5084 let mut p = pattern.trim_end_matches('/').to_owned();
5085 if !p.is_empty() {
5086 p.push('/');
5087 }
5088 p
5089 }
5090 };
5091 let prefix = prefix_owned.as_str();
5092 let all = list_refs(git_dir, prefix)?;
5093 let mut results = Vec::new();
5094 for (refname, oid) in all {
5095 if ref_matches_glob(&refname, pattern) {
5096 results.push((refname, oid));
5097 }
5098 }
5099 Ok(results)
5100}
5101
5102pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
5106 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
5108 return refname == pattern
5109 || refname.ends_with(&format!("/{pattern}"))
5110 || refname.starts_with(&format!("{pattern}/"));
5111 }
5112 glob_match(pattern, refname)
5113}
5114
5115fn glob_match(pattern: &str, text: &str) -> bool {
5116 let pat = pattern.as_bytes();
5117 let txt = text.as_bytes();
5118 let (mut pi, mut ti) = (0, 0);
5119 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
5120 while ti < txt.len() {
5121 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
5122 pi += 1;
5123 ti += 1;
5124 } else if pi < pat.len() && pat[pi] == b'*' {
5125 star_pi = pi;
5126 star_ti = ti;
5127 pi += 1;
5128 } else if star_pi != usize::MAX {
5129 pi = star_pi + 1;
5130 star_ti += 1;
5131 ti = star_ti;
5132 } else {
5133 return false;
5134 }
5135 }
5136 while pi < pat.len() && pat[pi] == b'*' {
5137 pi += 1;
5138 }
5139 pi == pat.len()
5140}
5141
5142fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
5144 let content = fs::read_to_string(path).ok()?;
5145 let content = content.trim_end_matches('\n').trim();
5146 if ObjectId::is_full_hex(content) {
5147 content.parse().ok()
5148 } else {
5149 None
5150 }
5151}
5152
5153fn collect_loose_refs_into_map(
5154 dir: &Path,
5155 prefix: &str,
5156 filter_prefix: &str,
5157 resolve_git_dir: &Path,
5158 physical_keys: bool,
5159 out: &mut HashMap<String, ObjectId>,
5160) -> Result<()> {
5161 let read = match fs::read_dir(dir) {
5162 Ok(r) => r,
5163 Err(e)
5164 if e.kind() == io::ErrorKind::NotFound
5165 || e.kind() == io::ErrorKind::NotADirectory
5166 || e.raw_os_error() == Some(libc::ENOTDIR) =>
5167 {
5168 return Ok(());
5169 }
5170 Err(e) => return Err(Error::Io(e)),
5171 };
5172
5173 for entry in read {
5174 let entry = entry?;
5175 let name = entry.file_name();
5176 let name_str = name.to_string_lossy();
5177 let refname = format!("{prefix}{name_str}");
5178 let path = entry.path();
5179 let meta = match fs::metadata(&path) {
5180 Ok(m) => m,
5181 Err(_) => continue,
5182 };
5183
5184 if meta.is_dir() {
5185 collect_loose_refs_into_map(
5186 &path,
5187 &format!("{refname}/"),
5188 filter_prefix,
5189 resolve_git_dir,
5190 physical_keys,
5191 out,
5192 )?;
5193 } else if meta.is_file() {
5194 if physical_keys {
5195 if let Some(oid) = loose_ref_file_direct_oid(&path) {
5196 out.insert(refname, oid);
5197 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
5198 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
5199 out.insert(refname, oid);
5200 }
5201 }
5202 } else {
5203 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
5204 .unwrap_or_else(|| refname.clone());
5205 if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
5206 continue;
5207 }
5208 if let Some(oid) = loose_ref_file_direct_oid(&path) {
5209 out.insert(logical, oid);
5210 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
5211 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
5212 out.insert(logical, oid);
5213 }
5214 }
5215 }
5216 }
5217 }
5218 Ok(())
5219}
5220
5221pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
5230 let inner = spec
5232 .strip_prefix("@{-")
5233 .and_then(|s| s.strip_suffix('}'))
5234 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
5235 let n: usize = inner
5236 .parse()
5237 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
5238 if n == 0 {
5239 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
5240 }
5241 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
5242 let mut count = 0usize;
5243 for entry in entries.iter().rev() {
5244 let msg = &entry.message;
5245 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
5246 count += 1;
5247 if count == n {
5248 if let Some(to_pos) = rest.find(" to ") {
5249 return Ok(rest[..to_pos].to_string());
5250 }
5251 }
5252 }
5253 }
5254 Err(Error::InvalidRef(format!(
5255 "{spec}: only {count} checkout(s) in reflog"
5256 )))
5257}
5258
5259fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
5260 let prefix = prefix.trim_end_matches('/');
5261 if prefix.is_empty() {
5262 return true;
5263 }
5264 if refname.starts_with(prefix) {
5265 return true;
5266 }
5267 if refname == prefix {
5268 return true;
5269 }
5270 false
5271}
5272
5273fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
5274 let base = prefix.trim_end_matches('/');
5275 let (head, tail) = base.rsplit_once('/')?;
5276 let mut chars: Vec<char> = tail.chars().collect();
5277 while let Some(last) = chars.pop() {
5278 if last != '\u{10ffff}' {
5279 chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
5280 return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
5281 }
5282 }
5283 None
5284}
5285
5286pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
5289 let trimmed = prefix.trim_end_matches('/');
5290 if trimmed.is_empty() {
5291 return (git_dir.join("refs"), "refs/".to_owned());
5292 }
5293 let direct = git_dir.join(trimmed);
5294 if direct.is_dir() {
5295 return (direct, format!("{trimmed}/"));
5296 }
5297 if let Some(slash) = trimmed.rfind('/') {
5298 let parent = &trimmed[..slash];
5299 return (git_dir.join(parent), format!("{parent}/"));
5300 }
5301 (git_dir.to_path_buf(), String::new())
5302}
5303
5304fn collect_packed_refs_into_map(
5305 git_dir: &Path,
5306 prefix: &str,
5307 physical_keys: bool,
5308 out: &mut HashMap<String, ObjectId>,
5309) -> Result<()> {
5310 let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
5311 for (refname, oid) in map {
5312 let key = if physical_keys {
5313 refname.clone()
5314 } else {
5315 crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
5316 };
5317 out.insert(key, oid);
5318 }
5319 Ok(())
5320}
5321
5322#[cfg(test)]
5323mod update_branch_for_commit_tests {
5324 use super::*;
5325 use crate::reflog::read_reflog;
5326 use crate::repo::init_repository;
5327 use std::sync::{Arc, Condvar, Mutex};
5328 use std::thread;
5329 use tempfile::TempDir;
5330
5331 fn sample_oid(hex: &str) -> ObjectId {
5332 hex.parse().unwrap()
5333 }
5334
5335 #[test]
5336 fn update_branch_for_commit_holds_lock_until_ref_commit() {
5337 let _pause_serial = super::test_pause_injection_serial_guard();
5338 let tmp = TempDir::new().unwrap();
5339 let repo = init_repository(
5340 tmp.path(),
5341 false,
5342 "main",
5343 None,
5344 crate::RefStorageFormat::Files,
5345 )
5346 .unwrap();
5347 let git_dir = repo.git_dir().to_path_buf();
5348 let oid1 = test_commit(&git_dir, "one");
5349 let oid2 = test_commit(&git_dir, "two");
5350 let oid3 = test_commit(&git_dir, "three");
5351 write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
5352
5353 let gate = Arc::new(branch_ref_pause_inject::PauseGate {
5354 wait: Mutex::new(false),
5355 cv: Condvar::new(),
5356 });
5357 set_test_inject_branch_ref_pause_before_commit(Some(gate.clone()));
5358
5359 let git_dir_thread = git_dir.clone();
5360 let handle = thread::spawn(move || {
5361 update_branch_for_commit(
5362 &git_dir_thread,
5363 &BranchCommitRefUpdate {
5364 branch_ref: "refs/heads/main",
5365 expected_old: Some(oid1),
5366 new_oid: oid2,
5367 identity: "T <t@e.com> 1 +0000",
5368 reflog_message: "commit: test",
5369 },
5370 )
5371 });
5372
5373 let mut wait = gate.wait.lock().unwrap();
5374 while !*wait {
5375 wait = gate.cv.wait(wait).unwrap();
5376 }
5377
5378 assert!(
5379 write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
5380 "concurrent branch update must block on the held ref lock"
5381 );
5382
5383 *wait = false;
5384 gate.cv.notify_all();
5385 drop(wait);
5386 handle.join().unwrap().unwrap();
5387 set_test_inject_branch_ref_pause_before_commit(None);
5388
5389 assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
5390 }
5391
5392 #[test]
5393 fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
5394 let tmp = TempDir::new().unwrap();
5395 let repo = init_repository(
5396 tmp.path(),
5397 false,
5398 "main",
5399 None,
5400 crate::RefStorageFormat::Reftable,
5401 )
5402 .unwrap();
5403 let git_dir = repo.git_dir().to_path_buf();
5404 let new_oid = sample_oid("3333333333333333333333333333333333333333");
5405
5406 crate::reftable::set_test_inject_reftable_transaction_fail(true);
5407 let err = update_branch_for_commit(
5408 &git_dir,
5409 &BranchCommitRefUpdate {
5410 branch_ref: "refs/heads/main",
5411 expected_old: None,
5412 new_oid,
5413 identity: "T <t@e.com> 1 +0000",
5414 reflog_message: "commit (initial): test",
5415 },
5416 )
5417 .unwrap_err();
5418 crate::reftable::set_test_inject_reftable_transaction_fail(false);
5419 assert!(!matches!(err, Error::Message(_)));
5420
5421 assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
5422 assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
5423 assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
5424 }
5425}
5426
5427#[cfg(test)]
5428mod refname_available_tests {
5429 use super::*;
5430 use std::collections::{BTreeSet, HashSet};
5431 use tempfile::tempdir;
5432
5433 #[test]
5434 fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
5435 let dir = tempdir().unwrap();
5436 let git_dir = dir.path();
5437 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5438 let items = vec![
5439 RefBatchItem {
5440 name: "refs/heads/c".to_owned(),
5441 new_oid: Some(sample_oid()),
5442 },
5443 RefBatchItem {
5444 name: "refs/heads/c/x".to_owned(),
5445 new_oid: Some(sample_oid()),
5446 },
5447 ];
5448 let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
5449 assert!(matches!(
5450 err,
5451 RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
5452 ));
5453 }
5454
5455 fn sample_oid() -> ObjectId {
5456 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5457 }
5458
5459 #[test]
5460 fn loose_parent_blocks_child_create() {
5461 let dir = tempdir().unwrap();
5462 let git_dir = dir.path();
5463 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5464 fs::write(
5465 git_dir.join("refs/1l/c"),
5466 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5467 )
5468 .unwrap();
5469 assert_eq!(
5470 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
5471 RawRefLookup::Exists
5472 );
5473 let extras = BTreeSet::from([
5474 "refs/1l/b".to_string(),
5475 "refs/1l/c/x".to_string(),
5476 "refs/1l/d".to_string(),
5477 ]);
5478 let skip = HashSet::new();
5479 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
5480 .unwrap_err();
5481 assert!(matches!(
5482 err,
5483 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5484 ));
5485 }
5486
5487 #[test]
5488 fn verify_sees_loose_ref_after_canonical_git_dir() {
5489 let dir = tempdir().unwrap();
5490 let git_dir = dir.path().join(".git");
5491 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
5492 fs::write(
5493 git_dir.join("refs/1l/c"),
5494 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5495 )
5496 .unwrap();
5497 let skip = HashSet::new();
5498 let extras = BTreeSet::new();
5499 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
5500 .unwrap_err();
5501 assert!(matches!(
5502 err,
5503 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
5504 ));
5505 }
5506
5507 fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
5508 if existing == candidate {
5509 return false;
5510 }
5511 existing
5512 .strip_prefix(candidate)
5513 .is_some_and(|rest| rest.starts_with('/'))
5514 || candidate
5515 .strip_prefix(existing)
5516 .is_some_and(|rest| rest.starts_with('/'))
5517 }
5518
5519 #[test]
5520 fn packed_namespace_index_matches_pairwise_scan() {
5521 let mut sorted: BTreeSet<String> = BTreeSet::new();
5522 for i in 0..500 {
5523 sorted.insert(format!("refs/heads/b{i:05}"));
5524 }
5525 sorted.insert("refs/heads/parent".into());
5526 sorted.insert("refs/heads/a/b".into());
5527 let candidates = [
5528 "refs/heads/b00001",
5529 "refs/heads/parent/child",
5530 "refs/heads/new",
5531 "refs/heads/a",
5532 ];
5533 for candidate in candidates {
5534 let indexed = packed_refname_namespace_conflict(&sorted, candidate);
5535 let linear = sorted
5536 .iter()
5537 .any(|existing| refname_namespace_conflicts(existing, candidate));
5538 assert_eq!(indexed, linear, "candidate {candidate}");
5539 }
5540 }
5541
5542 #[test]
5543 fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
5544 let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
5545 if !git_dir.is_dir() {
5546 return;
5547 }
5548 let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
5549 assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
5550 assert!(listed
5551 .iter()
5552 .all(|(name, _)| name.starts_with("refs/heads/b05")));
5553 }
5554
5555 #[test]
5556 fn list_refs_finds_sibling_under_parent_directory() {
5557 let dir = tempdir().unwrap();
5558 let git_dir = dir.path();
5559 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5560 fs::write(
5561 git_dir.join("refs/ns/p/x"),
5562 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5563 )
5564 .unwrap();
5565 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
5566 assert!(
5567 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
5568 "got {listed:?}"
5569 );
5570 }
5571
5572 #[test]
5573 fn verify_blocks_parent_when_child_ref_exists() {
5574 let dir = tempdir().unwrap();
5575 let git_dir = dir.path();
5576 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
5577 fs::write(
5578 git_dir.join("refs/ns/p/x"),
5579 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5580 )
5581 .unwrap();
5582 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
5583 let skip = HashSet::new();
5584 let err =
5585 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
5586 assert!(matches!(
5587 err,
5588 RefnameUnavailable::DescendantExists { ref blocking, .. }
5589 if blocking == "refs/ns/p/x"
5590 ));
5591 }
5592
5593 #[test]
5594 fn verify_blocks_parent_when_only_a_packed_child_exists() {
5595 let dir = tempdir().unwrap();
5598 let git_dir = dir.path();
5599 fs::create_dir_all(git_dir.join("refs")).unwrap();
5600 fs::write(
5601 store::paths::packed_refs_path(git_dir),
5602 "# pack-refs with: peeled fully-peeled\n\
5603 67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
5604 )
5605 .unwrap();
5606 let err = verify_refname_available_for_create(
5607 git_dir,
5608 "refs/ns/q",
5609 &BTreeSet::new(),
5610 &HashSet::new(),
5611 )
5612 .unwrap_err();
5613 assert!(matches!(
5614 err,
5615 RefnameUnavailable::DescendantExists { ref blocking, .. }
5616 if blocking == "refs/ns/q/x"
5617 ));
5618 }
5619
5620 #[test]
5621 fn verify_allows_name_whose_siblings_share_a_prefix() {
5622 let dir = tempdir().unwrap();
5623 let git_dir = dir.path();
5624 fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
5625 for name in ["v1.0.1", "v1.0-rc1"] {
5626 fs::write(
5627 git_dir.join("refs/tags").join(name),
5628 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5629 )
5630 .unwrap();
5631 }
5632 verify_refname_available_for_create(
5633 git_dir,
5634 "refs/tags/v1.0",
5635 &BTreeSet::new(),
5636 &HashSet::new(),
5637 )
5638 .expect("siblings are not descendants");
5639 }
5640
5641 #[test]
5642 fn verify_blocks_parent_git_style_nested_path() {
5643 let dir = tempdir().unwrap();
5644 let git_dir = dir.path();
5645 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
5646 fs::write(
5647 git_dir.join("refs/3l/c/x"),
5648 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5649 )
5650 .unwrap();
5651 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
5652 let skip = HashSet::new();
5653 let err =
5654 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
5655 assert!(matches!(
5656 err,
5657 RefnameUnavailable::DescendantExists { ref blocking, .. }
5658 if blocking == "refs/3l/c/x"
5659 ));
5660 }
5661
5662 #[test]
5663 fn intermediate_directory_does_not_block_nested_create() {
5664 let dir = tempdir().unwrap();
5665 let git_dir = dir.path();
5666 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
5667 fs::write(
5668 git_dir.join("refs/ns/existing"),
5669 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
5670 )
5671 .unwrap();
5672 assert_eq!(
5673 read_raw_ref(git_dir, "refs/ns").unwrap(),
5674 RawRefLookup::IsDirectory
5675 );
5676 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
5677 let skip = HashSet::new();
5678 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
5679 }
5680}
5681
5682#[cfg(test)]
5683mod ref_storage_traversal_guard_tests {
5684 use super::*;
5685 use crate::objects::ObjectId;
5686 use tempfile::tempdir;
5687
5688 const TRAVERSAL_REF: &str = "refs/heads/../../config";
5689
5690 fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
5691 let dir = tempdir().unwrap();
5692 let git_dir = dir.path().to_path_buf();
5693 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5694 fs::write(
5695 git_dir.join("config"),
5696 "[core]\nrepositoryformatversion = 0\n",
5697 )
5698 .unwrap();
5699 (dir, git_dir)
5700 }
5701
5702 fn sample_oid() -> ObjectId {
5703 "67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
5704 }
5705
5706 fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
5707 assert!(matches!(result, Err(Error::InvalidRef(_))));
5708 assert!(
5709 git_dir.join("config").is_file(),
5710 "repository config must not be touched"
5711 );
5712 }
5713
5714 #[test]
5715 fn write_ref_rejects_traversal_refname() {
5716 let (_dir, git_dir) = bare_repo_with_config();
5717 let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
5718 assert_invalid_ref_preserves_config(&git_dir, result);
5719 }
5720
5721 #[test]
5722 fn write_ref_cached_rejects_traversal_refname() {
5723 let (_dir, git_dir) = bare_repo_with_config();
5724 let mut packed = PackedRefs::load(&git_dir).unwrap();
5725 let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
5726 assert_invalid_ref_preserves_config(&git_dir, result);
5727 }
5728
5729 #[test]
5730 fn write_symbolic_ref_rejects_traversal_refname() {
5731 let (_dir, git_dir) = bare_repo_with_config();
5732 let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
5733 assert_invalid_ref_preserves_config(&git_dir, result);
5734 }
5735
5736 #[test]
5737 fn delete_ref_rejects_traversal_refname() {
5738 let (_dir, git_dir) = bare_repo_with_config();
5739 let result = delete_ref(&git_dir, TRAVERSAL_REF);
5740 assert_invalid_ref_preserves_config(&git_dir, result);
5741 }
5742
5743 #[test]
5744 fn write_ref_rejects_illegal_branch_refname() {
5745 let (_dir, git_dir) = bare_repo_with_config();
5746 for refname in [
5747 "refs/heads/bad name",
5748 "refs/heads/x..y",
5749 "refs/heads/a~b",
5750 "refs/heads/a.lock",
5751 ] {
5752 let result = write_ref(&git_dir, refname, &sample_oid());
5753 assert_invalid_ref_preserves_config(&git_dir, result);
5754 assert!(
5755 !git_dir
5756 .join(refname.strip_prefix("refs/").unwrap())
5757 .exists(),
5758 "must not write loose ref for {refname}"
5759 );
5760 }
5761 }
5762
5763 #[test]
5764 fn write_ref_accepts_refs_heads_head() {
5765 let (_dir, git_dir) = bare_repo_with_config();
5766 let commit = test_commit(&git_dir, "head-named branch");
5767 write_ref(&git_dir, "refs/heads/HEAD", &commit)
5768 .expect("refs/heads/HEAD is valid ref syntax");
5769 assert!(git_dir.join("refs/heads/HEAD").is_file());
5770 }
5771}
5772
5773#[cfg(test)]
5774mod log_refs_config_tests {
5775 use super::*;
5776 use crate::config::{ConfigFile, ConfigScope};
5777
5778 fn set(text: &str) -> ConfigSet {
5779 let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
5780 let mut set = ConfigSet::new();
5781 set.merge(&file);
5782 set
5783 }
5784
5785 #[test]
5786 fn parse_accepts_modes_and_bool_spellings() {
5787 for (raw, mode) in [
5788 ("always", LogRefsConfig::Always),
5789 (" ALWAYS ", LogRefsConfig::Always),
5790 ("true", LogRefsConfig::Normal),
5791 ("Yes", LogRefsConfig::Normal),
5792 ("on", LogRefsConfig::Normal),
5793 ("1", LogRefsConfig::Normal),
5794 ("false", LogRefsConfig::None),
5795 ("no", LogRefsConfig::None),
5796 ("OFF", LogRefsConfig::None),
5797 ("0", LogRefsConfig::None),
5798 ("never", LogRefsConfig::None),
5799 ] {
5800 assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
5801 }
5802 assert_eq!(LogRefsConfig::parse("sometimes"), None);
5803 }
5804
5805 #[test]
5806 fn effective_mode_applies_bare_default_only_when_unset() {
5807 assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
5808 assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
5809 let bare = set("[core]\n\tbare = true\n");
5810 assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
5811 let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
5812 assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
5813 assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
5814 let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
5815 assert_eq!(
5816 bare_logged.effective_log_refs_config(),
5817 LogRefsConfig::Normal
5818 );
5819 let always = set("[core]\n\tlogAllRefUpdates = always\n");
5820 assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
5821 }
5822
5823 #[test]
5824 fn autocreate_with_config_matches_mode() {
5825 let normal = set("[core]\n\tlogAllRefUpdates = true\n");
5826 let git_dir = Path::new(".git");
5827 assert!(should_autocreate_reflog_with_config(
5828 &normal,
5829 git_dir,
5830 "refs/heads/main"
5831 ));
5832 assert!(!should_autocreate_reflog_with_config(
5833 &normal,
5834 git_dir,
5835 "refs/tags/v1"
5836 ));
5837 let none = set("[core]\n\tlogAllRefUpdates = false\n");
5838 assert!(!should_autocreate_reflog_with_config(
5839 &none, git_dir, "HEAD"
5840 ));
5841 }
5842}
5843
5844#[cfg(test)]
5845mod read_raw_ref_tests {
5846 use super::*;
5847 use tempfile::tempdir;
5848
5849 #[test]
5850 fn loose_ref_file_is_exists() {
5851 let dir = tempdir().unwrap();
5852 let git_dir = dir.path();
5853 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5854 fs::write(
5855 git_dir.join("refs/heads/side"),
5856 "0000000000000000000000000000000000000000\n",
5857 )
5858 .unwrap();
5859 assert_eq!(
5860 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
5861 RawRefLookup::Exists
5862 );
5863 }
5864
5865 #[test]
5866 fn missing_ref_is_not_found() {
5867 let dir = tempdir().unwrap();
5868 let git_dir = dir.path();
5869 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5870 assert_eq!(
5871 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
5872 RawRefLookup::NotFound
5873 );
5874 }
5875
5876 #[test]
5877 fn directory_where_ref_expected_is_is_directory() {
5878 let dir = tempdir().unwrap();
5879 let git_dir = dir.path();
5880 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
5881 assert_eq!(
5882 read_raw_ref(git_dir, "refs/heads").unwrap(),
5883 RawRefLookup::IsDirectory
5884 );
5885 }
5886
5887 #[test]
5888 fn packed_ref_name_is_exists() {
5889 let dir = tempdir().unwrap();
5890 let git_dir = dir.path();
5891 fs::write(
5892 store::paths::packed_refs_path(git_dir),
5893 "# pack-refs with: peeled fully-peeled \n\
5894 0000000000000000000000000000000000000000 refs/heads/packed\n",
5895 )
5896 .unwrap();
5897 assert_eq!(
5898 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
5899 RawRefLookup::Exists
5900 );
5901 }
5902
5903 #[test]
5904 fn access_denied_on_a_directory_is_a_missing_file() {
5905 let dir = tempdir().unwrap();
5906 let ref_dir = dir.path().join("refs/heads/rel");
5907 fs::create_dir_all(&ref_dir).unwrap();
5908 let file = dir.path().join("refs/heads/main");
5909 fs::write(&file, "x\n").unwrap();
5910 let denied = || io::Error::from(io::ErrorKind::PermissionDenied);
5911
5912 assert!(io_error_is_missing_file(&denied(), &ref_dir));
5914 assert!(
5915 !io_error_is_missing_file(&denied(), &file),
5916 "an unreadable ref file is a real I/O error"
5917 );
5918 assert!(!io_error_is_missing_file(
5919 &denied(),
5920 &dir.path().join("refs/heads/none")
5921 ));
5922 for kind in [
5923 io::ErrorKind::NotFound,
5924 io::ErrorKind::IsADirectory,
5925 io::ErrorKind::NotADirectory,
5926 ] {
5927 assert!(io_error_is_missing_file(&io::Error::from(kind), &file));
5928 }
5929 assert!(io_error_is_missing_file(
5930 &io::Error::from_raw_os_error(libc::EISDIR),
5931 &file
5932 ));
5933 assert!(!io_error_is_missing_file(
5934 &io::Error::from(io::ErrorKind::InvalidData),
5935 &ref_dir
5936 ));
5937 }
5938
5939 #[test]
5940 fn reading_a_ref_directory_is_not_found() {
5941 let dir = tempdir().unwrap();
5942 let ref_dir = dir.path().join("refs/heads/rel");
5943 fs::create_dir_all(ref_dir.join("1.0")).unwrap();
5944 match read_ref_file(&ref_dir) {
5945 Err(Error::Io(e)) => assert_eq!(e.kind(), io::ErrorKind::NotFound),
5946 other => panic!("expected a missing ref, got {other:?}"),
5947 }
5948 }
5949}
5950
5951#[cfg(test)]
5952mod packed_refs_parse_tests {
5953 use super::*;
5954
5955 use tempfile::tempdir;
5956
5957 #[test]
5958 fn packed_removal_validates_the_whole_file_before_any_change() {
5959 let dir = tempdir().unwrap();
5960 let path = store::paths::packed_refs_path(dir.path());
5961 let record = "1111111111111111111111111111111111111111 refs/heads/keep\n";
5962 let malformed = [
5963 format!("{record}garbage\n").into_bytes(),
5964 format!("{record}\n").into_bytes(),
5965 format!("{record}^bad-id\n").into_bytes(),
5966 format!("{record}^{}\n^{}\n", "1".repeat(40), "2".repeat(40)).into_bytes(),
5967 format!("{record}{} refs/heads/bad..name\n", "2".repeat(40)).into_bytes(),
5968 format!("{record}# comment\r\n").into_bytes(),
5969 record.trim_end().as_bytes().to_vec(),
5970 [record.as_bytes(), b"\xff\n"].concat(),
5971 ];
5972 for content in malformed {
5973 for name in ["refs/heads/missing", "refs/heads/keep"] {
5974 fs::write(&path, &content).unwrap();
5975 assert!(
5976 remove_packed_ref(dir.path(), name).is_err(),
5977 "{name}: {content:?}"
5978 );
5979 assert_eq!(fs::read(&path).unwrap(), content);
5980 assert!(!path.with_extension("lock").exists());
5981 assert!(!dir.path().join("packed-refs.new").exists());
5982 }
5983 }
5984 }
5985
5986 #[test]
5987 fn packed_removal_without_a_match_preserves_valid_file_bytes() {
5988 let dir = tempdir().unwrap();
5989 let path = store::paths::packed_refs_path(dir.path());
5990 for hash_len in [40, 64] {
5991 let body = format!(
5992 "# pack-refs with: peeled\n# keep this comment\n{} refs/tags/tag\n^{}\n{} refs/heads/main\n",
5993 "1".repeat(hash_len), "2".repeat(hash_len), "3".repeat(hash_len)
5994 );
5995 fs::write(&path, &body).unwrap();
5996 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
5997 assert_eq!(fs::read_to_string(&path).unwrap(), body);
5998 }
5999 fs::write(&path, "").unwrap();
6000 remove_packed_ref(dir.path(), "refs/heads/topic").unwrap();
6001 assert!(fs::read(&path).unwrap().is_empty());
6002 }
6003
6004 #[test]
6005 fn parse_rejects_garbage_line() {
6006 let dir = tempdir().unwrap();
6007 let path = store::paths::packed_refs_path(dir.path());
6008 fs::write(
6009 &path,
6010 "# pack-refs with: peeled\n\
6011 0000000000000000000000000000000000000000 refs/heads/main\n\
6012 not a ref line\n",
6013 )
6014 .unwrap();
6015 let data = fs::read(&path).unwrap();
6016 assert!(matches!(
6017 parse_packed_refs_to_map(&path, &data),
6018 Err(Error::PackedRefsUnexpectedLine { .. })
6019 ));
6020 }
6021
6022 #[test]
6023 fn parse_rejects_carriage_return_bytes() {
6024 let dir = tempdir().unwrap();
6025 let path = store::paths::packed_refs_path(dir.path());
6026 fs::write(
6027 &path,
6028 "# pack-refs with: peeled\n\
6029 0000000000000000000000000000000000000000 refs/heads/main\r\n",
6030 )
6031 .unwrap();
6032 let data = fs::read(&path).unwrap();
6033 assert!(matches!(
6034 parse_packed_refs_to_map(&path, &data),
6035 Err(Error::PackedRefsUnexpectedLine { .. })
6036 ));
6037 }
6038
6039 #[test]
6040 fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
6041 let dir = tempdir().unwrap();
6042 let git_dir = dir.path();
6043 let oid = "1111111111111111111111111111111111111111";
6044 fs::write(
6045 store::paths::packed_refs_path(git_dir),
6046 format!(
6047 "# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
6048 ),
6049 )
6050 .unwrap();
6051 let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
6052 assert_eq!(listed.len(), 1);
6053 assert_eq!(listed[0].0, "refs/heads/aaa");
6054 }
6055
6056 #[test]
6057 fn parse_rejects_blank_line_and_missing_final_newline() {
6058 let dir = tempdir().unwrap();
6059 let path = store::paths::packed_refs_path(dir.path());
6060 fs::write(
6061 &path,
6062 "# pack-refs with: peeled\n\n\
6063 0000000000000000000000000000000000000000 refs/heads/main\n",
6064 )
6065 .unwrap();
6066 let data = fs::read(&path).unwrap();
6067 assert!(matches!(
6068 parse_packed_refs_to_map(&path, &data),
6069 Err(Error::PackedRefsUnexpectedLine { .. })
6070 ));
6071
6072 fs::write(
6073 &path,
6074 "# pack-refs with: peeled\n\
6075 0000000000000000000000000000000000000000 refs/heads/main",
6076 )
6077 .unwrap();
6078 let data = fs::read(&path).unwrap();
6079 assert!(matches!(
6080 parse_packed_refs_to_map(&path, &data),
6081 Err(Error::PackedRefsUnexpectedLine { .. })
6082 ));
6083 }
6084
6085 #[test]
6086 fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
6087 let dir = tempdir().unwrap();
6088 let path = store::paths::packed_refs_path(dir.path());
6089 let a = "1111111111111111111111111111111111111111";
6090 let b = "2222222222222222222222222222222222222222";
6091 let peel = "3333333333333333333333333333333333333333";
6092 let body = |header: &str| {
6093 format!(
6094 "{header}{a} refs/heads/main\n\
6095 {b} refs/heads/tagged\n\
6096 ^{peel}\n\
6097 {a} refs/tags/light\n\
6098 {b} refs/tags/v1\n\
6099 ^{peel}\n"
6100 )
6101 };
6102 let a: ObjectId = a.parse().unwrap();
6103 let b: ObjectId = b.parse().unwrap();
6104 let peel: ObjectId = peel.parse().unwrap();
6105 let cases = [
6106 (
6107 "# pack-refs with: peeled fully-peeled sorted \n",
6108 [
6109 PackedPeel::NotTag,
6110 PackedPeel::Peeled(peel),
6111 PackedPeel::NotTag,
6112 PackedPeel::Peeled(peel),
6113 ],
6114 ),
6115 (
6116 "# pack-refs with: peeled sorted \n",
6117 [
6118 PackedPeel::Unknown,
6119 PackedPeel::Unknown,
6120 PackedPeel::NotTag,
6121 PackedPeel::Peeled(peel),
6122 ],
6123 ),
6124 ("", [PackedPeel::Unknown; 4]),
6125 ];
6126 for (header, want) in cases {
6127 let data = body(header).into_bytes();
6128 let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
6129 let seen = std::cell::RefCell::new(Vec::new());
6130 let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
6131 seen.borrow_mut().push((name.to_owned(), *oid, peel));
6132 name.starts_with("refs/tags/")
6133 };
6134 let kept = view
6135 .map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
6136 .unwrap();
6137 assert_eq!(
6138 seen.into_inner(),
6139 vec![
6140 ("refs/heads/main".to_owned(), a, want[0]),
6141 ("refs/heads/tagged".to_owned(), b, want[1]),
6142 ("refs/tags/light".to_owned(), a, want[2]),
6143 ("refs/tags/v1".to_owned(), b, want[3]),
6144 ],
6145 "header {header:?}"
6146 );
6147 let mut kept: Vec<_> = kept.into_iter().collect();
6148 kept.sort();
6149 assert_eq!(
6150 kept,
6151 vec![
6152 ("refs/tags/light".to_owned(), a),
6153 ("refs/tags/v1".to_owned(), b)
6154 ]
6155 );
6156 assert_eq!(
6157 view.map_for_prefix_filtered(&path, Some("refs"), None)
6158 .unwrap(),
6159 view.map_for_prefix(&path, Some("refs")).unwrap()
6160 );
6161 }
6162
6163 let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
6165 let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
6166 assert!(matches!(
6167 view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
6168 Err(Error::PackedRefsUnexpectedLine { .. })
6169 ));
6170 }
6171
6172 #[test]
6173 fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
6174 let dir = tempdir().unwrap();
6175 let path = store::paths::packed_refs_path(dir.path());
6176 let oid = "1111111111111111111111111111111111111111";
6177 let peel = "2222222222222222222222222222222222222222";
6178 let sorted_body = format!(
6179 "# pack-refs with: peeled fully-peeled sorted\n\
6180 {oid} refs/heads/aaa\n\
6181 {oid} refs/heads/main\n\
6182 {oid} refs/heads/zzz\n\
6183 {oid} refs/tags/v1\n\
6184 ^{peel}\n"
6185 );
6186 fs::write(&path, &sorted_body).unwrap();
6187 let data = fs::read(&path).unwrap();
6188 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6189 let full = parse_packed_refs_to_map(&path, &data).unwrap();
6190 for (name, want) in &full {
6191 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
6192 }
6193 assert_eq!(
6194 lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
6195 .unwrap()
6196 .map(|(o, p)| (o, p.unwrap())),
6197 Some((oid.parse().unwrap(), peel.parse().unwrap()))
6198 );
6199
6200 let unsorted_body = format!(
6201 "# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
6202 );
6203 fs::write(&path, unsorted_body).unwrap();
6204 let data = fs::read(&path).unwrap();
6205 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6206 let full = parse_packed_refs_to_map(&path, &data).unwrap();
6207 for (name, want) in &full {
6208 assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
6209 }
6210 assert_eq!(
6211 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6212 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6213 );
6214 }
6215
6216 #[test]
6217 fn sorted_prefix_without_upper_bound_filters_names() {
6218 let dir = tempdir().unwrap();
6219 let path = store::paths::packed_refs_path(dir.path());
6220 let oid = "1111111111111111111111111111111111111111";
6221 let sorted_body = format!(
6222 "# pack-refs with: peeled fully-peeled sorted\n\
6223 {oid} refs/heads/aaa\n\
6224 {oid} refs/heads/zzz\n\
6225 {oid} refs/tags/v1\n"
6226 );
6227 fs::write(&path, sorted_body).unwrap();
6228 let data = fs::read(&path).unwrap();
6229 let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
6230 assert_eq!(
6231 view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
6232 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
6233 );
6234 assert!(
6235 !view
6236 .map_for_prefix(&path, Some("refs/heads/a"))
6237 .unwrap()
6238 .contains_key("refs/heads/zzz"),
6239 "prefix refs/heads/a must not include refs/heads/zzz"
6240 );
6241 assert_eq!(
6242 view.map_for_prefix(&path, Some("refs")).unwrap(),
6243 parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
6244 );
6245 }
6246
6247 #[test]
6248 fn sorted_prefix_scan_validates_only_its_range() {
6249 let dir = tempdir().unwrap();
6250 let path = store::paths::packed_refs_path(dir.path());
6251 let oid = "1111111111111111111111111111111111111111";
6252 let view_of =
6253 |body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
6254
6255 let bad_peel_on_branch = view_of(format!(
6256 "# pack-refs with: peeled fully-peeled sorted\n\
6257 {oid} refs/heads/main\n\
6258 ^not-hex\n\
6259 {oid} refs/heads/topic\n\
6260 {oid} refs/tags/v1\n"
6261 ))
6262 .unwrap();
6263 assert!(matches!(
6264 bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
6265 Err(Error::PackedRefsUnexpectedLine { .. })
6266 ));
6267 let tags = bad_peel_on_branch
6268 .map_for_prefix(&path, Some("refs/tags"))
6269 .unwrap();
6270 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
6271 assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
6272
6273 let blank_among_tags = view_of(format!(
6274 "# pack-refs with: peeled fully-peeled sorted\n\
6275 {oid} refs/heads/main\n\
6276 {oid} refs/tags/v1\n\
6277 \n\
6278 {oid} refs/tags/v2\n"
6279 ))
6280 .unwrap();
6281 assert_eq!(
6282 blank_among_tags
6283 .map_for_prefix(&path, Some("refs/heads"))
6284 .unwrap()
6285 .len(),
6286 1
6287 );
6288 assert!(blank_among_tags
6289 .map_for_prefix(&path, Some("refs/tags"))
6290 .is_err());
6291
6292 let no_match = blank_among_tags
6293 .map_for_prefix(&path, Some("refs/notes"))
6294 .unwrap();
6295 assert!(no_match.is_empty());
6296 }
6297
6298 #[test]
6299 fn non_utf8_record_only_breaks_reads_that_cover_it() {
6300 let dir = tempdir().unwrap();
6301 let path = store::paths::packed_refs_path(dir.path());
6302 let oid = "1111111111111111111111111111111111111111";
6303 let mut body =
6304 format!("# pack-refs with: peeled fully-peeled sorted \n{oid} refs/heads/main\n")
6305 .into_bytes();
6306 body.extend_from_slice(format!("{oid} refs/tags/v1\n{oid} refs/tags/").as_bytes());
6307 body.extend_from_slice(b"\xff\n");
6308 let view = PackedRefsFileView::from_bytes(&path, body, HashAlgo::Sha1).unwrap();
6309 assert!(view.sorted);
6310 for name in ["refs/heads/main", "refs/tags/v1"] {
6311 assert_eq!(
6312 view.lookup_oid(&path, name).unwrap(),
6313 Some(oid.parse().unwrap())
6314 );
6315 }
6316 assert_eq!(view.lookup_oid(&path, "refs/tags/v2").unwrap(), None);
6317 let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
6318 assert_eq!(heads.keys().collect::<Vec<_>>(), ["refs/heads/main"]);
6319 assert!(matches!(
6320 view.map_for_prefix(&path, Some("refs/tags")),
6321 Err(Error::PackedRefsUnexpectedLine { .. })
6322 ));
6323 assert!(view.map_for_prefix(&path, None).is_err());
6324
6325 let mut bad_header = b"# pack-refs with: \xff sorted \n".to_vec();
6326 bad_header.extend_from_slice(format!("{oid} refs/heads/main\n").as_bytes());
6327 assert!(matches!(
6328 PackedRefsFileView::from_bytes(&path, bad_header, HashAlgo::Sha1),
6329 Err(Error::PackedRefsUnexpectedLine { .. })
6330 ));
6331 }
6332
6333 #[test]
6334 fn scan_of_file_without_ref_lines_still_validates_it() {
6335 let dir = tempdir().unwrap();
6336 let path = store::paths::packed_refs_path(dir.path());
6337 let view = PackedRefsFileView::from_bytes(
6338 &path,
6339 b"# pack-refs with: peeled fully-peeled sorted\n\
6340 ^1111111111111111111111111111111111111111\n"
6341 .to_vec(),
6342 HashAlgo::Sha1,
6343 )
6344 .unwrap();
6345 for prefix in [None, Some("refs/heads")] {
6346 assert!(matches!(
6347 view.map_for_prefix(&path, prefix),
6348 Err(Error::PackedRefsUnexpectedLine { .. })
6349 ));
6350 }
6351 let header_only = PackedRefsFileView::from_bytes(
6352 &path,
6353 b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
6354 HashAlgo::Sha1,
6355 )
6356 .unwrap();
6357 assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
6358 }
6359
6360 #[test]
6361 fn view_checks_last_line_length_against_the_repository_hash() {
6362 let dir = tempdir().unwrap();
6363 let path = store::paths::packed_refs_path(dir.path());
6364 let sha1 = "1111111111111111111111111111111111111111";
6365 let sha256 = "2".repeat(64);
6366 let view =
6367 |body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
6368 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6369 let fifty_bytes = "x".repeat(49);
6370
6371 for (body, algo) in [
6372 (
6373 format!("{header}{sha1} refs/heads/main\ngarbage\n"),
6374 HashAlgo::Sha1,
6375 ),
6376 (
6377 format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
6378 HashAlgo::Sha1,
6379 ),
6380 (
6381 format!("{header}{sha1} refs/heads/main\n#\n"),
6382 HashAlgo::Sha1,
6383 ),
6384 (
6385 format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
6386 HashAlgo::Sha256,
6387 ),
6388 (format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
6389 ] {
6390 assert!(
6391 matches!(
6392 view(body.clone(), algo),
6393 Err(Error::PackedRefsUnexpectedLine { .. })
6394 ),
6395 "{algo:?}: {body:?}"
6396 );
6397 }
6398 for (body, algo) in [
6399 (header.to_owned(), HashAlgo::Sha1),
6400 (header.to_owned(), HashAlgo::Sha256),
6401 ("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
6402 (
6403 format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
6404 HashAlgo::Sha1,
6405 ),
6406 (
6407 format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
6408 HashAlgo::Sha1,
6409 ),
6410 (
6411 format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
6412 HashAlgo::Sha256,
6413 ),
6414 ] {
6415 assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
6416 }
6417 }
6418
6419 #[test]
6420 fn sha256_short_last_record_fails_branch_listing() {
6421 let dir = tempdir().unwrap();
6422 let path = store::paths::packed_refs_path(dir.path());
6423 let sha256 = "2".repeat(64);
6424 let body = format!(
6425 "# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
6426 "x".repeat(49)
6427 );
6428 assert!(
6429 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
6430 );
6431 }
6432
6433 #[test]
6434 fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
6435 let dir = tempdir().unwrap();
6436 let path = store::paths::packed_refs_path(dir.path());
6437 let oid = "1111111111111111111111111111111111111111";
6438 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6439 let view = |body: String| {
6440 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6441 };
6442
6443 let garbage_after_first_branch = view(format!(
6444 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
6445 {oid} refs/tags/t1\n"
6446 ));
6447 assert!(matches!(
6448 garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
6449 Err(Error::PackedRefsUnexpectedLine { .. })
6450 ));
6451 let tags = garbage_after_first_branch
6452 .map_for_prefix(&path, Some("refs/tags"))
6453 .unwrap();
6454 assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
6455
6456 let garbage_before_range = view(format!(
6457 "{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
6458 ));
6459 for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
6460 assert!(
6461 garbage_before_range
6462 .map_for_prefix(&path, Some(prefix))
6463 .is_err(),
6464 "{prefix}"
6465 );
6466 }
6467 assert_eq!(
6468 garbage_before_range
6469 .map_for_prefix(&path, Some("refs/zz"))
6470 .unwrap()
6471 .len(),
6472 0
6473 );
6474
6475 let peeled_tag_before_range = view(format!(
6476 "{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
6477 ));
6478 let orphan_peel_before_range = view(format!(
6479 "{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
6480 ));
6481 assert!(orphan_peel_before_range
6482 .map_for_prefix(&path, Some("refs/tags"))
6483 .is_err());
6484 assert_eq!(
6485 peeled_tag_before_range
6486 .map_for_prefix(&path, Some("refs/tags"))
6487 .unwrap()
6488 .len(),
6489 1
6490 );
6491 }
6492
6493 fn lying_and_unsorted_views(path: &Path) -> (PackedRefsFileView, PackedRefsFileView) {
6495 let oid = "1111111111111111111111111111111111111111";
6496 let records = format!("{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n");
6497 let view_of = |header: &str| {
6498 PackedRefsFileView::from_bytes(
6499 path,
6500 format!("{header}{records}").into_bytes(),
6501 HashAlgo::Sha1,
6502 )
6503 .unwrap()
6504 };
6505 (
6506 view_of("# pack-refs with: peeled fully-peeled sorted\n"),
6507 view_of("# pack-refs with: peeled fully-peeled\n"),
6508 )
6509 }
6510
6511 fn sorted_names(map: HashMap<String, ObjectId>) -> Vec<String> {
6512 let mut names: Vec<String> = map.into_keys().collect();
6513 names.sort();
6514 names
6515 }
6516
6517 #[test]
6518 fn full_scans_of_a_lying_sorted_file_return_every_ref() {
6519 let dir = tempdir().unwrap();
6520 let path = store::paths::packed_refs_path(dir.path());
6521 let (lying, unsorted) = lying_and_unsorted_views(&path);
6522 assert!(lying.sorted);
6523 let every = ["refs/heads/a", "refs/heads/b", "refs/tags/t"];
6524 for view in [&lying, &unsorted] {
6525 for prefix in [None, Some("refs"), Some("refs/")] {
6526 assert_eq!(
6527 sorted_names(view.map_for_prefix(&path, prefix).unwrap()),
6528 every,
6529 "{prefix:?}"
6530 );
6531 }
6532 }
6533 fs::write(&path, lying.data.as_ref() as &[u8]).unwrap();
6534 let loaded = PackedRefs::load(dir.path()).unwrap();
6535 for name in every {
6536 assert!(loaded.get(name).is_some(), "{name}");
6537 }
6538 }
6539
6540 #[test]
6541 fn garbage_between_sorted_records_rejects_listing() {
6542 let dir = tempdir().unwrap();
6543 let git_dir = dir.path();
6544 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
6545 let oid = "1111111111111111111111111111111111111111";
6546 fs::write(
6547 store::paths::packed_refs_path(git_dir),
6548 format!(
6549 "# pack-refs with: sorted\n{oid} refs/heads/a\nthis is not a valid packed-refs line\n{oid} refs/heads/b\n"
6550 ),
6551 )
6552 .unwrap();
6553 assert!(matches!(
6554 list_refs(git_dir, "refs/"),
6555 Err(Error::PackedRefsUnexpectedLine { .. })
6556 ));
6557 }
6558
6559 #[test]
6560 fn for_each_record_streaming_matches_map_and_stops_early() {
6561 let dir = tempdir().unwrap();
6562 let path = store::paths::packed_refs_path(dir.path());
6563 let oid = "1111111111111111111111111111111111111111";
6564 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6565 let body = format!("{header}{oid} refs/heads/a\n{oid} refs/heads/b\n{oid} refs/tags/t\n");
6566 let view =
6567 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap();
6568 let mut streamed = Vec::new();
6569 let _ = view
6570 .for_each_record(&path, Some("refs/heads"), None, |name, _, _| {
6571 streamed.push(name.to_owned());
6572 ControlFlow::Continue(())
6573 })
6574 .unwrap();
6575 assert_eq!(
6576 streamed,
6577 sorted_names(view.map_for_prefix(&path, Some("refs/heads")).unwrap())
6578 );
6579 let mut count = 0;
6580 assert!(view
6581 .for_each_record(&path, Some("refs/heads"), None, |_, _, _| {
6582 count += 1;
6583 ControlFlow::Break(())
6584 })
6585 .unwrap()
6586 .is_break());
6587 assert_eq!(count, 1);
6588 }
6589
6590 #[test]
6591 fn for_each_record_break_before_corrupt_tail_ok_while_full_scan_errors() {
6592 let dir = tempdir().unwrap();
6593 let path = store::paths::packed_refs_path(dir.path());
6594 let oid = "1111111111111111111111111111111111111111";
6595 let good_only = format!(
6596 "# pack-refs with: peeled fully-peeled sorted\n{oid} refs/heads/good\n{oid} refs/heads/good2\n"
6597 );
6598 let with_corrupt_tail = format!(
6599 "# pack-refs with: peeled fully-peeled sorted\n{oid} refs/heads/good\n{oid} refs/heads/good2\n{oid} refs/heads/bad..name\n"
6600 );
6601 let view =
6602 PackedRefsFileView::from_bytes(&path, good_only.into_bytes(), HashAlgo::Sha1).unwrap();
6603 assert!(view
6604 .for_each_record(&path, None, None, |_, _, _| ControlFlow::Break(()))
6605 .unwrap()
6606 .is_break());
6607 let view =
6608 PackedRefsFileView::from_bytes(&path, with_corrupt_tail.into_bytes(), HashAlgo::Sha1)
6609 .unwrap();
6610 assert!(view
6611 .for_each_record(&path, None, None, |_, _, _| ControlFlow::Continue(()))
6612 .is_err());
6613 }
6614
6615 #[test]
6616 fn prefix_scan_over_an_out_of_order_range_falls_back_to_the_whole_file() {
6617 let dir = tempdir().unwrap();
6618 let path = store::paths::packed_refs_path(dir.path());
6619 let (lying, _) = lying_and_unsorted_views(&path);
6620 let heads = lying.map_for_prefix(&path, Some("refs/heads")).unwrap();
6621 assert_eq!(sorted_names(heads), ["refs/heads/a", "refs/heads/b"]);
6622
6623 let oid = "1111111111111111111111111111111111111111";
6625 let view_of = |names: &[&str]| {
6626 let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
6627 for name in names {
6628 body.push_str(&format!("{oid} {name}\n"));
6629 }
6630 PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
6631 };
6632 let interleaved = view_of(&["refs/heads/a", "refs/tags/t", "refs/heads/b", "refs/tags/u"]);
6633 let (begin, end) = interleaved.prefix_region("refs/heads").unwrap();
6634 let (_, in_order) = interleaved
6635 .parse_region::<HashMap<_, _>>(&path, begin, end, Some("refs/heads"), None)
6636 .unwrap();
6637 assert!(!in_order);
6638 assert_eq!(
6639 sorted_names(
6640 interleaved
6641 .map_for_prefix(&path, Some("refs/heads"))
6642 .unwrap()
6643 ),
6644 ["refs/heads/a", "refs/heads/b"]
6645 );
6646
6647 let bad_neighbour = view_of(&["refs/heads/x", "refs/heads/a"]);
6650 assert_eq!(bad_neighbour.prefix_region("refs/heads/a"), None);
6651 assert_eq!(
6652 sorted_names(
6653 bad_neighbour
6654 .map_for_prefix(&path, Some("refs/heads/a"))
6655 .unwrap()
6656 ),
6657 ["refs/heads/a"]
6658 );
6659
6660 let corrupt = PackedRefsFileView::from_bytes(
6662 &path,
6663 format!(
6664 "# pack-refs with: peeled fully-peeled sorted\n\
6665 {oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/bad..name\n"
6666 )
6667 .into_bytes(),
6668 HashAlgo::Sha1,
6669 )
6670 .unwrap();
6671 assert!(matches!(
6672 corrupt.map_for_prefix(&path, Some("refs/heads")),
6673 Err(Error::PackedRefsUnexpectedLine { .. })
6674 ));
6675 }
6676
6677 #[test]
6678 fn full_map_keeps_every_name_and_the_last_duplicate_value() {
6679 let dir = tempdir().unwrap();
6680 let path = store::paths::packed_refs_path(dir.path());
6681 let first = "1111111111111111111111111111111111111111";
6682 let last = "2222222222222222222222222222222222222222";
6683 for header in ["# pack-refs with: sorted\n", "# pack-refs with: peeled\n"] {
6684 for names in [
6685 ["refs/heads/a", "refs/heads/b", "refs/heads/b"],
6686 ["refs/heads/b", "refs/heads/a", "refs/heads/b"],
6687 ] {
6688 let body = format!(
6689 "{header}{first} {}\n{first} {}\n{last} {}\n",
6690 names[0], names[1], names[2]
6691 );
6692 let view = PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1)
6693 .unwrap();
6694 let full = view.full_map(&path).unwrap();
6695 assert_eq!(full.len(), 2);
6696 assert_eq!(full["refs/heads/b"], last.parse().unwrap());
6697 assert_eq!(
6698 full.into_iter().collect::<HashMap<_, _>>(),
6699 view.map_for_prefix(&path, None).unwrap()
6700 );
6701 }
6702 }
6703 }
6704
6705 #[test]
6706 fn prefix_scan_validates_both_neighbouring_records_and_their_peels() {
6707 let dir = tempdir().unwrap();
6708 let path = store::paths::packed_refs_path(dir.path());
6709 let oid = "1111111111111111111111111111111111111111";
6710 let header = "# pack-refs with: peeled fully-peeled sorted\n";
6711 for neighbour in ["refs/heads/a", "refs/tags/z"] {
6712 for corrupt in [
6713 format!("{oid} {neighbour}..bad\n"),
6714 format!("{} {neighbour}\n", "g".repeat(40)),
6715 format!("{oid} {neighbour}\n^{}\n", "g".repeat(40)),
6716 ] {
6717 let records = if neighbour.starts_with("refs/heads/") {
6718 format!("{corrupt}{oid} refs/heads/m\n{oid} refs/tags/z\n")
6719 } else {
6720 format!("{oid} refs/heads/a\n{oid} refs/heads/m\n{corrupt}")
6721 };
6722 let view = PackedRefsFileView::from_bytes(
6723 &path,
6724 format!("{header}{records}").into_bytes(),
6725 HashAlgo::Sha1,
6726 )
6727 .unwrap();
6728 assert!(
6729 matches!(
6730 view.map_for_prefix(&path, Some("refs/heads/m")),
6731 Err(Error::PackedRefsUnexpectedLine { .. })
6732 ),
6733 "{records}"
6734 );
6735 }
6736 }
6737 }
6738
6739 #[test]
6742 fn point_lookup_in_a_lying_sorted_file_behaves_like_git() {
6743 let dir = tempdir().unwrap();
6744 let path = store::paths::packed_refs_path(dir.path());
6745 let (lying, unsorted) = lying_and_unsorted_views(&path);
6746 assert!(lying.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
6748 assert_eq!(lying.lookup_oid(&path, "refs/heads/b").unwrap(), None);
6749 assert!(lying.lookup_oid(&path, "refs/tags/t").unwrap().is_some());
6750
6751 assert!(!unsorted.sorted);
6752 for name in ["refs/heads/a", "refs/heads/b", "refs/tags/t"] {
6753 assert!(
6754 unsorted.lookup_oid(&path, name).unwrap().is_some(),
6755 "{name}"
6756 );
6757 }
6758 }
6759
6760 #[test]
6761 fn records_are_only_record_shaped_lines() {
6762 let dir = tempdir().unwrap();
6763 let path = store::paths::packed_refs_path(dir.path());
6764 let view = |data: &[u8]| {
6765 PackedRefsFileView::from_bytes(&path, data.to_vec(), HashAlgo::Sha1).unwrap()
6766 };
6767 let hex = "1".repeat(40);
6768 let data = format!("# header\n{hex} x\n^peel\n\n{hex} y\n#c\nshort\n{hex}z\n{hex} z\n");
6769 let starts: Vec<usize> = view(data.as_bytes()).records().collect();
6770 assert_eq!(starts, [9, 59, 153]);
6771 assert_eq!(view(b"").records().count(), 0);
6772 let out_of_order = format!("{hex} b\n{hex} a\n");
6773 assert_eq!(
6774 view(out_of_order.as_bytes()).records().collect::<Vec<_>>(),
6775 [0, 43]
6776 );
6777 }
6778
6779 #[test]
6780 fn parse_rejects_orphan_peel_and_double_space() {
6781 let dir = tempdir().unwrap();
6782 let path = store::paths::packed_refs_path(dir.path());
6783 fs::write(
6784 &path,
6785 "# pack-refs with: peeled\n\
6786 ^0000000000000000000000000000000000000000\n\
6787 0000000000000000000000000000000000000000 refs/heads/main\n",
6788 )
6789 .unwrap();
6790 let data = fs::read(&path).unwrap();
6791 assert!(matches!(
6792 parse_packed_refs_to_map(&path, &data),
6793 Err(Error::PackedRefsUnexpectedLine { .. })
6794 ));
6795
6796 fs::write(
6797 &path,
6798 "# pack-refs with: peeled\n\
6799 0000000000000000000000000000000000000000 refs/heads/main\n",
6800 )
6801 .unwrap();
6802 let data = fs::read(&path).unwrap();
6803 assert!(matches!(
6804 parse_packed_refs_to_map(&path, &data),
6805 Err(Error::PackedRefsUnexpectedLine { .. })
6806 ));
6807 }
6808}
6809
6810#[cfg(test)]
6811mod pack_refs_tests {
6812 use super::*;
6813 use crate::objects::{serialize_commit, CommitData, ObjectKind};
6814 use crate::repo::{init_repository, Repository};
6815 use std::fs;
6816 use std::sync::Arc;
6817
6818 fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
6819 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
6820 let raw = serialize_commit(&CommitData {
6821 tree,
6822 parents,
6823 author: "T <t@example.com> 1 +0000".into(),
6824 committer: "T <t@example.com> 1 +0000".into(),
6825 author_raw: Vec::new(),
6826 committer_raw: Vec::new(),
6827 encoding: None,
6828 message: "m\n".into(),
6829 raw_message: None,
6830 extra_headers: Vec::new(),
6831 });
6832 repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
6833 }
6834
6835 #[test]
6836 fn pack_refs_skips_concurrent_loose_update() {
6837 let dir = tempfile::tempdir().expect("tempdir");
6838 let repo = init_repository(
6839 dir.path(),
6840 false,
6841 "main",
6842 None,
6843 crate::RefStorageFormat::Files,
6844 )
6845 .expect("init");
6846 let git_dir = repo.git_dir().to_path_buf();
6847 let c1 = write_commit(&repo, vec![]);
6848 let c2 = write_commit(&repo, vec![c1]);
6849 write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
6850
6851 let git_dir_hook = git_dir.clone();
6852 let c2 = Arc::new(c2);
6853 let c2_hook = Arc::clone(&c2);
6854 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
6855 write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
6856 })));
6857
6858 let (_path, packed, pruned) =
6859 pack_refs_files(&git_dir, repo.odb(), true, false).expect("pack");
6860 pack_refs_test_hooks::set_before_prune(None);
6861 assert!(packed >= 1);
6862 assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
6863
6864 let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
6865 assert_eq!(resolved, *c2);
6866 let packed_body =
6867 fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
6868 assert!(
6869 !packed_body.contains("refs/heads/race"),
6870 "packed entry for concurrently updated ref must be removed"
6871 );
6872 }
6873
6874 #[test]
6875 fn clone_packing_retains_symbolic_refs_and_concurrent_tag_updates() {
6876 let dir = tempfile::tempdir().expect("tempdir");
6877 let repo = init_repository(
6878 dir.path(),
6879 false,
6880 "main",
6881 None,
6882 crate::RefStorageFormat::Files,
6883 )
6884 .expect("init");
6885 let git_dir = repo.git_dir().to_path_buf();
6886 let first = write_commit(&repo, vec![]);
6887 let second = write_commit(&repo, vec![first]);
6888 write_ref(&git_dir, "refs/tags/race", &first).expect("tag");
6889 write_ref(&git_dir, "refs/heads/main", &first).expect("branch");
6890 write_ref(&git_dir, "refs/remotes/origin/main", &first).expect("remote");
6891 write_symbolic_ref(
6892 &git_dir,
6893 "refs/remotes/origin/HEAD",
6894 "refs/remotes/origin/main",
6895 )
6896 .expect("symbolic");
6897 let tag = repo
6898 .odb()
6899 .write(
6900 ObjectKind::Tag,
6901 format!("object {first}\ntype commit\n\nremote tag\n").as_bytes(),
6902 )
6903 .expect("tag object");
6904 write_ref(&git_dir, "refs/remotes/origin/tag", &tag).expect("tagged remote");
6905 let hook_dir = git_dir.clone();
6906 pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
6907 write_ref(&hook_dir, "refs/tags/race", &second).expect("concurrent update");
6908 })));
6909 pack_remote_tracking_refs_for_clone(&git_dir, "origin").expect("pack");
6910 assert_eq!(
6911 resolve_ref(&git_dir, "refs/tags/race").expect("resolve"),
6912 second
6913 );
6914 assert!(git_dir.join("refs/heads/main").exists());
6915 assert!(git_dir.join("refs/remotes/origin/HEAD").exists());
6916 assert!(!git_dir.join("refs/remotes/origin/main").exists());
6917 let body = fs::read_to_string(store::paths::packed_refs_path(&git_dir)).expect("packed");
6918 assert!(!body.contains("refs/tags/race"));
6919 assert!(!body.contains("refs/remotes/origin/HEAD"));
6920 assert!(
6921 body.contains(&format!("{tag} refs/remotes/origin/tag\n^{first}\n")),
6922 "{body}"
6923 );
6924 }
6925
6926 #[test]
6927 fn clone_packing_routes_reftable_without_creating_packed_refs() {
6928 let dir = tempfile::tempdir().expect("tempdir");
6929 let repo = init_repository(
6930 dir.path(),
6931 false,
6932 "main",
6933 None,
6934 crate::RefStorageFormat::Reftable,
6935 )
6936 .expect("init");
6937 let commit = write_commit(&repo, vec![]);
6938 write_ref(repo.git_dir(), "refs/tags/tip", &commit).expect("tag");
6939 pack_remote_tracking_refs_for_clone(repo.git_dir(), "origin").expect("pack");
6940 assert_eq!(
6941 resolve_ref(repo.git_dir(), "refs/tags/tip").expect("resolve"),
6942 commit
6943 );
6944 assert!(!store::paths::packed_refs_path(repo.git_dir()).exists());
6945 }
6946
6947 #[test]
6948 fn pack_refs_nested_tag_peel_line_points_at_commit() {
6949 let dir = tempfile::tempdir().expect("tempdir");
6950 let repo = init_repository(
6951 dir.path(),
6952 false,
6953 "main",
6954 None,
6955 crate::RefStorageFormat::Files,
6956 )
6957 .expect("init");
6958 let commit = write_commit(&repo, vec![]);
6959 let inner_tag_body = format!(
6960 "object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
6961 commit.to_hex()
6962 );
6963 let inner = repo
6964 .odb()
6965 .write(ObjectKind::Tag, inner_tag_body.as_bytes())
6966 .expect("inner tag");
6967 let outer_tag_body = format!(
6968 "object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
6969 inner.to_hex()
6970 );
6971 let outer = repo
6972 .odb()
6973 .write(ObjectKind::Tag, outer_tag_body.as_bytes())
6974 .expect("outer tag");
6975 write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
6976
6977 pack_refs_files(repo.git_dir(), repo.odb(), false, false).expect("pack");
6978 let body =
6979 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
6980 let mut lines = body.lines();
6981 let tag_line = lines
6982 .find(|l| l.contains("refs/tags/outer"))
6983 .expect("tag line");
6984 assert!(tag_line.starts_with(&outer.to_hex()));
6985 let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
6986 assert!(
6987 peel_line.contains(&commit.to_hex()),
6988 "peel must reach commit through nested tags, got {peel_line}"
6989 );
6990 }
6991
6992 #[test]
6993 fn pack_refs_tree_target_tag_has_peel_line() {
6994 let dir = tempfile::tempdir().expect("tempdir");
6995 let repo = init_repository(
6996 dir.path(),
6997 false,
6998 "main",
6999 None,
7000 crate::RefStorageFormat::Files,
7001 )
7002 .expect("init");
7003 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
7004 let tag_body = format!(
7005 "object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
7006 tree.to_hex()
7007 );
7008 let tag = repo
7009 .odb()
7010 .write(ObjectKind::Tag, tag_body.as_bytes())
7011 .expect("tag");
7012 write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
7013
7014 pack_refs_files(repo.git_dir(), repo.odb(), true, false).expect("pack");
7015 let body =
7016 fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
7017 assert!(body.contains("refs/tags/tree-tag"));
7018 assert!(
7019 body.contains(&format!("^{}", tree.to_hex())),
7020 "expected peel to tree: {body}"
7021 );
7022 }
7023
7024 #[test]
7025 fn write_packed_refs_file_survives_publish_failure() {
7026 let dir = tempfile::tempdir().expect("tempdir");
7027 let git_dir = dir.path();
7028 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
7029 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7030 fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
7031 fs::create_dir_all(store::paths::packed_refs_path(git_dir))
7032 .expect("block packed-refs file");
7033 let err = write_packed_refs_file(
7034 git_dir,
7035 &[PackedRefRecord {
7036 refname: "refs/heads/keep".into(),
7037 oid,
7038 peeled: None,
7039 }],
7040 )
7041 .expect_err("publish should fail");
7042 assert!(matches!(err, Error::Io(_)));
7043 assert!(git_dir.join("refs/heads/keep").is_file());
7044 }
7045
7046 #[test]
7047 fn write_packed_refs_file_replaces_loose_and_sorts() {
7048 let dir = tempfile::tempdir().expect("tempdir");
7049 let git_dir = dir.path();
7050 fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
7051 let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
7052 let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
7053 fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
7054 write_packed_refs_file(
7055 git_dir,
7056 &[
7057 PackedRefRecord {
7058 refname: "refs/heads/z-last".into(),
7059 oid: oid_b,
7060 peeled: None,
7061 },
7062 PackedRefRecord {
7063 refname: "refs/heads/a-first".into(),
7064 oid: oid_a,
7065 peeled: None,
7066 },
7067 ],
7068 )
7069 .expect("write packed-refs");
7070 assert!(!git_dir.join("refs/heads/a-first").exists());
7071 let body =
7072 fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
7073 assert!(body.contains("refs/heads/a-first"));
7074 assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
7075 }
7076
7077 #[test]
7078 fn write_packed_refs_file_keeps_refs_dir_for_top_level_ref() {
7079 let dir = tempfile::tempdir().expect("tempdir");
7080 let git_dir = dir.path();
7081 fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
7082 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7083 fs::write(git_dir.join("refs/stash"), format!("{oid}\n")).expect("loose stash");
7084 write_packed_refs_file(
7085 git_dir,
7086 &[PackedRefRecord {
7087 refname: "refs/stash".into(),
7088 oid,
7089 peeled: None,
7090 }],
7091 )
7092 .expect("write packed-refs");
7093 assert!(!git_dir.join("refs/stash").exists());
7094 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
7095 assert!(git_dir.join("refs/tags").is_dir());
7096 }
7097
7098 #[test]
7099 fn write_packed_refs_file_prunes_emptied_ref_dirs_below_refs() {
7100 let dir = tempfile::tempdir().expect("tempdir");
7101 let git_dir = dir.path();
7102 fs::create_dir_all(git_dir.join("refs/heads/topic/empty")).expect("refs/heads");
7103 let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7104 fs::write(git_dir.join("refs/heads/topic/a"), format!("{oid}\n")).expect("loose ref");
7105 write_packed_refs_file(
7106 git_dir,
7107 &[PackedRefRecord {
7108 refname: "refs/heads/topic/a".into(),
7109 oid,
7110 peeled: None,
7111 }],
7112 )
7113 .expect("write packed-refs");
7114 assert!(!git_dir.join("refs/heads/topic/a").exists());
7115 assert!(!git_dir.join("refs/heads/topic").exists());
7116 assert!(git_dir.join("refs/heads").is_dir(), "refs/heads/ kept");
7117 assert!(git_dir.join("refs").is_dir(), "refs/ must remain");
7118 }
7119
7120 #[test]
7121 fn pack_refs_files_keeps_refs_dir_after_packing_stash() {
7122 let dir = tempfile::tempdir().expect("tempdir");
7123 let repo = init_repository(
7124 dir.path(),
7125 false,
7126 "main",
7127 None,
7128 crate::RefStorageFormat::Files,
7129 )
7130 .expect("init");
7131 let blob = repo.odb().write(ObjectKind::Blob, b"x").expect("blob");
7132 write_ref(repo.git_dir(), "refs/stash", &blob).expect("stash");
7133 let refs = repo.git_dir().join("refs");
7134 pack_refs_files(repo.git_dir(), repo.odb(), true, false).expect("pack");
7135 assert!(!refs.join("stash").exists());
7136 assert!(refs.is_dir(), "refs/ must remain");
7137 }
7138}
7139
7140#[cfg(test)]
7141mod packed_ref_peels_tests {
7142 use super::*;
7143
7144 fn oid(n: u8) -> ObjectId {
7145 ObjectId::from_bytes(&[n; 20]).expect("oid")
7146 }
7147
7148 fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
7149 let dir = tempfile::tempdir().expect("tempdir");
7150 fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
7151 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7152 (dir, peels)
7153 }
7154
7155 fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
7156 let mut out = String::from(header);
7157 for (name, oid, peel) in records {
7158 out.push_str(&format!("{oid} {name}\n"));
7159 if let Some(peel) = peel {
7160 out.push_str(&format!("^{peel}\n"));
7161 }
7162 }
7163 out
7164 }
7165
7166 #[test]
7167 fn fully_peeled_file_answers_for_every_ref() {
7168 for header in [
7169 "# pack-refs with: peeled fully-peeled sorted \n",
7170 "# pack-refs with: peeled fully-peeled \n",
7171 ] {
7172 let records = [
7173 ("refs/heads/main", oid(1), None),
7174 ("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
7175 ("refs/tags/v1", oid(4), Some(oid(1))),
7176 ("refs/tags/light", oid(1), None),
7177 ];
7178 let mut ordered = records;
7180 if header.contains(" sorted") {
7181 ordered.sort_by_key(|(name, _, _)| *name);
7182 } else {
7183 ordered.reverse();
7184 }
7185 let (_dir, peels) = peels_for(&body(header, &ordered));
7186 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
7187 assert_eq!(
7188 peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
7189 Some(Some(oid(3)))
7190 );
7191 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
7192 assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
7193 assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
7195 assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
7196 }
7197 }
7198
7199 #[test]
7200 fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
7201 let records = [
7202 ("refs/heads/main", oid(1), None),
7203 ("refs/tags/v1", oid(4), Some(oid(1))),
7204 ];
7205 let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
7206 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7207 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
7208
7209 for header in [
7210 "",
7211 "# pack-refs with: sorted\n",
7212 "# some comment\n",
7213 "#pack-refs with: peeled fully-peeled\n",
7214 "# pack-refs with: peeled fully-peeled\n",
7215 ] {
7216 let (_dir, peels) = peels_for(&body(header, &records));
7217 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
7218 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
7219 }
7220 }
7221
7222 #[test]
7223 fn missing_file_and_other_backends_know_nothing() {
7224 let dir = tempfile::tempdir().expect("tempdir");
7225 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7226 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7227
7228 let records = [("refs/heads/main", oid(1), None)];
7229 fs::write(
7230 store::paths::packed_refs_path(dir.path()),
7231 body("# pack-refs with: peeled fully-peeled sorted\n", &records),
7232 )
7233 .expect("packed-refs");
7234 let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
7235 assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
7236 }
7237
7238 #[test]
7239 fn lookups_in_any_order_agree_with_a_fresh_snapshot() {
7240 let records = [
7241 ("refs/heads/main", oid(1), None),
7242 ("refs/tags/a", oid(2), Some(oid(1))),
7243 ("refs/tags/b", oid(1), None),
7244 ("refs/tags/c", oid(3), Some(oid(4))),
7245 ("refs/tags/d", oid(5), None),
7246 ];
7247 let text = body("# pack-refs with: peeled fully-peeled sorted \n", &records);
7248 let (_dir, peels) = peels_for(&text);
7249 let fresh = |name: &str, value: &ObjectId| peels_for(&text).1.peeled(name, value);
7250 let queries = [
7251 ("refs/tags/a", oid(2)),
7252 ("refs/tags/b", oid(1)),
7253 ("refs/tags/b", oid(1)),
7254 ("refs/tags/bb", oid(1)),
7255 ("refs/tags/c", oid(3)),
7256 ("refs/tags/d", oid(9)),
7257 ("refs/tags/d", oid(5)),
7258 ("refs/heads/main", oid(1)),
7259 ("refs/tags/c", oid(3)),
7260 ("refs/tags/a", oid(2)),
7261 ("refs/tags/zzz", oid(1)),
7262 ("refs/tags/a", oid(2)),
7263 ("refs/tags/b", oid(1)),
7264 ];
7265 for (name, value) in queries {
7266 assert_eq!(
7267 peels.peeled(name, &value),
7268 fresh(name, &value),
7269 "{name} {value}"
7270 );
7271 }
7272 assert_eq!(peels.peeled("refs/tags/c", &oid(3)), Some(Some(oid(4))));
7273 assert_eq!(peels.peeled("refs/tags/d", &oid(5)), Some(None));
7274 }
7275
7276 #[test]
7277 fn store_snapshot_reuses_the_files_store_view_and_follows_rewrites() {
7278 let dir = tempfile::tempdir().expect("tempdir");
7279 let path = store::paths::packed_refs_path(dir.path());
7280 let header = "# pack-refs with: peeled fully-peeled sorted \n";
7281 let records = [
7282 ("refs/heads/main", oid(1), None),
7283 ("refs/tags/v1", oid(2), Some(oid(1))),
7284 ("refs/tags/v2", oid(3), None),
7285 ];
7286 fs::write(&path, body(header, &records)).expect("packed-refs");
7287 let files = store::FilesRefStore::from_git_dir(dir.path()).expect("files store");
7288
7289 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7290 let loaded = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
7291 for (name, value, _) in records {
7292 assert_eq!(
7293 peels.peeled(name, &value),
7294 loaded.peeled(name, &value),
7295 "{name}"
7296 );
7297 }
7298 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), Some(Some(oid(1))));
7299 let cached = files
7300 .packed_refs_view()
7301 .expect("view")
7302 .expect("packed-refs");
7303 let shared = peels.view.as_ref().expect("snapshot view");
7304 assert!(std::sync::Arc::ptr_eq(&cached.data, &shared.data));
7305
7306 let rewritten = [
7307 ("refs/heads/main", oid(1), None),
7308 ("refs/tags/v1", oid(4), Some(oid(5))),
7309 ("refs/tags/v2", oid(3), None),
7310 ("refs/tags/v3", oid(6), Some(oid(1))),
7311 ];
7312 fs::write(&path, body(header, &rewritten)).expect("rewrite packed-refs");
7313 let peels = PackedRefPeels::for_store(&files, dir.path()).expect("for_store");
7314 assert_eq!(peels.peeled("refs/tags/v1", &oid(2)), None);
7315 assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(5))));
7316 assert_eq!(peels.peeled("refs/tags/v3", &oid(6)), Some(Some(oid(1))));
7317
7318 let memory = store::MemoryRefStore::new();
7319 let fallback = PackedRefPeels::for_store(&memory, dir.path()).expect("fallback");
7320 let direct = PackedRefPeels::load(dir.path(), memory.format()).expect("load");
7321 for (name, value, _) in rewritten {
7322 assert_eq!(
7323 fallback.peeled(name, &value),
7324 direct.peeled(name, &value),
7325 "{name}"
7326 );
7327 }
7328 }
7329
7330 fn raw_view(data: &[u8], hex_len: usize) -> PackedRefsFileView {
7332 PackedRefsFileView {
7333 data: crate::pack_map::PackData::from_owned(data.to_vec()),
7334 sorted: true,
7335 hex_len,
7336 body_start: 0,
7337 peel_trait: PackedPeelTrait::None,
7338 }
7339 }
7340
7341 fn naive_record_starts(data: &[u8], hex_len: usize) -> Vec<usize> {
7343 let mut out = Vec::new();
7344 let mut start = 0;
7345 for line in data.split(|&b| b == b'\n') {
7346 if start >= data.len() {
7347 break;
7348 }
7349 if packed_line_is_record(line, hex_len) {
7350 out.push(start);
7351 }
7352 start += line.len() + 1;
7353 }
7354 out
7355 }
7356
7357 fn assert_record_walks_match(data: &[u8], hex_len: usize) {
7358 let view = raw_view(data, hex_len);
7359 let want = naive_record_starts(data, hex_len);
7360 assert_eq!(
7361 view.records().collect::<Vec<_>>(),
7362 want,
7363 "{:?} hex_len={hex_len}",
7364 String::from_utf8_lossy(data)
7365 );
7366 let mut line_starts: Vec<usize> = memchr::memchr_iter(b'\n', data)
7367 .map(|nl| nl + 1)
7368 .filter(|&s| s < data.len())
7369 .collect();
7370 line_starts.insert(0, 0);
7371 if data.ends_with(b"\n") {
7373 line_starts.push(data.len());
7374 }
7375 for before in line_starts {
7376 let prev = want.iter().copied().filter(|&s| s < before).last();
7377 assert_eq!(view.prev_record(before), prev, "{data:?} before={before}");
7378 }
7379 }
7380
7381 fn xorshift(state: &mut u64) -> u64 {
7382 *state ^= *state << 13;
7383 *state ^= *state >> 7;
7384 *state ^= *state << 17;
7385 *state
7386 }
7387
7388 #[test]
7389 fn record_walks_match_a_line_by_line_scan() {
7390 let hex = oid(1).to_hex();
7391 let inputs = [
7392 String::new(),
7393 "\n".to_owned(),
7394 format!("# pack-refs with: peeled \n{hex} refs/heads/a\n^{hex}\n{hex} refs/tags/b\n"),
7395 format!("{hex} refs/heads/z\n{hex} refs/heads/a\n"),
7396 format!("{hex} refs/heads/a\n\n{hex} refs/heads/b"),
7397 format!("{hex}\n{hex} \n{hex} x\nshort\n#{hex} refs/heads/c\n"),
7398 format!("{hex} refs/tags/a\n^{hex}\n^{hex}\n{hex} refs/tags/b\n^{hex}"),
7399 format!("^ab\n{}\n{hex} refs/heads/a\n", "x".repeat(37)),
7400 format!("^\n{hex} refs/heads/a\n^{hex}x\n{hex} refs/heads/b\n"),
7401 format!("^{hex}\n"),
7402 format!("^{}", &hex[1..]),
7403 ];
7404 for input in &inputs {
7405 assert_record_walks_match(input.as_bytes(), 40);
7406 }
7407 }
7408
7409 #[test]
7410 fn record_walks_match_a_line_by_line_scan_on_random_bytes() {
7411 let alphabet = b"^\n #ab";
7412 let mut state = 0x2545_f491_4f6c_dd1du64;
7413 for len in 0..400 {
7414 let input: Vec<u8> = (0..len % 97)
7415 .map(|_| alphabet[(xorshift(&mut state) % alphabet.len() as u64) as usize])
7416 .collect();
7417 for hex_len in [1, 2, 3] {
7418 assert_record_walks_match(&input, hex_len);
7419 }
7420 }
7421 }
7422
7423 #[test]
7426 fn search_matches_a_linear_scan_on_sorted_records() {
7427 let hex_len = 2;
7428 let mut state = 0x9e37_79b9_7f4a_7c15u64;
7429 for _ in 0..300 {
7430 let mut names: Vec<String> = (0..xorshift(&mut state) % 12)
7431 .map(|_| format!("n{}", xorshift(&mut state) % 40))
7432 .collect();
7433 names.sort();
7434 names.dedup();
7435 let mut data = b"# h\n".to_vec();
7436 for name in &names {
7437 data.extend_from_slice(format!("ab {name}\n").as_bytes());
7438 match xorshift(&mut state) % 4 {
7439 0 => data.extend_from_slice(b"^ab\n"),
7440 1 => data.extend_from_slice(b"junk\n"),
7441 _ => {}
7442 }
7443 }
7444 let mut view = raw_view(&data, hex_len);
7445 view.body_start = 4;
7446 let records: Vec<usize> = view.records().collect();
7447 for probe in 0..42 {
7448 let target = format!("n{probe}");
7449 let want = records
7450 .iter()
7451 .copied()
7452 .find(|&s| view.record_cmp(s, &target).is_ge())
7453 .unwrap_or(data.len());
7454 assert_eq!(view.lower_bound(&target), want, "{target} in {data:?}");
7455 let hit = view.search(&target).ok();
7456 assert_eq!(
7457 hit,
7458 names.contains(&target).then_some(want),
7459 "{target} in {data:?}"
7460 );
7461 }
7462 }
7463 }
7464}