weavatrix-git 0.3.1

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

use crate::{GitError, HashKind, Result, error::invalid, refs};

pub(crate) fn discover(start: &Path) -> Result<(Option<PathBuf>, PathBuf)> {
    let canonical = start.canonicalize()?;
    let mut current = if canonical.is_file() {
        canonical
            .parent()
            .ok_or_else(|| GitError::NotRepository(start.display().to_string()))?
            .to_owned()
    } else {
        canonical
    };
    loop {
        let dot_git = current.join(".git");
        if dot_git.is_dir() {
            return Ok((Some(current), dot_git));
        }
        if dot_git.is_file() {
            return Ok((Some(current.clone()), parse_git_file(&dot_git, &current)?));
        }
        if current.join("HEAD").is_file() && current.join("objects").is_dir() {
            return Ok((None, current));
        }
        if !current.pop() {
            return Err(GitError::NotRepository(start.display().to_string()));
        }
    }
}

pub(crate) fn common_dir(git_dir: &Path) -> Result<PathBuf> {
    let Some(value) = refs::read_text(&git_dir.join("commondir"))? else {
        return Ok(git_dir.to_owned());
    };
    let path = Path::new(&value);
    Ok(if path.is_absolute() {
        path.to_owned()
    } else {
        git_dir.join(path)
    })
}

pub(crate) fn hash_kind(common_dir: &Path) -> Result<HashKind> {
    let config = fs::read_to_string(common_dir.join("config"))?;
    let mut extensions = false;
    for line in config.lines().map(str::trim) {
        if line.starts_with('[') {
            extensions = line.eq_ignore_ascii_case("[extensions]");
        } else if extensions
            && let Some((key, value)) = line.split_once('=')
            && key.trim().eq_ignore_ascii_case("objectformat")
        {
            return match value.trim().to_ascii_lowercase().as_str() {
                "sha1" => Ok(HashKind::Sha1),
                "sha256" => Ok(HashKind::Sha256),
                other => Err(GitError::Unsupported(format!("object format {other}"))),
            };
        }
    }
    Ok(HashKind::Sha1)
}

fn parse_git_file(path: &Path, work_dir: &Path) -> Result<PathBuf> {
    let text = fs::read_to_string(path)?;
    let value = text
        .trim()
        .strip_prefix("gitdir: ")
        .ok_or_else(|| invalid("invalid .git file"))?;
    let path = Path::new(value);
    Ok(if path.is_absolute() {
        path.to_owned()
    } else {
        work_dir.join(path)
    })
}