weavatrix-rust 1.0.1

Read-only Rust repository intelligence and MCP server
Documentation
use crate::RepositoryState;
#[cfg(feature = "git")]
use crate::tools::{arg_str, optional_bool, optional_str, optional_u64};
use blazingly_json::Value;
#[cfg(feature = "git")]
use blazingly_json::json;
#[cfg(feature = "git")]
use std::time::{SystemTime, UNIX_EPOCH};

#[cfg(feature = "git")]
use weavatrix_git::RepositorySet;
#[cfg(feature = "git")]
use weavatrix_git::{HistoryOptions, ObjectId, Repository};

#[cfg(feature = "git")]
pub fn history(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    let repository = Repository::open(state.root()).map_err(|error| error.to_string())?;
    let start = resolve_revision(
        &repository,
        optional_str(args, "revision")?.unwrap_or("HEAD"),
    )?;
    let max = usize::try_from(optional_u64(args, "max_commits")?.unwrap_or(100))
        .map_err(|_| "max_commits is too large")?;
    let months = optional_u64(args, "months")?.unwrap_or(6);
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|error| error.to_string())?
        .as_secs();
    let window = months.saturating_mul(2_629_746);
    let since = i64::try_from(now.saturating_sub(window)).unwrap_or(i64::MIN);
    let records = repository
        .history(
            start,
            HistoryOptions {
                max_commits: max,
                since: Some(since),
                first_parent: optional_bool(args, "first_parent")?.unwrap_or(false),
                ..HistoryOptions::default()
            },
        )
        .map_err(|error| error.to_string())?;
    let analytics = super::history_analytics::analyze(state, &repository, &records, args)?;
    Ok(json!({
        "status": "COMPLETE",
        "git_evidence": {"present": true},
        "revision": start.to_string(),
        "months": months,
        "analytics": analytics
    }))
}

#[cfg(feature = "git")]
pub fn cross_repo(_state: &RepositoryState, args: &Value) -> Result<Value, String> {
    let inputs = args
        .get("repositories")
        .and_then(Value::as_array)
        .ok_or_else(|| "repositories must be an array".to_owned())?
        .iter()
        .enumerate()
        .map(|(index, item)| {
            let path = item
                .get("path")
                .and_then(Value::as_str)
                .ok_or_else(|| "repository.path must be a string".to_owned())?;
            let name = item
                .get("name")
                .map(|value| {
                    value
                        .as_str()
                        .map(str::to_owned)
                        .ok_or_else(|| "repository.name must be a string".to_owned())
                })
                .transpose()?
                .unwrap_or_else(|| format!("repository-{index}"));
            Ok((name, path.to_owned()))
        })
        .collect::<Result<Vec<_>, String>>()?;
    if inputs.is_empty() {
        return Err("repositories must not be empty".to_owned());
    }
    let set = RepositorySet::open_parallel(inputs).map_err(|error| error.to_string())?;
    let options = HistoryOptions {
        max_commits: usize::try_from(optional_u64(args, "max_commits")?.unwrap_or(1_000))
            .map_err(|_| "max_commits is too large")?,
        first_parent: optional_bool(args, "first_parent")?.unwrap_or(false),
        ..HistoryOptions::default()
    };
    let revision = optional_str(args, "revision")?.unwrap_or("HEAD");
    match optional_str(args, "action")?.unwrap_or("histories") {
        "histories" => {
            let histories = set
                .histories_from_parallel(revision, options)
                .map_err(|error| error.to_string())?;
            Ok(json!({
                "status": "COMPLETE",
                "git_evidence": {"present": true},
                "action": "histories",
                "revision": revision,
                "repositories": histories.into_iter().map(|history| json!({
                    "name": set.name(history.repository),
                    "head": history.head.to_string(),
                    "commits": history.commits.into_iter()
                        .map(|id| id.to_string()).collect::<Vec<_>>()
                })).collect::<Vec<_>>()
            }))
        }
        "shared_commits" => {
            let shared = set
                .shared_commits_from(revision, options)
                .map_err(|error| error.to_string())?;
            Ok(json!({
                "status": "COMPLETE",
                "git_evidence": {"present": true},
                "action": "shared_commits",
                "revision": revision,
                "commits": shared.into_iter().map(|commit| json!({
                    "id": commit.id.to_string(),
                    "repositories": commit.repositories.into_iter()
                        .filter_map(|id| set.name(id)).collect::<Vec<_>>()
                })).collect::<Vec<_>>()
            }))
        }
        "diff" => {
            let left = arg_str(args, "left")?;
            let right = arg_str(args, "right")?;
            let left_id = set
                .id(left)
                .ok_or_else(|| format!("repository is not registered: {left}"))?;
            let right_id = set
                .id(right)
                .ok_or_else(|| format!("repository is not registered: {right}"))?;
            let base = optional_str(args, "base_ref")?.unwrap_or(revision);
            let head = optional_str(args, "head_ref")?.unwrap_or(revision);
            let changes = set
                .diff_commits(left_id, base, right_id, head)
                .map_err(|error| error.to_string())?;
            Ok(json!({
                "status": "COMPLETE",
                "git_evidence": {"present": true},
                "action": "diff",
                "left": {"repository": left, "revision": base},
                "right": {"repository": right, "revision": head},
                "changes": changes.into_iter().map(change_json).collect::<Vec<_>>()
            }))
        }
        action => Err(format!(
            "cross_repo_git action {action:?} is invalid; expected histories, shared_commits, or diff"
        )),
    }
}

