weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::{collections::BTreeMap, path::Path, thread};

use crate::{
    GitError, HistoryOptions, ObjectId, Repository, Result, TreeChange, diff, error::invalid,
};

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RepositoryId(usize);

impl RepositoryId {
    #[must_use]
    pub const fn index(self) -> usize {
        self.0
    }
}

pub struct RepositorySet {
    entries: Vec<Entry>,
    names: BTreeMap<String, RepositoryId>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositoryHistory {
    pub repository: RepositoryId,
    pub head: ObjectId,
    pub commits: Vec<ObjectId>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SharedCommit {
    pub id: ObjectId,
    pub repositories: Vec<RepositoryId>,
}

struct Entry {
    id: RepositoryId,
    name: String,
    repository: Repository,
}

impl RepositorySet {
    pub fn open<I, S, P>(repositories: I) -> Result<Self>
    where
        I: IntoIterator<Item = (S, P)>,
        S: Into<String>,
        P: AsRef<Path>,
    {
        let mut entries = Vec::new();
        let mut names = BTreeMap::new();
        for (name, path) in repositories {
            let name = name.into();
            if name.is_empty() || name.contains(':') {
                return Err(invalid("repository name is empty or contains ':'"));
            }
            let id = RepositoryId(entries.len());
            if names.insert(name.clone(), id).is_some() {
                return Err(invalid(format!("duplicate repository name {name:?}")));
            }
            entries.push(Entry {
                id,
                name,
                repository: Repository::open(path)?,
            });
        }
        Ok(Self { entries, names })
    }

    pub fn open_parallel<I, S, P>(repositories: I) -> Result<Self>
    where
        I: IntoIterator<Item = (S, P)>,
        S: Into<String>,
        P: AsRef<Path>,
    {
        let mut inputs = Vec::new();
        let mut names = BTreeMap::new();
        for (name, path) in repositories {
            let name = name.into();
            if name.is_empty() || name.contains(':') {
                return Err(invalid("repository name is empty or contains ':'"));
            }
            let id = RepositoryId(inputs.len());
            if names.insert(name.clone(), id).is_some() {
                return Err(invalid(format!("duplicate repository name {name:?}")));
            }
            inputs.push((id, name, path.as_ref().to_path_buf()));
        }
        let repositories = thread::scope(|scope| {
            inputs
                .iter()
                .map(|(_, _, path)| scope.spawn(move || Repository::open(path)))
                .collect::<Vec<_>>()
                .into_iter()
                .map(|handle| {
                    handle.join().map_err(|_| {
                        GitError::Unsupported("repository open worker panicked".to_owned())
                    })?
                })
                .collect::<Result<Vec<_>>>()
        })?;
        let entries = inputs
            .into_iter()
            .zip(repositories)
            .map(|((id, name, _), repository)| Entry {
                id,
                name,
                repository,
            })
            .collect();
        Ok(Self { entries, names })
    }

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

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    #[must_use]
    pub fn ids(&self) -> impl ExactSizeIterator<Item = RepositoryId> + '_ {
        self.entries.iter().map(|entry| entry.id)
    }

    #[must_use]
    pub fn id(&self, name: &str) -> Option<RepositoryId> {
        self.names.get(name).copied()
    }

    #[must_use]
    pub fn name(&self, id: RepositoryId) -> Option<&str> {
        self.entries.get(id.0).map(|entry| entry.name.as_str())
    }

    #[must_use]
    pub fn repository(&self, id: RepositoryId) -> Option<&Repository> {
        self.entries.get(id.0).map(|entry| &entry.repository)
    }

    pub fn resolve(&self, id: RepositoryId, revision: &str) -> Result<ObjectId> {
        self.get(id)?.resolve(revision)
    }

    #[must_use]
    pub fn find_object(&self, id: ObjectId) -> Vec<RepositoryId> {
        self.entries
            .iter()
            .filter(|entry| entry.repository.contains(id))
            .map(|entry| entry.id)
            .collect()
    }

    pub fn histories(&self, options: HistoryOptions) -> Result<Vec<RepositoryHistory>> {
        self.entries
            .iter()
            .map(|entry| history(entry, options))
            .collect()
    }

    pub fn histories_parallel(&self, options: HistoryOptions) -> Result<Vec<RepositoryHistory>> {
        thread::scope(|scope| {
            let handles = self
                .entries
                .iter()
                .map(|entry| scope.spawn(move || history(entry, options)))
                .collect::<Vec<_>>();
            handles
                .into_iter()
                .map(|handle| {
                    handle
                        .join()
                        .map_err(|_| GitError::Unsupported("history worker panicked".to_owned()))?
                })
                .collect()
        })
    }

    pub fn shared_commits(&self, options: HistoryOptions) -> Result<Vec<SharedCommit>> {
        self.shared_commits_from("HEAD", options)
    }

    pub fn diff_commits(
        &self,
        old_repository: RepositoryId,
        old_revision: &str,
        new_repository: RepositoryId,
        new_revision: &str,
    ) -> Result<Vec<TreeChange>> {
        let old_repository = self.get(old_repository)?;
        let new_repository = self.get(new_repository)?;
        let old = old_repository.commit(old_repository.resolve(old_revision)?)?;
        let new = new_repository.commit(new_repository.resolve(new_revision)?)?;
        diff::across(old_repository, old.tree, new_repository, new.tree)
    }

    pub(crate) fn get(&self, id: RepositoryId) -> Result<&Repository> {
        self.repository(id)
            .ok_or_else(|| invalid(format!("unknown repository id {}", id.0)))
    }
}

fn history(entry: &Entry, options: HistoryOptions) -> Result<RepositoryHistory> {
    let head = entry
        .repository
        .head()?
        .target
        .ok_or_else(|| GitError::NotFound(format!("unborn HEAD in {}", entry.name)))?;
    Ok(RepositoryHistory {
        repository: entry.id,
        head,
        commits: entry.repository.history_ids(head, options)?,
    })
}