weavatrix-git 0.2.0

Dependency-free, evidence-carrying Git repository reader
Documentation
mod graph_history;

use std::{
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
};

use crate::{
    Commit, CommitMetadata, GitError, HashKind, Head, HistoryOptions, HistoryRecord, Object,
    ObjectId, ObjectKind, Reference, Result, Tag, Tree, TreeChange,
    commit_graph::CommitGraph,
    diff,
    error::invalid,
    history, index, layout,
    refs::{self, RefTarget},
    store::ObjectStore,
};

#[derive(Clone, Copy, Debug)]
pub struct Limits {
    pub max_object_bytes: usize,
    pub max_delta_depth: usize,
    pub max_ref_depth: usize,
    pub max_tree_depth: usize,
    pub max_tree_entries: usize,
    pub max_history_commits: usize,
    pub max_parents: usize,
    pub object_cache_bytes: usize,
    pub delta_cache_bytes: usize,
    pub max_bitmap_objects: usize,
    pub max_reflog_entries: usize,
    pub max_index_entries: usize,
}

impl Default for Limits {
    fn default() -> Self {
        Self {
            max_object_bytes: 512 * 1024 * 1024,
            max_delta_depth: 64,
            max_ref_depth: 16,
            max_tree_depth: 256,
            max_tree_entries: 5_000_000,
            max_history_commits: 1_000_000,
            max_parents: 256,
            object_cache_bytes: 32 * 1024 * 1024,
            delta_cache_bytes: 16 * 1024 * 1024,
            max_bitmap_objects: 10_000_000,
            max_reflog_entries: 1_000_000,
            max_index_entries: 10_000_000,
        }
    }
}

