pub mod handle;
pub mod listing;
pub mod query_reachability;
pub mod store;
pub mod version_sort;
mod domain_reexports;
pub use domain_reexports::*;
pub use handle::References;
pub mod gc {
pub use crate::gc::*;
}
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fs;
use std::io;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use store::{
open_ref_store, Expected, RawRef as StoreRawRef, RefCaseOnlyRename, RefEntry, RefReflogRename,
RefStorageFormat, RefStore, RefTransaction, RefUpdate, RefUpdateFlags, ReflogUpdate,
};
use crate::check_ref_format::{check_refname_format, RefNameOptions};
use crate::config::ConfigSet;
use crate::error::{Error, RefLockError, Result};
use crate::objects::ObjectId;
use crate::pack;
pub const SYMREF_MAXDEPTH: usize = 5;
fn map_public_ref_store_error(err: Error, refname: &str) -> Error {
match err {
Error::RefStore(store::RefStoreError::SymrefLoop) => {
Error::InvalidRef(format!("ref symlink too deep: {refname}"))
}
other => other,
}
}
fn files_legacy_write_precheck(git_dir: &Path, refname: &str) -> Result<()> {
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return Ok(());
}
let storage_dir = ref_storage_dir(git_dir, refname);
let stor = crate::ref_namespace::storage_ref_name(refname);
let path = storage_dir.join(stor);
remove_empty_ref_directory(&path);
if fs::symlink_metadata(&path)
.map(|m| m.file_type().is_dir())
.unwrap_or(false)
{
let display = ref_path_for_display(&path);
return Err(crate::error::RefLockError::DirectoryInTheWay {
refname: refname.to_owned(),
path: display,
}
.into());
}
if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
return Err(Error::InvalidRef(format!(
"cannot update ref '{refname}': reference broken"
)));
}
Ok(())
}
fn legacy_ref_update_flags(refname: &str) -> RefUpdateFlags {
let mut flags = RefUpdateFlags::default();
if refname == "HEAD" {
flags.no_deref = true;
}
flags
}
pub(crate) fn commit_store_update(git_dir: &Path, update: RefUpdate) -> Result<()> {
let store = open_ref_store(git_dir)?;
commit_ref_update_on_store(store.as_ref(), update)
}
pub fn commit_ref_update_on_store(store: &dyn RefStore, update: RefUpdate) -> Result<()> {
let refname = update.name.clone();
let txn = RefTransaction::new()
.update(update)
.map_err(crate::error::Error::from)?;
let prepared = store
.prepare(txn)
.map_err(|e| map_public_ref_store_error(Error::from(e), &refname))?;
prepared
.commit()
.map_err(|e| map_public_ref_store_error(Error::from(e), &refname))
}
pub fn update_direct_ref_on_store(
store: &dyn RefStore,
git_dir: &Path,
refname: &str,
oid: &ObjectId,
) -> Result<()> {
store_write_value_on_store(
store,
git_dir,
refname,
Some(StoreRawRef::Direct(*oid)),
Expected::Any,
None,
)
}
pub(crate) fn store_write_value_on_store(
store: &dyn RefStore,
git_dir: &Path,
refname: &str,
new_value: Option<StoreRawRef>,
expected: Expected,
reflog: Option<ReflogUpdate>,
) -> Result<()> {
ensure_refname_safe_for_storage(refname)?;
if new_value.is_some() && store.format() == RefStorageFormat::Files {
files_legacy_write_precheck(git_dir, refname)?;
}
commit_ref_update_on_store(
store,
RefUpdate {
name: refname.to_owned(),
new_value,
expected,
reflog,
flags: legacy_ref_update_flags(refname),
},
)
}
pub fn commit_ref_store_batch(git_dir: &Path, updates: &[RefUpdate]) -> Result<()> {
if updates.is_empty() {
return Ok(());
}
let store = open_ref_store(git_dir)?;
commit_ref_updates_on_store(store.as_ref(), updates, &[])
}
pub fn commit_ref_updates_on_store(
store: &dyn RefStore,
updates: &[RefUpdate],
reflog_renames: &[RefReflogRename],
) -> Result<()> {
commit_ref_batch_on_store(store, updates, reflog_renames, &[])
}
pub fn commit_ref_batch_on_store(
store: &dyn RefStore,
updates: &[RefUpdate],
reflog_renames: &[RefReflogRename],
case_only_renames: &[RefCaseOnlyRename],
) -> Result<()> {
if updates.is_empty() && reflog_renames.is_empty() && case_only_renames.is_empty() {
return Ok(());
}
let label = updates
.first()
.map(|u| u.name.as_str())
.or_else(|| reflog_renames.first().map(|r| r.from.as_str()))
.or_else(|| case_only_renames.first().map(|r| r.from.as_str()))
.unwrap_or("refs");
let mut txn = RefTransaction::new();
for update in updates {
let name = update.name.clone();
txn = txn
.update(update.clone())
.map_err(|e| map_public_ref_store_error(Error::from(e), &name))?;
}
for rename in reflog_renames {
txn = txn.with_reflog_rename(rename.clone());
}
for rename in case_only_renames {
txn = txn.with_case_only_rename(rename.clone());
}
let prepared = store
.prepare(txn)
.map_err(|e| map_public_ref_store_error(Error::from(e), label))?;
prepared
.commit()
.map_err(|e| map_public_ref_store_error(Error::from(e), label))
}
fn store_write_value(
git_dir: &Path,
refname: &str,
new_value: Option<StoreRawRef>,
expected: Expected,
reflog: Option<ReflogUpdate>,
) -> Result<()> {
let store = open_ref_store(git_dir)?;
store_write_value_on_store(
store.as_ref(),
git_dir,
refname,
new_value,
expected,
reflog,
)
}
fn collect_resolved_refs(store: &dyn RefStore, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
collect_resolved_ref_entries(store, |f| store.for_each_ref(prefix, f))
}
fn collect_resolved_ref_entries(
store: &dyn RefStore,
for_each: impl FnOnce(&mut dyn FnMut(&RefEntry) -> ControlFlow<()>) -> Result<()>,
) -> Result<Vec<(String, ObjectId)>> {
let mut results = Vec::new();
for_each(&mut |entry: &RefEntry| {
let oid = entry
.peeled
.or_else(|| match &entry.value {
StoreRawRef::Direct(oid) => Some(*oid),
StoreRawRef::Symbolic(target) => store.resolve(target).ok(),
})
.or_else(|| store.resolve(&entry.name).ok());
if let Some(oid) = oid {
results.push((entry.name.clone(), oid));
}
ControlFlow::Continue(())
})?;
Ok(results)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ref {
Direct(ObjectId),
Symbolic(String),
}
pub fn read_ref_file(path: &Path) -> Result<Ref> {
if let Ok(target) = fs::read_link(path) {
return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
}
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e)
if e.kind() == io::ErrorKind::IsADirectory
|| e.kind() == io::ErrorKind::NotADirectory
|| e.raw_os_error() == Some(libc::EISDIR)
|| e.raw_os_error() == Some(libc::ENOTDIR) =>
{
return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, e)));
}
Err(e) => return Err(Error::Io(e)),
};
let content = content.trim_end_matches('\n');
parse_ref_content(content)
}
pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
if let Some(target) = content.strip_prefix("ref: ") {
Ok(Ref::Symbolic(target.trim().to_owned()))
} else if let Some(token) = content.split_whitespace().next() {
if ObjectId::is_full_hex(token) {
let oid: ObjectId = token.parse()?;
Ok(Ref::Direct(oid))
} else if content == "unknown-oid" {
const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
let oid = ObjectId::from_bytes(PLACEHOLDER)?;
Ok(Ref::Direct(oid))
} else {
Err(Error::InvalidRef(content.to_owned()))
}
} else {
Err(Error::InvalidRef(content.to_owned()))
}
}
pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
open_ref_store(git_dir)?
.resolve(refname)
.map_err(|err| map_public_ref_store_error(err, refname))
}
pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
let commondir_file = git_dir.join("commondir");
if !commondir_file.is_file() {
return None;
}
let raw = fs::read_to_string(commondir_file).ok()?;
let rel = raw.trim();
let path = if Path::new(rel).is_absolute() {
PathBuf::from(rel)
} else {
git_dir.join(rel)
};
path.canonicalize().ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RawRefLookup {
Exists,
NotFound,
IsDirectory,
}
pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
let store = open_ref_store(git_dir)?;
match store.read_raw(refname)? {
Some(_) => Ok(RawRefLookup::Exists),
None => {
let (storage_dir, stor_name) =
crate::worktree_ref::resolve_ref_storage(git_dir, refname);
let path = storage_dir.join(&stor_name);
if fs::symlink_metadata(&path)
.map(|m| m.is_dir())
.unwrap_or(false)
{
Ok(RawRefLookup::IsDirectory)
} else {
Ok(RawRefLookup::NotFound)
}
}
}
}
#[must_use]
pub fn lock_path_for_ref(path: &Path) -> PathBuf {
let mut s = path.as_os_str().to_owned();
s.push(".lock");
PathBuf::from(s)
}
fn first_packed_descendant_ref(store: &Path, refname: &str) -> Result<Option<String>> {
let leaf_dir = format!("{refname}/");
let map = read_packed_refs_map_for_prefix(store, refname)?;
let sorted: BTreeSet<String> = map.keys().cloned().collect();
Ok(sorted
.range(leaf_dir.clone()..)
.next()
.filter(|name| name.starts_with(&leaf_dir))
.cloned())
}
fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
Ok(lookup_packed_ref(git_dir, refname)?.is_some())
}
pub(crate) fn packed_refname_namespace_conflict(
sorted_packed: &BTreeSet<String>,
candidate: &str,
) -> bool {
let segments: Vec<&str> = candidate.split('/').filter(|s| !s.is_empty()).collect();
if segments.len() > 1 {
let mut dirname = String::new();
for part in &segments[..segments.len() - 1] {
if !dirname.is_empty() {
dirname.push('/');
}
dirname.push_str(part);
if sorted_packed.contains(&dirname) {
return true;
}
}
}
let leaf_dir = format!("{candidate}/");
if let Some(existing) = sorted_packed.range(leaf_dir.clone()..).next() {
if existing.starts_with(&leaf_dir) {
return true;
}
}
false
}
fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
Ok(PackedRefs::load(git_dir)?.has_namespace_conflict(refname))
}
pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
if refname == "HEAD" {
return Ok(false);
}
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return Ok(false);
}
let storage_dir = ref_storage_dir(git_dir, refname);
packed_ref_name_exists(&storage_dir, refname)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefnameUnavailable {
AncestorExists {
blocking: String,
new_ref: String,
},
DescendantExists {
blocking: String,
new_ref: String,
},
SameBatch {
refname: String,
other: String,
},
}
impl RefnameUnavailable {
#[must_use]
pub fn lock_message_suffix(&self) -> String {
match self {
RefnameUnavailable::AncestorExists { blocking, new_ref } => {
format!("'{blocking}' exists; cannot create '{new_ref}'")
}
RefnameUnavailable::DescendantExists { blocking, new_ref } => {
format!("'{blocking}' exists; cannot create '{new_ref}'")
}
RefnameUnavailable::SameBatch { refname, other } => {
format!("cannot process '{refname}' and '{other}' at the same time")
}
}
}
}
fn find_descendant_in_sorted_extras(
dirname_with_slash: &str,
extras: &BTreeSet<String>,
) -> Option<String> {
let start = extras
.range(dirname_with_slash.to_string()..)
.next()
.cloned()?;
if start.starts_with(dirname_with_slash) {
Some(start)
} else {
None
}
}
#[must_use]
pub fn is_valid_fetch_advertised_ref(name: &str) -> bool {
crate::check_ref_format::is_valid_fetch_advertised_ref(name)
}
#[must_use]
pub fn is_valid_ls_refs_advertised_name(name: &str) -> bool {
name == "HEAD" || is_valid_fetch_advertised_ref(name)
}
#[must_use]
pub fn is_valid_storable_ref_name(name: &str) -> bool {
is_valid_fetch_advertised_ref(name)
}
#[must_use]
pub fn is_valid_advertised_symref_target(name: &str) -> bool {
crate::check_ref_format::is_valid_advertised_symref_target(name)
}
pub fn verify_refname_available_for_create(
git_dir: &Path,
refname: &str,
extras: &BTreeSet<String>,
skip: &HashSet<String>,
) -> std::result::Result<(), RefnameUnavailable> {
let git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
let mut seen_dirnames: HashSet<String> = HashSet::new();
let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
if segments.len() <= 1 {
} else {
let mut dirname = String::new();
for part in &segments[..segments.len() - 1] {
if !dirname.is_empty() {
dirname.push('/');
}
dirname.push_str(part);
if !seen_dirnames.insert(dirname.clone()) {
continue;
}
if skip.contains(&dirname) {
continue;
}
match read_raw_ref(&git_dir, &dirname) {
Ok(RawRefLookup::Exists) => {
return Err(RefnameUnavailable::AncestorExists {
blocking: dirname.clone(),
new_ref: refname.to_owned(),
});
}
Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
Err(_) => {}
}
if extras.contains(&dirname) {
return Err(RefnameUnavailable::SameBatch {
refname: refname.to_owned(),
other: dirname.clone(),
});
}
}
}
let mut leaf_dir = String::with_capacity(refname.len() + 1);
leaf_dir.push_str(refname);
leaf_dir.push('/');
let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
let may_have_loose_descendants = packed_dir.join("reftable").is_dir()
|| git_dir.join(refname).is_dir()
|| packed_dir.join(refname).is_dir();
let under = if may_have_loose_descendants {
list_refs(&git_dir, &leaf_dir).unwrap_or_default()
} else {
Vec::new()
};
if under.is_empty() {
if let Ok(Some(name)) = first_packed_descendant_ref(&packed_dir, refname) {
if !skip.contains(&name) {
return Err(RefnameUnavailable::DescendantExists {
blocking: name,
new_ref: refname.to_owned(),
});
}
}
if packed_dir != git_dir {
if let Ok(Some(name)) = first_packed_descendant_ref(&git_dir, refname) {
if !skip.contains(&name) {
return Err(RefnameUnavailable::DescendantExists {
blocking: name,
new_ref: refname.to_owned(),
});
}
}
}
}
if under.is_empty()
&& fs::symlink_metadata(git_dir.join(refname))
.map(|m| m.is_dir())
.unwrap_or(false)
{
let mut blocking: Option<String> = None;
let dir_path = git_dir.join(refname);
if let Ok(read) = fs::read_dir(&dir_path) {
for entry in read.flatten() {
let path = entry.path();
let Ok(meta) = fs::metadata(&path) else {
continue;
};
if !meta.is_file() {
continue;
}
let name = entry.file_name().to_string_lossy().into_owned();
let full = format!("{refname}/{name}");
blocking = Some(full);
break;
}
}
if let Some(b) = blocking {
if !skip.contains(&b) {
return Err(RefnameUnavailable::DescendantExists {
blocking: b,
new_ref: refname.to_owned(),
});
}
}
}
for (existing, _) in under {
if skip.contains(&existing) {
continue;
}
return Err(RefnameUnavailable::DescendantExists {
blocking: existing,
new_ref: refname.to_owned(),
});
}
if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
if !skip.contains(&extra) {
return Err(RefnameUnavailable::SameBatch {
refname: refname.to_owned(),
other: extra,
});
}
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct RefBatchItem {
pub name: String,
pub new_oid: Option<ObjectId>,
}
pub fn verify_ref_transaction_batch(
git_dir: &Path,
items: &[RefBatchItem],
) -> std::result::Result<(), RefnameUnavailable> {
if let Some((parent, child)) =
batch_refname_prefix_conflict(items.iter().map(|i| i.name.as_str()))
{
return Err(RefnameUnavailable::SameBatch {
refname: child,
other: parent,
});
}
if crate::ref_storage::detect_format(git_dir).is_ok_and(|f| f.is_reftable()) {
return crate::reftable::verify_ref_transaction_batch_reftable(git_dir, items);
}
let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
let empty_skip = HashSet::new();
for item in items {
let Some(_oid) = item.new_oid else {
continue;
};
if resolve_ref(git_dir, &item.name).is_ok() {
continue;
}
verify_refname_available_for_create(git_dir, &item.name, &extras, &empty_skip)?;
}
Ok(())
}
pub(crate) fn batch_refname_prefix_conflict<'a>(
names: impl Iterator<Item = &'a str>,
) -> Option<(String, String)> {
let mut sorted: Vec<&str> = names.collect();
sorted.sort_unstable();
for pair in sorted.windows(2) {
let (a, b) = (pair[0], pair[1]);
if refname_is_strict_prefix(a, b) {
return Some((a.to_owned(), b.to_owned()));
}
if refname_is_strict_prefix(b, a) {
return Some((b.to_owned(), a.to_owned()));
}
}
None
}
fn refname_is_strict_prefix(parent: &str, child: &str) -> bool {
child.len() > parent.len()
&& child.as_bytes().get(parent.len()) == Some(&b'/')
&& child.starts_with(parent)
}
fn packed_refs_unexpected_line(path: &Path, line: impl Into<String>) -> Error {
Error::PackedRefsUnexpectedLine {
path: path.display().to_string(),
line: line.into(),
}
}
fn packed_refs_unterminated_line(path: &Path, line: impl Into<String>) -> Error {
Error::PackedRefsUnexpectedLine {
path: path.display().to_string(),
line: line.into(),
}
}
fn parse_packed_ref_peel_line(path: &Path, line: &str, expect_peel: bool) -> Result<()> {
if !expect_peel {
return Err(packed_refs_unexpected_line(path, line));
}
let inner = line
.strip_prefix('^')
.ok_or_else(|| packed_refs_unexpected_line(path, line))?;
let mut j = 0usize;
while j < inner.len() && inner.as_bytes()[j].is_ascii_hexdigit() {
j += 1;
}
if !ObjectId::is_hex_len(j) || j != inner.len() {
return Err(packed_refs_unexpected_line(path, line));
}
Ok(())
}
fn packed_ref_data_line_refname(line: &str) -> Option<&str> {
let bytes = line.as_bytes();
let mut j = 0usize;
while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
return None;
}
let refname = &line[j + 1..];
if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
return None;
}
Some(refname)
}
fn parse_packed_ref_data_line(
path: &Path,
line: &str,
ref_opts: &RefNameOptions,
) -> Result<(String, ObjectId)> {
let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, ref_opts)?;
Ok((refname.to_owned(), oid))
}
fn parse_packed_ref_data_line_borrowed<'a>(
path: &Path,
line: &'a str,
ref_opts: &RefNameOptions,
) -> Result<(&'a str, ObjectId)> {
let bytes = line.as_bytes();
let mut j = 0usize;
while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if !ObjectId::is_hex_len(j) || j + 1 >= bytes.len() || bytes[j] != b' ' {
return Err(packed_refs_unexpected_line(path, line));
}
let refname = &line[j + 1..];
if refname.is_empty() || refname.as_bytes().iter().any(|b| b.is_ascii_whitespace()) {
return Err(packed_refs_unexpected_line(path, line));
}
if check_refname_format(refname, ref_opts).is_err() {
return Err(packed_refs_unexpected_line(path, line));
}
let oid: ObjectId = line[..j].parse()?;
Ok((refname, oid))
}
#[derive(Debug, Clone)]
pub(crate) struct PackedRefsFileView {
data: std::sync::Arc<crate::pack_map::PackData>,
sorted: bool,
hex_len: usize,
data_line_starts: Vec<usize>,
peel_trait: PackedPeelTrait,
}
impl PackedRefsFileView {
#[cfg(test)]
fn from_bytes(path: &Path, data: Vec<u8>, hash_algo: crate::objects::HashAlgo) -> Result<Self> {
Self::from_data(path, crate::pack_map::PackData::from_owned(data), hash_algo)
}
fn from_data(
path: &Path,
data: std::sync::Arc<crate::pack_map::PackData>,
hash_algo: crate::objects::HashAlgo,
) -> Result<Self> {
let hex_len = hash_algo.hex_len();
if data.is_empty() {
return Ok(Self {
data,
sorted: false,
hex_len,
data_line_starts: Vec::new(),
peel_trait: PackedPeelTrait::None,
});
}
if memchr::memchr(b'\r', &data).is_some() {
return Err(packed_refs_unexpected_line(
path,
"<contains carriage return>",
));
}
if !data.ends_with(b"\n") {
let tail = std::str::from_utf8(&data)
.map(|s| s.rsplit('\n').next().unwrap_or(s))
.unwrap_or("<invalid utf-8>");
return Err(packed_refs_unterminated_line(path, tail));
}
let text = std::str::from_utf8(&data)
.map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
let body_start = if text.starts_with('#') {
text.find('\n').map_or(text.len(), |nl| nl + 1)
} else {
0
};
let body = &text[body_start..];
if let Some(rest) = body.strip_suffix('\n') {
let last_line = rest.rfind('\n').map_or(rest, |nl| &rest[nl + 1..]);
if last_line.len() < hex_len + 1 {
return Err(packed_refs_unexpected_line(path, last_line));
}
}
let (data_line_starts, in_order) = packed_refs_data_line_starts(&data, hex_len);
Ok(Self {
sorted: in_order && packed_refs_file_declares_sorted(text),
hex_len,
data_line_starts,
peel_trait: packed_refs_peel_trait(text),
data,
})
}
fn record_name(&self, line_start: usize) -> &[u8] {
let end = packed_line_end(&self.data, line_start);
&self.data[line_start + self.hex_len + 1..end]
}
fn record_cmp(&self, line_start: usize, name: &str) -> std::cmp::Ordering {
self.record_name(line_start).cmp(name.as_bytes())
}
fn lower_bound(&self, name: &str) -> usize {
self.data_line_starts
.partition_point(|&start| self.record_cmp(start, name).is_lt())
}
fn lookup_entry(
&self,
path: &Path,
refname: &str,
) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
let ref_opts = RefNameOptions {
allow_onelevel: false,
refspec_pattern: false,
normalize: false,
};
let line_start = if self.sorted {
self.data_line_starts
.get(self.lower_bound(refname))
.copied()
.filter(|&start| self.record_cmp(start, refname).is_eq())
} else {
self.data_line_starts
.iter()
.copied()
.find(|&start| self.record_cmp(start, refname).is_eq())
};
let Some(start) = line_start else {
return Ok(None);
};
let (name, oid) = parse_packed_line_at(path, &self.data, start, &ref_opts)?;
debug_assert_eq!(name, refname);
let line_end = packed_line_end(&self.data, start);
if line_end + 1 < self.data.len() && self.data[line_end + 1] == b'^' {
let peel_line = packed_line_as_str(&self.data, line_end + 1);
parse_packed_ref_peel_line(path, peel_line, true)?;
}
let peel = peel_oid_following_line(&self.data, start);
Ok(Some((oid, peel)))
}
fn lookup_oid(&self, path: &Path, refname: &str) -> Result<Option<ObjectId>> {
Ok(self.lookup_entry(path, refname)?.map(|(oid, _)| oid))
}
pub(crate) fn first_descendant(
&self,
path: &Path,
refname: &str,
) -> Result<Option<(String, ObjectId)>> {
let leaf_dir = format!("{refname}/");
Ok(self
.map_for_prefix(path, Some(refname))?
.into_iter()
.filter(|(name, _)| name.starts_with(&leaf_dir))
.min_by(|a, b| a.0.cmp(&b.0)))
}
fn map_for_prefix(
&self,
path: &Path,
prefix: Option<&str>,
) -> Result<HashMap<String, ObjectId>> {
self.map_for_prefix_filtered(path, prefix, None)
}
fn map_for_prefix_filtered(
&self,
path: &Path,
prefix: Option<&str>,
keep: Option<PackedRecordFilter<'_>>,
) -> Result<HashMap<String, ObjectId>> {
let prefix_trim = prefix.map(|p| p.trim_end_matches('/'));
let range_prefix = prefix_trim.filter(|p| self.sorted && !p.is_empty() && *p != "refs");
let (lo, hi) = match range_prefix {
Some(prefix) => (
self.lower_bound(prefix),
list_prefix_upper_bound(prefix).map_or(self.data_line_starts.len(), |upper| {
self.lower_bound(&upper)
}),
),
None => (0, self.data_line_starts.len()),
};
let begin = match lo.checked_sub(1) {
Some(prev) => {
let after = packed_line_end(&self.data, self.data_line_starts[prev]) + 1;
if self.data.get(after) == Some(&b'^') {
packed_line_end(&self.data, after) + 1
} else {
after
}
}
None => 0,
};
let end = self
.data_line_starts
.get(hi)
.copied()
.unwrap_or(self.data.len());
let region = std::str::from_utf8(&self.data[begin..end])
.map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
parse_packed_refs_region(path, region, prefix_trim, keep, self.peel_trait)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PackedPeel {
Peeled(ObjectId),
NotTag,
Unknown,
}
pub(crate) type PackedRecordFilter<'a> = &'a dyn Fn(&str, &ObjectId, PackedPeel) -> bool;
fn parse_packed_refs_region(
path: &Path,
region: &str,
prefix: Option<&str>,
keep: Option<PackedRecordFilter<'_>>,
peel_trait: PackedPeelTrait,
) -> Result<HashMap<String, ObjectId>> {
let ref_opts = RefNameOptions {
allow_onelevel: false,
refspec_pattern: false,
normalize: false,
};
let mut map = HashMap::new();
let mut pending: Option<(&str, ObjectId, Option<ObjectId>)> = None;
let flush = |map: &mut HashMap<String, ObjectId>,
pending: &mut Option<(&str, ObjectId, Option<ObjectId>)>| {
let (Some(keep), Some((refname, oid, peeled))) = (keep, pending.take()) else {
return;
};
let peel = if peel_trait.covers(refname) {
peeled.map_or(PackedPeel::NotTag, PackedPeel::Peeled)
} else {
PackedPeel::Unknown
};
if keep(refname, &oid, peel) {
map.insert(refname.to_owned(), oid);
}
};
let mut expect_peel = false;
let mut lines = region.split('\n').peekable();
while let Some(line) = lines.next() {
if line.is_empty() {
if lines.peek().is_some() {
return Err(packed_refs_unexpected_line(path, line));
}
break;
}
if line.starts_with('#') {
flush(&mut map, &mut pending);
expect_peel = false;
continue;
}
if let Some(peel_hex) = line.strip_prefix('^') {
parse_packed_ref_peel_line(path, line, expect_peel)?;
if let Some((_, _, peeled)) = pending.as_mut() {
*peeled = ObjectId::from_hex(peel_hex).ok();
}
expect_peel = false;
continue;
}
flush(&mut map, &mut pending);
let (refname, oid) = parse_packed_ref_data_line_borrowed(path, line, &ref_opts)?;
if prefix.is_none_or(|p| ref_name_matches_list_prefix(refname, p)) {
if keep.is_some() {
pending = Some((refname, oid, None));
} else {
map.insert(refname.to_owned(), oid);
}
}
expect_peel = true;
}
flush(&mut map, &mut pending);
Ok(map)
}
fn read_packed_refs_file_view(store: &Path) -> Result<Option<PackedRefsFileView>> {
let packed_path = store::paths::packed_refs_path(store);
let data = match crate::pack_map::PackData::open_with_min_len(&packed_path, 0) {
Ok(d) => d,
Err(Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
let hash_algo = crate::repo::read_repository_format_from_git_dir(store)
.map(|format| format.hash_algo())
.unwrap_or(crate::objects::HashAlgo::Sha1);
Ok(Some(PackedRefsFileView::from_data(
&packed_path,
data,
hash_algo,
)?))
}
fn packed_refs_data_line_starts(data: &[u8], hex_len: usize) -> (Vec<usize>, bool) {
let mut out = Vec::new();
let mut in_order = true;
let mut prev_name: &[u8] = &[];
let mut start = 0usize;
while start < data.len() {
let end = memchr::memchr(b'\n', &data[start..]).map_or(data.len(), |p| start + p);
let line = &data[start..end];
if line.len() > hex_len + 1 && line[hex_len] == b' ' && !matches!(line[0], b'#' | b'^') {
let name = &line[hex_len + 1..];
in_order &= prev_name <= name;
prev_name = name;
out.push(start);
}
start = end + 1;
}
(out, in_order)
}
fn packed_line_end(data: &[u8], line_start: usize) -> usize {
memchr::memchr(b'\n', &data[line_start..]).map_or(data.len(), |p| line_start + p)
}
fn packed_line_as_str(data: &[u8], line_start: usize) -> &str {
std::str::from_utf8(&data[line_start..packed_line_end(data, line_start)]).unwrap_or("")
}
fn peel_oid_following_line(data: &[u8], line_start: usize) -> Option<ObjectId> {
let line_end = packed_line_end(data, line_start);
if line_end >= data.len() || data[line_end] != b'\n' {
return None;
}
let peel_start = line_end + 1;
if peel_start >= data.len() || data[peel_start] != b'^' {
return None;
}
let peel_end = packed_line_end(data, peel_start);
let inner = std::str::from_utf8(&data[peel_start + 1..peel_end]).ok()?;
ObjectId::from_hex(inner.trim()).ok()
}
fn parse_packed_line_at(
path: &Path,
data: &[u8],
line_start: usize,
ref_opts: &RefNameOptions,
) -> Result<(String, ObjectId)> {
let line = packed_line_as_str(data, line_start);
parse_packed_ref_data_line(path, line, ref_opts)
}
pub(crate) fn parse_packed_refs_to_map(
path: &Path,
data: &[u8],
) -> Result<HashMap<String, ObjectId>> {
parse_packed_refs_to_map_with_prefix(path, data, None)
}
fn packed_refs_file_declares_sorted(text: &str) -> bool {
for line in text.split('\n') {
if line.is_empty() {
break;
}
if !line.starts_with('#') {
break;
}
let body = line[1..].trim();
let Some(traits) = body.strip_prefix("pack-refs with:") else {
continue;
};
return traits.split_whitespace().any(|t| t == "sorted");
}
false
}
fn parse_packed_refs_to_map_with_prefix(
path: &Path,
data: &[u8],
prefix_filter: Option<&str>,
) -> Result<HashMap<String, ObjectId>> {
if data.is_empty() {
return Ok(HashMap::new());
}
if data.contains(&b'\r') {
return Err(packed_refs_unexpected_line(
path,
"<contains carriage return>",
));
}
if !data.ends_with(b"\n") {
let tail = std::str::from_utf8(data)
.map(|s| s.rsplit('\n').next().unwrap_or(s))
.unwrap_or("<invalid utf-8>");
return Err(packed_refs_unterminated_line(path, tail));
}
let text = std::str::from_utf8(data)
.map_err(|_| packed_refs_unexpected_line(path, "<invalid utf-8>"))?;
let ref_opts = RefNameOptions {
allow_onelevel: false,
refspec_pattern: false,
normalize: false,
};
let mut map = HashMap::new();
let mut expect_peel = false;
let prefix_trim = prefix_filter.map(|p| p.trim_end_matches('/'));
let packed_sorted = packed_refs_file_declares_sorted(text);
let prefix_upper = if packed_sorted {
prefix_trim.and_then(list_prefix_upper_bound)
} else {
None
};
let mut lines = text.split('\n').peekable();
while let Some(line) = lines.next() {
if line.is_empty() {
if lines.peek().is_some() {
return Err(packed_refs_unexpected_line(path, line));
}
break;
}
if line.starts_with('#') {
expect_peel = false;
continue;
}
if line.starts_with('^') {
parse_packed_ref_peel_line(path, line, expect_peel)?;
expect_peel = false;
continue;
}
if let Some(prefix) = prefix_trim {
let Some(refname) = packed_ref_data_line_refname(line) else {
return Err(packed_refs_unexpected_line(path, line));
};
if packed_sorted {
if refname < prefix {
expect_peel = true;
continue;
}
if let Some(upper) = &prefix_upper {
if refname >= upper.as_str() {
break;
}
}
}
if !ref_name_matches_list_prefix(refname, prefix) {
expect_peel = true;
continue;
}
}
let (refname, oid) = parse_packed_ref_data_line(path, line, &ref_opts)?;
map.insert(refname, oid);
expect_peel = true;
}
Ok(map)
}
pub(crate) fn read_packed_refs_map(store: &Path) -> Result<HashMap<String, ObjectId>> {
read_packed_refs_map_for_prefix(store, "")
}
pub(crate) fn read_packed_refs_map_for_prefix(
store: &Path,
prefix: &str,
) -> Result<HashMap<String, ObjectId>> {
read_packed_refs_map_for_prefix_filtered(store, prefix, None)
}
pub(crate) fn read_packed_refs_map_for_prefix_filtered(
store: &Path,
prefix: &str,
keep: Option<PackedRecordFilter<'_>>,
) -> Result<HashMap<String, ObjectId>> {
let Some(view) = read_packed_refs_file_view(store)? else {
return Ok(HashMap::new());
};
let packed_path = store::paths::packed_refs_path(store);
let filter = if prefix.is_empty() {
None
} else {
Some(prefix)
};
view.map_for_prefix_filtered(&packed_path, filter, keep)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PackedPeelTrait {
None,
Tags,
All,
}
impl PackedPeelTrait {
fn covers(self, refname: &str) -> bool {
match self {
Self::None => false,
Self::Tags => refname.starts_with("refs/tags/"),
Self::All => true,
}
}
}
pub(crate) struct PackedRefPeels {
view: Option<PackedRefsFileView>,
unsorted: Option<HashMap<Box<[u8]>, usize>>,
peel_trait: PackedPeelTrait,
}
impl PackedRefPeels {
pub(crate) fn load(git_dir: &Path, format: RefStorageFormat) -> Result<Self> {
let empty = Self {
view: None,
unsorted: None,
peel_trait: PackedPeelTrait::None,
};
if format != RefStorageFormat::Files {
return Ok(empty);
}
let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
let Some(view) = read_packed_refs_file_view(&store)? else {
return Ok(empty);
};
let peel_trait = view.peel_trait;
if peel_trait == PackedPeelTrait::None {
return Ok(empty);
}
let unsorted = (!view.sorted).then(|| {
view.data_line_starts
.iter()
.map(|&start| (Box::<[u8]>::from(view.record_name(start)), start))
.collect()
});
Ok(Self {
view: Some(view),
unsorted,
peel_trait,
})
}
pub(crate) fn peeled(&self, refname: &str, oid: &ObjectId) -> Option<Option<ObjectId>> {
if !self.peel_trait.covers(refname) {
return None;
}
let view = self.view.as_ref()?;
let start = match &self.unsorted {
Some(map) => *map.get(refname.as_bytes())?,
None => view
.data_line_starts
.get(view.lower_bound(refname))
.copied()
.filter(|&start| view.record_cmp(start, refname).is_eq())?,
};
let hex = std::str::from_utf8(&view.data[start..start + view.hex_len]).ok()?;
let packed_oid = ObjectId::from_hex(hex).ok()?;
(packed_oid == *oid).then(|| peel_oid_following_line(&view.data, start))
}
}
fn packed_refs_peel_trait(text: &str) -> PackedPeelTrait {
let Some(header) = text.strip_prefix("# pack-refs with: ") else {
return PackedPeelTrait::None;
};
let traits = header.split('\n').next().unwrap_or_default();
let mut peel_trait = PackedPeelTrait::None;
for t in traits.split_whitespace() {
match t {
"fully-peeled" => return PackedPeelTrait::All,
"peeled" => peel_trait = PackedPeelTrait::Tags,
_ => {}
}
}
peel_trait
}
fn acquire_packed_refs_lock(
git_dir: &Path,
lock: &Path,
lock_display: &Path,
) -> Result<crate::lockfile::LockFile> {
use crate::lockfile::{LockError, LockFile, LockWait};
let wait = LockWait::packed_refs_for_git_dir(git_dir);
LockFile::acquire(lock, wait).map_err(|err| match err {
LockError::Held { .. } => Error::from(RefLockError::PackedRefsLockHeld {
lock_path: lock_display.display().to_string(),
}),
LockError::Io { source, .. } => Error::Io(source),
})
}
pub(crate) fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
let Some(view) = read_packed_refs_file_view(git_dir)? else {
return Ok(None);
};
let packed_path = store::paths::packed_refs_path(git_dir);
view.lookup_oid(&packed_path, refname)
}
#[cfg(test)]
pub(crate) fn lookup_packed_ref_with_peel(
git_dir: &Path,
refname: &str,
) -> Result<Option<(ObjectId, Option<ObjectId>)>> {
let Some(view) = read_packed_refs_file_view(git_dir)? else {
return Ok(None);
};
let packed_path = store::paths::packed_refs_path(git_dir);
view.lookup_entry(&packed_path, refname)
}
pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
store_write_value(
git_dir,
refname,
Some(StoreRawRef::Symbolic(target.to_owned())),
Expected::Any,
None,
)
}
pub(crate) fn ensure_refname_safe_for_storage(refname: &str) -> Result<()> {
if let Some(rest) = refname.strip_prefix("refs/") {
if rest.is_empty() || !crate::check_ref_format::is_valid_fetch_advertised_ref(refname) {
return Err(Error::InvalidRef(format!(
"refusing to update ref with bad name '{refname}'"
)));
}
return Ok(());
}
if crate::check_ref_format::refname_is_safe(refname) {
Ok(())
} else {
Err(Error::InvalidRef(format!(
"refusing to update ref with bad name '{refname}'"
)))
}
}
pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
store_write_value(
git_dir,
refname,
Some(StoreRawRef::Direct(*oid)),
Expected::Any,
None,
)
}
#[must_use]
pub fn ref_update_flags(refname: &str) -> RefUpdateFlags {
legacy_ref_update_flags(refname)
}
pub fn create_ref_with_reflog(
git_dir: &Path,
refname: &str,
oid: &ObjectId,
identity: &str,
message: &str,
) -> Result<()> {
let config = ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.unwrap_or_default();
let store = open_ref_store(git_dir)?;
create_ref_with_reflog_on_store(
store.as_ref(),
git_dir,
&config,
refname,
oid,
identity,
message,
)
}
pub(crate) fn create_ref_with_reflog_on_store(
store: &dyn RefStore,
git_dir: &Path,
config: &ConfigSet,
refname: &str,
oid: &ObjectId,
identity: &str,
message: &str,
) -> Result<()> {
let reflog =
should_autocreate_reflog_with_config(config, git_dir, refname).then(|| ReflogUpdate {
identity: identity.to_owned(),
message: message.to_owned(),
time: time::OffsetDateTime::now_utc(),
old_oid: None,
new_oid: None,
});
store_write_value_on_store(
store,
git_dir,
refname,
Some(StoreRawRef::Direct(*oid)),
Expected::Missing,
reflog,
)
}
fn write_ref_at_storage(
git_dir: &Path,
storage_dir: &Path,
refname: &str,
oid: &ObjectId,
) -> Result<()> {
let stor = crate::ref_namespace::storage_ref_name(refname);
let path = storage_dir.join(stor);
remove_empty_ref_directory(&path);
if fs::symlink_metadata(&path)
.map(|m| m.file_type().is_dir())
.unwrap_or(false)
{
let display = ref_path_for_display(&path);
return Err(crate::error::RefLockError::DirectoryInTheWay {
refname: refname.to_owned(),
path: display,
}
.into());
}
if path.is_file() && matches!(read_ref_file(&path), Err(Error::InvalidRef(_))) {
return Err(Error::InvalidRef(format!(
"cannot update ref '{refname}': reference broken"
)));
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let content = format!("{oid}\n");
let lock = lock_path_for_ref(&path);
use crate::lockfile::{LockError, LockFile, LockWait};
use std::io::Write as _;
let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
let mut lock = LockFile::acquire(&lock, wait).map_err(|err| match err {
LockError::Held { path } => {
Error::Io(crate::lockfile::lock_held_as_ref_present(LockError::Held {
path,
}))
}
LockError::Io { source, .. } => Error::Io(source),
})?;
lock.write_all(content.as_bytes()).map_err(Error::Io)?;
lock.commit_to(&path).map_err(|err| match err {
LockError::Io { source, .. } => Error::Io(source),
LockError::Held { path } => {
Error::Io(crate::lockfile::lock_held_as_ref_present(LockError::Held {
path,
}))
}
})?;
Ok(())
}
pub fn write_ref_cas(
git_dir: &Path,
refname: &str,
oid: &ObjectId,
expected_old: ObjectId,
) -> Result<()> {
store_write_value(
git_dir,
refname,
Some(StoreRawRef::Direct(*oid)),
Expected::Oid(expected_old),
None,
)
}
pub fn delete_ref_cas(git_dir: &Path, refname: &str, expected_old: ObjectId) -> Result<()> {
store_write_value(git_dir, refname, None, Expected::Oid(expected_old), None)
}
pub(crate) fn prune_loose_ref_cas(
git_dir: &Path,
refname: &str,
expected: ObjectId,
) -> Result<bool> {
ensure_refname_safe_for_storage(refname)?;
let storage_dir = ref_storage_dir(git_dir, refname);
let stor = crate::ref_namespace::storage_ref_name(refname);
let path = storage_dir.join(&stor);
let lock_path = lock_path_for_ref(&path);
use crate::lockfile::{LockFile, LockWait};
let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
let _lock = LockFile::acquire(&lock_path, wait)
.map_err(|err| Error::Io(crate::lockfile::lock_held_as_ref_present(err)))?;
let outcome = (|| -> Result<bool> {
if !path.is_file() {
return Ok(true);
}
let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
return Ok(false);
};
if oid != expected {
return Ok(false);
}
remove_empty_ref_directory(&path);
fs::remove_file(&path).map_err(Error::Io)?;
if let Some(parent) = path.parent() {
remove_empty_ref_directory(parent);
}
Ok(true)
})();
outcome
}
pub struct PackedRefs {
map: HashMap<String, ObjectId>,
sorted_names: BTreeSet<String>,
}
fn packed_refs_from_map(map: HashMap<String, ObjectId>) -> PackedRefs {
let sorted_names = map.keys().cloned().collect();
PackedRefs { map, sorted_names }
}
impl PackedRefs {
pub fn load(git_dir: &Path) -> Result<Self> {
let store = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
let map = read_packed_refs_map(&store)?;
Ok(packed_refs_from_map(map))
}
#[must_use]
pub fn get(&self, refname: &str) -> Option<ObjectId> {
self.map.get(refname).copied()
}
#[must_use]
pub fn has_namespace_conflict(&self, refname: &str) -> bool {
packed_refname_namespace_conflict(&self.sorted_names, refname)
}
pub(crate) fn record_loose_write(&mut self, refname: &str, oid: ObjectId) {
self.map.insert(refname.to_owned(), oid);
self.sorted_names.insert(refname.to_owned());
}
}
pub fn resolve_ref_cached(
git_dir: &Path,
refname: &str,
packed: &PackedRefs,
) -> Result<Option<ObjectId>> {
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return Ok(resolve_ref(git_dir, refname).ok());
}
resolve_files_ref_cached(git_dir, refname, packed)
}
pub(crate) struct CachedRefResolver<'a> {
git_dir: &'a Path,
packed: Option<PackedRefs>,
}
impl<'a> CachedRefResolver<'a> {
pub(crate) fn open(git_dir: &'a Path) -> Result<Self> {
let packed = if open_ref_store(git_dir)?.format() == RefStorageFormat::Files {
Some(PackedRefs::load(git_dir)?)
} else {
None
};
Ok(Self { git_dir, packed })
}
pub(crate) fn resolve(&self, refname: &str) -> Result<Option<ObjectId>> {
match &self.packed {
Some(packed) => resolve_files_ref_cached(self.git_dir, refname, packed),
None => Ok(resolve_ref(self.git_dir, refname).ok()),
}
}
}
fn resolve_files_ref_cached(
git_dir: &Path,
refname: &str,
packed: &PackedRefs,
) -> Result<Option<ObjectId>> {
let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
let try_names: Vec<&str> = if storage_owned != stor_name {
vec![storage_owned.as_str(), stor_name.as_str()]
} else {
vec![stor_name.as_str()]
};
for name in &try_names {
let path = store.join(name);
match read_ref_file(&path) {
Ok(Ref::Direct(oid)) => return Ok(Some(oid)),
Ok(Ref::Symbolic(target)) => return Ok(resolve_ref(git_dir, &target).ok()),
Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
}
for name in &try_names {
if let Some(oid) = packed.get(name) {
return Ok(Some(oid));
}
}
Ok(None)
}
pub fn write_ref_cached(
git_dir: &Path,
refname: &str,
oid: &ObjectId,
packed: &mut PackedRefs,
) -> Result<()> {
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return write_ref(git_dir, refname, oid);
}
ensure_refname_safe_for_storage(refname)?;
if packed.has_namespace_conflict(refname) {
return Err(Error::InvalidRef(format!(
"cannot update ref '{refname}': reference namespace conflict"
)));
}
let storage_dir = ref_storage_dir(git_dir, refname);
write_ref_at_storage(git_dir, &storage_dir, refname, oid)?;
packed.record_loose_write(refname, *oid);
Ok(())
}
fn ref_path_for_display(path: &Path) -> String {
let cwd = crate::environment::Environment::empty().cwd.clone();
if let Ok(rel) = path.strip_prefix(&cwd) {
return rel.to_string_lossy().into_owned();
}
if let (Ok(cwd_c), Ok(path_c)) = (cwd.canonicalize(), path.canonicalize()) {
if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
return rel.to_string_lossy().into_owned();
}
}
path.to_string_lossy().into_owned()
}
pub(crate) fn remove_empty_ref_directory(path: &Path) {
match fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_dir() => {}
_ => return,
}
if dir_tree_has_files(path) {
return;
}
let _ = remove_dir_tree(path);
}
fn dir_tree_has_files(dir: &Path) -> bool {
let Ok(entries) = fs::read_dir(dir) else {
return true;
};
for entry in entries.flatten() {
match entry.file_type() {
Ok(ft) if ft.is_dir() => {
if dir_tree_has_files(&entry.path()) {
return true;
}
}
Ok(_) => return true, Err(_) => return true,
}
}
false
}
pub(crate) fn prune_empty_loose_ref_parents(storage_dir: &Path, ref_path: &Path) {
let refs_root = storage_dir.join("refs");
let mut current = ref_path.parent();
while let Some(dir) = current {
if dir == refs_root.as_path() || !dir.starts_with(&refs_root) {
break;
}
if dir_tree_has_files(dir) {
break;
}
if fs::remove_dir(dir).is_err() {
break;
}
current = dir.parent();
}
}
fn remove_dir_tree(dir: &Path) -> io::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
remove_dir_tree(&entry.path())?;
}
}
fs::remove_dir(dir)
}
pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
if matches!(read_raw_ref(git_dir, refname)?, RawRefLookup::NotFound) {
return Err(store::RefStoreError::not_found(refname).into());
}
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
let store = open_ref_store(git_dir)?;
let mut flags = legacy_ref_update_flags(refname);
if refname != "HEAD" {
if let Ok(Some(StoreRawRef::Symbolic(_))) = store.read_raw(refname) {
flags.no_deref = true;
}
}
return commit_store_update(
git_dir,
RefUpdate {
name: refname.to_owned(),
new_value: None,
expected: Expected::Exists,
reflog: None,
flags,
},
)
.map_err(|e| map_public_ref_store_error(e, refname));
}
ensure_refname_safe_for_storage(refname)?;
let storage_dir = ref_storage_dir(git_dir, refname);
let stor = crate::ref_namespace::storage_ref_name(refname);
let path = storage_dir.join(&stor);
let lock = lock_path_for_ref(&path);
if fs::symlink_metadata(&lock).is_ok() {
return Err(Error::Io(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("ref lock present: {}", lock.display()),
)));
}
remove_packed_ref(&storage_dir, &stor)?;
remove_empty_ref_directory(&path);
match fs::remove_file(&path) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e)
if e.kind() == io::ErrorKind::NotADirectory
|| e.raw_os_error() == Some(libc::ENOTDIR) => {}
Err(e)
if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
Err(e) => return Err(Error::Io(e)),
}
prune_empty_loose_ref_parents(&storage_dir, &path);
let log_path = storage_dir.join("logs").join(&stor);
let _ = fs::remove_file(&log_path);
let logs_root = storage_dir.join("logs");
let mut parent = log_path.parent();
while let Some(p) = parent {
if p == logs_root.as_path() || !p.starts_with(&logs_root) {
break;
}
if fs::remove_dir(p).is_err() {
break; }
parent = p.parent();
}
Ok(())
}
pub(crate) fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
remove_packed_ref_inner(git_dir, refname, false)
}
pub(crate) fn remove_packed_ref_under_lock(git_dir: &Path, refname: &str) -> Result<()> {
remove_packed_ref_inner(git_dir, refname, true)
}
fn remove_packed_ref_inner(git_dir: &Path, refname: &str, lock_already_held: bool) -> Result<()> {
let packed_path = store::paths::packed_refs_path(git_dir);
let content = match fs::read(&packed_path) {
Ok(c) => c,
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::NotADirectory
|| e.raw_os_error() == Some(libc::ENOTDIR) =>
{
return Ok(());
}
Err(e) => return Err(Error::Io(e)),
};
parse_packed_refs_to_map(&packed_path, &content)?;
let content = String::from_utf8(content)
.map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
let mut out = String::new();
let mut skip_peeled = false;
let mut changed = false;
let mut header_written = false;
for line in content.lines() {
if skip_peeled {
if line.starts_with('^') {
changed = true;
continue;
}
skip_peeled = false;
}
if line.starts_with('#') {
continue;
}
if line.starts_with('^') {
out.push_str(line);
out.push('\n');
continue;
}
if !header_written {
out.insert_str(0, "# pack-refs with: peeled fully-peeled sorted\n");
header_written = true;
}
let mut parts = line.splitn(2, ' ');
let _hash = parts.next().unwrap_or("");
let name = parts.next().unwrap_or("").trim();
if name == refname {
changed = true;
skip_peeled = true;
continue;
}
out.push_str(line);
out.push('\n');
}
if changed {
let lock = lock_path_for_ref(&packed_path); let _packed_lock = if !lock_already_held {
let abs_lock = fs::canonicalize(git_dir)
.map(|d| d.join("packed-refs.lock"))
.unwrap_or_else(|_| lock.clone());
Some(acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?)
} else {
None
};
let tmp = packed_path.with_extension("new");
let mut created_tmp = false;
let write_result = (|| -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(Error::Io)?;
created_tmp = true;
use std::io::Write as _;
file.write_all(out.as_bytes()).map_err(Error::Io)?;
drop(file);
fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
created_tmp = false; Ok(())
})();
if write_result.is_err() && created_tmp {
let _ = fs::remove_file(&tmp);
}
write_result?;
}
Ok(())
}
pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
read_symbolic_ref(git_dir, "HEAD")
}
pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
match open_ref_store(git_dir)?.read_raw(refname)? {
Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
Some(StoreRawRef::Direct(_)) | None => Ok(None),
}
}
pub fn read_symbolic_ref_for_repository(
repo: &crate::repo::Repository,
refname: &str,
) -> Result<Option<String>> {
match repo.refs().read_raw(refname)? {
Some(StoreRawRef::Symbolic(target)) => Ok(Some(target)),
Some(StoreRawRef::Direct(_)) | None => Ok(None),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogRefsConfig {
Unset,
None,
Normal,
Always,
}
impl LogRefsConfig {
pub(crate) fn parse(raw: &str) -> Option<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"always" => Some(Self::Always),
"1" | "true" | "yes" | "on" => Some(Self::Normal),
"0" | "false" | "no" | "off" | "never" => Some(Self::None),
_ => None,
}
}
}
pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.map(|cfg| cfg.log_refs_config())
.unwrap_or(LogRefsConfig::Unset)
}
pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.map(|cfg| cfg.effective_log_refs_config())
.unwrap_or(LogRefsConfig::Normal)
}
#[must_use]
pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
match mode {
LogRefsConfig::Always => true,
LogRefsConfig::Normal => {
refname == "HEAD"
|| refname.starts_with("refs/heads/")
|| refname.starts_with("refs/remotes/")
|| refname.starts_with("refs/notes/")
}
LogRefsConfig::None | LogRefsConfig::Unset => false,
}
}
#[must_use]
pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
}
#[must_use]
pub fn should_autocreate_reflog_with_config(
config: &ConfigSet,
_git_dir: &Path,
refname: &str,
) -> bool {
should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config())
}
fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
let Ok(rel) = target.strip_prefix(logs_root) else {
return;
};
let mut cur = logs_root.to_path_buf();
for component in rel.components() {
cur.push(component);
match fs::symlink_metadata(&cur) {
Ok(meta) if meta.file_type().is_dir() => {}
Ok(_) => {
let _ = fs::remove_file(&cur);
}
Err(_) => break, }
}
}
pub(crate) fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
let width = if !old.is_zero() {
old.algo().hex_len()
} else if !new.is_zero() {
new.algo().hex_len()
} else {
old.algo().hex_len()
};
let render = |oid: &ObjectId| {
if oid.is_zero() {
"0".repeat(width)
} else {
oid.to_hex()
}
};
(render(old), render(new))
}
pub fn append_reflog(
git_dir: &Path,
refname: &str,
old_oid: &ObjectId,
new_oid: &ObjectId,
identity: &str,
message: &str,
force_create: bool,
) -> Result<()> {
append_reflog_with_config(
git_dir,
refname,
old_oid,
new_oid,
identity,
message,
force_create,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn append_reflog_with_config(
git_dir: &Path,
refname: &str,
old_oid: &ObjectId,
new_oid: &ObjectId,
identity: &str,
message: &str,
force_create: bool,
config: Option<&ConfigSet>,
) -> Result<()> {
test_inject_reflog_fail(refname)?;
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return crate::reftable::reftable_append_reflog(
git_dir,
refname,
old_oid,
new_oid,
identity,
message,
force_create,
);
}
let storage_dir = ref_storage_dir(git_dir, refname);
let stor = crate::ref_namespace::storage_ref_name(refname);
let log_path = storage_dir.join("logs").join(&stor);
let may_create = force_create
|| match config {
Some(cfg) => should_autocreate_reflog_with_config(cfg, git_dir, refname),
None => should_autocreate_reflog(git_dir, refname),
};
if !may_create && !log_path.exists() {
return Ok(());
}
if let Some(parent) = log_path.parent() {
let logs_root = storage_dir.join("logs");
clear_conflicting_reflog_files(&logs_root, parent);
fs::create_dir_all(parent)?;
}
let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
let line = if message.is_empty() {
format!("{old_hex} {new_hex} {identity}\n")
} else {
format!("{old_hex} {new_hex} {identity}\t{message}\n")
};
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)?;
use io::Write;
file.write_all(line.as_bytes())?;
Ok(())
}
fn pack_ref_include_pattern(all: bool) -> &'static [u8] {
if all {
b"*"
} else {
b"refs/tags/*"
}
}
fn ref_matches_pack_include(refname: &str, all: bool) -> bool {
crate::wildmatch::wildmatch(pack_ref_include_pattern(all), refname.as_bytes(), 0)
}
fn collect_loose_ref_files(
dir: &Path,
prefix: &str,
out: &mut Vec<(String, PathBuf)>,
) -> Result<()> {
let read = match fs::read_dir(dir) {
Ok(r) => r,
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::NotADirectory
|| e.raw_os_error() == Some(libc::ENOTDIR) =>
{
return Ok(());
}
Err(e) => return Err(Error::Io(e)),
};
for entry in read {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
let refname = format!("{prefix}{name}");
let path = entry.path();
if path.is_dir() {
collect_loose_ref_files(&path, &format!("{refname}/"), out)?;
} else if path.is_file() {
out.push((refname, path));
}
}
Ok(())
}
fn load_packed_refs_with_peel(
packed_path: &Path,
merged: &mut HashMap<String, (ObjectId, Option<ObjectId>)>,
) -> Result<()> {
if !packed_path.is_file() {
return Ok(());
}
let data = fs::read(packed_path)?;
parse_packed_refs_to_map(packed_path, &data)?;
let content = String::from_utf8(data)
.map_err(|_| packed_refs_unexpected_line(packed_path, "<invalid utf-8>"))?;
let mut last_name: Option<String> = None;
for line in content.lines() {
if line.starts_with('#') {
continue;
}
if let Some(hex) = line.strip_prefix('^') {
let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
continue;
};
if let Some(name) = &last_name {
if let Some(entry) = merged.get_mut(name) {
entry.1 = Some(peel_oid);
}
}
continue;
}
let mut parts = line.splitn(2, ' ');
let Some(hex) = parts.next() else {
continue;
};
let Some(name) = parts.next() else {
continue;
};
let name = name.trim();
let Ok(oid) = ObjectId::from_hex(hex) else {
continue;
};
merged.insert(name.to_owned(), (oid, None));
last_name = Some(name.to_owned());
}
Ok(())
}
pub(crate) fn pack_refs_for_repository(
repo: &crate::repo::Repository,
all: bool,
) -> Result<(PathBuf, usize, usize)> {
let git_dir = repo.git_dir();
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return pack_refs_reftable(git_dir);
}
pack_refs_files(git_dir, repo.odb(), all)
}
fn pack_refs_reftable(git_dir: &Path) -> Result<(PathBuf, usize, usize)> {
use crate::reftable::ReftableStack;
let store_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
let rt_dir = store_git_dir.join("reftable");
let tables_before = fs::read_to_string(rt_dir.join("tables.list"))
.map(|s| s.lines().filter(|l| !l.is_empty()).count())
.unwrap_or(0);
let mut stack = ReftableStack::open(&store_git_dir)?;
stack.compact()?;
let tables_after = fs::read_to_string(rt_dir.join("tables.list"))
.map(|s| s.lines().filter(|l| !l.is_empty()).count())
.unwrap_or(0);
Ok((rt_dir, 0, tables_before.saturating_sub(tables_after)))
}
fn pack_refs_files(
git_dir: &Path,
odb: &crate::odb::Odb,
all: bool,
) -> Result<(PathBuf, usize, usize)> {
let storage_git_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
let packed_path = store::paths::packed_refs_path(&storage_git_dir);
let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
load_packed_refs_with_peel(&packed_path, &mut merged)?;
let mut loose_files = Vec::new();
collect_loose_ref_files(&storage_git_dir.join("refs"), "refs/", &mut loose_files)?;
let mut packed_loose = 0usize;
let mut to_prune: Vec<(String, ObjectId)> = Vec::new();
for (refname, path) in loose_files {
if crate::worktree_ref::is_per_worktree_ref(&refname) {
continue;
}
if !ref_matches_pack_include(&refname, all) {
continue;
}
let Ok(Ref::Direct(oid)) = read_ref_file(&path) else {
continue;
};
if odb.read(&oid).is_err() {
continue;
}
let peel = if refname.starts_with("refs/tags/") {
packed_ref_peel(odb, oid)
} else {
None
};
merged.insert(refname.clone(), (oid, peel));
to_prune.push((refname, oid));
let _ = path;
packed_loose += 1;
}
if packed_loose == 0 && merged.is_empty() {
return Ok((packed_path, 0, 0));
}
let mut names: Vec<String> = merged.keys().cloned().collect();
names.sort();
let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
for name in &names {
let Some((oid, peel)) = merged.get(name) else {
continue;
};
body.push_str(&oid.to_hex());
body.push(' ');
body.push_str(name);
body.push('\n');
if let Some(p) = peel {
body.push('^');
body.push_str(&p.to_hex());
body.push('\n');
}
}
atomic_rewrite_packed_refs(&storage_git_dir, &body)?;
#[cfg(test)]
pack_refs_test_hooks::run_before_prune();
let mut pruned = 0usize;
for (refname, expected) in &to_prune {
match prune_loose_ref_cas(git_dir, refname, *expected) {
Ok(true) => pruned += 1,
Ok(false) => {
remove_packed_ref(git_dir, refname)?;
}
Err(e) => return Err(e),
}
}
Ok((packed_path, packed_loose, pruned))
}
#[cfg(test)]
mod pack_refs_test_hooks {
use std::cell::RefCell;
thread_local! {
static BEFORE_PRUNE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
}
pub fn set_before_prune(f: Option<Box<dyn Fn()>>) {
BEFORE_PRUNE.with(|c| *c.borrow_mut() = f);
}
pub fn run_before_prune() {
BEFORE_PRUNE.with(|c| {
if let Some(f) = c.borrow_mut().take() {
f();
}
});
}
}
#[cfg(test)]
mod reflog_fail_inject {
use std::cell::RefCell;
thread_local! {
pub static INJECT: RefCell<Option<String>> = const { RefCell::new(None) };
}
}
#[cfg(test)]
pub fn set_test_inject_reflog_fail(refname: Option<&str>) {
reflog_fail_inject::INJECT.with(|c| {
*c.borrow_mut() = refname.map(str::to_owned);
});
}
#[cfg(test)]
fn test_inject_reflog_fail(refname: &str) -> Result<()> {
reflog_fail_inject::INJECT.with(|c| {
if c.borrow().as_deref() == Some(refname) {
return Err(Error::Io(std::io::Error::other("injected reflog failure")));
}
Ok(())
})
}
#[cfg(not(test))]
fn test_inject_reflog_fail(_refname: &str) -> Result<()> {
Ok(())
}
pub struct BranchCommitRefUpdate<'a> {
pub branch_ref: &'a str,
pub expected_old: Option<ObjectId>,
pub new_oid: ObjectId,
pub identity: &'a str,
pub reflog_message: &'a str,
}
pub fn verify_branch_commit_cas(
expected: Option<ObjectId>,
current: Option<ObjectId>,
refname: &str,
) -> Result<()> {
verify_branch_cas(expected, current, refname)
}
fn verify_branch_cas(
expected: Option<ObjectId>,
current: Option<ObjectId>,
refname: &str,
) -> Result<()> {
if expected == current {
return Ok(());
}
Err(store::RefStoreError::ExpectedMismatch {
name: refname.to_owned(),
expected: expected.map_or(Expected::Missing, Expected::Oid),
actual: current.map(StoreRawRef::Direct),
}
.into())
}
fn read_loose_or_packed_oid(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
let try_names: Vec<&str> = if storage_owned != stor_name {
vec![storage_owned.as_str(), stor_name.as_str()]
} else {
vec![stor_name.as_str()]
};
for name in try_names {
let path = store.join(name);
if let Ok(Ref::Direct(oid)) = read_ref_file(&path) {
return Ok(Some(oid));
}
if let Some(oid) = lookup_packed_ref(&store, name)? {
return Ok(Some(oid));
}
}
Ok(None)
}
#[cfg(test)]
mod test_pause_inject_serial {
use std::sync::{Mutex, OnceLock};
static SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
pub fn lock() -> std::sync::MutexGuard<'static, ()> {
SERIAL
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
}
#[cfg(test)]
pub fn test_pause_injection_serial_guard() -> std::sync::MutexGuard<'static, ()> {
test_pause_inject_serial::lock()
}
#[cfg(test)]
mod branch_ref_pause_inject {
use std::sync::{Condvar, Mutex, OnceLock};
pub struct PauseGate {
pub wait: Mutex<bool>,
pub cv: Condvar,
}
static PAUSE_BEFORE_COMMIT: OnceLock<Mutex<Option<std::sync::Arc<PauseGate>>>> =
OnceLock::new();
pub fn pause_slot() -> &'static Mutex<Option<std::sync::Arc<PauseGate>>> {
PAUSE_BEFORE_COMMIT.get_or_init(|| Mutex::new(None))
}
}
#[cfg(test)]
pub type BranchRefPauseGate = std::sync::Arc<branch_ref_pause_inject::PauseGate>;
#[cfg(test)]
pub fn set_test_inject_branch_ref_pause_before_commit(gate: Option<BranchRefPauseGate>) {
*branch_ref_pause_inject::pause_slot()
.lock()
.unwrap_or_else(|e| e.into_inner()) = gate;
}
#[cfg(test)]
fn test_branch_ref_pause_before_commit() {
let gate = branch_ref_pause_inject::pause_slot()
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
if let Some(gate) = gate {
let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
*wait = true;
gate.cv.notify_all();
while *wait {
wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
}
}
}
#[cfg(not(test))]
fn test_branch_ref_pause_before_commit() {}
#[cfg(test)]
mod ref_store_batch_pause_inject {
use std::sync::{Condvar, Mutex, OnceLock};
pub struct PauseGate {
pub wait: Mutex<bool>,
pub cv: Condvar,
}
pub type PauseSlot = Option<(usize, std::path::PathBuf, std::sync::Arc<PauseGate>)>;
static PAUSE_AFTER_REF_WRITE: OnceLock<Mutex<PauseSlot>> = OnceLock::new();
pub fn pause_slot() -> &'static Mutex<PauseSlot> {
PAUSE_AFTER_REF_WRITE.get_or_init(|| Mutex::new(None))
}
}
#[cfg(test)]
pub type RefStoreBatchPauseGate = std::sync::Arc<ref_store_batch_pause_inject::PauseGate>;
#[cfg(test)]
pub fn new_ref_store_batch_pause_gate() -> RefStoreBatchPauseGate {
std::sync::Arc::new(ref_store_batch_pause_inject::PauseGate {
wait: std::sync::Mutex::new(false),
cv: std::sync::Condvar::new(),
})
}
#[cfg(test)]
pub fn set_test_inject_ref_store_batch_pause_after_apply(
apply_index: usize,
git_dir: &Path,
gate: Option<RefStoreBatchPauseGate>,
) {
*ref_store_batch_pause_inject::pause_slot()
.lock()
.unwrap_or_else(|e| e.into_inner()) = gate.map(|g| (apply_index, git_dir.to_path_buf(), g));
}
#[cfg(test)]
struct RefStorePauseTestSerial {
holder: Option<std::thread::ThreadId>,
}
#[cfg(test)]
static REF_STORE_PAUSE_TEST_SERIAL: std::sync::Mutex<RefStorePauseTestSerial> =
std::sync::Mutex::new(RefStorePauseTestSerial { holder: None });
#[cfg(test)]
pub(crate) struct RefStoreBatchPauseTestScope {
_serial: std::sync::MutexGuard<'static, RefStorePauseTestSerial>,
}
#[cfg(test)]
pub(crate) fn ref_store_batch_pause_test_scope() -> RefStoreBatchPauseTestScope {
let mut serial = REF_STORE_PAUSE_TEST_SERIAL
.lock()
.unwrap_or_else(|e| e.into_inner());
assert!(
serial.holder.is_none(),
"ref_store_batch_pause_test_scope must not nest"
);
serial.holder = Some(std::thread::current().id());
set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
RefStoreBatchPauseTestScope { _serial: serial }
}
#[cfg(test)]
impl Drop for RefStoreBatchPauseTestScope {
fn drop(&mut self) {
set_test_inject_ref_store_batch_pause_after_apply(0, Path::new(""), None);
self._serial.holder = None;
}
}
#[cfg(test)]
pub(crate) fn test_ref_store_batch_pause_after_ref_write(idx: usize, git_dir: &Path) {
let slot = ref_store_batch_pause_inject::pause_slot()
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
if let Some((target, target_dir, gate)) = slot {
let same_repo = |a: &Path, b: &Path| {
a == b || matches!((a.canonicalize(), b.canonicalize()), (Ok(a), Ok(b)) if a == b)
};
if idx != target || !same_repo(git_dir, &target_dir) {
return;
}
let mut wait = gate.wait.lock().unwrap_or_else(|e| e.into_inner());
*wait = true;
gate.cv.notify_all();
while *wait {
wait = gate.cv.wait(wait).unwrap_or_else(|e| e.into_inner());
}
}
}
#[cfg(not(test))]
pub(crate) fn test_ref_store_batch_pause_after_ref_write(_idx: usize, _git_dir: &Path) {}
pub struct BranchCommitRefLock {
lock: Option<crate::lockfile::LockFile>,
disarmed: bool,
}
impl BranchCommitRefLock {
pub fn acquire(
git_dir: &Path,
branch_ref: &str,
expected_old: Option<ObjectId>,
) -> Result<Self> {
ensure_refname_safe_for_storage(branch_ref)?;
let storage_dir = ref_storage_dir(git_dir, branch_ref);
if packed_ref_namespace_conflict(&storage_dir, branch_ref)? {
return Err(Error::InvalidRef(format!(
"cannot update ref '{}': reference namespace conflict",
branch_ref
)));
}
let stor = crate::ref_namespace::storage_ref_name(branch_ref);
let path = storage_dir.join(&stor);
remove_empty_ref_directory(&path);
if fs::symlink_metadata(&path)
.map(|m| m.file_type().is_dir())
.unwrap_or(false)
{
let display = ref_path_for_display(&path);
return Err(crate::error::RefLockError::DirectoryInTheWay {
refname: branch_ref.to_owned(),
path: display,
}
.into());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let lock_path = lock_path_for_ref(&path);
use crate::lockfile::{LockError, LockFile, LockWait};
let wait = LockWait::files_ref_lock_for_git_dir(git_dir);
let lock = LockFile::acquire(&lock_path, wait).map_err(|err| match err {
LockError::Held { path } => {
Error::Io(crate::lockfile::lock_held_as_ref_present(LockError::Held {
path,
}))
}
LockError::Io { source, .. } => Error::Io(source),
})?;
let current = read_loose_or_packed_oid(git_dir, branch_ref)?;
verify_branch_cas(expected_old, current, branch_ref)?;
Ok(Self {
lock: Some(lock),
disarmed: false,
})
}
pub fn write_reflogs(
&mut self,
git_dir: &Path,
update: &BranchCommitRefUpdate<'_>,
reflog_old: &ObjectId,
config: &ConfigSet,
) -> Result<()> {
write_branch_commit_reflogs(git_dir, self, update, reflog_old, config)
}
pub fn finish_ref_commit(
mut self,
git_dir: &Path,
update: &BranchCommitRefUpdate<'_>,
) -> Result<()> {
finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
self.disarmed = true;
Ok(())
}
pub fn commit(
mut self,
git_dir: &Path,
update: &BranchCommitRefUpdate<'_>,
reflog_old: &ObjectId,
config: &ConfigSet,
) -> Result<()> {
write_branch_commit_reflogs(git_dir, &mut self, update, reflog_old, config)?;
finish_branch_commit_ref_lock_files(git_dir, &mut self, update)?;
self.disarmed = true;
Ok(())
}
}
impl Drop for BranchCommitRefLock {
fn drop(&mut self) {
drop(self.lock.take());
}
}
fn write_branch_commit_reflogs(
git_dir: &Path,
lock: &mut BranchCommitRefLock,
update: &BranchCommitRefUpdate<'_>,
reflog_old: &ObjectId,
config: &ConfigSet,
) -> Result<()> {
let mut branch_reflog_written = false;
if should_autocreate_reflog_with_config(config, git_dir, update.branch_ref) {
match append_reflog_with_config(
git_dir,
update.branch_ref,
reflog_old,
&update.new_oid,
update.identity,
update.reflog_message,
false,
Some(config),
) {
Ok(()) => branch_reflog_written = true,
Err(err) => {
drop(lock.lock.take());
return Err(err);
}
}
}
if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
if let Err(err) = append_reflog_with_config(
git_dir,
"HEAD",
reflog_old,
&update.new_oid,
update.identity,
update.reflog_message,
false,
Some(config),
) {
if branch_reflog_written {
let _ = crate::reflog::truncate_last_reflog_line(git_dir, update.branch_ref);
}
drop(lock.lock.take());
return Err(err);
}
}
Ok(())
}
fn finish_branch_commit_ref_lock_files(
git_dir: &Path,
lock: &mut BranchCommitRefLock,
update: &BranchCommitRefUpdate<'_>,
) -> Result<()> {
test_branch_ref_pause_before_commit();
let storage_dir = ref_storage_dir(git_dir, update.branch_ref);
let stor = crate::ref_namespace::storage_ref_name(update.branch_ref);
let path = storage_dir.join(&stor);
let content = format!("{}\n", update.new_oid);
use crate::lockfile::LockError;
use std::io::Write as _;
let mut file_lock = lock
.lock
.take()
.ok_or_else(|| Error::Message("branch ref lock already released".into()))?;
file_lock.write_all(content.as_bytes()).map_err(Error::Io)?;
file_lock.commit_to(&path).map_err(|err| match err {
LockError::Io { source, .. } => Error::Io(source),
LockError::Held { path } => {
Error::Io(crate::lockfile::lock_held_as_ref_present(LockError::Held {
path,
}))
}
})?;
Ok(())
}
fn update_branch_for_commit_files(
git_dir: &Path,
update: &BranchCommitRefUpdate<'_>,
reflog_old: &ObjectId,
config: &ConfigSet,
) -> Result<()> {
let lock = BranchCommitRefLock::acquire(git_dir, update.branch_ref, update.expected_old)?;
lock.commit(git_dir, update, reflog_old, config)
}
fn update_branch_for_commit_reftable(
git_dir: &Path,
update: &BranchCommitRefUpdate<'_>,
reflog_old: &ObjectId,
) -> Result<()> {
let current = resolve_ref(git_dir, update.branch_ref).ok();
verify_branch_cas(update.expected_old, current, update.branch_ref)?;
let mut updates = Vec::new();
let branch_log = if should_autocreate_reflog(git_dir, update.branch_ref) {
Some(crate::reftable::reftable_log_record_for_commit(
git_dir,
update.branch_ref,
reflog_old,
&update.new_oid,
update.identity,
update.reflog_message,
)?)
} else {
None
};
updates.push(crate::reftable::ReftableTransactionUpdate {
refname: update.branch_ref.to_owned(),
value: Some(crate::reftable::RefValue::Val1(update.new_oid)),
log: branch_log,
expected_old: update.expected_old,
});
if should_autocreate_reflog(git_dir, "HEAD") {
updates.push(crate::reftable::ReftableTransactionUpdate {
refname: "HEAD".to_owned(),
value: None,
log: Some(crate::reftable::reftable_log_record_for_commit(
git_dir,
"HEAD",
reflog_old,
&update.new_oid,
update.identity,
update.reflog_message,
)?),
expected_old: None,
});
}
crate::reftable::reftable_write_transaction(git_dir, updates)
}
pub fn update_branch_for_commit(git_dir: &Path, update: &BranchCommitRefUpdate<'_>) -> Result<()> {
let config = ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.unwrap_or_default();
update_branch_for_commit_with_config(git_dir, update, &config)
}
pub fn update_branch_for_commit_with_config(
git_dir: &Path,
update: &BranchCommitRefUpdate<'_>,
config: &ConfigSet,
) -> Result<()> {
let head_target = read_head(git_dir)?;
if head_target.as_deref() != Some(update.branch_ref) {
return Err(Error::Message(format!(
"HEAD does not point at branch '{}'",
update.branch_ref
)));
}
let reflog_old = update.expected_old.unwrap_or_else(crate::diff::zero_oid);
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return update_branch_for_commit_reftable(git_dir, update, &reflog_old);
}
update_branch_for_commit_files(git_dir, update, &reflog_old, config)
}
pub(crate) fn update_detached_head_for_commit_with_config(
git_dir: &Path,
expected_old: Option<ObjectId>,
new_oid: ObjectId,
identity: &str,
reflog_message: &str,
config: &ConfigSet,
) -> Result<()> {
if read_head(git_dir)?.is_some() {
return Err(Error::Message(
"HEAD points at a branch; use update_branch_for_commit".into(),
));
}
let current = resolve_ref(git_dir, "HEAD").ok();
verify_branch_cas(expected_old, current, "HEAD")?;
let reflog_old = expected_old.unwrap_or_else(crate::diff::zero_oid);
write_ref_cas(git_dir, "HEAD", &new_oid, reflog_old)?;
if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
if let Err(err) = append_reflog_with_config(
git_dir,
"HEAD",
&reflog_old,
&new_oid,
identity,
reflog_message,
false,
Some(config),
) {
let _ = write_ref_cas(git_dir, "HEAD", &reflog_old, new_oid);
return Err(err);
}
}
Ok(())
}
#[must_use]
pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
store.join("logs").join(stor_name)
}
fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
}
fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
if prefix.is_empty() {
return String::new();
}
if prefix.ends_with('/') {
return prefix.to_string();
}
let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
if candidate.is_file() {
prefix.to_string()
} else {
format!("{prefix}/")
}
}
fn packed_ref_peel(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
odb.peel_tag_target(oid).ok().flatten()
}
pub fn pack_remote_tracking_refs_for_clone(git_dir: &Path, remote: &str) -> Result<()> {
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return Ok(());
}
let prefix = format!("refs/remotes/{remote}/");
let head_sym = format!("refs/remotes/{remote}/HEAD");
let mut entries = list_refs(git_dir, &prefix)?;
entries.retain(|(name, _)| name != &head_sym);
if entries.is_empty() {
return Ok(());
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let odb =
crate::odb::Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir.to_path_buf());
let mut merged: HashMap<String, (ObjectId, Option<ObjectId>)> = HashMap::new();
let packed_path = store::paths::packed_refs_path(git_dir);
if let Ok(data) = fs::read(&packed_path) {
parse_packed_refs_to_map(&packed_path, &data)?;
let content = String::from_utf8(data)
.map_err(|_| packed_refs_unexpected_line(&packed_path, "<invalid utf-8>"))?;
let mut last_name: Option<String> = None;
for line in content.split('\n') {
if line.starts_with('#') {
continue;
}
if let Some(hex) = line.strip_prefix('^') {
let Ok(peel_oid) = ObjectId::from_hex(hex.trim()) else {
continue;
};
if let Some(name) = &last_name {
if let Some(entry) = merged.get_mut(name) {
entry.1 = Some(peel_oid);
}
}
continue;
}
let mut parts = line.splitn(2, ' ');
let Some(hex) = parts.next() else {
continue;
};
let Some(name) = parts.next() else {
continue;
};
let name = name.trim();
let Ok(oid) = ObjectId::from_hex(hex) else {
continue;
};
if name.starts_with(&prefix) {
last_name = None;
continue;
}
merged.insert(name.to_owned(), (oid, None));
last_name = Some(name.to_owned());
}
}
for (name, oid) in &entries {
merged.insert(name.clone(), (*oid, packed_ref_peel(&odb, *oid)));
}
let mut names: Vec<String> = merged.keys().cloned().collect();
names.sort();
let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
for name in &names {
let Some((oid, peel)) = merged.get(name) else {
continue;
};
body.push_str(&oid.to_hex());
body.push(' ');
body.push_str(name);
body.push('\n');
if let Some(p) = peel {
body.push('^');
body.push_str(&p.to_hex());
body.push('\n');
}
}
atomic_rewrite_packed_refs(git_dir, &body)?;
for (name, _) in &entries {
let storage_dir = ref_storage_dir(git_dir, name);
let stor = crate::ref_namespace::storage_ref_name(name);
let path = storage_dir.join(&stor);
if path.is_file() {
let _ = fs::remove_file(&path);
}
if let Some(parent) = path.parent() {
remove_empty_ref_directory(parent);
}
}
Ok(())
}
fn atomic_rewrite_packed_refs(git_dir: &Path, body: &str) -> Result<()> {
let packed_path = store::paths::packed_refs_path(git_dir);
let lock = lock_path_for_ref(&packed_path);
let abs_lock = fs::canonicalize(git_dir)
.map(|d| d.join("packed-refs.lock"))
.unwrap_or_else(|_| lock.clone());
let _packed_lock = acquire_packed_refs_lock(git_dir, &lock, &abs_lock)?;
let tmp = packed_path.with_extension("new");
let mut created_tmp = false;
let write_result = (|| -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
.map_err(Error::Io)?;
created_tmp = true;
use std::io::Write as _;
file.write_all(body.as_bytes()).map_err(Error::Io)?;
drop(file);
fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
created_tmp = false;
Ok(())
})();
if write_result.is_err() && created_tmp {
let _ = fs::remove_file(&tmp);
}
write_result
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackedRefRecord {
pub refname: String,
pub oid: ObjectId,
pub peeled: Option<ObjectId>,
}
pub fn write_packed_refs_file(git_dir: &Path, records: &[PackedRefRecord]) -> Result<()> {
let mut sorted: Vec<&PackedRefRecord> = records.iter().collect();
sorted.sort_by(|a, b| a.refname.cmp(&b.refname));
for rec in &sorted {
ensure_refname_safe_for_storage(&rec.refname)?;
}
let mut body = String::from("# pack-refs with: peeled fully-peeled sorted\n");
for rec in &sorted {
body.push_str(&rec.oid.to_hex());
body.push(' ');
body.push_str(&rec.refname);
body.push('\n');
if let Some(peeled) = rec.peeled {
body.push('^');
body.push_str(&peeled.to_hex());
body.push('\n');
}
}
atomic_rewrite_packed_refs(git_dir, &body)?;
for rec in &sorted {
let storage_dir = ref_storage_dir(git_dir, &rec.refname);
let stor = crate::ref_namespace::storage_ref_name(&rec.refname);
let loose = storage_dir.join(stor);
if loose.is_file() {
fs::remove_file(&loose).map_err(Error::Io)?;
}
if let Some(parent) = loose.parent() {
remove_empty_ref_directory(parent);
}
}
Ok(())
}
pub fn list_refs_for_repository(
repo: &crate::repo::Repository,
prefix: &str,
) -> Result<Vec<(String, ObjectId)>> {
let git_dir = &repo.git_dir();
let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
let mut results = collect_resolved_refs(repo.refs(), prefix_norm.as_str())?;
if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
}
results.sort_by(|a, b| a.0.cmp(&b.0));
Ok(results)
}
pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
let prefix = prefix_norm.as_str();
let store = open_ref_store(git_dir)?;
if store.format() != RefStorageFormat::Files {
return crate::reftable::reftable_list_refs(git_dir, prefix);
}
let mut results = collect_resolved_refs(store.as_ref(), prefix)?;
if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
}
results.sort_by(|a, b| a.0.cmp(&b.0));
Ok(results)
}
pub(crate) fn list_refs_filtering_packed(
git_dir: &Path,
prefix: &str,
keep: PackedRecordFilter<'_>,
) -> Result<Vec<(String, ObjectId)>> {
let Some(store) = store::open_files_ref_store(git_dir)? else {
return list_refs(git_dir, prefix);
};
let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
let prefix = prefix_norm.as_str();
let mut results = collect_resolved_ref_entries(&store, |f| {
store.for_each_ref_filtered(prefix, Some(keep), f)
})?;
if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
}
results.sort_by(|a, b| a.0.cmp(&b.0));
Ok(results)
}
pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
}
pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
if open_ref_store(git_dir)?.format() != RefStorageFormat::Files {
return crate::reftable::reftable_list_refs(git_dir, prefix);
}
let mut by_name: HashMap<String, ObjectId> = HashMap::new();
let stored_prefix = prefix.to_owned();
if let Some(cdir) = common_dir(git_dir) {
if cdir != git_dir {
collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
let (loose_root, loose_prefix) = loose_ref_list_walk_root(&cdir, &stored_prefix);
collect_loose_refs_into_map(
&loose_root,
&loose_prefix,
&stored_prefix,
&cdir,
true,
&mut by_name,
)?;
}
}
collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
let (loose_root, loose_prefix) = loose_ref_list_walk_root(git_dir, &stored_prefix);
collect_loose_refs_into_map(
&loose_root,
&loose_prefix,
&stored_prefix,
git_dir,
true,
&mut by_name,
)?;
let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
results.sort_by(|a, b| a.0.cmp(&b.0));
Ok(results)
}
pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
let config = ConfigSet::load(
&crate::environment::Environment::empty(),
Some(receiving_git_dir),
true,
)?;
let objects_dir = receiving_git_dir.join("objects");
let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
let mut out = Vec::new();
let mut seen = std::collections::HashSet::new();
for alt_objects in alternates {
let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
continue;
};
if !alt_git_dir.join("refs").is_dir() {
continue;
}
if let Some(prefixes) = config
.get("core.alternaterefsprefixes")
.or_else(|| config.get("core.alternateRefsPrefixes"))
{
for part in prefixes.split_whitespace() {
if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
if seen.insert(oid) {
out.push(oid);
}
continue;
}
for (_, oid) in list_refs(&alt_git_dir, part)? {
if seen.insert(oid) {
out.push(oid);
}
}
}
} else {
for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
if seen.insert(oid) {
out.push(oid);
}
}
}
}
Ok(out)
}
pub fn list_refs_glob_for_repository(
repo: &crate::repo::Repository,
pattern: &str,
) -> Result<Vec<(String, ObjectId)>> {
let glob_pos = pattern.find(['*', '?', '[']);
let prefix_owned: String = match glob_pos {
Some(pos) => pattern[..pos].to_owned(),
None => {
let mut p = pattern.trim_end_matches('/').to_owned();
if !p.is_empty() {
p.push('/');
}
p
}
};
let all = list_refs_for_repository(repo, prefix_owned.as_str())?;
let mut results = Vec::new();
for (refname, oid) in all {
if ref_matches_glob(&refname, pattern) {
results.push((refname, oid));
}
}
Ok(results)
}
pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
let glob_pos = pattern.find(['*', '?', '[']);
let prefix_owned: String = match glob_pos {
Some(pos) => pattern[..pos].to_owned(),
None => {
let mut p = pattern.trim_end_matches('/').to_owned();
if !p.is_empty() {
p.push('/');
}
p
}
};
let prefix = prefix_owned.as_str();
let all = list_refs(git_dir, prefix)?;
let mut results = Vec::new();
for (refname, oid) in all {
if ref_matches_glob(&refname, pattern) {
results.push((refname, oid));
}
}
Ok(results)
}
pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
return refname == pattern
|| refname.ends_with(&format!("/{pattern}"))
|| refname.starts_with(&format!("{pattern}/"));
}
glob_match(pattern, refname)
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pat = pattern.as_bytes();
let txt = text.as_bytes();
let (mut pi, mut ti) = (0, 0);
let (mut star_pi, mut star_ti) = (usize::MAX, 0);
while ti < txt.len() {
if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
pi += 1;
ti += 1;
} else if pi < pat.len() && pat[pi] == b'*' {
star_pi = pi;
star_ti = ti;
pi += 1;
} else if star_pi != usize::MAX {
pi = star_pi + 1;
star_ti += 1;
ti = star_ti;
} else {
return false;
}
}
while pi < pat.len() && pat[pi] == b'*' {
pi += 1;
}
pi == pat.len()
}
fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
let content = fs::read_to_string(path).ok()?;
let content = content.trim_end_matches('\n').trim();
if ObjectId::is_full_hex(content) {
content.parse().ok()
} else {
None
}
}
fn collect_loose_refs_into_map(
dir: &Path,
prefix: &str,
filter_prefix: &str,
resolve_git_dir: &Path,
physical_keys: bool,
out: &mut HashMap<String, ObjectId>,
) -> Result<()> {
let read = match fs::read_dir(dir) {
Ok(r) => r,
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::NotADirectory
|| e.raw_os_error() == Some(libc::ENOTDIR) =>
{
return Ok(());
}
Err(e) => return Err(Error::Io(e)),
};
for entry in read {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
let refname = format!("{prefix}{name_str}");
let path = entry.path();
let meta = match fs::metadata(&path) {
Ok(m) => m,
Err(_) => continue,
};
if meta.is_dir() {
collect_loose_refs_into_map(
&path,
&format!("{refname}/"),
filter_prefix,
resolve_git_dir,
physical_keys,
out,
)?;
} else if meta.is_file() {
if physical_keys {
if let Some(oid) = loose_ref_file_direct_oid(&path) {
out.insert(refname, oid);
} else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
out.insert(refname, oid);
}
}
} else {
let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
.unwrap_or_else(|| refname.clone());
if !ref_name_matches_list_prefix(&logical, filter_prefix.trim_end_matches('/')) {
continue;
}
if let Some(oid) = loose_ref_file_direct_oid(&path) {
out.insert(logical, oid);
} else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
out.insert(logical, oid);
}
}
}
}
}
Ok(())
}
pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
let inner = spec
.strip_prefix("@{-")
.and_then(|s| s.strip_suffix('}'))
.ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
let n: usize = inner
.parse()
.map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
if n == 0 {
return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
}
let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
let mut count = 0usize;
for entry in entries.iter().rev() {
let msg = &entry.message;
if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
count += 1;
if count == n {
if let Some(to_pos) = rest.find(" to ") {
return Ok(rest[..to_pos].to_string());
}
}
}
}
Err(Error::InvalidRef(format!(
"{spec}: only {count} checkout(s) in reflog"
)))
}
fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
let prefix = prefix.trim_end_matches('/');
if prefix.is_empty() {
return true;
}
if refname.starts_with(prefix) {
return true;
}
if refname == prefix {
return true;
}
false
}
fn list_prefix_upper_bound(prefix: &str) -> Option<String> {
let base = prefix.trim_end_matches('/');
let (head, tail) = base.rsplit_once('/')?;
let mut chars: Vec<char> = tail.chars().collect();
while let Some(last) = chars.pop() {
if last != '\u{10ffff}' {
chars.push(char::from_u32(last as u32 + 1).unwrap_or(last));
return Some(format!("{head}/{}", chars.into_iter().collect::<String>()));
}
}
None
}
pub(crate) fn loose_ref_list_walk_root(git_dir: &Path, prefix: &str) -> (PathBuf, String) {
let trimmed = prefix.trim_end_matches('/');
if trimmed.is_empty() {
return (git_dir.join("refs"), "refs/".to_owned());
}
let direct = git_dir.join(trimmed);
if direct.is_dir() {
return (direct, format!("{trimmed}/"));
}
if let Some(slash) = trimmed.rfind('/') {
let parent = &trimmed[..slash];
return (git_dir.join(parent), format!("{parent}/"));
}
(git_dir.to_path_buf(), String::new())
}
fn collect_packed_refs_into_map(
git_dir: &Path,
prefix: &str,
physical_keys: bool,
out: &mut HashMap<String, ObjectId>,
) -> Result<()> {
let map = read_packed_refs_map_for_prefix(git_dir, prefix)?;
for (refname, oid) in map {
let key = if physical_keys {
refname.clone()
} else {
crate::ref_namespace::logical_ref_name_from_storage(&refname).unwrap_or(refname)
};
out.insert(key, oid);
}
Ok(())
}
#[cfg(test)]
mod update_branch_for_commit_tests {
use super::*;
use crate::reflog::read_reflog;
use crate::repo::init_repository;
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use tempfile::TempDir;
fn sample_oid(hex: &str) -> ObjectId {
hex.parse().unwrap()
}
#[test]
fn update_branch_for_commit_holds_lock_until_ref_commit() {
let _pause_serial = super::test_pause_injection_serial_guard();
let tmp = TempDir::new().unwrap();
let repo = init_repository(
tmp.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.unwrap();
let git_dir = repo.git_dir().to_path_buf();
let oid1 = sample_oid("67bf698f3ab735e92fb011a99cff3497c44d30c1");
let oid2 = sample_oid("1111111111111111111111111111111111111111");
let oid3 = sample_oid("2222222222222222222222222222222222222222");
write_ref(&git_dir, "refs/heads/main", &oid1).unwrap();
let gate = Arc::new(branch_ref_pause_inject::PauseGate {
wait: Mutex::new(false),
cv: Condvar::new(),
});
set_test_inject_branch_ref_pause_before_commit(Some(gate.clone()));
let git_dir_thread = git_dir.clone();
let handle = thread::spawn(move || {
update_branch_for_commit(
&git_dir_thread,
&BranchCommitRefUpdate {
branch_ref: "refs/heads/main",
expected_old: Some(oid1),
new_oid: oid2,
identity: "T <t@e.com> 1 +0000",
reflog_message: "commit: test",
},
)
});
let mut wait = gate.wait.lock().unwrap();
while !*wait {
wait = gate.cv.wait(wait).unwrap();
}
assert!(
write_ref(&git_dir, "refs/heads/main", &oid3).is_err(),
"concurrent branch update must block on the held ref lock"
);
*wait = false;
gate.cv.notify_all();
drop(wait);
handle.join().unwrap().unwrap();
set_test_inject_branch_ref_pause_before_commit(None);
assert_eq!(resolve_ref(&git_dir, "refs/heads/main").unwrap(), oid2);
}
#[test]
fn update_branch_for_commit_reftable_transaction_is_all_or_nothing() {
let tmp = TempDir::new().unwrap();
let repo = init_repository(
tmp.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.unwrap();
let git_dir = repo.git_dir().to_path_buf();
let new_oid = sample_oid("3333333333333333333333333333333333333333");
crate::reftable::set_test_inject_reftable_transaction_fail(true);
let err = update_branch_for_commit(
&git_dir,
&BranchCommitRefUpdate {
branch_ref: "refs/heads/main",
expected_old: None,
new_oid,
identity: "T <t@e.com> 1 +0000",
reflog_message: "commit (initial): test",
},
)
.unwrap_err();
crate::reftable::set_test_inject_reftable_transaction_fail(false);
assert!(!matches!(err, Error::Message(_)));
assert!(resolve_ref(&git_dir, "refs/heads/main").is_err());
assert!(read_reflog(&git_dir, "refs/heads/main").unwrap().is_empty());
assert!(read_reflog(&git_dir, "HEAD").unwrap().is_empty());
}
}
#[cfg(test)]
mod refname_available_tests {
use super::*;
use std::collections::{BTreeSet, HashSet};
use tempfile::tempdir;
#[test]
fn verify_ref_transaction_batch_rejects_same_batch_parent_child() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
let items = vec![
RefBatchItem {
name: "refs/heads/c".to_owned(),
new_oid: Some(sample_oid()),
},
RefBatchItem {
name: "refs/heads/c/x".to_owned(),
new_oid: Some(sample_oid()),
},
];
let err = verify_ref_transaction_batch(git_dir, &items).unwrap_err();
assert!(matches!(
err,
RefnameUnavailable::SameBatch { refname, .. } if refname == "refs/heads/c/x"
));
}
fn sample_oid() -> ObjectId {
"67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
}
#[test]
fn loose_parent_blocks_child_create() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
fs::write(
git_dir.join("refs/1l/c"),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
assert_eq!(
read_raw_ref(git_dir, "refs/1l/c").unwrap(),
RawRefLookup::Exists
);
let extras = BTreeSet::from([
"refs/1l/b".to_string(),
"refs/1l/c/x".to_string(),
"refs/1l/d".to_string(),
]);
let skip = HashSet::new();
let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
.unwrap_err();
assert!(matches!(
err,
RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
));
}
#[test]
fn verify_sees_loose_ref_after_canonical_git_dir() {
let dir = tempdir().unwrap();
let git_dir = dir.path().join(".git");
fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
fs::write(
git_dir.join("refs/1l/c"),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
let skip = HashSet::new();
let extras = BTreeSet::new();
let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
.unwrap_err();
assert!(matches!(
err,
RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
));
}
fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
if existing == candidate {
return false;
}
existing
.strip_prefix(candidate)
.is_some_and(|rest| rest.starts_with('/'))
|| candidate
.strip_prefix(existing)
.is_some_and(|rest| rest.starts_with('/'))
}
#[test]
fn packed_namespace_index_matches_pairwise_scan() {
let mut sorted: BTreeSet<String> = BTreeSet::new();
for i in 0..500 {
sorted.insert(format!("refs/heads/b{i:05}"));
}
sorted.insert("refs/heads/parent".into());
sorted.insert("refs/heads/a/b".into());
let candidates = [
"refs/heads/b00001",
"refs/heads/parent/child",
"refs/heads/new",
"refs/heads/a",
];
for candidate in candidates {
let indexed = packed_refname_namespace_conflict(&sorted, candidate);
let linear = sorted
.iter()
.any(|existing| refname_namespace_conflicts(existing, candidate));
assert_eq!(indexed, linear, "candidate {candidate}");
}
}
#[test]
fn list_refs_glob_b05_star_matches_one_thousand_packed_refs() {
let git_dir = Path::new("/tmp/grit-bench-refs-cache/refs-100k-packed");
if !git_dir.is_dir() {
return;
}
let listed = list_refs_glob(git_dir, "refs/heads/b05*").unwrap();
assert_eq!(listed.len(), 1000, "expected 1k b05* refs");
assert!(listed
.iter()
.all(|(name, _)| name.starts_with("refs/heads/b05")));
}
#[test]
fn list_refs_finds_sibling_under_parent_directory() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
fs::write(
git_dir.join("refs/ns/p/x"),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
assert!(
listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
"got {listed:?}"
);
}
#[test]
fn verify_blocks_parent_when_child_ref_exists() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
fs::write(
git_dir.join("refs/ns/p/x"),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
let extras = BTreeSet::from(["refs/ns/p".to_string()]);
let skip = HashSet::new();
let err =
verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
assert!(matches!(
err,
RefnameUnavailable::DescendantExists { ref blocking, .. }
if blocking == "refs/ns/p/x"
));
}
#[test]
fn verify_blocks_parent_when_only_a_packed_child_exists() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs")).unwrap();
fs::write(
store::paths::packed_refs_path(git_dir),
"# pack-refs with: peeled fully-peeled\n\
67bf698f3ab735e92fb011a99cff3497c44d30c1 refs/ns/q/x\n",
)
.unwrap();
let err = verify_refname_available_for_create(
git_dir,
"refs/ns/q",
&BTreeSet::new(),
&HashSet::new(),
)
.unwrap_err();
assert!(matches!(
err,
RefnameUnavailable::DescendantExists { ref blocking, .. }
if blocking == "refs/ns/q/x"
));
}
#[test]
fn verify_allows_name_whose_siblings_share_a_prefix() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/tags")).unwrap();
for name in ["v1.0.1", "v1.0-rc1"] {
fs::write(
git_dir.join("refs/tags").join(name),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
}
verify_refname_available_for_create(
git_dir,
"refs/tags/v1.0",
&BTreeSet::new(),
&HashSet::new(),
)
.expect("siblings are not descendants");
}
#[test]
fn verify_blocks_parent_git_style_nested_path() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
fs::write(
git_dir.join("refs/3l/c/x"),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
let extras = BTreeSet::from(["refs/3l/c".to_string()]);
let skip = HashSet::new();
let err =
verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
assert!(matches!(
err,
RefnameUnavailable::DescendantExists { ref blocking, .. }
if blocking == "refs/3l/c/x"
));
}
#[test]
fn intermediate_directory_does_not_block_nested_create() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
fs::write(
git_dir.join("refs/ns/existing"),
"67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
)
.unwrap();
assert_eq!(
read_raw_ref(git_dir, "refs/ns").unwrap(),
RawRefLookup::IsDirectory
);
let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
let skip = HashSet::new();
verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
}
}
#[cfg(test)]
mod ref_storage_traversal_guard_tests {
use super::*;
use crate::objects::ObjectId;
use tempfile::tempdir;
const TRAVERSAL_REF: &str = "refs/heads/../../config";
fn bare_repo_with_config() -> (tempfile::TempDir, PathBuf) {
let dir = tempdir().unwrap();
let git_dir = dir.path().to_path_buf();
fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
fs::write(
git_dir.join("config"),
"[core]\nrepositoryformatversion = 0\n",
)
.unwrap();
(dir, git_dir)
}
fn sample_oid() -> ObjectId {
"67bf698f3ab735e92fb011a99cff3497c44d30c1".parse().unwrap()
}
fn assert_invalid_ref_preserves_config(git_dir: &Path, result: Result<()>) {
assert!(matches!(result, Err(Error::InvalidRef(_))));
assert!(
git_dir.join("config").is_file(),
"repository config must not be touched"
);
}
#[test]
fn write_ref_rejects_traversal_refname() {
let (_dir, git_dir) = bare_repo_with_config();
let result = write_ref(&git_dir, TRAVERSAL_REF, &sample_oid());
assert_invalid_ref_preserves_config(&git_dir, result);
}
#[test]
fn write_ref_cached_rejects_traversal_refname() {
let (_dir, git_dir) = bare_repo_with_config();
let mut packed = PackedRefs::load(&git_dir).unwrap();
let result = write_ref_cached(&git_dir, TRAVERSAL_REF, &sample_oid(), &mut packed);
assert_invalid_ref_preserves_config(&git_dir, result);
}
#[test]
fn write_symbolic_ref_rejects_traversal_refname() {
let (_dir, git_dir) = bare_repo_with_config();
let result = write_symbolic_ref(&git_dir, TRAVERSAL_REF, "refs/heads/main");
assert_invalid_ref_preserves_config(&git_dir, result);
}
#[test]
fn delete_ref_rejects_traversal_refname() {
let (_dir, git_dir) = bare_repo_with_config();
let result = delete_ref(&git_dir, TRAVERSAL_REF);
assert_invalid_ref_preserves_config(&git_dir, result);
}
#[test]
fn write_ref_rejects_illegal_branch_refname() {
let (_dir, git_dir) = bare_repo_with_config();
for refname in [
"refs/heads/bad name",
"refs/heads/x..y",
"refs/heads/a~b",
"refs/heads/a.lock",
] {
let result = write_ref(&git_dir, refname, &sample_oid());
assert_invalid_ref_preserves_config(&git_dir, result);
assert!(
!git_dir
.join(refname.strip_prefix("refs/").unwrap())
.exists(),
"must not write loose ref for {refname}"
);
}
}
#[test]
fn write_ref_accepts_refs_heads_head() {
let (_dir, git_dir) = bare_repo_with_config();
write_ref(&git_dir, "refs/heads/HEAD", &sample_oid())
.expect("refs/heads/HEAD is valid ref syntax");
assert!(git_dir.join("refs/heads/HEAD").is_file());
}
}
#[cfg(test)]
mod log_refs_config_tests {
use super::*;
use crate::config::{ConfigFile, ConfigScope};
fn set(text: &str) -> ConfigSet {
let file = ConfigFile::parse(Path::new("config"), text, ConfigScope::Local).unwrap();
let mut set = ConfigSet::new();
set.merge(&file);
set
}
#[test]
fn parse_accepts_modes_and_bool_spellings() {
for (raw, mode) in [
("always", LogRefsConfig::Always),
(" ALWAYS ", LogRefsConfig::Always),
("true", LogRefsConfig::Normal),
("Yes", LogRefsConfig::Normal),
("on", LogRefsConfig::Normal),
("1", LogRefsConfig::Normal),
("false", LogRefsConfig::None),
("no", LogRefsConfig::None),
("OFF", LogRefsConfig::None),
("0", LogRefsConfig::None),
("never", LogRefsConfig::None),
] {
assert_eq!(LogRefsConfig::parse(raw), Some(mode), "{raw}");
}
assert_eq!(LogRefsConfig::parse("sometimes"), None);
}
#[test]
fn effective_mode_applies_bare_default_only_when_unset() {
assert_eq!(set("").log_refs_config(), LogRefsConfig::Unset);
assert_eq!(set("").effective_log_refs_config(), LogRefsConfig::Normal);
let bare = set("[core]\n\tbare = true\n");
assert_eq!(bare.effective_log_refs_config(), LogRefsConfig::None);
let bogus = set("[core]\n\tbare = true\n\tlogAllRefUpdates = sometimes\n");
assert_eq!(bogus.log_refs_config(), LogRefsConfig::Unset);
assert_eq!(bogus.effective_log_refs_config(), LogRefsConfig::None);
let bare_logged = set("[core]\n\tbare = true\n\tlogAllRefUpdates\n");
assert_eq!(
bare_logged.effective_log_refs_config(),
LogRefsConfig::Normal
);
let always = set("[core]\n\tlogAllRefUpdates = always\n");
assert_eq!(always.effective_log_refs_config(), LogRefsConfig::Always);
}
#[test]
fn autocreate_with_config_matches_mode() {
let normal = set("[core]\n\tlogAllRefUpdates = true\n");
let git_dir = Path::new(".git");
assert!(should_autocreate_reflog_with_config(
&normal,
git_dir,
"refs/heads/main"
));
assert!(!should_autocreate_reflog_with_config(
&normal,
git_dir,
"refs/tags/v1"
));
let none = set("[core]\n\tlogAllRefUpdates = false\n");
assert!(!should_autocreate_reflog_with_config(
&none, git_dir, "HEAD"
));
}
}
#[cfg(test)]
mod read_raw_ref_tests {
use super::*;
use tempfile::tempdir;
#[test]
fn loose_ref_file_is_exists() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
fs::write(
git_dir.join("refs/heads/side"),
"0000000000000000000000000000000000000000\n",
)
.unwrap();
assert_eq!(
read_raw_ref(git_dir, "refs/heads/side").unwrap(),
RawRefLookup::Exists
);
}
#[test]
fn missing_ref_is_not_found() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
assert_eq!(
read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
RawRefLookup::NotFound
);
}
#[test]
fn directory_where_ref_expected_is_is_directory() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
assert_eq!(
read_raw_ref(git_dir, "refs/heads").unwrap(),
RawRefLookup::IsDirectory
);
}
#[test]
fn packed_ref_name_is_exists() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
fs::write(
store::paths::packed_refs_path(git_dir),
"# pack-refs with: peeled fully-peeled \n\
0000000000000000000000000000000000000000 refs/heads/packed\n",
)
.unwrap();
assert_eq!(
read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
RawRefLookup::Exists
);
}
}
#[cfg(test)]
mod packed_refs_parse_tests {
use super::*;
use crate::objects::HashAlgo;
use tempfile::tempdir;
#[test]
fn parse_rejects_garbage_line() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
fs::write(
&path,
"# pack-refs with: peeled\n\
0000000000000000000000000000000000000000 refs/heads/main\n\
not a ref line\n",
)
.unwrap();
let data = fs::read(&path).unwrap();
assert!(matches!(
parse_packed_refs_to_map(&path, &data),
Err(Error::PackedRefsUnexpectedLine { .. })
));
}
#[test]
fn parse_rejects_carriage_return_bytes() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
fs::write(
&path,
"# pack-refs with: peeled\n\
0000000000000000000000000000000000000000 refs/heads/main\r\n",
)
.unwrap();
let data = fs::read(&path).unwrap();
assert!(matches!(
parse_packed_refs_to_map(&path, &data),
Err(Error::PackedRefsUnexpectedLine { .. })
));
}
#[test]
fn prefix_filter_unsorted_packed_refs_scans_past_later_names() {
let dir = tempdir().unwrap();
let git_dir = dir.path();
let oid = "1111111111111111111111111111111111111111";
fs::write(
store::paths::packed_refs_path(git_dir),
format!(
"# pack-refs with: peeled fully-peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
),
)
.unwrap();
let listed = list_refs_glob(git_dir, "refs/heads/a*").unwrap();
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].0, "refs/heads/aaa");
}
#[test]
fn parse_rejects_blank_line_and_missing_final_newline() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
fs::write(
&path,
"# pack-refs with: peeled\n\n\
0000000000000000000000000000000000000000 refs/heads/main\n",
)
.unwrap();
let data = fs::read(&path).unwrap();
assert!(matches!(
parse_packed_refs_to_map(&path, &data),
Err(Error::PackedRefsUnexpectedLine { .. })
));
fs::write(
&path,
"# pack-refs with: peeled\n\
0000000000000000000000000000000000000000 refs/heads/main",
)
.unwrap();
let data = fs::read(&path).unwrap();
assert!(matches!(
parse_packed_refs_to_map(&path, &data),
Err(Error::PackedRefsUnexpectedLine { .. })
));
}
#[test]
fn packed_record_filter_sees_trusted_peels_and_keeps_strictness() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let a = "1111111111111111111111111111111111111111";
let b = "2222222222222222222222222222222222222222";
let peel = "3333333333333333333333333333333333333333";
let body = |header: &str| {
format!(
"{header}{a} refs/heads/main\n\
{b} refs/heads/tagged\n\
^{peel}\n\
{a} refs/tags/light\n\
{b} refs/tags/v1\n\
^{peel}\n"
)
};
let a: ObjectId = a.parse().unwrap();
let b: ObjectId = b.parse().unwrap();
let peel: ObjectId = peel.parse().unwrap();
let cases = [
(
"# pack-refs with: peeled fully-peeled sorted \n",
[
PackedPeel::NotTag,
PackedPeel::Peeled(peel),
PackedPeel::NotTag,
PackedPeel::Peeled(peel),
],
),
(
"# pack-refs with: peeled sorted \n",
[
PackedPeel::Unknown,
PackedPeel::Unknown,
PackedPeel::NotTag,
PackedPeel::Peeled(peel),
],
),
("", [PackedPeel::Unknown; 4]),
];
for (header, want) in cases {
let data = body(header).into_bytes();
let view = PackedRefsFileView::from_bytes(&path, data, HashAlgo::Sha1).unwrap();
let seen = std::cell::RefCell::new(Vec::new());
let keep = |name: &str, oid: &ObjectId, peel: PackedPeel| {
seen.borrow_mut().push((name.to_owned(), *oid, peel));
name.starts_with("refs/tags/")
};
let kept = view
.map_for_prefix_filtered(&path, Some("refs"), Some(&keep))
.unwrap();
assert_eq!(
seen.into_inner(),
vec![
("refs/heads/main".to_owned(), a, want[0]),
("refs/heads/tagged".to_owned(), b, want[1]),
("refs/tags/light".to_owned(), a, want[2]),
("refs/tags/v1".to_owned(), b, want[3]),
],
"header {header:?}"
);
let mut kept: Vec<_> = kept.into_iter().collect();
kept.sort();
assert_eq!(
kept,
vec![
("refs/tags/light".to_owned(), a),
("refs/tags/v1".to_owned(), b)
]
);
assert_eq!(
view.map_for_prefix_filtered(&path, Some("refs"), None)
.unwrap(),
view.map_for_prefix(&path, Some("refs")).unwrap()
);
}
let bad = format!("# pack-refs with: peeled fully-peeled \n{a} refs/heads/bad..name\n");
let view = PackedRefsFileView::from_bytes(&path, bad.into_bytes(), HashAlgo::Sha1).unwrap();
assert!(matches!(
view.map_for_prefix_filtered(&path, None, Some(&|_: &str, _: &ObjectId, _| false)),
Err(Error::PackedRefsUnexpectedLine { .. })
));
}
#[test]
fn indexed_lookup_matches_full_parse_sorted_and_unsorted() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let oid = "1111111111111111111111111111111111111111";
let peel = "2222222222222222222222222222222222222222";
let sorted_body = format!(
"# pack-refs with: peeled fully-peeled sorted\n\
{oid} refs/heads/aaa\n\
{oid} refs/heads/main\n\
{oid} refs/heads/zzz\n\
{oid} refs/tags/v1\n\
^{peel}\n"
);
fs::write(&path, &sorted_body).unwrap();
let data = fs::read(&path).unwrap();
let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
let full = parse_packed_refs_to_map(&path, &data).unwrap();
for (name, want) in &full {
assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
}
assert_eq!(
lookup_packed_ref_with_peel(dir.path(), "refs/tags/v1")
.unwrap()
.map(|(o, p)| (o, p.unwrap())),
Some((oid.parse().unwrap(), peel.parse().unwrap()))
);
let unsorted_body = format!(
"# pack-refs with: peeled\n{oid} refs/heads/zzz\n{oid} refs/heads/aaa\n{oid} refs/heads/main\n"
);
fs::write(&path, unsorted_body).unwrap();
let data = fs::read(&path).unwrap();
let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
let full = parse_packed_refs_to_map(&path, &data).unwrap();
for (name, want) in &full {
assert_eq!(view.lookup_oid(&path, name).unwrap(), Some(*want));
}
assert_eq!(
view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
);
}
#[test]
fn sorted_prefix_without_upper_bound_filters_names() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let oid = "1111111111111111111111111111111111111111";
let sorted_body = format!(
"# pack-refs with: peeled fully-peeled sorted\n\
{oid} refs/heads/aaa\n\
{oid} refs/heads/zzz\n\
{oid} refs/tags/v1\n"
);
fs::write(&path, sorted_body).unwrap();
let data = fs::read(&path).unwrap();
let view = PackedRefsFileView::from_bytes(&path, data.clone(), HashAlgo::Sha1).unwrap();
assert_eq!(
view.map_for_prefix(&path, Some("refs/heads/a")).unwrap(),
parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs/heads/a")).unwrap()
);
assert!(
!view
.map_for_prefix(&path, Some("refs/heads/a"))
.unwrap()
.contains_key("refs/heads/zzz"),
"prefix refs/heads/a must not include refs/heads/zzz"
);
assert_eq!(
view.map_for_prefix(&path, Some("refs")).unwrap(),
parse_packed_refs_to_map_with_prefix(&path, &data, Some("refs")).unwrap()
);
}
#[test]
fn sorted_prefix_scan_validates_only_its_range() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let oid = "1111111111111111111111111111111111111111";
let view_of =
|body: String| PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1);
let bad_peel_on_branch = view_of(format!(
"# pack-refs with: peeled fully-peeled sorted\n\
{oid} refs/heads/main\n\
^not-hex\n\
{oid} refs/tags/v1\n"
))
.unwrap();
assert!(matches!(
bad_peel_on_branch.map_for_prefix(&path, Some("refs/heads")),
Err(Error::PackedRefsUnexpectedLine { .. })
));
let tags = bad_peel_on_branch
.map_for_prefix(&path, Some("refs/tags"))
.unwrap();
assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/v1"]);
assert!(bad_peel_on_branch.map_for_prefix(&path, None).is_err());
let blank_among_tags = view_of(format!(
"# pack-refs with: peeled fully-peeled sorted\n\
{oid} refs/heads/main\n\
{oid} refs/tags/v1\n\
\n\
{oid} refs/tags/v2\n"
))
.unwrap();
assert_eq!(
blank_among_tags
.map_for_prefix(&path, Some("refs/heads"))
.unwrap()
.len(),
1
);
assert!(blank_among_tags
.map_for_prefix(&path, Some("refs/tags"))
.is_err());
let no_match = blank_among_tags
.map_for_prefix(&path, Some("refs/notes"))
.unwrap();
assert!(no_match.is_empty());
}
#[test]
fn scan_of_file_without_ref_lines_still_validates_it() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let view = PackedRefsFileView::from_bytes(
&path,
b"# pack-refs with: peeled fully-peeled sorted\n\
^1111111111111111111111111111111111111111\n"
.to_vec(),
HashAlgo::Sha1,
)
.unwrap();
for prefix in [None, Some("refs/heads")] {
assert!(matches!(
view.map_for_prefix(&path, prefix),
Err(Error::PackedRefsUnexpectedLine { .. })
));
}
let header_only = PackedRefsFileView::from_bytes(
&path,
b"# pack-refs with: peeled fully-peeled sorted\n".to_vec(),
HashAlgo::Sha1,
)
.unwrap();
assert!(header_only.map_for_prefix(&path, None).unwrap().is_empty());
}
#[test]
fn view_checks_last_line_length_against_the_repository_hash() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let sha1 = "1111111111111111111111111111111111111111";
let sha256 = "2".repeat(64);
let view =
|body: String, algo| PackedRefsFileView::from_bytes(&path, body.into_bytes(), algo);
let header = "# pack-refs with: peeled fully-peeled sorted\n";
let fifty_bytes = "x".repeat(49);
for (body, algo) in [
(
format!("{header}{sha1} refs/heads/main\ngarbage\n"),
HashAlgo::Sha1,
),
(
format!("{header}{sha1} refs/heads/main\n^{}\n", &sha1[1..]),
HashAlgo::Sha1,
),
(
format!("{header}{sha1} refs/heads/main\n#\n"),
HashAlgo::Sha1,
),
(
format!("{header}{sha256} refs/heads/main\n{fifty_bytes}\n"),
HashAlgo::Sha256,
),
(format!("{header}{sha1} refs/heads/a\n"), HashAlgo::Sha256),
] {
assert!(
matches!(
view(body.clone(), algo),
Err(Error::PackedRefsUnexpectedLine { .. })
),
"{algo:?}: {body:?}"
);
}
for (body, algo) in [
(header.to_owned(), HashAlgo::Sha1),
(header.to_owned(), HashAlgo::Sha256),
("# pack-refs with: peeled\n".to_owned(), HashAlgo::Sha256),
(
format!("{header}{sha1} refs/heads/main\n{fifty_bytes}\n"),
HashAlgo::Sha1,
),
(
format!("{header}{sha1} refs/heads/main\n^{sha1}\n"),
HashAlgo::Sha1,
),
(
format!("{header}{sha256} refs/heads/main\n^{sha256}\n"),
HashAlgo::Sha256,
),
] {
assert!(view(body.clone(), algo).is_ok(), "{algo:?}: {body:?}");
}
}
#[test]
fn sha256_short_last_record_fails_branch_listing() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let sha256 = "2".repeat(64);
let body = format!(
"# pack-refs with: peeled fully-peeled sorted\n{sha256} refs/heads/main\n{}\n",
"x".repeat(49)
);
assert!(
PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha256).is_err()
);
}
#[test]
fn corrupt_line_inside_prefix_range_fails_instead_of_dropping_refs() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let oid = "1111111111111111111111111111111111111111";
let header = "# pack-refs with: peeled fully-peeled sorted\n";
let view = |body: String| {
PackedRefsFileView::from_bytes(&path, body.into_bytes(), HashAlgo::Sha1).unwrap()
};
let garbage_after_first_branch = view(format!(
"{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/heads/b\n{oid} refs/heads/c\n\
{oid} refs/tags/t1\n"
));
assert!(matches!(
garbage_after_first_branch.map_for_prefix(&path, Some("refs/heads")),
Err(Error::PackedRefsUnexpectedLine { .. })
));
let tags = garbage_after_first_branch
.map_for_prefix(&path, Some("refs/tags"))
.unwrap();
assert_eq!(tags.keys().collect::<Vec<_>>(), ["refs/tags/t1"]);
let garbage_before_range = view(format!(
"{header}{oid} refs/heads/a\ngarbage line\n{oid} refs/tags/t1\n"
));
for prefix in ["refs/heads", "refs/tags", "refs/notes"] {
assert!(
garbage_before_range
.map_for_prefix(&path, Some(prefix))
.is_err(),
"{prefix}"
);
}
assert_eq!(
garbage_before_range
.map_for_prefix(&path, Some("refs/zz"))
.unwrap()
.len(),
0
);
let peeled_tag_before_range = view(format!(
"{header}{oid} refs/heads/a\n^{oid}\n{oid} refs/tags/t1\n"
));
let orphan_peel_before_range = view(format!(
"{header}{oid} refs/heads/a\n^{oid}\n^{oid}\n{oid} refs/tags/t1\n"
));
assert!(orphan_peel_before_range
.map_for_prefix(&path, Some("refs/tags"))
.is_err());
assert_eq!(
peeled_tag_before_range
.map_for_prefix(&path, Some("refs/tags"))
.unwrap()
.len(),
1
);
}
#[test]
fn sorted_trait_is_ignored_when_names_are_out_of_order() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
let oid = "1111111111111111111111111111111111111111";
let view = PackedRefsFileView::from_bytes(
&path,
format!(
"# pack-refs with: peeled fully-peeled sorted\n\
{oid} refs/heads/b\n{oid} refs/heads/a\n{oid} refs/tags/t\n"
)
.into_bytes(),
HashAlgo::Sha1,
)
.unwrap();
assert!(!view.sorted);
let heads = view.map_for_prefix(&path, Some("refs/heads")).unwrap();
assert_eq!(heads.len(), 2);
assert!(view.lookup_oid(&path, "refs/heads/a").unwrap().is_some());
}
#[test]
fn line_index_keeps_only_record_shaped_lines() {
let data = b"# header\nAAA x\n^peel\n\nBBB y\n#c\nshort\nCCCz\nDDD z";
let (starts, in_order) = packed_refs_data_line_starts(data, 3);
assert_eq!(starts, [9, 22, 42]);
assert!(in_order);
assert_eq!(packed_refs_data_line_starts(b"", 3), (Vec::new(), true));
let (starts, in_order) = packed_refs_data_line_starts(b"AAA b\nAAA a\n", 3);
assert_eq!(starts, [0, 6]);
assert!(!in_order);
}
#[test]
fn parse_rejects_orphan_peel_and_double_space() {
let dir = tempdir().unwrap();
let path = store::paths::packed_refs_path(dir.path());
fs::write(
&path,
"# pack-refs with: peeled\n\
^0000000000000000000000000000000000000000\n\
0000000000000000000000000000000000000000 refs/heads/main\n",
)
.unwrap();
let data = fs::read(&path).unwrap();
assert!(matches!(
parse_packed_refs_to_map(&path, &data),
Err(Error::PackedRefsUnexpectedLine { .. })
));
fs::write(
&path,
"# pack-refs with: peeled\n\
0000000000000000000000000000000000000000 refs/heads/main\n",
)
.unwrap();
let data = fs::read(&path).unwrap();
assert!(matches!(
parse_packed_refs_to_map(&path, &data),
Err(Error::PackedRefsUnexpectedLine { .. })
));
}
}
#[cfg(test)]
mod pack_refs_tests {
use super::*;
use crate::objects::{serialize_commit, CommitData, ObjectKind};
use crate::repo::{init_repository, Repository};
use std::fs;
use std::sync::Arc;
fn write_commit(repo: &Repository, parents: Vec<ObjectId>) -> ObjectId {
let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
let raw = serialize_commit(&CommitData {
tree,
parents,
author: "T <t@example.com> 1 +0000".into(),
committer: "T <t@example.com> 1 +0000".into(),
author_raw: Vec::new(),
committer_raw: Vec::new(),
encoding: None,
message: "m\n".into(),
raw_message: None,
extra_headers: Vec::new(),
});
repo.odb().write(ObjectKind::Commit, &raw).expect("commit")
}
#[test]
fn pack_refs_skips_concurrent_loose_update() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.expect("init");
let git_dir = repo.git_dir().to_path_buf();
let c1 = write_commit(&repo, vec![]);
let c2 = write_commit(&repo, vec![c1]);
write_ref(&git_dir, "refs/heads/race", &c1).expect("seed loose");
let git_dir_hook = git_dir.clone();
let c2 = Arc::new(c2);
let c2_hook = Arc::clone(&c2);
pack_refs_test_hooks::set_before_prune(Some(Box::new(move || {
write_ref(&git_dir_hook, "refs/heads/race", &c2_hook).expect("concurrent update");
})));
let (_path, packed, pruned) = pack_refs_files(&git_dir, repo.odb(), true).expect("pack");
pack_refs_test_hooks::set_before_prune(None);
assert!(packed >= 1);
assert_eq!(pruned, 0, "stale CAS must not delete the updated loose ref");
let resolved = resolve_ref(&git_dir, "refs/heads/race").expect("resolve");
assert_eq!(resolved, *c2);
let packed_body =
fs::read_to_string(store::paths::packed_refs_path(&git_dir)).unwrap_or_default();
assert!(
!packed_body.contains("refs/heads/race"),
"packed entry for concurrently updated ref must be removed"
);
}
#[test]
fn pack_refs_nested_tag_peel_line_points_at_commit() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.expect("init");
let commit = write_commit(&repo, vec![]);
let inner_tag_body = format!(
"object {}\ntype commit\ntagger T <t@example.com> 1 +0000\n\ninner\n",
commit.to_hex()
);
let inner = repo
.odb()
.write(ObjectKind::Tag, inner_tag_body.as_bytes())
.expect("inner tag");
let outer_tag_body = format!(
"object {}\ntype tag\ntagger T <t@example.com> 2 +0000\n\nouter\n",
inner.to_hex()
);
let outer = repo
.odb()
.write(ObjectKind::Tag, outer_tag_body.as_bytes())
.expect("outer tag");
write_ref(repo.git_dir(), "refs/tags/outer", &outer).expect("tag ref");
pack_refs_files(repo.git_dir(), repo.odb(), false).expect("pack");
let body =
fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
let mut lines = body.lines();
let tag_line = lines
.find(|l| l.contains("refs/tags/outer"))
.expect("tag line");
assert!(tag_line.starts_with(&outer.to_hex()));
let peel_line = lines.find(|l| l.starts_with('^')).expect("peel line");
assert!(
peel_line.contains(&commit.to_hex()),
"peel must reach commit through nested tags, got {peel_line}"
);
}
#[test]
fn pack_refs_tree_target_tag_has_peel_line() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.expect("init");
let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
let tag_body = format!(
"object {}\ntype tree\ntagger T <t@example.com> 1 +0000\n\ntree tag\n",
tree.to_hex()
);
let tag = repo
.odb()
.write(ObjectKind::Tag, tag_body.as_bytes())
.expect("tag");
write_ref(repo.git_dir(), "refs/tags/tree-tag", &tag).expect("tag ref");
pack_refs_files(repo.git_dir(), repo.odb(), true).expect("pack");
let body =
fs::read_to_string(store::paths::packed_refs_path(repo.git_dir())).expect("packed");
assert!(body.contains("refs/tags/tree-tag"));
assert!(
body.contains(&format!("^{}", tree.to_hex())),
"expected peel to tree: {body}"
);
}
#[test]
fn write_packed_refs_file_survives_publish_failure() {
let dir = tempfile::tempdir().expect("tempdir");
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
fs::write(git_dir.join("refs/heads/keep"), format!("{oid}\n")).expect("loose ref");
fs::create_dir_all(store::paths::packed_refs_path(git_dir))
.expect("block packed-refs file");
let err = write_packed_refs_file(
git_dir,
&[PackedRefRecord {
refname: "refs/heads/keep".into(),
oid,
peeled: None,
}],
)
.expect_err("publish should fail");
assert!(matches!(err, Error::Io(_)));
assert!(git_dir.join("refs/heads/keep").is_file());
}
#[test]
fn write_packed_refs_file_replaces_loose_and_sorts() {
let dir = tempfile::tempdir().expect("tempdir");
let git_dir = dir.path();
fs::create_dir_all(git_dir.join("refs/heads")).expect("refs/heads");
let oid_a = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid a");
let oid_b = ObjectId::from_hex("0000000000000000000000000000000000000002").expect("oid b");
fs::write(git_dir.join("refs/heads/a-first"), format!("{oid_b}\n")).expect("loose ref");
write_packed_refs_file(
git_dir,
&[
PackedRefRecord {
refname: "refs/heads/z-last".into(),
oid: oid_b,
peeled: None,
},
PackedRefRecord {
refname: "refs/heads/a-first".into(),
oid: oid_a,
peeled: None,
},
],
)
.expect("write packed-refs");
assert!(!git_dir.join("refs/heads/a-first").exists());
let body =
fs::read_to_string(store::paths::packed_refs_path(git_dir)).expect("packed-refs");
assert!(body.contains("refs/heads/a-first"));
assert!(body.find("a-first").expect("a-first") < body.find("z-last").expect("z-last"));
}
}
#[cfg(test)]
mod packed_ref_peels_tests {
use super::*;
fn oid(n: u8) -> ObjectId {
ObjectId::from_bytes(&[n; 20]).expect("oid")
}
fn peels_for(body: &str) -> (tempfile::TempDir, PackedRefPeels) {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(store::paths::packed_refs_path(dir.path()), body).expect("packed-refs");
let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
(dir, peels)
}
fn body(header: &str, records: &[(&str, ObjectId, Option<ObjectId>)]) -> String {
let mut out = String::from(header);
for (name, oid, peel) in records {
out.push_str(&format!("{oid} {name}\n"));
if let Some(peel) = peel {
out.push_str(&format!("^{peel}\n"));
}
}
out
}
#[test]
fn fully_peeled_file_answers_for_every_ref() {
for header in [
"# pack-refs with: peeled fully-peeled sorted \n",
"# pack-refs with: peeled fully-peeled \n",
] {
let records = [
("refs/heads/main", oid(1), None),
("refs/remotes/origin/tag-ish", oid(2), Some(oid(3))),
("refs/tags/v1", oid(4), Some(oid(1))),
("refs/tags/light", oid(1), None),
];
let mut unsorted = records;
unsorted.reverse();
let (_dir, peels) = peels_for(&body(header, &unsorted));
assert_eq!(peels.peeled("refs/heads/main", &oid(1)), Some(None));
assert_eq!(
peels.peeled("refs/remotes/origin/tag-ish", &oid(2)),
Some(Some(oid(3)))
);
assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
assert_eq!(peels.peeled("refs/tags/light", &oid(1)), Some(None));
assert_eq!(peels.peeled("refs/tags/v1", &oid(9)), None);
assert_eq!(peels.peeled("refs/tags/missing", &oid(1)), None);
}
}
#[test]
fn peeled_trait_only_covers_tags_and_no_trait_covers_nothing() {
let records = [
("refs/heads/main", oid(1), None),
("refs/tags/v1", oid(4), Some(oid(1))),
];
let (_dir, peels) = peels_for(&body("# pack-refs with: peeled sorted\n", &records));
assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), Some(Some(oid(1))));
for header in [
"",
"# pack-refs with: sorted\n",
"# some comment\n",
"#pack-refs with: peeled fully-peeled\n",
"# pack-refs with: peeled fully-peeled\n",
] {
let (_dir, peels) = peels_for(&body(header, &records));
assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None, "{header:?}");
assert_eq!(peels.peeled("refs/tags/v1", &oid(4)), None, "{header:?}");
}
}
#[test]
fn missing_file_and_other_backends_know_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Files).expect("load");
assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
let records = [("refs/heads/main", oid(1), None)];
fs::write(
store::paths::packed_refs_path(dir.path()),
body("# pack-refs with: peeled fully-peeled sorted\n", &records),
)
.expect("packed-refs");
let peels = PackedRefPeels::load(dir.path(), RefStorageFormat::Reftable).expect("load");
assert_eq!(peels.peeled("refs/heads/main", &oid(1)), None);
}
}