#[cfg(feature = "git")]
fn change_json(change: weavatrix_git::TreeChange) -> Value {
    json!({
        "path": String::from_utf8_lossy(&change.path),
        "kind": format!("{:?}", change.kind).to_ascii_lowercase(),
        "old": change.old.map(|entry| entry.id.to_string()),
        "new": change.new.map(|entry| entry.id.to_string())
    })
}

#[cfg(not(feature = "git"))]
pub fn cross_repo(_state: &RepositoryState, _args: &Value) -> Result<Value, String> {
    Err("git capability is not compiled".to_owned())
}

#[cfg(not(feature = "git"))]
pub fn history(_state: &RepositoryState, _args: &Value) -> Result<Value, String> {
    Err("git capability is not compiled".to_owned())
}

#[cfg(feature = "git")]
pub fn changes(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    let repository = Repository::open(state.root()).map_err(|error| error.to_string())?;
    let head = resolve_revision(
        &repository,
        optional_str(args, "head_ref")?.unwrap_or("HEAD"),
    )?;
    let base_ref = optional_str(args, "base_ref")?;
    let base_alias = optional_str(args, "base")?;
    let base = if let Some(value) = base_ref.or(base_alias) {
        resolve_revision(&repository, value)?
    } else {
        first_parent(&repository, head)?
    };
    let changes = repository
        .diff_commits(base, head)
        .map_err(|error| error.to_string())?;
    Ok(json!({
        "status": "COMPLETE",
        "git_evidence": {"present": true},
        "base": base.to_string(),
        "head": head.to_string(),
        "changes": changes.into_iter().map(|change| json!({
            "path": String::from_utf8_lossy(&change.path),
            "kind": format!("{:?}", change.kind).to_ascii_lowercase(),
            "old": change.old.map(|entry| entry.id.to_string()),
            "new": change.new.map(|entry| entry.id.to_string())
        })).collect::<Vec<_>>()
    }))
}

#[cfg(not(feature = "git"))]
pub fn changes(_state: &RepositoryState, _args: &Value) -> Result<Value, String> {
    Err("git capability is not compiled".to_owned())
}

#[cfg(feature = "git")]
pub fn graph_diff(state: &RepositoryState, args: &Value) -> Result<Value, String> {
    super::history_diff::graph_diff(state, args)
}

#[cfg(not(feature = "git"))]
pub fn graph_diff(_state: &RepositoryState, _args: &Value) -> Result<Value, String> {
    Err("git capability is not compiled".to_owned())
}

#[cfg(feature = "git")]
pub(super) fn worktree_changed_files(
    state: &RepositoryState,
    base_ref: &str,
) -> Result<Vec<String>, String> {
    super::history_diff::worktree_changed_files(state, base_ref)
}

#[cfg(not(feature = "git"))]
pub(super) fn worktree_changed_files(
    _state: &RepositoryState,
    _base_ref: &str,
) -> Result<Vec<String>, String> {
    Err("git capability is not compiled".to_owned())
}

#[cfg(feature = "git")]
pub(super) fn resolve_revision(repository: &Repository, value: &str) -> Result<ObjectId, String> {
    if let Some((base, hops)) = value.rsplit_once('~') {
        let hops = hops
            .parse::<usize>()
            .map_err(|_| format!("invalid first-parent revision: {value}"))?;
        let mut id = repository
            .resolve(base)
            .map_err(|error| error.to_string())?;
        for _ in 0..hops {
            id = first_parent(repository, id)?;
        }
        Ok(id)
    } else {
        repository.resolve(value).map_err(|error| error.to_string())
    }
}

#[cfg(feature = "git")]
fn first_parent(repository: &Repository, id: ObjectId) -> Result<ObjectId, String> {
    repository
        .commit_metadata(id)
        .map_err(|error| error.to_string())?
        .parents
        .first()
        .copied()
        .ok_or_else(|| format!("commit {id} has no parent"))
}