pub struct Repository {
    work_dir: Option<PathBuf>,
    git_dir: PathBuf,
    common_dir: PathBuf,
    hash: HashKind,
    pub(crate) limits: Limits,
    pub(crate) graph: Option<CommitGraph>,
    pub(crate) store: ObjectStore,
    pub(crate) backends: Vec<Arc<dyn crate::ObjectBackend>>,
    pub(crate) index_cache: Mutex<Option<index::CachedIndex>>,
}
impl Repository {
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        Self::open_with_limits(path, Limits::default())
    }

    pub fn open_with_limits(path: impl AsRef<Path>, limits: Limits) -> Result<Self> {
        Self::open_with_backends(path, limits, Vec::new())
    }

    pub fn open_with_backends(
        path: impl AsRef<Path>,
        limits: Limits,
        backends: Vec<Arc<dyn crate::ObjectBackend>>,
    ) -> Result<Self> {
        let (work_dir, git_dir) = layout::discover(path.as_ref())?;
        let common_dir = layout::common_dir(&git_dir)?;
        let hash = layout::hash_kind(&common_dir)?;
        let graph = CommitGraph::open(&common_dir, hash)?;
        let store = ObjectStore::open(
            common_dir.join("objects"),
            hash,
            limits.object_cache_bytes,
            limits.delta_cache_bytes,
        )?;
        Ok(Self {
            work_dir,
            git_dir,
            common_dir,
            hash,
            limits,
            graph,
            store,
            backends,
            index_cache: Mutex::new(None),
        })
    }

    #[must_use]
    pub fn work_dir(&self) -> Option<&Path> {
        self.work_dir.as_deref()
    }

    #[must_use]
    pub fn git_dir(&self) -> &Path {
        &self.git_dir
    }

    #[must_use]
    pub fn common_dir(&self) -> &Path {
        &self.common_dir
    }

    #[must_use]
    pub const fn hash_kind(&self) -> HashKind {
        self.hash
    }

    #[must_use]
    pub const fn limits(&self) -> &Limits {
        &self.limits
    }

    #[must_use]
    pub fn pack_count(&self) -> usize {
        self.store.pack_count()
    }

    pub fn head(&self) -> Result<Head> {
        let value = refs::read_text(&self.git_dir.join("HEAD"))?
            .ok_or_else(|| GitError::NotRepository("HEAD is missing".to_owned()))?;
        match refs::parse_target(&value, self.hash)? {
            RefTarget::Direct(id) => Ok(Head {
                symbolic: None,
                target: Some(id),
            }),
            RefTarget::Symbolic(name) => Ok(Head {
                target: self.resolve_ref_optional(&name, 0)?,
                symbolic: Some(name),
            }),
        }
    }

    pub fn reference(&self, name: &str) -> Result<Reference> {
        refs::validate_name(name)?;
        Ok(Reference {
            name: name.to_owned(),
            target: self
                .resolve_ref_optional(name, 0)?
                .ok_or_else(|| refs::missing_ref(name))?,
        })
    }

    pub fn resolve(&self, value: &str) -> Result<ObjectId> {
        if value.len() == self.hash.hex_len() && value.bytes().all(|byte| byte.is_ascii_hexdigit())
        {
            return ObjectId::from_hex_for(value, self.hash);
        }
        if value == "HEAD" {
            return self
                .head()?
                .target
                .ok_or_else(|| GitError::NotFound("unborn HEAD".to_owned()));
        }
        for candidate in [
            value.to_owned(),
            format!("refs/heads/{value}"),
            format!("refs/tags/{value}"),
        ] {
            if let Some(id) = self.resolve_ref_optional(&candidate, 0)? {
                return Ok(id);
            }
        }
        Err(refs::missing_ref(value))
    }

    pub fn object(&self, id: ObjectId) -> Result<Object> {
        Ok((*self.object_shared(id)?).clone())
    }

    #[must_use]
    pub fn contains(&self, id: ObjectId) -> bool {
        self.contains_checked(id).unwrap_or(false)
    }

    pub fn contains_checked(&self, id: ObjectId) -> Result<bool> {
        if id.kind() != self.hash {
            return Ok(false);
        }
        Ok(crate::backend::contains(&self.backends, id)? || self.store.contains(id))
    }

    pub fn commit(&self, id: ObjectId) -> Result<Commit> {
        let object = self.object(id)?;
        expect_kind(object.kind, ObjectKind::Commit)?;
        Commit::parse(id, &object.data, self.limits.max_parents)
    }

    pub fn commit_metadata(&self, id: ObjectId) -> Result<CommitMetadata> {
        if let Some(graph) = self
            .graph
            .as_ref()
            .map_or(Ok(None), |graph| graph.find(id))?
        {
            return Ok(CommitMetadata {
                id: graph.id,
                tree: graph.tree,
                parents: graph.parents,
                committer_time: graph.time,
            });
        }
        let commit = self.commit(id)?;
        let committer_time = commit
            .committer
            .as_ref()
            .or(commit.author.as_ref())
            .map_or(0, |signature| signature.timestamp);
        Ok(CommitMetadata {
            id,
            tree: commit.tree,
            parents: commit.parents,
            committer_time,
        })
    }

    pub fn tree(&self, id: ObjectId) -> Result<Tree> {
        let object = self.object(id)?;
        expect_kind(object.kind, ObjectKind::Tree)?;
        Tree::parse(&object.data, self.hash, self.limits.max_tree_entries)
    }

    pub fn tag(&self, id: ObjectId) -> Result<Tag> {
        let object = self.object(id)?;
        expect_kind(object.kind, ObjectKind::Tag)?;
        Tag::parse(id, &object.data)
    }

    pub fn history(&self, start: ObjectId, options: HistoryOptions) -> Result<Vec<HistoryRecord>> {
        history::walk(self, start, options)
    }
    pub fn history_ids(&self, start: ObjectId, options: HistoryOptions) -> Result<Vec<ObjectId>> {
        history::walk_ids(self, start, options)
    }

    pub fn diff_trees(&self, old: ObjectId, new: ObjectId) -> Result<Vec<TreeChange>> {
        diff::between(self, old, new)
    }

    pub fn diff_commits(&self, old: ObjectId, new: ObjectId) -> Result<Vec<TreeChange>> {
        self.diff_trees(self.commit(old)?.tree, self.commit(new)?.tree)
    }

    fn resolve_ref_optional(&self, name: &str, depth: usize) -> Result<Option<ObjectId>> {
        refs::validate_name(name)?;
        if depth >= self.limits.max_ref_depth {
            return Err(GitError::LimitExceeded {
                resource: "symbolic ref depth",
                limit: self.limits.max_ref_depth,
            });
        }
        for root in [&self.git_dir, &self.common_dir] {
            if let Some(value) = refs::read_text(&root.join(name))? {
                return match refs::parse_target(&value, self.hash)? {
                    RefTarget::Direct(id) => Ok(Some(id)),
                    RefTarget::Symbolic(next) => self.resolve_ref_optional(&next, depth + 1),
                };
            }
        }
        for root in [&self.git_dir, &self.common_dir] {
            if let Some(id) = refs::packed_target(&root.join("packed-refs"), name, self.hash)? {
                return Ok(Some(id));
            }
        }
        Ok(None)
    }
}

fn expect_kind(actual: ObjectKind, expected: ObjectKind) -> Result<()> {
    if actual != expected {
        return Err(invalid(format!(
            "expected {} object, found {}",
            expected.as_str(),
            actual.as_str()
        )));
    }
    Ok(())
}