weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::{
    collections::{HashMap, HashSet, VecDeque},
    fs,
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
};

use crate::{
    GitError, HashKind, Object, ObjectId, Repository, Result, cache::ByteCache, loose,
    midx::MultiPackIndex, pack::PackIndex,
};

struct MidxRoute {
    index: MultiPackIndex,
    packs: HashMap<String, usize>,
}

pub(crate) struct ObjectStore {
    directories: Vec<PathBuf>,
    packs: Vec<PackIndex>,
    midx: Vec<MidxRoute>,
    object_cache: Mutex<ByteCache<ObjectId, Arc<Object>>>,
}

impl ObjectStore {
    pub(crate) fn open(
        primary: PathBuf,
        hash: HashKind,
        object_cache_bytes: usize,
        delta_cache_bytes: usize,
    ) -> Result<Self> {
        let directories = object_directories(primary)?;
        let mut packs = Vec::new();
        let mut midx = Vec::new();
        for directory in &directories {
            let pack_directory = directory.join("pack");
            let entries = match fs::read_dir(&pack_directory) {
                Ok(entries) => entries,
                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
                Err(error) => return Err(error.into()),
            };
            let mut paths = entries
                .filter_map(std::result::Result::ok)
                .map(|entry| entry.path())
                .filter(|path| path.extension().is_some_and(|value| value == "idx"))
                .collect::<Vec<_>>();
            paths.sort_unstable();
            let mut route = HashMap::new();
            for path in paths {
                let name = path
                    .file_name()
                    .and_then(|value| value.to_str())
                    .ok_or_else(|| crate::error::invalid("pack index name is not UTF-8"))?
                    .to_owned();
                let slot = packs.len();
                packs.push(PackIndex::open(&path, hash, delta_cache_bytes)?);
                route.insert(name, slot);
            }
            if let Some(index) = MultiPackIndex::open(&pack_directory, hash)? {
                midx.push(MidxRoute {
                    index,
                    packs: route,
                });
            }
        }
        Ok(Self {
            directories,
            packs,
            midx,
            object_cache: Mutex::new(ByteCache::new(object_cache_bytes)),
        })
    }

    pub(crate) fn read_shared(
        &self,
        id: ObjectId,
        max_object_bytes: usize,
        max_delta_depth: usize,
    ) -> Result<Arc<Object>> {
        if let Some(object) = self
            .object_cache
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .get(id)
        {
            return Ok(object);
        }
        let object = self.read_inner(id, max_object_bytes, max_delta_depth, 0)?;
        let bytes = object.data.len();
        let object = Arc::new(object);
        self.object_cache
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert(id, Arc::clone(&object), bytes);
        Ok(object)
    }

    pub(crate) fn pack_count(&self) -> usize {
        self.packs.len()
    }

    pub(crate) fn multi_pack_index_count(&self) -> usize {
        self.midx.len()
    }

    pub(crate) fn bitmap_reachable(
        &self,
        id: ObjectId,
        max_objects: usize,
    ) -> Result<Option<Vec<ObjectId>>> {
        for route in &self.midx {
            if let Some(objects) = route.index.bitmap_reachable(id, max_objects)? {
                return Ok(Some(objects));
            }
        }
        for pack in &self.packs {
            if let Some(objects) = pack.bitmap_reachable(id, max_objects)? {
                return Ok(Some(objects));
            }
        }
        Ok(None)
    }

    pub(crate) fn contains(&self, id: ObjectId) -> bool {
        let hex = id.to_hex();
        self.directories
            .iter()
            .any(|directory| directory.join(&hex[..2]).join(&hex[2..]).is_file())
            || self
                .midx
                .iter()
                .any(|route| route.index.find(id).is_ok_and(|value| value.is_some()))
            || self.packs.iter().any(|pack| pack.offset(id).is_some())
    }

    fn read_inner(
        &self,
        id: ObjectId,
        max_object_bytes: usize,
        max_delta_depth: usize,
        depth: usize,
    ) -> Result<Object> {
        if depth > max_delta_depth {
            return Err(GitError::LimitExceeded {
                resource: "pack delta depth",
                limit: max_delta_depth,
            });
        }
        for directory in &self.directories {
            if let Some(object) = loose::read(directory, id, max_object_bytes)? {
                return Ok(object);
            }
        }
        for route in &self.midx {
            let Some(location) = route.index.find(id)? else {
                continue;
            };
            let pack = route
                .packs
                .get(location.pack)
                .and_then(|slot| self.packs.get(*slot))
                .ok_or_else(|| crate::error::invalid("MIDX references a missing pack index"))?;
            let external = |base, next_depth| {
                self.read_inner(base, max_object_bytes, max_delta_depth, next_depth)
            };
            return pack.read_at(
                id,
                location.offset,
                max_object_bytes,
                max_delta_depth,
                &external,
                depth,
            );
        }
        for pack in &self.packs {
            let external = |base, next_depth| {
                self.read_inner(base, max_object_bytes, max_delta_depth, next_depth)
            };
            if let Some(object) =
                pack.read(id, max_object_bytes, max_delta_depth, &external, depth)?
            {
                return Ok(object);
            }
        }
        Err(GitError::NotFound(id.to_string()))
    }
}

impl Repository {
    pub fn object_shared(&self, id: ObjectId) -> Result<Arc<Object>> {
        if id.kind() != self.hash_kind() {
            return Err(crate::error::invalid(
                "object id hash kind differs from repository",
            ));
        }
        if let Some(object) =
            crate::backend::read(&self.backends, id, self.limits.max_object_bytes)?
        {
            return Ok(Arc::new(object));
        }
        self.store.read_shared(
            id,
            self.limits.max_object_bytes,
            self.limits.max_delta_depth,
        )
    }
}

fn object_directories(primary: PathBuf) -> Result<Vec<PathBuf>> {
    let mut queue = VecDeque::from([primary]);
    let mut seen = HashSet::new();
    let mut result = Vec::new();
    while let Some(directory) = queue.pop_front() {
        let identity = directory
            .canonicalize()
            .unwrap_or_else(|_| directory.clone());
        if !seen.insert(identity) {
            continue;
        }
        if result.len() >= 32 {
            return Err(GitError::LimitExceeded {
                resource: "object alternates",
                limit: 32,
            });
        }
        let alternates = directory.join("info").join("alternates");
        match fs::read_to_string(alternates) {
            Ok(value) => {
                for line in value.lines().filter(|line| !line.trim().is_empty()) {
                    let path = Path::new(line.trim());
                    queue.push_back(if path.is_absolute() {
                        path.to_owned()
                    } else {
                        directory.join(path)
                    });
                }
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => return Err(error.into()),
        }
        result.push(directory);
    }
    Ok(result)
}