use alloc::format;
use alloc::vec::Vec;
use crate::error::{Error, ObjectKindHint, Result};
use crate::oid::{HashAlgo, ObjectId};
use crate::pack::{Pack, PackIndex};
use crate::vfs::Vfs;
use super::{LooseOdb, ObjectDatabase};
struct LoadedPack {
index: PackIndex,
pack: Pack,
}
pub struct CombinedOdb<V: Vfs + Clone> {
loose: LooseOdb<V>,
packs: Vec<LoadedPack>,
alternates: Vec<CombinedOdb<V>>,
algo: HashAlgo,
max_delta_depth: usize,
}
impl<V: Vfs + Clone> CombinedOdb<V> {
pub fn open(vfs: V, algo: HashAlgo) -> Result<Self> {
let alternates = Self::load_alternates(&vfs, algo)?;
Self::open_inner(vfs, algo, alternates)
}
fn open_inner(vfs: V, algo: HashAlgo, alternates: Vec<CombinedOdb<V>>) -> Result<Self> {
let mut packs = Vec::new();
if vfs.exists("pack") {
for entry in vfs.read_dir("pack")? {
if !entry.name.ends_with(".idx") {
continue;
}
let stem = &entry.name[..entry.name.len() - 4];
let idx_path = format!("pack/{}", entry.name);
let pack_path = format!("pack/{stem}.pack");
if !vfs.exists(&pack_path) {
continue; }
let index = PackIndex::parse(algo, &vfs.read(&idx_path)?)?;
let pack = Pack::new(vfs.read(&pack_path)?, algo)?;
packs.push(LoadedPack { index, pack });
}
}
Ok(CombinedOdb {
loose: LooseOdb::new(vfs, algo),
packs,
alternates,
algo,
max_delta_depth: 50,
})
}
fn load_alternates(vfs: &V, algo: HashAlgo) -> Result<Vec<CombinedOdb<V>>> {
let mut out = Vec::new();
let Ok(bytes) = vfs.read("info/alternates") else {
return Ok(out);
};
let text = core::str::from_utf8(&bytes).unwrap_or("");
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Ok(sub) = vfs.open_relative(line)
&& let Ok(alt) = Self::open_inner(sub, algo, Vec::new())
{
out.push(alt);
}
}
Ok(out)
}
pub fn pack_count(&self) -> usize {
self.packs.len()
}
fn read_depth(
&self,
id: &ObjectId,
depth: usize,
) -> Result<(crate::object::ObjectType, Vec<u8>)> {
if depth > self.max_delta_depth {
return Err(Error::Pack("delta chain too deep (possible cycle)".into()));
}
if self.loose.contains(id) {
return self.loose.read(id);
}
for lp in &self.packs {
if let Some(offset) = lp.index.offset_of(id) {
let resolver = |base: &ObjectId| self.read_depth(base, depth + 1);
return lp.pack.read_at(offset, &resolver);
}
}
for alt in &self.alternates {
if let Ok(obj) = alt.read_depth(id, depth) {
return Ok(obj);
}
}
Err(Error::NotFound(ObjectKindHint::Object(id.to_hex())))
}
}
impl<V: Vfs + Clone> ObjectDatabase for CombinedOdb<V> {
fn algo(&self) -> HashAlgo {
self.algo
}
fn contains(&self, id: &ObjectId) -> bool {
self.loose.contains(id)
|| self.packs.iter().any(|lp| lp.index.contains(id))
|| self.alternates.iter().any(|alt| alt.contains(id))
}
fn read(&self, id: &ObjectId) -> Result<(crate::object::ObjectType, Vec<u8>)> {
self.read_depth(id, 0)
}
fn write(&self, ty: crate::object::ObjectType, payload: &[u8]) -> Result<ObjectId> {
self.loose.write(ty, payload)
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::object::ObjectType;
use crate::vfs::StdFs;
#[test]
fn reads_loose_through_combined() {
let dir = std::env::temp_dir().join(format!("puregit-combined-{}", core::line!()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let loose = LooseOdb::new(StdFs::new(&dir), HashAlgo::Sha1);
let id = loose.write(ObjectType::Blob, b"hello\n").unwrap();
let odb = CombinedOdb::open(StdFs::new(&dir), HashAlgo::Sha1).unwrap();
assert_eq!(odb.pack_count(), 0);
assert!(odb.contains(&id));
let (ty, payload) = odb.read(&id).unwrap();
assert_eq!(ty, ObjectType::Blob);
assert_eq!(payload, b"hello\n");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reads_through_an_alternate_object_store() {
let base = std::env::temp_dir().join(format!("puregit-alt-{}", core::line!()));
let _ = std::fs::remove_dir_all(&base);
let main_obj = base.join("main");
let alt_obj = base.join("alt");
std::fs::create_dir_all(&main_obj).unwrap();
std::fs::create_dir_all(&alt_obj).unwrap();
let alt = LooseOdb::new(StdFs::new(&alt_obj), HashAlgo::Sha1);
let id = alt.write(ObjectType::Blob, b"in the alternate\n").unwrap();
let plain = CombinedOdb::open(StdFs::new(&main_obj), HashAlgo::Sha1).unwrap();
assert!(!plain.contains(&id));
std::fs::create_dir_all(main_obj.join("info")).unwrap();
std::fs::write(
main_obj.join("info/alternates"),
format!("{}\n", alt_obj.display()),
)
.unwrap();
let odb = CombinedOdb::open(StdFs::new(&main_obj), HashAlgo::Sha1).unwrap();
assert!(odb.contains(&id));
assert_eq!(odb.read(&id).unwrap().1, b"in the alternate\n");
let _ = std::fs::remove_dir_all(&base);
}
}