weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use mcport::{RequestContext, ToolReply, Value, json};

use super::{
    arguments,
    output::{commit_value, entry_kind, lower_debug, page_result, path, status_kind, tree_entry},
};
use crate::{HistoryOptions, Repository};

pub fn head(context: &RequestContext, repository: &Repository, arguments: &Value) -> ToolReply {
    reply((|| {
        arguments::object(arguments)?;
        active(context)?;
        let head = repository.head().map_err(message)?;
        Ok(json!({
            "repository": repository.work_dir().unwrap_or_else(|| repository.git_dir()).display().to_string(),
            "gitDirectory": repository.git_dir().display().to_string(),
            "hash": lower_debug(repository.hash_kind()),
            "symbolic": head.symbolic,
            "target": head.target.map(|id| id.to_string()),
            "packCount": repository.pack_count()
        }))
    })())
}

pub fn history(context: &RequestContext, repository: &Repository, arguments: &Value) -> ToolReply {
    reply((|| {
        let revision = arguments::revision(arguments, "revision", Some("HEAD"))?;
        let page = arguments::page(arguments, 50, 200)?;
        let first_parent = arguments::boolean(arguments, "firstParent", false)?;
        let include_message = arguments::boolean(arguments, "includeMessage", false)?;
        let since = arguments::optional_i64(arguments, "since")?;
        let until = arguments::optional_i64(arguments, "until")?;
        let requested = page
            .cursor
            .checked_add(page.limit)
            .and_then(|value| value.checked_add(1))
            .ok_or_else(|| "cursor and limit overflow".to_owned())?;
        if requested > repository.limits().max_history_commits {
            return Err("cursor exceeds the bounded history window".to_owned());
        }
        active(context)?;
        progress(context, 0.0, "reading commit history")?;
        let start = repository.resolve(&revision).map_err(message)?;
        let records = repository
            .history(
                start,
                HistoryOptions {
                    max_commits: requested,
                    first_parent,
                    since,
                    until,
                },
            )
            .map_err(message)?;
        active(context)?;
        let total = records.len();
        let commits = records
            .into_iter()
            .skip(page.cursor)
            .take(page.limit)
            .enumerate()
            .map(|(index, record)| {
                periodic(context, index)?;
                Ok(commit_value(&record.commit, include_message))
            })
            .collect::<Result<Vec<_>, String>>()?;
        progress(context, 1.0, "history ready")?;
        let total_known = total <= page.cursor + page.limit;
        Ok(page_result(
            revision,
            commits,
            page.cursor,
            total,
            total_known,
            "commits",
        ))
    })())
}

pub fn diff(context: &RequestContext, repository: &Repository, arguments: &Value) -> ToolReply {
    reply((|| {
        let old = arguments::revision(arguments, "old", None)?;
        let new = arguments::revision(arguments, "new", Some("HEAD"))?;
        let page = arguments::page(arguments, 200, 1000)?;
        active(context)?;
        progress(context, 0.0, "comparing trees")?;
        let old_id = repository.resolve(&old).map_err(message)?;
        let new_id = repository.resolve(&new).map_err(message)?;
        let changes = repository.diff_commits(old_id, new_id).map_err(message)?;
        active(context)?;
        let total = changes.len();
        let values = changes
            .into_iter()
            .skip(page.cursor)
            .take(page.limit)
            .enumerate()
            .map(|(index, change)| {
                periodic(context, index)?;
                let (path, path_hex) = path(&change.path);
                Ok(json!({
                    "path": path,
                    "pathHex": path_hex,
                    "kind": lower_debug(change.kind),
                    "old": change.old.as_ref().map(tree_entry),
                    "new": change.new.as_ref().map(tree_entry)
                }))
            })
            .collect::<Result<Vec<_>, String>>()?;
        progress(context, 1.0, "diff ready")?;
        let mut result = page_result(new, values, page.cursor, total, true, "changes");
        result["old"] = Value::String(old);
        Ok(result)
    })())
}

pub fn status(context: &RequestContext, repository: &Repository, arguments: &Value) -> ToolReply {
    reply((|| {
        let page = arguments::page(arguments, 200, 1000)?;
        active(context)?;
        progress(context, 0.0, "reading tracked status")?;
        let entries = repository.status().map_err(message)?;
        active(context)?;
        let total = entries.len();
        let values = entries
            .into_iter()
            .skip(page.cursor)
            .take(page.limit)
            .enumerate()
            .map(|(index, entry)| {
                periodic(context, index)?;
                let (path, path_hex) = path(&entry.path);
                Ok(json!({
                    "path": path,
                    "pathHex": path_hex,
                    "index": status_kind(entry.index),
                    "worktree": status_kind(entry.worktree)
                }))
            })
            .collect::<Result<Vec<_>, String>>()?;
        progress(context, 1.0, "status ready")?;
        Ok(page_result(
            "tracked",
            values,
            page.cursor,
            total,
            true,
            "entries",
        ))
    })())
}

pub fn snapshot(context: &RequestContext, repository: &Repository, arguments: &Value) -> ToolReply {
    reply((|| {
        let revision = arguments::revision(arguments, "revision", Some("HEAD"))?;
        let page = arguments::page(arguments, 200, 1000)?;
        active(context)?;
        progress(context, 0.0, "building immutable manifest")?;
        let snapshot = repository.snapshot(&revision).map_err(message)?;
        active(context)?;
        let total = snapshot.entries.len();
        let entries = snapshot
            .entries
            .into_iter()
            .skip(page.cursor)
            .take(page.limit)
            .enumerate()
            .map(|(index, entry)| {
                periodic(context, index)?;
                let (path, path_hex) = path(&entry.path);
                Ok(json!({
                    "path": path,
                    "pathHex": path_hex,
                    "mode": entry.mode,
                    "id": entry.id.to_string(),
                    "kind": entry_kind(entry.kind)
                }))
            })
            .collect::<Result<Vec<_>, String>>()?;
        progress(context, 1.0, "snapshot ready")?;
        let mut result = page_result(revision, entries, page.cursor, total, true, "entries");
        result["commit"] = Value::String(snapshot.commit.to_string());
        result["tree"] = Value::String(snapshot.tree.to_string());
        Ok(result)
    })())
}

fn active(context: &RequestContext) -> Result<(), String> {
    if context.is_cancelled() {
        Err("request cancelled or deadline elapsed".to_owned())
    } else {
        Ok(())
    }
}

fn periodic(context: &RequestContext, index: usize) -> Result<(), String> {
    if index.is_multiple_of(64) {
        active(context)
    } else {
        Ok(())
    }
}

fn progress(context: &RequestContext, value: f64, message: &str) -> Result<(), String> {
    context
        .report_progress(value, Some(1.0), Some(message))
        .map(|_| ())
        .map_err(|error| error.to_string())
}

fn reply(result: Result<Value, String>) -> ToolReply {
    match result {
        Ok(value) => ToolReply::structured(value),
        Err(error) => ToolReply::error(error),
    }
}

fn message(error: impl std::fmt::Display) -> String {
    error.to_string()
}