pub mod builder;
mod peel;
pub mod store;
#[doc(inline)]
pub use builder::OdbBuilder;
pub(crate) use store::loose::{
build_store_bytes, decompress_zlib_loose_bytes, for_each_loose_object_id, parse_object_bytes,
read_loose_object_info, zlib_compress_store_bytes,
};
#[doc(inline)]
pub use store::LooseStore;
pub use store::{CompositeStore, FilesSource, ObjectStream};
#[doc(inline)]
pub use store::{MemoryStore, ObjectStore, WritableObjectStore};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::hash::{Hash, Hasher};
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use crate::config::ConfigSet;
use crate::diagnostics::NullDiagnostics;
use crate::error::{Error, Result};
use crate::hash;
use crate::midx::validate_midx_referenced_packs_with_diagnostics;
use crate::objects::{
oid_hash_map, oid_hash_set, HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind, OidHashMap,
OidHashSet,
};
use crate::pack;
use crate::pack_store::PackStore;
use filetime::{set_file_mtime, FileTime};
use flate2::Compression;
use store::PackedObjects;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UnreachablePackRetention {
KeepAll,
KeepGraceProtected(std::time::SystemTime),
DropAll,
}
type MemOdbOverlayStore = Arc<RwLock<Option<Arc<MemoryStore>>>>;
struct AlternateSourcesCache {
file_generation: u64,
env_fingerprint: u64,
submodule_generation: u64,
sources: Option<Arc<CompositeStore>>,
}
struct AlternateSourcesKey {
file_generation: u64,
env_fingerprint: u64,
submodule_generation: u64,
}
struct FileAlternatesCache {
generation: u64,
snapshot: Option<Arc<Vec<PathBuf>>>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WriteOptions {
pub silent: bool,
pub assume_loose_only_existence: bool,
pub trust_new_loose: bool,
}
pub const BULK_NEW_LOOSE_WRITE: WriteOptions = WriteOptions {
assume_loose_only_existence: true,
trust_new_loose: true,
silent: false,
};
fn exists_materialized_in_objects_dir(objects_dir: &Path, oid: &ObjectId) -> bool {
store::local_object_materialized(objects_dir, oid)
}
fn objects_dir_has_pack_index_files(objects_dir: &Path) -> bool {
let pack_dir = objects_dir.join("pack");
let Ok(entries) = fs::read_dir(pack_dir) else {
return false;
};
entries
.filter_map(|e| e.ok())
.any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
}
#[derive(Clone)]
pub struct Odb {
objects_dir: PathBuf,
work_tree: Option<PathBuf>,
submodule_alternate_dirs: Arc<Mutex<Vec<PathBuf>>>,
config_git_dir: Option<PathBuf>,
core_multi_pack_index_cache: Arc<OnceLock<bool>>,
local_pack_indexes_seen: Arc<OnceLock<bool>>,
file_alternate_dirs_cache: Arc<RwLock<FileAlternatesCache>>,
#[cfg(test)]
exists_probe: Arc<AtomicUsize>,
#[cfg(test)]
pub(crate) hot_path_test_metrics: Arc<crate::hot_path_test_metrics::HotPathTestMetrics>,
env_alternate_dirs: Vec<PathBuf>,
env_alternate_lazy: Arc<OnceLock<Arc<Vec<PathBuf>>>>,
mem_overlay: MemOdbOverlayStore,
mem_overlay_active: Arc<AtomicBool>,
primary_source: Arc<OnceLock<Arc<FilesSource>>>,
alternate_sources_cache: Arc<RwLock<AlternateSourcesCache>>,
midx_packs_validated: Arc<OnceLock<()>>,
hash_algo_cache: Arc<OnceLock<HashAlgo>>,
loose_zlib_cache: Arc<OnceLock<Compression>>,
shared_config_state: Option<crate::repo::RepositoryConfigSnapshot>,
delta_base_cache_sync: Arc<OnceLock<()>>,
shared_environment: Option<Arc<crate::environment::Environment>>,
empty_config_env: Arc<OnceLock<crate::environment::Environment>>,
freshened_packs: Arc<Mutex<HashSet<PathBuf>>>,
pack_store: Arc<PackStore>,
alternate_pack_stores: Arc<Mutex<HashMap<PathBuf, Arc<PackStore>>>>,
submodule_sources_generation: Arc<AtomicU64>,
custom_primary: Option<Arc<dyn WritableObjectStore>>,
extra_read_sources: Vec<Arc<dyn ObjectStore>>,
alternates_enabled: bool,
session_loose_reads: Arc<std::sync::Mutex<OidHashMap<Object>>>,
session_tree_entries: Arc<Mutex<SessionTreeEntries>>,
}
#[derive(Default)]
struct SessionTreeEntries {
trees: OidHashMap<Arc<Vec<crate::objects::TreeEntry>>>,
entries: usize,
}
impl SessionTreeEntries {
const ENTRY_BUDGET: usize = 1 << 16;
fn insert(&mut self, oid: ObjectId, tree: Arc<Vec<crate::objects::TreeEntry>>) {
if tree.len() > Self::ENTRY_BUDGET {
return;
}
if self.entries + tree.len() > Self::ENTRY_BUDGET {
self.trees.clear();
self.entries = 0;
}
self.entries += tree.len();
self.trees.insert(oid, tree);
}
}
#[cfg(test)]
mod session_tree_entries_tests {
use super::*;
use crate::objects::TreeEntry;
fn tree(entries: usize) -> Arc<Vec<TreeEntry>> {
let entry = TreeEntry {
mode: 0o100644,
name: b"f".to_vec(),
oid: ObjectId::zero(),
};
Arc::new(vec![entry; entries])
}
fn oid(n: u8) -> ObjectId {
ObjectId::from_bytes(&[n; 20]).expect("oid")
}
#[test]
fn cache_starts_over_when_the_entry_budget_is_exceeded() {
let mut cache = SessionTreeEntries::default();
let half = SessionTreeEntries::ENTRY_BUDGET / 2;
cache.insert(oid(1), tree(half));
cache.insert(oid(2), tree(half));
assert_eq!(cache.entries, SessionTreeEntries::ENTRY_BUDGET);
assert_eq!(cache.trees.len(), 2);
cache.insert(oid(3), tree(1));
assert_eq!(cache.trees.len(), 1);
assert!(cache.trees.contains_key(&oid(3)));
assert_eq!(cache.entries, 1);
cache.insert(oid(4), tree(SessionTreeEntries::ENTRY_BUDGET + 1));
assert!(
!cache.trees.contains_key(&oid(4)),
"an oversized tree is not cached"
);
assert!(cache.trees.contains_key(&oid(3)));
assert!(cache.entries <= SessionTreeEntries::ENTRY_BUDGET);
}
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
let _ = assert_send_sync::<Odb>;
};
impl std::fmt::Debug for Odb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Odb")
.field("objects_dir", &self.objects_dir)
.field("work_tree", &self.work_tree)
.field("submodule_alternate_dirs", &"<mutex>")
.field("config_git_dir", &self.config_git_dir)
.finish()
}
}
#[must_use]
pub fn hash_algo_for_git_dir(env: &crate::environment::Environment, git_dir: &Path) -> HashAlgo {
ConfigSet::load(env, Some(git_dir), true)
.ok()
.and_then(|cfg| cfg.get("extensions.objectformat"))
.and_then(|v| HashAlgo::from_name(&v))
.unwrap_or(HashAlgo::Sha1)
}
#[must_use]
pub fn hash_algo_for_objects_dir(
env: &crate::environment::Environment,
objects_dir: &Path,
) -> HashAlgo {
objects_dir
.parent()
.map(|d| hash_algo_for_git_dir(env, d))
.unwrap_or(HashAlgo::Sha1)
}
impl Odb {
#[must_use]
pub fn new(objects_dir: &Path) -> Self {
Self {
objects_dir: objects_dir.to_path_buf(),
work_tree: None,
submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
config_git_dir: None,
core_multi_pack_index_cache: Arc::new(OnceLock::new()),
local_pack_indexes_seen: Arc::new(OnceLock::new()),
file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
generation: 0,
snapshot: None,
})),
env_alternate_dirs: Vec::new(),
env_alternate_lazy: Arc::new(OnceLock::new()),
mem_overlay: Arc::new(RwLock::new(None)),
mem_overlay_active: Arc::new(AtomicBool::new(false)),
primary_source: Arc::new(OnceLock::new()),
alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
file_generation: 0,
env_fingerprint: 0,
submodule_generation: 0,
sources: None,
})),
midx_packs_validated: Arc::new(OnceLock::new()),
hash_algo_cache: Arc::new(OnceLock::new()),
loose_zlib_cache: Arc::new(OnceLock::new()),
shared_config_state: None,
delta_base_cache_sync: Arc::new(OnceLock::new()),
shared_environment: None,
empty_config_env: Arc::new(OnceLock::new()),
freshened_packs: Arc::new(Mutex::new(HashSet::new())),
pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
submodule_sources_generation: Arc::new(AtomicU64::new(0)),
custom_primary: None,
extra_read_sources: Vec::new(),
alternates_enabled: true,
session_loose_reads: Arc::new(Mutex::new(oid_hash_map())),
session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
#[cfg(test)]
exists_probe: Arc::new(AtomicUsize::new(0)),
#[cfg(test)]
hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
}
}
#[must_use]
pub fn with_work_tree(objects_dir: &Path, work_tree: &Path) -> Self {
Self {
objects_dir: objects_dir.to_path_buf(),
work_tree: Some(work_tree.to_path_buf()),
submodule_alternate_dirs: Arc::new(Mutex::new(Vec::new())),
config_git_dir: None,
core_multi_pack_index_cache: Arc::new(OnceLock::new()),
local_pack_indexes_seen: Arc::new(OnceLock::new()),
file_alternate_dirs_cache: Arc::new(RwLock::new(FileAlternatesCache {
generation: 0,
snapshot: None,
})),
env_alternate_dirs: Vec::new(),
env_alternate_lazy: Arc::new(OnceLock::new()),
mem_overlay: Arc::new(RwLock::new(None)),
mem_overlay_active: Arc::new(AtomicBool::new(false)),
primary_source: Arc::new(OnceLock::new()),
alternate_sources_cache: Arc::new(RwLock::new(AlternateSourcesCache {
file_generation: 0,
env_fingerprint: 0,
submodule_generation: 0,
sources: None,
})),
midx_packs_validated: Arc::new(OnceLock::new()),
hash_algo_cache: Arc::new(OnceLock::new()),
loose_zlib_cache: Arc::new(OnceLock::new()),
shared_config_state: None,
delta_base_cache_sync: Arc::new(OnceLock::new()),
shared_environment: None,
empty_config_env: Arc::new(OnceLock::new()),
freshened_packs: Arc::new(Mutex::new(HashSet::new())),
pack_store: Arc::new(PackStore::new(objects_dir.to_path_buf())),
alternate_pack_stores: Arc::new(Mutex::new(HashMap::new())),
submodule_sources_generation: Arc::new(AtomicU64::new(0)),
custom_primary: None,
extra_read_sources: Vec::new(),
alternates_enabled: true,
session_loose_reads: Arc::new(Mutex::new(oid_hash_map())),
session_tree_entries: Arc::new(Mutex::new(SessionTreeEntries::default())),
#[cfg(test)]
exists_probe: Arc::new(AtomicUsize::new(0)),
#[cfg(test)]
hot_path_test_metrics: crate::hot_path_test_metrics::HotPathTestMetrics::new(),
}
}
pub(crate) fn cached_tree_entries(
&self,
oid: &ObjectId,
) -> Result<Arc<Vec<crate::objects::TreeEntry>>> {
if let Ok(guard) = self.session_tree_entries.lock() {
if let Some(entries) = guard.trees.get(oid) {
return Ok(Arc::clone(entries));
}
}
let obj = self.read(oid)?;
if obj.kind != ObjectKind::Tree {
return Err(Error::CorruptObject(format!(
"expected tree, got {}",
obj.kind.as_str()
)));
}
let parsed = crate::objects::parse_tree_with_oid_len(&obj.data, self.hash_algo().len())?;
let arc = Arc::new(parsed);
if let Ok(mut guard) = self.session_tree_entries.lock() {
guard.insert(*oid, Arc::clone(&arc));
}
Ok(arc)
}
fn session_cached_loose_read(&self, oid: &ObjectId) -> Option<Object> {
self.session_loose_reads.lock().ok()?.get(oid).cloned()
}
fn remember_session_loose_read(&self, oid: &ObjectId, obj: &Object) {
if let Ok(mut guard) = self.session_loose_reads.lock() {
if guard.len() > 256 {
guard.clear();
}
guard.insert(*oid, obj.clone());
}
}
fn finish_session_loose_read(&self, oid: &ObjectId, obj: Object) -> Result<Object> {
self.remember_session_loose_read(oid, &obj);
Ok(obj)
}
#[cfg(test)]
#[must_use]
pub(crate) fn hot_path_metrics(&self) -> &crate::hot_path_test_metrics::HotPathTestMetrics {
&self.hot_path_test_metrics
}
#[cfg(test)]
#[must_use]
pub(crate) fn hot_path_metrics_arc(
&self,
) -> Arc<crate::hot_path_test_metrics::HotPathTestMetrics> {
Arc::clone(&self.hot_path_test_metrics)
}
#[must_use]
pub(crate) fn with_shared_config_state(
mut self,
state: crate::repo::RepositoryConfigSnapshot,
env: Arc<crate::environment::Environment>,
) -> Self {
self.pack_store.set_repo_caches(Arc::clone(&state));
self.shared_config_state = Some(state);
self.shared_environment = Some(env);
self
}
fn diagnostics_handle(&self) -> crate::diagnostics::DiagnosticsHandle {
self.shared_config_state
.as_ref()
.map(|state| state.diagnostics_handle())
.unwrap_or_else(|| Arc::new(NullDiagnostics))
}
fn load_config_cascade(&self) -> Result<ConfigSet> {
if let (Some(caches), Some(env)) = (&self.shared_config_state, &self.shared_environment) {
let git_dir = self
.config_git_dir
.as_deref()
.or_else(|| self.objects_dir.parent());
let cfg = caches.load_config(env.as_ref(), git_dir, true)?;
return Ok(cfg.as_ref().clone());
}
let git_dir = self
.config_git_dir
.as_deref()
.or_else(|| self.objects_dir.parent());
if let Some(git_dir) = git_dir {
ConfigSet::load(self.config_environment(), Some(git_dir), true)
} else {
Ok(ConfigSet::new())
}
}
fn config_environment(&self) -> &crate::environment::Environment {
if let Some(env) = self.shared_environment.as_deref() {
return env;
}
self.empty_config_env
.get_or_init(crate::environment::Environment::empty)
}
fn env_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
if !self.env_alternate_dirs.is_empty() {
return Arc::new(self.env_alternate_dirs.clone());
}
Arc::clone(self.env_alternate_lazy.get_or_init(|| {
Arc::new(Self::env_alternate_dirs_from_var(
self.config_environment(),
self.work_tree.as_deref(),
))
}))
}
#[must_use]
pub fn env_alternate_dirs_from_var(
env: &crate::environment::Environment,
resolve_base: Option<&Path>,
) -> Vec<PathBuf> {
match env.var("GIT_ALTERNATE_OBJECT_DIRECTORIES") {
Some(val) if !val.is_empty() => {
let mut dirs = parse_alternate_env(&val);
if let Some(base) = resolve_base {
for dir in &mut dirs {
if dir.is_relative() {
*dir = base.join(&dir);
}
}
}
dirs
}
_ => Vec::new(),
}
}
#[must_use]
pub fn with_env_alternate_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
self.env_alternate_dirs = dirs;
self.clear_alternate_sources_cache();
self
}
fn clear_alternate_sources_cache(&self) {
if let Ok(mut guard) = self.alternate_sources_cache.write() {
guard.sources = None;
}
}
fn alternate_sources_key(&self) -> AlternateSourcesKey {
AlternateSourcesKey {
file_generation: self
.file_alternate_dirs_cache
.read()
.map(|g| g.generation)
.unwrap_or(0),
env_fingerprint: self.env_alternate_fingerprint(),
submodule_generation: self.submodule_sources_generation.load(Ordering::Relaxed),
}
}
fn env_alternate_fingerprint(&self) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for path in self.env_alternate_dirs_snapshot().iter() {
path.hash(&mut hasher);
}
hasher.finish()
}
pub fn invalidate_alternates_cache(&self) {
if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
guard.generation = guard.generation.wrapping_add(1);
guard.snapshot = None;
}
self.clear_alternate_sources_cache();
}
pub fn refresh_file_alternates_from_disk(&self) -> Result<()> {
let snapshot = Arc::new(pack::read_alternates_recursive(&self.objects_dir)?);
if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
guard.generation = guard.generation.wrapping_add(1);
guard.snapshot = Some(snapshot);
}
self.clear_alternate_sources_cache();
Ok(())
}
pub fn append_file_alternate(&self, alternate_objects_dir: &Path) -> Result<()> {
Self::append_alternate_objects_line(&self.objects_dir, alternate_objects_dir)?;
self.refresh_file_alternates_from_disk()?;
Ok(())
}
pub fn append_alternate_objects_line(
objects_dir: &Path,
alternate_objects_dir: &Path,
) -> Result<()> {
let info = objects_dir.join("info");
fs::create_dir_all(&info)?;
let alt_path = info.join("alternates");
let alt_abs = alternate_objects_dir
.canonicalize()
.unwrap_or_else(|_| alternate_objects_dir.to_path_buf());
let line = format!("{}\n", alt_abs.display());
let existing = match fs::read_to_string(&alt_path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(e) => return Err(Error::Io(e)),
};
if existing
.lines()
.any(|l| l.trim() == alt_abs.to_string_lossy())
{
return Ok(());
}
let mut out = existing;
out.push_str(&line);
fs::write(alt_path, out).map_err(Error::Io)
}
fn file_alternate_dirs_snapshot(&self) -> Arc<Vec<PathBuf>> {
loop {
let observed_gen = self
.file_alternate_dirs_cache
.read()
.map(|g| g.generation)
.unwrap_or(0);
if let Ok(guard) = self.file_alternate_dirs_cache.read() {
if guard.generation == observed_gen {
if let Some(snapshot) = &guard.snapshot {
return Arc::clone(snapshot);
}
}
}
if let Ok(mut guard) = self.file_alternate_dirs_cache.write() {
if guard.generation != observed_gen {
continue;
}
if let Some(snapshot) = &guard.snapshot {
return Arc::clone(snapshot);
}
let snapshot = Arc::new(
pack::read_alternates_recursive(&self.objects_dir).unwrap_or_default(),
);
guard.snapshot = Some(Arc::clone(&snapshot));
return snapshot;
}
break;
}
Arc::new(Vec::new())
}
#[cfg(test)]
pub(crate) fn exists_probe_count(&self) -> usize {
self.exists_probe.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn reset_exists_probe(&self) {
self.exists_probe.store(0, Ordering::Relaxed);
}
pub fn enable_mem_overlay(&self) {
self.mem_overlay_active.store(true, Ordering::Release);
if let Ok(mut guard) = self.mem_overlay.write() {
if guard.is_none() {
*guard = Some(Arc::new(MemoryStore::new(self.hash_algo())));
}
}
}
pub fn disable_mem_overlay(&self) {
self.mem_overlay_active.store(false, Ordering::Release);
if let Ok(mut guard) = self.mem_overlay.write() {
*guard = None;
}
}
#[must_use]
pub(crate) fn is_mem_overlay_active(&self) -> bool {
self.mem_overlay_active.load(Ordering::Acquire)
}
fn overlay_active(&self) -> bool {
self.mem_overlay
.read()
.ok()
.and_then(|g| g.as_ref().map(|_| ()))
.is_some()
}
fn overlay_store(&self) -> Option<Arc<MemoryStore>> {
if !self.mem_overlay_active.load(Ordering::Acquire) {
return None;
}
self.mem_overlay.read().ok().and_then(|g| g.clone())
}
#[cfg(test)]
pub(crate) fn mem_overlay_len_for_tests(&self) -> Option<usize> {
let store = self.overlay_store()?;
let mut count = 0usize;
store
.for_each_object(&mut |_| {
count += 1;
ControlFlow::Continue(())
})
.ok()?;
Some(count)
}
fn overlay_store_object(&self, oid: ObjectId, kind: ObjectKind, data: &[u8]) -> bool {
let Some(store) = self.overlay_store() else {
return false;
};
store.insert_object(oid, kind, Arc::from(data.to_owned()));
true
}
fn overlay_read(&self, oid: &ObjectId) -> Option<Object> {
let store = self.overlay_store()?;
store.read(oid).ok().flatten()
}
pub fn primary(&self) -> Result<Arc<FilesSource>> {
self.require_files_primary("primary")?;
if let Some(src) = self.primary_source.get() {
return Ok(Arc::clone(src));
}
let src = self.build_files_source(
&self.objects_dir,
self.multi_pack_index_reads_enabled(),
self.midx_tip_listing_enabled(),
)?;
match self.primary_source.set(Arc::clone(&src)) {
Ok(()) => Ok(src),
Err(existing) => Ok(existing),
}
}
#[must_use]
fn alternate_object_lookup_needed(&self) -> bool {
if !self.file_alternate_dirs_snapshot().is_empty() {
return true;
}
if !self.env_alternate_dirs_snapshot().is_empty() {
return true;
}
self.submodule_alternate_dirs
.lock()
.ok()
.is_some_and(|guard| !guard.is_empty())
}
pub fn sources(&self) -> Result<Arc<CompositeStore>> {
if !self.alternates_enabled {
return Ok(Arc::new(CompositeStore::new(Vec::new(), self.hash_algo())));
}
let key = self.alternate_sources_key();
if let Ok(guard) = self.alternate_sources_cache.read() {
if guard.file_generation == key.file_generation
&& guard.env_fingerprint == key.env_fingerprint
&& guard.submodule_generation == key.submodule_generation
{
if let Some(sources) = guard.sources.as_ref() {
return Ok(Arc::clone(sources));
}
}
}
let use_midx = self.multi_pack_index_reads_enabled();
let midx_tip = self.midx_tip_listing_enabled();
let mut stores: Vec<Arc<dyn ObjectStore>> = Vec::new();
for alt_dir in self.file_alternate_dirs_snapshot().iter() {
stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
}
for alt_dir in self.env_alternate_dirs_snapshot().iter() {
stores.push(self.build_files_source(alt_dir, use_midx, midx_tip)?);
}
if let Ok(guard) = self.submodule_alternate_dirs.lock() {
for alt_dir in guard.iter() {
stores.push(self.build_files_source(alt_dir, false, false)?);
}
}
let composite = Arc::new(CompositeStore::new(stores, self.hash_algo()));
if let Ok(mut guard) = self.alternate_sources_cache.write() {
guard.file_generation = key.file_generation;
guard.env_fingerprint = key.env_fingerprint;
guard.submodule_generation = key.submodule_generation;
guard.sources = Some(Arc::clone(&composite));
}
Ok(composite)
}
fn midx_tip_listing_enabled(&self) -> bool {
self.config_git_dir.is_some() && self.core_multi_pack_index_enabled()
}
fn build_files_source(
&self,
objects_dir: &Path,
use_midx: bool,
midx_tip_listing_for_exists: bool,
) -> Result<Arc<FilesSource>> {
let loose = self.loose_at(objects_dir)?;
let pack_store = self.pack_store_for(objects_dir);
let diagnostics = if objects_dir == self.objects_dir.as_path() {
self.diagnostics_handle()
} else {
Arc::new(NullDiagnostics)
};
FilesSource::open(
pack_store,
loose,
use_midx,
midx_tip_listing_for_exists,
diagnostics,
)
.map(Arc::new)
}
pub fn register_submodule_object_directories_from_index(
&self,
work_tree: &Path,
index: &crate::index::Index,
) {
use crate::diff::submodule_embedded_git_dir;
let Ok(mut dirs) = self.submodule_alternate_dirs.lock() else {
return;
};
dirs.clear();
for e in &index.entries {
if e.stage() != 0 || e.mode != crate::index::MODE_GITLINK {
continue;
}
let path_str = String::from_utf8_lossy(&e.path);
let abs = work_tree.join(path_str.as_ref());
let Some(sub_git) = submodule_embedded_git_dir(&abs) else {
continue;
};
let objects = sub_git.join("objects");
if !objects.is_dir() {
continue;
}
let canon = objects.canonicalize().unwrap_or(objects);
if !dirs.iter().any(|p| p == &canon) {
dirs.push(canon);
}
}
self.submodule_sources_generation
.fetch_add(1, Ordering::Relaxed);
self.clear_alternate_sources_cache();
}
#[must_use]
pub fn with_config_git_dir(mut self, git_dir: PathBuf) -> Self {
self.config_git_dir = Some(git_dir);
self
}
#[must_use]
pub fn hash_algo(&self) -> HashAlgo {
if let Some(primary) = &self.custom_primary {
return primary.hash_algo();
}
*self.hash_algo_cache.get_or_init(|| {
if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
return HashAlgo::Sha1;
}
let cfg = self.load_config_cascade().unwrap_or_default();
cfg.get("extensions.objectformat")
.and_then(|v| HashAlgo::from_name(&v))
.unwrap_or(HashAlgo::Sha1)
})
}
fn resolve_loose_zlib_level(&self) -> Result<u32> {
if self.config_git_dir.is_none() && self.objects_dir.parent().is_none() {
return Ok(ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT as u32);
}
let cfg = self.load_config_cascade()?;
Ok(cfg.loose_objects_zlib_level()? as u32)
}
pub(crate) fn loose_compression(&self) -> Result<Compression> {
if let Some(cached) = self.loose_zlib_cache.get() {
return Ok(*cached);
}
let level = self.resolve_loose_zlib_level()?;
let compression = Compression::new(level);
let _ = self.loose_zlib_cache.set(compression);
Ok(compression)
}
fn sync_delta_base_cache_limit(&self) {
self.delta_base_cache_sync.get_or_init(|| {
if self.config_git_dir.is_some() {
let cfg = self.load_config_cascade().unwrap_or_default();
self.pack_store.configure_delta_base_from_config(Some(&cfg));
}
});
}
pub(crate) fn pack_store_for(&self, objects_dir: &Path) -> Arc<PackStore> {
if objects_dir == self.objects_dir.as_path() {
return Arc::clone(&self.pack_store);
}
if let Ok(mut guard) = self.alternate_pack_stores.lock() {
if let Some(store) = guard.get(objects_dir) {
return Arc::clone(store);
}
let store = Arc::new(PackStore::new(objects_dir.to_path_buf()));
guard.insert(objects_dir.to_path_buf(), Arc::clone(&store));
return store;
}
Arc::new(PackStore::new(objects_dir.to_path_buf()))
}
pub fn pack_store(&self) -> Result<&Arc<PackStore>> {
self.require_files_primary("pack_store")?;
Ok(&self.pack_store)
}
pub fn largest_pack_index_path(&self) -> Result<Option<PathBuf>> {
self.require_files_primary("largest_pack_index_path")?;
let mut indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
indexes.sort_by_key(|idx| std::cmp::Reverse(idx.len()));
Ok(indexes.first().map(|idx| idx.idx_path.clone()))
}
pub fn invalidate_packs(&self) {
self.pack_store.invalidate_all();
PackStore::invalidate_legacy_for_objects_dir(&self.objects_dir);
if let Ok(guard) = self.alternate_pack_stores.lock() {
for store in guard.values() {
store.invalidate_all();
PackStore::invalidate_legacy_for_objects_dir(store.objects_dir());
}
}
self.invalidate_change_id_header_probe();
}
fn invalidate_change_id_header_probe(&self) {
if let Some(caches) = self.shared_config_state.as_ref() {
crate::change_id::invalidate_stored_change_id_header_probe(caches, self.objects_dir());
}
}
fn note_commit_object_written(&self, kind: ObjectKind) {
if kind == ObjectKind::Commit {
self.invalidate_change_id_header_probe();
}
}
pub(crate) fn loosen_unreachable_from_superseded_packs(
&self,
pack_dir: &Path,
keep_pack: &Path,
reachable: impl Fn() -> Result<OidHashSet>,
retention: UnreachablePackRetention,
) -> Result<usize> {
if matches!(retention, UnreachablePackRetention::DropAll) {
return Ok(0);
}
self.require_files_primary("repack")?;
let keep_idx = keep_pack.with_extension("idx");
let keep_pack_oids = if keep_idx.is_file() {
PackedObjects::object_ids_from_index_path(&keep_idx)?
} else {
HashSet::new()
};
let kept_pack_oids = oids_in_marker_kept_packs(pack_dir, keep_pack)?;
let mut reachable_set: Option<OidHashSet> = None;
let mut loosened = 0usize;
for (idx_path, pack_mtime) in superseded_pack_indexes(pack_dir, keep_pack)? {
let oids = PackedObjects::object_ids_from_index_path(&idx_path)?;
for oid in oids {
let keep = match retention {
UnreachablePackRetention::KeepAll => true,
UnreachablePackRetention::KeepGraceProtected(cutoff) => pack_mtime >= cutoff,
UnreachablePackRetention::DropAll => false,
};
if !keep {
continue;
}
if keep_pack_oids.contains(&oid) || kept_pack_oids.contains(&oid) {
continue;
}
if reachable_set.is_none() {
reachable_set = Some(reachable()?);
}
if reachable_set.as_ref().is_some_and(|set| set.contains(&oid)) {
continue;
}
let loose_path = oid.loose_path_in(&self.objects_dir);
let pack_ft = system_time_to_file_time(pack_mtime);
match retention {
UnreachablePackRetention::KeepGraceProtected(cutoff) => {
if loose_path.is_file() {
let loose_mtime = fs::metadata(&loose_path)
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
if loose_mtime >= cutoff {
continue;
}
set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
loosened += 1;
continue;
}
}
UnreachablePackRetention::KeepAll => {
if loose_path.is_file() {
continue;
}
}
UnreachablePackRetention::DropAll => {}
}
let obj = self.read(&oid)?;
self.write_loose_materialize(obj.kind, &obj.data)?;
let primary = self.primary()?;
let loose_path = primary.loose_store().object_path(&oid);
set_file_mtime(&loose_path, pack_ft).map_err(Error::Io)?;
loosened += 1;
}
}
Ok(loosened)
}
fn core_multi_pack_index_enabled(&self) -> bool {
*self.core_multi_pack_index_cache.get_or_init(|| {
if self.config_git_dir.is_none() {
return false;
}
let cfg = self.load_config_cascade().unwrap_or_default();
match cfg.get_bool("core.multiPackIndex") {
Some(Ok(b)) => b,
Some(Err(_)) => true,
None => true,
}
})
}
fn multi_pack_index_reads_enabled(&self) -> bool {
if self.custom_primary.is_some() {
return false;
}
if self.config_git_dir.is_none() {
return false;
}
if !*self
.local_pack_indexes_seen
.get_or_init(|| objects_dir_has_pack_index_files(&self.objects_dir))
{
return false;
}
self.core_multi_pack_index_enabled()
}
fn ensure_midx_prepared(&self) {
if !self.multi_pack_index_reads_enabled() {
return;
}
let _ = self.midx_packs_validated.get_or_init(|| {
let diagnostics = self.diagnostics_handle();
validate_midx_referenced_packs_with_diagnostics(
&self.objects_dir,
diagnostics.as_ref(),
);
});
}
fn try_read_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<Object>> {
if self.custom_primary.is_some()
|| !PackStore::read_context_matches_objects_dir(&self.objects_dir)
|| self.multi_pack_index_reads_enabled()
{
return Ok(None);
}
self.sync_delta_base_cache_limit();
match pack::try_read_object_from_packs_with_options(
&self.objects_dir,
oid,
pack::PackLookupOptions::ALL_LOCAL_PACKS,
) {
Ok(obj) => Ok(Some(obj)),
Err(Error::ObjectNotFound(_)) => Ok(None),
Err(err) => Err(err),
}
}
fn try_read_info_packed_in_batch_context(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
if self.custom_primary.is_some()
|| !PackStore::read_context_matches_objects_dir(&self.objects_dir)
|| self.multi_pack_index_reads_enabled()
{
return Ok(None);
}
match pack::try_read_object_info_from_packs_with_options(
&self.objects_dir,
oid,
pack::PackLookupOptions::ALL_LOCAL_PACKS,
) {
Ok(info) => Ok(Some(info)),
Err(Error::ObjectNotFound(_)) => Ok(None),
Err(err) => Err(err),
}
}
#[must_use]
pub fn objects_dir(&self) -> &Path {
&self.objects_dir
}
fn try_read_extra_sources(&self, oid: &ObjectId) -> Result<Option<Object>> {
for src in &self.extra_read_sources {
if let Some(obj) = src.read(oid)? {
return Ok(Some(obj));
}
}
Ok(None)
}
fn try_read_info_extra_sources(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
for src in &self.extra_read_sources {
if let Some(info) = src.read_info(oid)? {
return Ok(Some(info));
}
}
Ok(None)
}
fn try_open_stream_extra_sources(
&self,
oid: &ObjectId,
) -> Result<Option<store::ObjectStream<'_>>> {
for src in &self.extra_read_sources {
if let Some(stream) = src.open_stream(oid)? {
return Ok(Some(stream));
}
}
Ok(None)
}
#[must_use]
pub fn config_git_dir(&self) -> Option<&Path> {
self.config_git_dir.as_deref()
}
#[must_use]
pub fn object_path(&self, oid: &ObjectId) -> PathBuf {
oid.loose_path_in(&self.objects_dir)
}
#[must_use]
pub fn exists_local(&self, oid: &ObjectId) -> bool {
if oid.is_canonical_empty_tree() {
return true;
}
if self.custom_primary.is_some() {
return self
.primary_writable()
.ok()
.and_then(|primary| primary.contains(oid).ok())
.unwrap_or(false);
}
self.with_pack_store_for(&self.objects_dir, || {
exists_materialized_in_objects_dir(&self.objects_dir, oid)
})
}
#[must_use]
pub fn exists_materialized_local(&self, oid: &ObjectId) -> bool {
if self.custom_primary.is_some() {
return self
.primary_writable()
.ok()
.and_then(|primary| primary.contains(oid).ok())
.unwrap_or(false);
}
self.with_pack_store_for(&self.objects_dir, || {
exists_materialized_in_objects_dir(&self.objects_dir, oid)
})
}
pub(crate) fn reprepare_local_packs(&self) {
if self.custom_primary.is_some() {
return;
}
self.with_pack_store_for(&self.objects_dir, || {
let _ = crate::pack::reprepare_pack_directory_on_miss(&self.objects_dir);
});
}
pub(crate) fn exists_materialized_local_without_rescan(&self, oid: &ObjectId) -> bool {
if self.custom_primary.is_some() {
return self.exists_materialized_local(oid);
}
self.with_pack_store_for(&self.objects_dir, || {
store::local_object_materialized_without_rescan(&self.objects_dir, oid)
})
}
#[must_use]
pub fn exists(&self, oid: &ObjectId) -> bool {
#[cfg(test)]
self.exists_probe.fetch_add(1, Ordering::Relaxed);
if oid.is_well_known_empty_tree() {
return true;
}
if self.overlay_read(oid).is_some() {
return true;
}
if self
.primary_writable()
.ok()
.and_then(|primary| primary.contains(oid).ok())
== Some(true)
{
return true;
}
for src in &self.extra_read_sources {
if src.contains(oid).ok() == Some(true) {
return true;
}
}
if !self.alternate_object_lookup_needed() {
return false;
}
self.sources()
.ok()
.and_then(|sources| sources.contains(oid).ok())
== Some(true)
}
fn with_pack_store_for<R>(&self, objects_dir: &Path, f: impl FnOnce() -> R) -> R {
let store = self.pack_store_for(objects_dir);
PackStore::with_context(store, f)
}
pub fn with_pack_read_context<R>(&self, f: impl FnOnce() -> R) -> R {
self.with_pack_store_for(&self.objects_dir, f)
}
pub(crate) fn open_session_pack_reader(&self) -> Result<Option<pack::SingleLocalPackReader>> {
if self.custom_primary.is_some()
|| !PackStore::read_context_matches_objects_dir(&self.objects_dir)
{
return Ok(None);
}
self.sync_delta_base_cache_limit();
pack::SingleLocalPackReader::open_allow_loose(&self.objects_dir)
}
#[must_use]
pub fn freshen_object(&self, oid: &ObjectId) -> bool {
if oid.is_well_known_empty_tree() {
return false;
}
let loose = self.object_path(oid);
if loose.is_file() {
return self.touch_object_mtime(&loose).is_some();
}
if self.freshen_object_in_objects_dir(&self.objects_dir, oid) {
return true;
}
let file_alts = self.file_alternate_dirs_snapshot();
for alt_dir in file_alts.iter() {
if self.freshen_object_in_objects_dir(alt_dir, oid) {
return true;
}
}
for alt_dir in self.env_alternate_dirs_snapshot().iter() {
if self.freshen_object_in_objects_dir(alt_dir, oid) {
return true;
}
}
if let Ok(guard) = self.submodule_alternate_dirs.lock() {
for alt_dir in guard.iter() {
if self.freshen_object_in_objects_dir(alt_dir, oid) {
return true;
}
}
}
false
}
fn freshen_object_in_objects_dir(&self, objects_dir: &Path, oid: &ObjectId) -> bool {
let loose = objects_dir
.join(oid.loose_prefix())
.join(oid.loose_suffix());
if loose.is_file() {
return self.touch_object_mtime(&loose).is_some();
}
self.with_pack_store_for(objects_dir, || {
let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
return false;
};
for idx in &indexes {
if idx.contains(oid).unwrap_or(false) {
return self.freshen_pack_once(&idx.pack_path, idx.is_cruft);
}
}
false
})
}
fn freshen_pack_once(&self, pack_path: &Path, is_cruft: bool) -> bool {
let Ok(mut guard) = self.freshened_packs.lock() else {
return false;
};
if guard.contains(pack_path) {
return true;
}
if is_cruft {
return false;
}
let Some(touched_at) = self.touch_object_mtime(pack_path) else {
return false;
};
pack::refresh_pack_bytes_signature(pack_path, touched_at);
guard.insert(pack_path.to_path_buf());
true
}
fn try_freshen_existing(&self, oid: &ObjectId) -> Option<ObjectId> {
if let Ok(primary) = self.primary() {
if let Ok(Some(existing)) =
primary.try_finish_duplicate_write(oid, WriteOptions::default())
{
return Some(existing);
}
}
if !self.exists(oid) {
return None;
}
if self.freshen_object(oid) {
return Some(*oid);
}
None
}
fn try_freshen_existing_local(&self, oid: &ObjectId) -> Option<ObjectId> {
let primary = self.primary().ok()?;
primary
.try_finish_duplicate_write(
oid,
WriteOptions {
assume_loose_only_existence: true,
..WriteOptions::default()
},
)
.ok()
.flatten()
}
fn object_exists_for_write_skip(&self, oid: &ObjectId) -> bool {
if oid.is_well_known_empty_tree() {
self.exists_materialized_local(oid)
} else {
self.exists(oid)
}
}
pub fn read_loose_verify_oid(path: &Path, expected_oid: &ObjectId) -> Result<Object> {
LooseStore::read_verify_oid(path, expected_oid)
}
fn local_loose(&self) -> Result<LooseStore> {
self.require_files_primary("loose_write")?;
Ok(LooseStore::new(
self.objects_dir.clone(),
self.hash_algo(),
self.loose_compression()?,
))
}
fn loose_at(&self, objects_dir: &Path) -> Result<LooseStore> {
Ok(LooseStore::new(
objects_dir.to_path_buf(),
self.hash_algo(),
self.loose_compression()?,
))
}
pub fn read(&self, oid: &ObjectId) -> Result<Object> {
if oid.is_well_known_empty_tree() {
return Ok(Object {
kind: ObjectKind::Tree,
data: Vec::new(),
});
}
if let Some(obj) = self.session_cached_loose_read(oid) {
return Ok(obj);
}
if let Some(obj) = self.overlay_read(oid) {
return Ok(obj);
}
if let Some(obj) = self.try_read_packed_in_batch_context(oid)? {
return Ok(obj);
}
let use_midx = self.multi_pack_index_reads_enabled();
if use_midx {
self.ensure_midx_prepared();
}
self.sync_delta_base_cache_limit();
let primary = self.primary_writable()?;
let files_primary = self.primary().ok();
let mut unreadable_local_loose = None;
let read_primary = || primary.read(oid);
match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
read_primary()
} else {
self.with_pack_store_for(&self.objects_dir, read_primary)
} {
Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj),
Ok(None) => {}
Err(err)
if files_primary
.as_ref()
.is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
{
unreadable_local_loose = Some(err);
}
Err(err) => return Err(err),
}
if let Some(obj) = self.try_read_extra_sources(oid)? {
return self.finish_session_loose_read(oid, obj);
}
if self.alternates_enabled {
match self.sources()?.read(oid) {
Ok(Some(obj)) => return self.finish_session_loose_read(oid, obj),
Ok(None) => {}
Err(err) => return Err(err),
}
}
if let Some(err) = unreadable_local_loose {
return Err(err);
}
Err(Error::ObjectNotFound(oid.to_hex()))
}
pub fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<ObjectKind> {
if oid.is_well_known_empty_tree() {
buf.clear();
return Ok(ObjectKind::Tree);
}
if let Some(obj) = self.session_cached_loose_read(oid) {
buf.clear();
buf.extend_from_slice(&obj.data);
return Ok(obj.kind);
}
if let Some(obj) = self.overlay_read(oid) {
buf.clear();
buf.extend_from_slice(&obj.data);
return Ok(obj.kind);
}
if let Some(kind) = self.try_read_packed_in_batch_context_into(oid, buf)? {
return Ok(kind);
}
let use_midx = self.multi_pack_index_reads_enabled();
if use_midx {
self.ensure_midx_prepared();
}
self.sync_delta_base_cache_limit();
let primary = self.primary_writable()?;
let files_primary = self.primary().ok();
let mut unreadable_local_loose = None;
let mut read_primary = || primary.read_into(oid, buf);
match if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
read_primary()
} else {
self.with_pack_store_for(&self.objects_dir, read_primary)
} {
Ok(Some(kind)) => return Ok(kind),
Ok(None) => {}
Err(err)
if files_primary
.as_ref()
.is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
{
unreadable_local_loose = Some(err);
}
Err(err) => return Err(err),
}
if let Some(kind) = self.try_read_into_extra_sources(oid, buf)? {
return Ok(kind);
}
if self.alternates_enabled {
match self.sources()?.read_into(oid, buf) {
Ok(Some(kind)) => return Ok(kind),
Ok(None) => {}
Err(err) => return Err(err),
}
}
if let Some(err) = unreadable_local_loose {
return Err(err);
}
Err(Error::ObjectNotFound(oid.to_hex()))
}
fn try_read_into_extra_sources(
&self,
oid: &ObjectId,
buf: &mut Vec<u8>,
) -> Result<Option<ObjectKind>> {
for src in &self.extra_read_sources {
if let Some(kind) = src.read_into(oid, buf)? {
return Ok(Some(kind));
}
}
Ok(None)
}
fn try_read_packed_in_batch_context_into(
&self,
oid: &ObjectId,
buf: &mut Vec<u8>,
) -> Result<Option<ObjectKind>> {
if self.custom_primary.is_some()
|| !PackStore::read_context_matches_objects_dir(&self.objects_dir)
|| self.multi_pack_index_reads_enabled()
{
return Ok(None);
}
self.sync_delta_base_cache_limit();
match pack::try_read_object_from_packs_with_options_into(
&self.objects_dir,
oid,
pack::PackLookupOptions::ALL_LOCAL_PACKS,
buf,
) {
Ok(kind) => Ok(Some(kind)),
Err(Error::ObjectNotFound(_)) => Ok(None),
Err(err) => Err(err),
}
}
pub fn read_info(&self, oid: &ObjectId) -> Result<ObjectInfo> {
if oid.is_well_known_empty_tree() {
return Ok(ObjectInfo {
kind: ObjectKind::Tree,
size: 0,
});
}
if let Some(obj) = self.overlay_read(oid) {
return Ok(ObjectInfo {
kind: obj.kind,
size: u64::try_from(obj.data.len())
.map_err(|_| Error::CorruptObject("object size overflow".to_owned()))?,
});
}
if let Some(info) = self.try_read_info_packed_in_batch_context(oid)? {
return Ok(info);
}
let use_midx = self.multi_pack_index_reads_enabled();
if use_midx {
self.ensure_midx_prepared();
}
let primary = self.primary_writable()?;
let files_primary = self.primary().ok();
let mut unreadable_local_loose = None;
match primary.read_info(oid) {
Ok(Some(info)) => return Ok(info),
Ok(None) => {}
Err(err)
if files_primary
.as_ref()
.is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
{
unreadable_local_loose = Some(err);
}
Err(err) => return Err(err),
}
if let Some(info) = self.try_read_info_extra_sources(oid)? {
return Ok(info);
}
if self.alternates_enabled {
match self.sources()?.read_info(oid) {
Ok(Some(info)) => return Ok(info),
Ok(None) => {}
Err(err) => return Err(err),
}
}
if let Some(err) = unreadable_local_loose {
return Err(err);
}
Err(Error::ObjectNotFound(oid.to_hex()))
}
pub fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'static>>> {
use std::io::Cursor;
if let Some(obj) = self.overlay_read(oid) {
let size = u64::try_from(obj.data.len()).map_err(|_| {
Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
})?;
return Ok(Some(ObjectStream {
kind: obj.kind,
size,
reader: Box::new(Cursor::new(obj.data)),
}));
}
if self.multi_pack_index_reads_enabled() {
self.ensure_midx_prepared();
}
self.sync_delta_base_cache_limit();
let primary = self.primary_writable()?;
let files_primary = self.primary().ok();
let mut unreadable_local_loose = None;
match primary.open_stream(oid) {
Ok(Some(stream)) => return materialize_stream(stream),
Ok(None) => {}
Err(err)
if files_primary
.as_ref()
.is_some_and(|p| p.local_loose_exists_but_unreadable(oid, &err)) =>
{
unreadable_local_loose = Some(err);
}
Err(err) => return Err(err),
}
if let Some(stream) = self.try_open_stream_extra_sources(oid)? {
return materialize_stream(stream);
}
if self.alternates_enabled {
match self.sources()?.open_stream(oid) {
Ok(Some(stream)) => return materialize_stream(stream),
Ok(None) => {}
Err(err) => return Err(err),
}
}
if let Some(err) = unreadable_local_loose {
return Err(err);
}
Ok(None)
}
pub fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
let mut seen = oid_hash_set();
let mut visit = |store: &dyn ObjectStore| -> Result<bool> {
store::for_each_propagate_break(store, &mut |oid| {
if !seen.insert(*oid) {
return ControlFlow::Continue(());
}
f(oid)
})
};
if let Some(overlay) = self.overlay_store() {
if visit(overlay.as_ref())? {
return Ok(());
}
}
if visit(self.primary_writable()?.as_ref())? {
return Ok(());
}
for src in &self.extra_read_sources {
if visit(src.as_ref())? {
return Ok(());
}
}
if self.alternates_enabled {
let _ = visit(self.sources()?.as_ref())?;
}
Ok(())
}
pub fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
let mut seen: OidHashSet = out.iter().copied().collect();
if let Some(overlay) = self.overlay_store() {
append_lookup_prefix_layer(overlay.as_ref(), prefix, limit, out, &mut seen)?;
if limit != 0 && out.len() >= limit {
return Ok(());
}
}
append_lookup_prefix_layer(
self.primary_writable()?.as_ref(),
prefix,
limit,
out,
&mut seen,
)?;
if limit != 0 && out.len() >= limit {
return Ok(());
}
for src in &self.extra_read_sources {
append_lookup_prefix_layer(src.as_ref(), prefix, limit, out, &mut seen)?;
if limit != 0 && out.len() >= limit {
return Ok(());
}
}
if self.alternates_enabled {
append_lookup_prefix_layer(self.sources()?.as_ref(), prefix, limit, out, &mut seen)?;
}
Ok(())
}
pub fn enumerate_local_loose_objects(&self) -> Result<Vec<(ObjectId, PathBuf)>> {
let primary = self.primary()?;
let loose = primary.loose_store();
let mut out = Vec::new();
loose.for_each_object(&mut |oid| {
out.push((*oid, loose.object_path(oid)));
ControlFlow::Continue(())
})?;
Ok(out)
}
pub fn hash(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
hash_object_data_with(self.hash_algo(), kind, data)
}
pub fn write(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
self.write_with_options(kind, data, WriteOptions::default())
}
pub fn ensure_all_loose_prefix_dirs(&self) -> Result<()> {
self.local_loose()?.ensure_all_loose_prefix_dirs()
}
#[cfg(test)]
#[must_use]
pub fn test_loose_object_path(&self, oid: &ObjectId) -> PathBuf {
self.object_path(oid)
}
#[cfg(test)]
pub fn test_remove_loose_object(&self, oid: &ObjectId) {
let _ = fs::remove_file(self.object_path(oid));
}
pub fn count_loose_objects(&self) -> Result<usize> {
let mut count = 0usize;
for_each_loose_object_id(&self.objects_dir, self.hash_algo(), &mut |_| {
count += 1;
ControlFlow::Continue(())
})?;
Ok(count)
}
pub fn approximate_object_count(&self) -> Result<usize> {
let pack_objects: Result<usize> = self.with_pack_read_context(|| {
let indexes = pack::read_local_pack_indexes_cached(self.objects_dir())?;
Ok(indexes.iter().map(|idx| idx.len()).sum())
});
let mut count = pack_objects?;
count += self.count_loose_objects()?;
Ok(count)
}
pub fn zlib_compress_loose_store(&self, store_bytes: &[u8]) -> Result<Vec<u8>> {
self.local_loose()?.zlib_compress_store_bytes(store_bytes)
}
pub fn write_loose_zlib_prehashed(
&self,
oid: &ObjectId,
zlib_store: &[u8],
options: WriteOptions,
) -> Result<ObjectId> {
if self.custom_primary.is_some() {
return Err(Error::UnsupportedObjectStore {
operation: "write_loose_zlib_prehashed",
});
}
let primary = self.primary()?;
if let Some(existing) = primary.try_finish_duplicate_write(oid, options)? {
return Ok(existing);
}
let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
&& self.object_exists_for_write_skip(oid);
if self.overlay_active() && !already_exists {
if let Ok(raw) = decompress_zlib_loose_bytes(zlib_store) {
if let Ok(obj) = parse_object_bytes(&raw) {
if self.overlay_store_object(*oid, obj.kind, &obj.data) {
return Ok(*oid);
}
}
}
}
if already_exists {
if !options.silent {
let _ = self.freshen_object(oid);
}
return Ok(*oid);
}
primary.write_zlib_prehashed(oid, zlib_store, options)
}
pub fn write_with_options(
&self,
kind: ObjectKind,
data: &[u8],
options: WriteOptions,
) -> Result<ObjectId> {
let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
if let Some(primary) = &self.custom_primary {
let already_exists = if options.trust_new_loose {
false
} else if options.assume_loose_only_existence {
primary.contains(&oid)?
} else {
self.object_exists_for_write_skip(&oid)
};
if self.overlay_active()
&& !already_exists
&& self.overlay_store_object(oid, kind, data)
{
return Ok(oid);
}
if already_exists {
return Ok(oid);
}
let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
self.note_commit_object_written(kind);
return Ok(oid);
}
let primary = self.primary()?;
if let Some(existing) = primary.try_finish_duplicate_write(&oid, options)? {
return Ok(existing);
}
let already_exists = !(options.trust_new_loose || options.assume_loose_only_existence)
&& self.object_exists_for_write_skip(&oid);
if self.overlay_active() && !already_exists && self.overlay_store_object(oid, kind, data) {
return Ok(oid);
}
if already_exists {
if !options.silent {
let _ = self.freshen_object(&oid);
}
return Ok(oid);
}
let oid = WritableObjectStore::write(primary.as_ref(), kind, data, options)?;
self.note_commit_object_written(kind);
Ok(oid)
}
pub fn write_local(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
if self.custom_primary.is_some() {
return Err(Error::UnsupportedObjectStore {
operation: "write_local",
});
}
let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
if let Some(existing) = self.try_freshen_existing_local(&oid) {
return Ok(existing);
}
let oid = self
.local_loose()?
.write(kind, data, WriteOptions::default())?;
self.note_commit_object_written(kind);
Ok(oid)
}
pub fn write_loose_materialize(&self, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
if self.custom_primary.is_some() {
return Err(Error::UnsupportedObjectStore {
operation: "write_loose_materialize",
});
}
let oid = hash_object_data_with(self.hash_algo(), kind, data)?;
if let Ok(primary) = self.primary() {
if let Ok(Some(existing)) = primary
.loose_store()
.try_finish_if_loose_present(&oid, WriteOptions::default())
{
return Ok(existing);
}
}
self.local_loose()?
.write(kind, data, WriteOptions::default())
}
pub fn write_raw(&self, store_bytes: &[u8]) -> Result<ObjectId> {
let obj = parse_object_bytes(store_bytes)?;
let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
if let Some(primary) = &self.custom_primary {
if self.exists(&oid) {
return Ok(oid);
}
if self.overlay_active() && self.overlay_store_object(oid, obj.kind, &obj.data) {
return Ok(oid);
}
return WritableObjectStore::write(
primary.as_ref(),
obj.kind,
&obj.data,
WriteOptions::default(),
);
}
if let Some(existing) = self.try_freshen_existing(&oid) {
return Ok(existing);
}
self.local_loose()?
.write_store_prehashed(&oid, store_bytes, WriteOptions::default())
}
pub fn write_raw_local(&self, store_bytes: &[u8]) -> Result<ObjectId> {
if self.custom_primary.is_some() {
return Err(Error::UnsupportedObjectStore {
operation: "write_raw_local",
});
}
let obj = parse_object_bytes(store_bytes)?;
let oid = hash_object_data_with(self.hash_algo(), obj.kind, &obj.data)?;
if let Some(existing) = self.try_freshen_existing_local(&oid) {
return Ok(existing);
}
self.local_loose()?
.write_store_prehashed(&oid, store_bytes, WriteOptions::default())
}
fn touch_object_mtime(&self, path: &Path) -> Option<std::time::SystemTime> {
#[cfg(test)]
self.hot_path_test_metrics.record_freshen();
let now = filetime::FileTime::now();
filetime::set_file_times(path, now, now).ok()?;
Some(
std::time::UNIX_EPOCH
+ std::time::Duration::new(now.unix_seconds() as u64, now.nanoseconds()),
)
}
}
fn hash_object_data_with(algo: HashAlgo, kind: ObjectKind, data: &[u8]) -> Result<ObjectId> {
hash::hash_object(algo, kind, data)
}
pub(crate) fn blob_store_bytes(data: &[u8]) -> Vec<u8> {
build_store_bytes(ObjectKind::Blob, data)
}
pub(crate) fn zlib_compress_loose_store_from_bytes(
store_bytes: &[u8],
compression: flate2::Compression,
) -> Result<Vec<u8>> {
zlib_compress_store_bytes(store_bytes, compression)
}
fn append_lookup_prefix_layer(
store: &dyn ObjectStore,
prefix: &str,
limit: usize,
out: &mut Vec<ObjectId>,
seen: &mut OidHashSet,
) -> Result<()> {
let mut scratch = Vec::new();
store.lookup_prefix(prefix, 0, &mut scratch)?;
for oid in scratch {
if seen.insert(oid) {
out.push(oid);
if limit != 0 && out.len() >= limit {
return Ok(());
}
}
}
Ok(())
}
fn materialize_stream(stream: ObjectStream<'_>) -> Result<Option<ObjectStream<'static>>> {
use std::io::{Cursor, Read};
let ObjectStream {
kind,
size,
mut reader,
} = stream;
let mut payload = Vec::new();
reader.read_to_end(&mut payload).map_err(Error::Io)?;
Ok(Some(ObjectStream {
kind,
size,
reader: Box::new(Cursor::new(payload)),
}))
}
fn parse_alternate_env(val: &str) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut chars = val.chars().peekable();
while chars.peek().is_some() {
if chars.peek() == Some(&':') {
chars.next();
continue;
}
if chars.peek() == Some(&'"') {
chars.next(); let saved: Vec<char> = chars.clone().collect();
let mut path = String::new();
let mut properly_closed = false;
loop {
match chars.next() {
None => break,
Some('"') => {
properly_closed = true;
break;
}
Some('\\') => match chars.peek() {
Some(c) if c.is_ascii_digit() => {
let mut oct = String::new();
for _ in 0..3 {
if let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
oct.push(c);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(byte) = u8::from_str_radix(&oct, 8) {
path.push(byte as char);
}
}
Some(_) => {
if let Some(c) = chars.next() {
match c {
'n' => path.push('\n'),
't' => path.push('\t'),
'r' => path.push('\r'),
_ => path.push(c),
}
}
}
None => {}
},
Some(c) => path.push(c),
}
}
if !properly_closed {
let raw: String = std::iter::once('"').chain(saved).collect();
let raw_path = raw.split(':').next().unwrap_or(&raw);
if !raw_path.is_empty() {
result.push(PathBuf::from(raw_path));
}
let remainder = &raw[raw_path.len()..];
if let Some(rest) = remainder.strip_prefix(':') {
result.extend(parse_alternate_env(rest));
}
return result;
} else if !path.is_empty() {
result.push(PathBuf::from(path));
}
} else {
let mut path = String::new();
while let Some(&c) = chars.peek() {
if c == ':' {
break;
}
path.push(c);
chars.next();
}
if !path.is_empty() {
result.push(PathBuf::from(path));
}
}
}
result
}
fn system_time_to_file_time(t: std::time::SystemTime) -> FileTime {
let dur = t
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap_or_default();
FileTime::from_unix_time(
i64::try_from(dur.as_secs()).unwrap_or(i64::MAX),
dur.subsec_nanos(),
)
}
fn superseded_pack_indexes(
pack_dir: &Path,
keep_pack: &Path,
) -> Result<Vec<(PathBuf, std::time::SystemTime)>> {
let keep_stem = keep_pack
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| {
Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"bad pack path",
))
})?;
if !pack_dir.is_dir() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
let entry = entry.map_err(Error::Io)?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == keep_stem || name_str.starts_with(&format!("{keep_stem}.")) {
continue;
}
if !name_str.ends_with(".idx") {
continue;
}
let idx_path = entry.path();
let pack_path = idx_path.with_extension("pack");
if !pack_path.is_file() {
continue;
}
if superseded_pack_has_keep_marker(pack_dir, &pack_path) {
continue;
}
let pack_mtime = fs::metadata(&pack_path)
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
out.push((idx_path, pack_mtime));
}
Ok(out)
}
fn superseded_pack_has_keep_marker(pack_dir: &Path, pack_path: &Path) -> bool {
let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
return false;
};
pack_dir.join(format!("{stem}.keep")).is_file()
}
fn oids_in_marker_kept_packs(pack_dir: &Path, keep_pack: &Path) -> Result<OidHashSet> {
let keep_stem = keep_pack.file_stem().and_then(|s| s.to_str()).unwrap_or("");
let mut oids = oid_hash_set();
if !pack_dir.is_dir() {
return Ok(oids);
}
for entry in fs::read_dir(pack_dir).map_err(Error::Io)? {
let entry = entry.map_err(Error::Io)?;
let pack_path = entry.path();
if pack_path.extension().is_none_or(|e| e != "pack") {
continue;
}
let Some(stem) = pack_path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
if stem == keep_stem {
continue;
}
if !superseded_pack_has_keep_marker(pack_dir, &pack_path) {
continue;
}
let idx_path = pack_path.with_extension("idx");
if idx_path.is_file() {
oids.extend(PackedObjects::object_ids_from_index_path(&idx_path)?);
}
}
Ok(oids)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use tempfile::TempDir;
#[test]
fn round_trip_blob() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let data = b"hello world";
let oid = odb.write(ObjectKind::Blob, data).unwrap();
let obj = odb.read(&oid).unwrap();
assert_eq!(obj.kind, ObjectKind::Blob);
assert_eq!(obj.data, data);
}
#[test]
fn known_blob_hash() {
let oid = HashAlgo::Sha1
.hash_object(ObjectKind::Blob, b"hello")
.expect("hash");
assert_eq!(oid.to_hex(), "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0");
}
#[test]
fn parse_object_header_prefix_unknown_kind_is_typed_error() {
let err = crate::odb::store::loose::parse_object_header_prefix(b"not-a-git-kind 0\0")
.unwrap_err();
assert!(matches!(err, Error::UnknownObjectType(_)));
}
fn git_dir_with_config(config: &str) -> (TempDir, PathBuf, PathBuf) {
let dir = TempDir::new().unwrap();
let git_dir = dir.path().join(".git");
let objects = git_dir.join("objects");
fs::create_dir_all(&objects).unwrap();
fs::write(git_dir.join("config"), config).unwrap();
(dir, git_dir, objects)
}
fn zlib_flevel(compressed: &[u8]) -> u8 {
assert!(compressed.len() >= 2);
(compressed[1] >> 6) & 3
}
fn tmp_loose_object_paths(objects: &Path) -> Vec<PathBuf> {
let mut tmp = Vec::new();
let Ok(shards) = fs::read_dir(objects) else {
return tmp;
};
for shard in shards.flatten() {
if !shard.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
let Ok(files) = fs::read_dir(shard.path()) else {
continue;
};
for file in files.flatten() {
let name = file.file_name();
if name.to_string_lossy().starts_with("tmp_") {
tmp.push(file.path());
}
}
}
tmp
}
#[test]
fn loose_write_honours_level() {
let payload: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
let (_dir, git_dir, objects) = git_dir_with_config("");
let odb = Odb::new(&objects).with_config_git_dir(git_dir.clone());
let oid_default = odb.write(ObjectKind::Blob, &payload).unwrap();
let bytes_default = fs::read(odb.object_path(&oid_default)).unwrap();
assert!(zlib_flevel(&bytes_default) <= 1);
assert_eq!(odb.read(&oid_default).unwrap().data, payload);
for level in -1i32..=9 {
let config = format!("[core]\n\tloosecompression = {level}\n");
let (_dir, git_dir, objects) = git_dir_with_config(&config);
let odb = Odb::new(&objects).with_config_git_dir(git_dir);
let byte = u8::try_from(level.rem_euclid(256)).unwrap_or(0);
let bytes: Vec<u8> = vec![byte; 512];
let oid = odb.write(ObjectKind::Blob, &bytes).unwrap();
let on_disk = fs::read(odb.object_path(&oid)).unwrap();
assert!(on_disk.len() >= 2, "level {level}");
assert_eq!(odb.read(&oid).unwrap().data, bytes.as_slice());
}
let config = "[core]\n\tcompression = 9\n";
let (_dir, git_dir, objects) = git_dir_with_config(config);
let odb = Odb::new(&objects).with_config_git_dir(git_dir);
let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 3);
let config = "[core]\n\tcompression = 0\n\tloosecompression = 1\n";
let (_dir, git_dir, objects) = git_dir_with_config(config);
let odb = Odb::new(&objects).with_config_git_dir(git_dir);
let oid = odb.write(ObjectKind::Blob, &payload).unwrap();
assert_eq!(zlib_flevel(&fs::read(odb.object_path(&oid)).unwrap()), 0);
let config = "[core]\n\tloosecompression = 99\n";
let (_dir, git_dir, objects) = git_dir_with_config(config);
let odb = Odb::new(&objects).with_config_git_dir(git_dir);
assert!(odb.write(ObjectKind::Blob, b"x").is_err());
assert!(tmp_loose_object_paths(&objects).is_empty());
}
fn git_run(dir: &Path, args: &[&str]) -> std::process::Output {
std::process::Command::new("git") .current_dir(dir)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "T")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_COMMITTER_NAME", "T")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.output()
.unwrap()
}
fn git_in(dir: &Path, args: &[&str]) {
let out = git_run(dir, args);
assert!(
out.status.success(),
"git {:?}: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
fn git_rev_parse_oid(dir: &Path, spec: &str) -> ObjectId {
let out = git_run(dir, &["rev-parse", spec]);
assert!(
out.status.success(),
"git rev-parse {spec}: {}",
String::from_utf8_lossy(&out.stderr)
);
ObjectId::from_hex(std::str::from_utf8(&out.stdout).unwrap().trim()).unwrap()
}
#[test]
fn write_loose_zlib_prehashed_matches_write() {
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
let odb = Odb::new(&objects);
let data = b"parallel prehash path";
let oid_normal = odb.write(ObjectKind::Blob, data).unwrap();
let dir2 = TempDir::new().unwrap();
let objects2 = dir2.path().join("objects");
let odb2 = Odb::new(&objects2);
let store = super::blob_store_bytes(data);
let oid = hash::hash_object(odb2.hash_algo(), ObjectKind::Blob, data).expect("hash");
assert_eq!(oid, oid_normal);
let zlib = odb2.zlib_compress_loose_store(&store).unwrap();
let oid2 = odb2
.write_loose_zlib_prehashed(
&oid,
&zlib,
WriteOptions {
assume_loose_only_existence: true,
..WriteOptions::default()
},
)
.unwrap();
assert_eq!(oid2, oid_normal);
assert_eq!(
fs::read(odb2.object_path(&oid2)).unwrap(),
fs::read(odb.object_path(&oid_normal)).unwrap()
);
}
#[test]
fn write_existing_loose_does_not_rewrite() {
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
fs::create_dir_all(&objects).unwrap();
let odb = Odb::new(&objects);
let payload = b"already loose";
let oid = odb.write(ObjectKind::Blob, payload).unwrap();
let path = odb.object_path(&oid);
let before_bytes = fs::read(&path).unwrap();
let meta_before = fs::metadata(&path).unwrap();
let before_mtime = filetime::FileTime::from_last_modification_time(&meta_before);
#[cfg(unix)]
let ino_before = {
use std::os::unix::fs::MetadataExt;
meta_before.ino()
};
std::thread::sleep(std::time::Duration::from_millis(20));
let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
assert_eq!(oid, oid2);
assert_eq!(fs::read(&path).unwrap(), before_bytes);
let meta_after = fs::metadata(&path).unwrap();
let after_mtime = filetime::FileTime::from_last_modification_time(&meta_after);
assert!(
after_mtime >= before_mtime,
"expected freshen to bump or preserve mtime"
);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_eq!(
meta_after.ino(),
ino_before,
"rewrite must not replace the loose object file"
);
}
}
#[test]
fn write_skips_loose_when_object_in_pack_only() {
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("f"), b"pack-only payload").unwrap();
git_in(dir.path(), &["add", "f"]);
git_in(dir.path(), &["commit", "-m", "c"]);
git_in(dir.path(), &["repack", "-ad"]);
let objects = dir.path().join(".git").join("objects");
let odb = Odb::new(&objects);
let oid = odb.write(ObjectKind::Blob, b"pack-only payload").unwrap();
assert!(!odb.object_path(&oid).exists());
assert!(odb.exists(&oid));
}
#[test]
fn write_freshens_loose_object_in_alternate() {
use filetime::FileTime;
use std::thread;
use std::time::Duration;
let alt = TempDir::new().unwrap();
git_in(alt.path(), &["init", "-q", "--bare"]);
git_in(alt.path(), &["config", "user.email", "t@example.com"]);
let alt_objects = alt.path().join("objects");
let alt_odb = Odb::new(&alt_objects);
let payload = b"alternate-loose";
let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
let alt_loose = alt_odb.object_path(&oid);
let primary = TempDir::new().unwrap();
let primary_objects = primary.path().join("objects");
fs::create_dir_all(primary_objects.join("info")).unwrap();
fs::write(
primary_objects.join("info").join("alternates"),
format!("{}\n", alt_objects.display()),
)
.unwrap();
let odb = Odb::new(&primary_objects);
thread::sleep(Duration::from_millis(50));
let before = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
filetime::set_file_mtime(&alt_loose, before).unwrap();
let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
assert_eq!(oid, oid2);
assert!(!odb.object_path(&oid).exists());
let after = FileTime::from_last_modification_time(&fs::metadata(&alt_loose).unwrap());
assert!(after > before, "write must freshen alternate loose object");
}
#[test]
fn write_freshens_packed_object_in_alternate() {
use filetime::FileTime;
use std::thread;
use std::time::Duration;
let alt = TempDir::new().unwrap();
git_in(alt.path(), &["init", "-q"]);
git_in(alt.path(), &["config", "user.email", "t@example.com"]);
git_in(alt.path(), &["config", "user.name", "T"]);
let payload = b"alternate-pack";
fs::write(alt.path().join("f"), payload).unwrap();
git_in(alt.path(), &["add", "f"]);
git_in(alt.path(), &["commit", "-m", "c"]);
git_in(alt.path(), &["repack", "-ad"]);
let alt_objects = alt.path().join(".git").join("objects");
let alt_odb = Odb::new(&alt_objects);
let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
let pack_path = fs::read_dir(alt_objects.join("pack"))
.unwrap()
.flatten()
.find(|e| e.path().extension().is_some_and(|x| x == "pack"))
.unwrap()
.path();
thread::sleep(Duration::from_millis(50));
let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
let primary = TempDir::new().unwrap();
let primary_objects = primary.path().join("objects");
fs::create_dir_all(primary_objects.join("info")).unwrap();
fs::write(
primary_objects.join("info").join("alternates"),
format!("{}\n", alt_objects.display()),
)
.unwrap();
pack::clear_pack_cache();
let odb = Odb::new(&primary_objects);
assert!(odb.exists(&oid));
let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
assert_eq!(oid, oid2);
assert!(!odb.object_path(&oid).exists());
let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
assert!(after >= before, "write must freshen alternate pack");
}
#[test]
fn write_skips_loose_when_object_only_in_alternate() {
let alt = TempDir::new().unwrap();
git_in(alt.path(), &["init", "-q", "--bare"]);
git_in(alt.path(), &["config", "user.email", "t@example.com"]);
let alt_objects = alt.path().join("objects");
let alt_odb = Odb::new(&alt_objects);
let payload = b"alternate-only";
let oid = alt_odb.write(ObjectKind::Blob, payload).unwrap();
let primary = TempDir::new().unwrap();
let primary_objects = primary.path().join("objects");
fs::create_dir_all(primary.path().join("objects").join("info")).unwrap();
fs::write(
primary
.path()
.join("objects")
.join("info")
.join("alternates"),
format!("{}\n", alt_objects.display()),
)
.unwrap();
let odb = Odb::new(&primary_objects);
assert!(odb.exists(&oid));
let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
assert_eq!(oid, oid2);
assert!(!odb.object_path(&oid).exists());
}
#[test]
fn write_with_mem_overlay_stays_in_memory() {
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
fs::create_dir_all(&objects).unwrap();
let odb = Odb::new(&objects);
odb.enable_mem_overlay();
let payload = b"overlay blob";
let oid = odb.write(ObjectKind::Blob, payload).unwrap();
assert!(!odb.object_path(&oid).exists());
assert_eq!(odb.read(&oid).unwrap().data, payload);
odb.disable_mem_overlay();
assert!(odb.read(&oid).is_err());
}
#[test]
fn read_alternates_relative_path_and_symlinked_objects_dir() {
use crate::pack::read_alternates_recursive;
let layout = TempDir::new().unwrap();
let alt_objects = layout.path().join("alt-store").join("objects");
fs::create_dir_all(&alt_objects).unwrap();
let primary_objects = layout.path().join("repo").join("objects");
fs::create_dir_all(primary_objects.join("info")).unwrap();
fs::write(
primary_objects.join("info").join("alternates"),
"../../alt-store/objects\n",
)
.unwrap();
let resolved = read_alternates_recursive(&primary_objects).unwrap();
let alt_canonical = fs::canonicalize(&alt_objects).unwrap_or(alt_objects.clone());
assert!(
resolved
.iter()
.any(|p| { fs::canonicalize(p).unwrap_or_else(|_| p.clone()) == alt_canonical }),
"relative alternate entry should resolve: {resolved:?}"
);
#[cfg(unix)]
{
let link_parent = TempDir::new().unwrap();
std::os::unix::fs::symlink(&primary_objects, link_parent.path().join("objects-link"))
.unwrap();
let via_link =
read_alternates_recursive(&link_parent.path().join("objects-link")).unwrap();
assert!(
!via_link.is_empty(),
"alternates through symlinked objects dir: {via_link:?}"
);
}
}
#[test]
fn empty_tree_exists_without_hex() {
let odb = Odb::new(std::path::Path::new("/nonexistent/objects"));
let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").unwrap();
assert!(odb.exists(&canon));
assert!(odb.exists(&legacy));
assert!(odb.exists_local(&canon));
assert!(!odb.freshen_object(&canon));
}
#[test]
fn empty_tree_virtual_presence_skips_materialized_local() {
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
fs::create_dir_all(&objects).unwrap();
let odb = Odb::new(&objects);
let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
assert!(odb.exists_local(&canon));
assert!(!odb.exists_materialized_local(&canon));
}
#[test]
fn write_materializes_well_known_empty_tree() {
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
fs::create_dir_all(&objects).unwrap();
let odb = Odb::new(&objects);
let canon = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
assert!(!odb.exists_materialized_local(&canon));
let oid = odb
.write(crate::objects::ObjectKind::Tree, &[])
.expect("write empty tree");
assert_eq!(oid, canon);
assert!(odb.exists_materialized_local(&canon));
}
#[test]
fn append_alternate_objects_line_errors_on_unreadable_existing_file() {
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
fs::create_dir_all(objects.join("info")).unwrap();
let alt_path = objects.join("info/alternates");
fs::write(&alt_path, b"not valid utf-8 \xff\n").unwrap();
let err = Odb::append_alternate_objects_line(&objects, Path::new("/tmp/other/objects"))
.unwrap_err();
assert!(matches!(err, Error::Io(_)));
assert_eq!(fs::read(&alt_path).unwrap(), b"not valid utf-8 \xff\n");
}
#[test]
fn append_file_alternate_refreshes_cached_snapshot() {
let primary = TempDir::new().unwrap();
let alternate = TempDir::new().unwrap();
let alt_objects = alternate.path().join("objects");
std::fs::create_dir_all(&alt_objects).unwrap();
let alt_odb = Odb::new(&alt_objects);
let oid = alt_odb.write(ObjectKind::Blob, b"via-alternate").unwrap();
let primary_objects = primary.path().join("objects");
std::fs::create_dir_all(primary_objects.join("info")).unwrap();
let primary_odb = Odb::new(&primary_objects);
assert!(!primary_odb.exists(&oid));
let _ = primary_odb.file_alternate_dirs_snapshot();
primary_odb.append_file_alternate(&alt_objects).unwrap();
assert!(primary_odb.exists(&oid));
}
#[test]
fn exists_via_env_alternate_dirs_at_construction() {
let primary = TempDir::new().unwrap();
let alternate = TempDir::new().unwrap();
let alt_objects = alternate.path().join("objects");
std::fs::create_dir_all(&alt_objects).unwrap();
let alt_odb = Odb::new(&alt_objects);
let oid = alt_odb.write(ObjectKind::Blob, b"env-alt").unwrap();
let primary_objects = primary.path().join("objects");
std::fs::create_dir_all(&primary_objects).unwrap();
let primary_odb = Odb::new(&primary_objects).with_env_alternate_dirs(vec![alt_objects]);
assert!(primary_odb.exists(&oid));
}
#[test]
fn write_existing_loose_uses_path_stat_not_exists() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let oid = odb.write(ObjectKind::Blob, b"once").unwrap();
let path = odb.object_path(&oid);
assert!(path.is_file());
odb.reset_exists_probe();
let oid2 = odb.write(ObjectKind::Blob, b"once").unwrap();
assert_eq!(oid, oid2);
assert_eq!(
odb.exists_probe_count(),
0,
"loose fast path must not call exists()"
);
}
#[test]
fn exists_and_write_after_invalidate_packs_use_odb_store() {
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
let payload = b"invalidate-pack-exists";
fs::write(dir.path().join("f"), payload).unwrap();
git_in(dir.path(), &["add", "f"]);
git_in(dir.path(), &["commit", "-m", "c"]);
git_in(dir.path(), &["repack", "-ad"]);
let objects = dir.path().join(".git").join("objects");
let odb = Odb::new(&objects);
let oid = odb.hash(ObjectKind::Blob, payload).expect("hash");
assert!(odb.exists(&oid), "blob must live in pack after repack");
let pack_dir = objects.join("pack");
for entry in fs::read_dir(&pack_dir).unwrap().flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
fs::remove_file(path).unwrap();
}
}
odb.invalidate_packs();
assert!(
!odb.exists(&oid),
"exists must not read stale legacy pack listing after invalidate_packs"
);
let oid2 = odb.write(ObjectKind::Blob, payload).unwrap();
assert_eq!(oid, oid2);
assert!(
odb.object_path(&oid).is_file(),
"write must materialize loose object when pack copy is gone"
);
odb.read(&oid).expect("read after write");
}
#[test]
fn write_uses_single_exists_probe_when_not_loose() {
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("f"), b"probe-once").unwrap();
git_in(dir.path(), &["add", "f"]);
git_in(dir.path(), &["commit", "-m", "c"]);
git_in(dir.path(), &["repack", "-ad"]);
let objects = dir.path().join(".git").join("objects");
let odb = Odb::new(&objects);
let oid = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
assert!(!odb.object_path(&oid).exists());
odb.reset_exists_probe();
let oid2 = odb.write(ObjectKind::Blob, b"probe-once").unwrap();
assert_eq!(oid, oid2);
assert_eq!(
odb.exists_probe_count(),
1,
"pack/alternate write path must call exists() exactly once"
);
}
#[test]
fn write_existing_object_freshens() {
use filetime::FileTime;
use std::fs;
use std::thread;
use std::time::Duration;
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
let path = odb.object_path(&oid);
thread::sleep(Duration::from_millis(50));
let before = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
filetime::set_file_mtime(&path, before).unwrap();
let _ = odb.write(ObjectKind::Blob, b"hello").unwrap();
let after = FileTime::from_last_modification_time(&fs::metadata(&path).unwrap());
assert!(
after > before,
"rewriting an existing loose object must freshen its mtime"
);
}
#[test]
fn freshen_pack_once_per_odb() {
use filetime::FileTime;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::fs;
use std::io::Write;
use std::thread;
use std::time::Duration;
fn append_pack_object(buf: &mut Vec<u8>, type_code: u8, data: &[u8]) {
let mut size = data.len();
let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
size >>= 4;
if size > 0 {
buf.push(first | 0x80);
while size > 0 {
let b = (size & 0x7f) as u8;
size >>= 7;
buf.push(if size > 0 { b | 0x80 } else { b });
}
} else {
buf.push(first);
}
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(data).unwrap();
buf.extend_from_slice(&enc.finish().unwrap());
}
fn write_v2_idx(
idx_path: &Path,
pack_path: &Path,
entries: &[(ObjectId, u64)],
) -> Result<()> {
let mut sorted = entries.to_vec();
sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
let n = sorted.len();
let mut fanout = [0u32; 256];
for byte in 0u32..256 {
let count = sorted
.iter()
.filter(|(oid, _)| u32::from(oid.as_bytes()[0]) <= byte)
.count();
fanout[byte as usize] = u32::try_from(count).unwrap_or(u32::MAX);
}
let mut buf = Vec::new();
buf.extend_from_slice(b"\xfftOc");
buf.extend_from_slice(&2u32.to_be_bytes());
for f in fanout {
buf.extend_from_slice(&f.to_be_bytes());
}
for (oid, _) in &sorted {
buf.extend_from_slice(oid.as_bytes());
}
for _ in 0..n {
buf.extend_from_slice(&0u32.to_be_bytes());
}
for (_, off) in &sorted {
buf.extend_from_slice(&u32::try_from(*off).unwrap_or(0x8000_0000).to_be_bytes());
}
let pack_bytes = fs::read(pack_path)?;
buf.extend_from_slice(&pack_bytes[pack_bytes.len() - 20..]);
buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
fs::write(idx_path, buf)?;
Ok(())
}
let dir = TempDir::new().unwrap();
let objects_dir = dir.path();
let odb = Odb::new(objects_dir);
let oid_a = odb.write(ObjectKind::Blob, b"alpha").unwrap();
let oid_b = odb.write(ObjectKind::Blob, b"beta").unwrap();
fs::remove_file(odb.object_path(&oid_a)).unwrap();
fs::remove_file(odb.object_path(&oid_b)).unwrap();
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&2u32.to_be_bytes());
let off_a = pack.len() as u64;
append_pack_object(&mut pack, 3, b"alpha");
let off_b = pack.len() as u64;
append_pack_object(&mut pack, 3, b"beta");
pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
let pack_dir = objects_dir.join("pack");
fs::create_dir_all(&pack_dir).unwrap();
let pack_path = pack_dir.join("test-177.pack");
let idx_path = pack_dir.join("test-177.idx");
fs::write(&pack_path, &pack).unwrap();
write_v2_idx(&idx_path, &pack_path, &[(oid_a, off_a), (oid_b, off_b)]).unwrap();
odb.invalidate_packs();
thread::sleep(Duration::from_millis(50));
let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
assert!(odb.freshen_object(&oid_a));
let after_first = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
assert!(after_first > before);
assert!(odb.freshen_object(&oid_b));
let after_second =
FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
assert_eq!(
after_second, after_first,
"second freshen of another object in the same pack must not touch the pack again"
);
}
#[test]
fn write_existing_packed_object_freshens_pack() {
use filetime::FileTime;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::fs;
use std::io::Write;
use std::thread;
use std::time::Duration;
let dir = TempDir::new().unwrap();
let objects_dir = dir.path();
let odb = Odb::new(objects_dir);
let oid = odb.write(ObjectKind::Blob, b"packed-only").unwrap();
fs::remove_file(odb.object_path(&oid)).unwrap();
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&1u32.to_be_bytes());
let off = pack.len() as u64;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(b"packed-only").unwrap();
let compressed = enc.finish().unwrap();
pack.push(0x30 | 0x03); pack.extend_from_slice(&compressed);
pack.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&pack));
let pack_dir = objects_dir.join("pack");
fs::create_dir_all(&pack_dir).unwrap();
let pack_path = pack_dir.join("single-177.pack");
fs::write(&pack_path, &pack).unwrap();
let idx_path = pack_dir.join("single-177.idx");
let mut sorted = [(oid, off)];
sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
let mut fanout = [0u32; 256];
for byte in 0u32..256 {
fanout[byte as usize] = if u32::from(oid.as_bytes()[0]) <= byte {
1
} else {
0
};
}
let mut buf = Vec::new();
buf.extend_from_slice(b"\xfftOc");
buf.extend_from_slice(&2u32.to_be_bytes());
for f in fanout {
buf.extend_from_slice(&f.to_be_bytes());
}
buf.extend_from_slice(oid.as_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&(off as u32).to_be_bytes());
buf.extend_from_slice(&pack[pack.len() - 20..]);
buf.extend_from_slice(&HashAlgo::Sha1.checksum_digest(&buf));
fs::write(&idx_path, buf).unwrap();
odb.invalidate_packs();
thread::sleep(Duration::from_millis(50));
let before = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
odb.write(ObjectKind::Blob, b"packed-only").unwrap();
let after = FileTime::from_last_modification_time(&fs::metadata(&pack_path).unwrap());
assert!(
after > before,
"write of an existing packed object must freshen the pack mtime"
);
}
#[test]
fn write_materializes_loose_when_pack_freshen_fails() {
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
let payload = b"packed-fallback";
fs::write(dir.path().join("f"), payload).unwrap();
git_in(dir.path(), &["add", "f"]);
git_in(dir.path(), &["commit", "-m", "c"]);
git_in(dir.path(), &["repack", "-ad"]);
let objects = dir.path().join(".git").join("objects");
let odb = Odb::new(&objects);
let oid = odb.write(ObjectKind::Blob, payload).unwrap();
assert!(!odb.object_path(&oid).exists());
assert!(odb.exists(&oid));
let pack_path = fs::read_dir(objects.join("pack"))
.unwrap()
.flatten()
.find(|e| e.path().extension().is_some_and(|x| x == "pack"))
.unwrap()
.path();
fs::remove_file(&pack_path).unwrap();
fs::create_dir(&pack_path).unwrap();
odb.invalidate_packs();
assert!(!odb.freshen_object(&oid));
odb.write(ObjectKind::Blob, payload).unwrap();
assert!(odb.object_path(&oid).exists());
}
#[test]
fn write_with_options_silent_skips_freshen_on_existing_loose_object() {
let dir = TempDir::new().unwrap();
let odb = Odb::new(dir.path());
let oid = odb.write(ObjectKind::Blob, b"stable").unwrap();
let path = odb.object_path(&oid);
let before = std::fs::metadata(&path).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
let again = odb
.write_with_options(
ObjectKind::Blob,
b"stable",
WriteOptions {
silent: true,
..WriteOptions::default()
},
)
.unwrap();
assert_eq!(oid, again);
let after = std::fs::metadata(&path).unwrap().modified().unwrap();
assert_eq!(before, after);
let touched = odb
.write_with_options(
ObjectKind::Blob,
b"stable",
WriteOptions {
silent: false,
..WriteOptions::default()
},
)
.unwrap();
assert_eq!(oid, touched);
let freshened = std::fs::metadata(&path).unwrap().modified().unwrap();
assert!(freshened >= before);
}
#[test]
fn promisor_marker_checked_once_per_prepare() {
use crate::pack::{
clear_pack_cache, pack_cache_test_guard, test_pack_marker_stat_count,
test_reset_pack_marker_stat_count,
};
let _guard = pack_cache_test_guard();
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
for i in 0..64 {
fs::write(dir.path().join(format!("f{i}.txt")), format!("body {i}")).unwrap();
git_in(dir.path(), &["add", "."]);
git_in(dir.path(), &["commit", "-m", &format!("c{i}")]);
}
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let oid = git_rev_parse_oid(dir.path(), "HEAD");
clear_pack_cache();
test_reset_pack_marker_stat_count();
let pack_count = fs::read_dir(objects.join("pack"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
.count();
let expected_prepare_stats = u64::try_from(pack_count * 2).unwrap();
let odb = Odb::new(&objects);
let before_prepare = test_pack_marker_stat_count();
assert!(odb.exists_local(&oid));
assert_eq!(
test_pack_marker_stat_count().saturating_sub(before_prepare),
expected_prepare_stats,
"prepare should stat promisor and mtimes once per pack"
);
test_reset_pack_marker_stat_count();
for _ in 0..10_000 {
let before_probe = test_pack_marker_stat_count();
assert!(odb.exists_local(&oid));
assert_eq!(
test_pack_marker_stat_count(),
before_probe,
"cached sidecar flags must not stat markers on each exists_local probe"
);
}
}
#[test]
fn promisor_marker_added_after_prepare_is_seen_after_reprepare() {
use crate::pack::{
clear_pack_cache, pack_cache_test_guard, reprepare_pack_directory_on_miss,
};
let _guard = pack_cache_test_guard();
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("one.txt"), b"x").unwrap();
git_in(dir.path(), &["add", "one.txt"]);
git_in(dir.path(), &["commit", "-m", "one"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let oid = git_rev_parse_oid(dir.path(), "HEAD");
clear_pack_cache();
let odb = Odb::new(&objects);
assert!(odb.exists_local(&oid));
let pack_path = fs::read_dir(objects.join("pack"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().is_some_and(|x| x == "pack"))
.unwrap();
fs::write(pack_path.with_extension("promisor"), b"").unwrap();
filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now()).unwrap();
assert!(
odb.with_pack_store_for(&objects, || {
reprepare_pack_directory_on_miss(&objects).unwrap()
}),
"pack directory change must require reprepare"
);
assert!(
!odb.exists_local(&oid),
"promisor marker must hide pack objects after reprepare refreshed sidecar flags"
);
}
#[test]
fn git_promisor_marker_pack_not_materialized_for_exists_local() {
use crate::pack::clear_pack_cache;
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("blob.txt"), b"git promisor compat").unwrap();
git_in(dir.path(), &["add", "blob.txt"]);
git_in(dir.path(), &["commit", "-m", "seed"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let oid = git_rev_parse_oid(dir.path(), "HEAD");
let pack_path = fs::read_dir(objects.join("pack"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| p.extension().is_some_and(|x| x == "pack"))
.unwrap();
fs::write(pack_path.with_extension("promisor"), b"").unwrap();
clear_pack_cache();
let odb = Odb::new(&objects);
assert!(
!odb.exists_local(&oid),
"git-written pack with .promisor must not count as local materialization"
);
let cat = git_run(dir.path(), &["cat-file", "-e", &oid.to_hex()]);
assert!(
cat.status.success(),
"git still resolves the object in a promisor-marked pack"
);
}
#[test]
fn packed_object_read_avoids_loose_path_open() {
use crate::hot_path_test_metrics::HotPathMetricsScope;
use crate::pack::clear_pack_cache;
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("packed.txt"), b"packed-only payload").unwrap();
git_in(dir.path(), &["add", "packed.txt"]);
git_in(dir.path(), &["commit", "-m", "pack"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let oid = git_rev_parse_oid(dir.path(), "HEAD");
clear_pack_cache();
let odb = Odb::new(&objects);
odb.hot_path_metrics().set_loose_open_counting(true);
odb.hot_path_metrics().reset_loose_path_open_attempts();
let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
let obj = odb.read(&oid).expect("packed read");
assert_eq!(obj.kind, ObjectKind::Commit);
assert_eq!(
odb.hot_path_metrics().loose_path_open_attempts(),
0,
"packed objects must not attempt a loose-path open"
);
}
#[test]
fn loose_only_read_records_loose_path_open() {
use crate::hot_path_test_metrics::HotPathMetricsScope;
let dir = TempDir::new().unwrap();
let objects = dir.path().join("objects");
fs::create_dir_all(&objects).unwrap();
let odb = Odb::new(&objects);
let oid = odb
.write(ObjectKind::Blob, b"loose-only")
.expect("write loose");
odb.hot_path_metrics().set_loose_open_counting(true);
odb.hot_path_metrics().reset_loose_path_open_attempts();
let _scope = HotPathMetricsScope::install(odb.hot_path_metrics_arc());
odb.read(&oid).expect("loose read");
assert_eq!(
odb.hot_path_metrics().loose_path_open_attempts(),
1,
"loose fallback must record a path open attempt"
);
}
#[test]
fn loose_and_packed_copies_return_identical_bytes() {
use crate::pack::clear_pack_cache;
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("dup.txt"), b"duplicate loose and packed\n").unwrap();
git_in(dir.path(), &["add", "dup.txt"]);
git_in(dir.path(), &["commit", "-m", "seed"]);
let objects = dir.path().join(".git/objects");
let tree_oid = git_rev_parse_oid(dir.path(), "HEAD^{tree}");
git_in(dir.path(), &["repack", "-a"]);
clear_pack_cache();
let odb = Odb::new(&objects);
let loose_path = tree_oid.loose_path_in(&objects);
assert!(
loose_path.is_file(),
"loose copy must remain after repack -a"
);
let loose = Odb::read_loose_verify_oid(&loose_path, &tree_oid).expect("loose tree");
let packed = odb.read(&tree_oid).expect("packed tree");
assert_eq!(packed.data, loose.data);
assert_eq!(packed.kind, loose.kind);
}
#[test]
fn freshly_written_loose_object_visible_before_repack() {
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("seed.txt"), b"seed").unwrap();
git_in(dir.path(), &["add", "seed.txt"]);
git_in(dir.path(), &["commit", "-m", "seed"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let odb = Odb::new(&objects);
let fresh = b"brand-new loose blob\n";
let oid = odb.write(ObjectKind::Blob, fresh).expect("loose write");
let obj = odb.read(&oid).expect("immediate loose read");
assert_eq!(obj.data.as_slice(), fresh);
}
#[test]
fn pack_added_after_first_read_found_via_odb_reprepare() {
use crate::pack::{clear_pack_cache, pack_cache_test_guard};
let _guard = pack_cache_test_guard();
clear_pack_cache();
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
git_in(dir.path(), &["add", "a.txt"]);
git_in(dir.path(), &["commit", "-m", "a"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let pack_dir = objects.join("pack");
let git_dir = dir.path().join(".git");
let odb = Odb::new(&objects).with_config_git_dir(git_dir);
let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
odb.read(&warm_oid).expect("warm read");
fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
git_in(dir.path(), &["add", "b.txt"]);
git_in(dir.path(), &["commit", "-m", "b"]);
let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
std::thread::sleep(std::time::Duration::from_secs(1));
filetime::set_file_mtime(&pack_dir, filetime::FileTime::now()).unwrap();
let obj = odb.read(&new_oid).expect("new pack via reprepare");
assert_eq!(obj.kind, ObjectKind::Commit);
}
#[test]
fn late_midx_after_warm_read_resolves_new_commit() {
use crate::pack::{clear_pack_cache, pack_cache_test_guard};
let _guard = pack_cache_test_guard();
clear_pack_cache();
let dir = TempDir::new().unwrap();
git_in(dir.path(), &["init", "-q", "-b", "main"]);
git_in(dir.path(), &["config", "user.email", "t@example.com"]);
git_in(dir.path(), &["config", "user.name", "T"]);
fs::write(dir.path().join("a.txt"), b"pack-a").unwrap();
git_in(dir.path(), &["add", "a.txt"]);
git_in(dir.path(), &["commit", "-m", "a"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
let objects = dir.path().join(".git/objects");
let git_dir = dir.path().join(".git");
assert!(
crate::midx::cached_tip_midx_path(&objects.join("pack")).is_none(),
"fixture starts without a MIDX"
);
let odb = Odb::new(&objects).with_config_git_dir(git_dir);
let warm_oid = git_rev_parse_oid(dir.path(), "HEAD");
odb.read(&warm_oid).expect("warm read before MIDX exists");
fs::write(dir.path().join("b.txt"), b"pack-b").unwrap();
git_in(dir.path(), &["add", "b.txt"]);
git_in(dir.path(), &["commit", "-m", "b"]);
git_in(dir.path(), &["repack", "-a", "-d"]);
git_in(dir.path(), &["multi-pack-index", "write"]);
let new_oid = git_rev_parse_oid(dir.path(), "HEAD");
let cat = git_run(dir.path(), &["cat-file", "-e", &new_oid.to_hex()]);
assert!(
cat.status.success(),
"system git must resolve the new commit"
);
let obj = odb
.read(&new_oid)
.expect("MIDX-covered commit after late MIDX write");
assert_eq!(obj.kind, ObjectKind::Commit);
assert!(
crate::midx::cached_tip_midx_path(&objects.join("pack")).is_some(),
"successful read after late MIDX must refresh the tip cache"
);
}
}