weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::str;

use mcport::{Value, json};

use crate::{Commit, EntryKind, Signature, StatusKind, TreeEntry};

pub(super) fn commit_value(commit: &Commit, include_message: bool) -> Value {
    json!({
        "id": commit.id.to_string(),
        "tree": commit.tree.to_string(),
        "parents": commit.parents.iter().map(ToString::to_string).collect::<Vec<_>>(),
        "author": commit.author.as_ref().map(signature),
        "committer": commit.committer.as_ref().map(signature),
        "encoding": commit.encoding,
        "summary": commit.summary_lossy(),
        "message": include_message.then(|| bounded_message(&commit.message))
    })
}

pub(super) fn tree_entry(value: &TreeEntry) -> Value {
    json!({
        "mode": value.mode,
        "id": value.id.to_string(),
        "kind": entry_kind(value.kind)
    })
}

pub(super) fn page_result(
    revision: impl Into<String>,
    values: Vec<Value>,
    cursor: usize,
    total: usize,
    total_known: bool,
    field: &str,
) -> Value {
    let next = (cursor + values.len() < total).then_some(cursor + values.len());
    let mut result = json!({
        "revision": revision.into(),
        "cursor": cursor,
        "nextCursor": next,
        "returned": values.len(),
        "scanned": total,
        "total": total_known.then_some(total),
        "truncated": next.is_some()
    });
    result[field] = Value::Array(values);
    result
}

pub(super) fn path(bytes: &[u8]) -> (String, Option<String>) {
    match str::from_utf8(bytes) {
        Ok(value) => (value.to_owned(), None),
        Err(_) => (
            String::from_utf8_lossy(bytes).into_owned(),
            Some(hex(bytes)),
        ),
    }
}

pub(super) fn status_kind(value: StatusKind) -> &'static str {
    match value {
        StatusKind::Unmodified => "unmodified",
        StatusKind::Added => "added",
        StatusKind::Modified => "modified",
        StatusKind::Deleted => "deleted",
        StatusKind::TypeChanged => "type-changed",
        StatusKind::Unmerged => "unmerged",
    }
}

pub(super) fn lower_debug(value: impl std::fmt::Debug) -> String {
    format!("{value:?}").to_ascii_lowercase()
}

fn signature(value: &Signature) -> Value {
    json!({
        "name": value.name,
        "email": value.email,
        "timestamp": value.timestamp,
        "timezoneMinutes": value.timezone_minutes
    })
}

pub(super) fn entry_kind(value: EntryKind) -> &'static str {
    match value {
        EntryKind::Blob => "blob",
        EntryKind::Tree => "tree",
        EntryKind::Commit => "commit",
    }
}

fn bounded_message(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).chars().take(4096).collect()
}

fn hex(bytes: &[u8]) -> String {
    const DIGITS: &[u8; 16] = b"0123456789abcdef";
    let mut result = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        result.push(char::from(DIGITS[usize::from(byte >> 4)]));
        result.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
    }
    result
}