weavatrix-git 0.3.1

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

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Reference {
    pub name: String,
    pub target: ObjectId,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Head {
    pub symbolic: Option<String>,
    pub target: Option<ObjectId>,
}

pub(crate) fn validate_name(name: &str) -> Result<()> {
    let invalid_part = name.is_empty()
        || name.starts_with('/')
        || name.ends_with('/')
        || name.contains("..")
        || name.contains("@{")
        || name.contains('\\')
        || name
            .bytes()
            .any(|byte| byte <= b' ' || byte == 0x7f || b"~^:?*[".contains(&byte))
        || name.split('/').any(|part| {
            part.is_empty() || part.starts_with('.') || part.to_ascii_lowercase().ends_with(".lock")
        });
    if invalid_part {
        return Err(invalid(format!("invalid reference name {name:?}")));
    }
    Ok(())
}

pub(crate) fn read_text(path: &Path) -> Result<Option<String>> {
    match fs::read_to_string(path) {
        Ok(value) => Ok(Some(value.trim().to_owned())),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(error.into()),
    }
}

pub(crate) fn parse_target(value: &str, hash: HashKind) -> Result<RefTarget> {
    if let Some(name) = value.strip_prefix("ref: ") {
        validate_name(name)?;
        return Ok(RefTarget::Symbolic(name.to_owned()));
    }
    Ok(RefTarget::Direct(ObjectId::from_hex_for(value, hash)?))
}

pub(crate) fn packed_target(path: &Path, name: &str, hash: HashKind) -> Result<Option<ObjectId>> {
    let Some(contents) = read_text(path)? else {
        return Ok(None);
    };
    for line in contents.lines() {
        if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
            continue;
        }
        let Some((hex, candidate)) = line.split_once(' ') else {
            return Err(invalid("malformed packed-refs entry"));
        };
        if candidate == name {
            return ObjectId::from_hex_for(hex, hash).map(Some);
        }
    }
    Ok(None)
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum RefTarget {
    Symbolic(String),
    Direct(ObjectId),
}

pub(crate) fn missing_ref(name: &str) -> GitError {
    GitError::NotFound(format!("reference {name}"))
}

#[cfg(test)]
mod tests {
    use super::validate_name;

    #[test]
    fn validates_reference_names() {
        assert!(validate_name("refs/heads/main").is_ok());
        for invalid in ["", "../HEAD", "refs//main", "refs/a.lock", "refs/a b"] {
            assert!(validate_name(invalid).is_err(), "{invalid}");
        }
    }
}