AkurAI Build
Menu

AkurAI-Build

public

Latest change 7f74b1a60a9685828debe95379405bac8d7d651e - mcp: never block the request loop on pipeline execution by Ólafur Búi Ólafsson

use std::path::{Path, PathBuf};

use anyhow::{Context, Result, ensure};
use serde::Serialize;

use crate::config::validate_ref;

/// Maximum age (seconds) of a cached mirror before a fetch is forced.
const MIRROR_TTL: u64 = 30;
/// Bound the number of tree entries we resolve per-file commits for.
const MAX_ENTRIES: usize = 500;

#[derive(Clone)]
pub struct RepoTree {
    root: PathBuf,
}

#[derive(Serialize)]
pub struct TreeEntry {
    pub name: String,
    pub kind: String,
    pub size: u64,
    pub commit_message: Option<String>,
    pub commit_at: Option<i64>,
}

#[derive(Serialize)]
pub struct LatestCommit {
    pub sha: String,
    pub message: String,
    pub author: String,
    pub committed_at: i64,
}

#[derive(Serialize)]
pub struct RepoCounts {
    pub commits: u64,
    pub branches: u64,
    pub tags: u64,
}

#[derive(Serialize)]
pub struct TreeResponse {
    pub reference: String,
    pub path: String,
    pub entries: Vec<TreeEntry>,
    pub latest_commit: Option<LatestCommit>,
    pub recent_commits: Vec<LatestCommit>,
    pub counts: RepoCounts,
    pub readme: Option<String>,
    pub readme_name: Option<String>,
    pub contributors: Vec<Contributor>,
    pub languages: Vec<Language>,
    pub description: Option<String>,
}

#[derive(Serialize)]
pub struct Contributor {
    pub name: String,
    pub email: String,
    pub commit_count: u64,
}

#[derive(Serialize)]
pub struct Language {
    pub name: String,
    pub color: String,
    pub percentage: f64,
}

impl RepoTree {
    pub fn new(data_root: &Path) -> Result<Self> {
        let root = data_root.join("mirrors");
        std::fs::create_dir_all(&root)?;
        Ok(Self { root })
    }

    fn mirror_path(&self, repository: &str) -> PathBuf {
        self.root.join(format!("{repository}.git"))
    }

    /// Ensure a bare mirror exists and is reasonably fresh.
    fn sync_mirror(&self, repository: &str, url: &str) -> Result<PathBuf> {
        let path = self.mirror_path(repository);
        if path.join("HEAD").is_file() {
            let fresh = std::fs::metadata(path.join("FETCH_HEAD"))
                .ok()
                .and_then(|m| m.modified().ok())
                .and_then(|t| t.elapsed().ok())
                .is_some_and(|age| age.as_secs() < MIRROR_TTL);
            if !fresh {
                let status = git(&path, ["fetch", "--prune", "origin"])?;
                ensure!(status, "git fetch failed for {repository}");
            }
            return Ok(path);
        }
        std::fs::create_dir_all(&path)?;
        let parent = path
            .parent()
            .context("mirror path has no parent")?
            .to_path_buf();
        let name = path
            .file_name()
            .context("mirror path has no file name")?
            .to_string_lossy()
            .to_string();
        let status = git_output(
            &parent,
            ["clone", "--bare", "--filter=blob:none", url, &name],
        )
        .is_ok();
        ensure!(status, "git clone --bare failed for {repository}");
        Ok(path)
    }

    /// Resolve the default branch of the mirror.
    fn default_ref(&self, path: &Path) -> Result<String> {
        if let Ok(head) = git_output(path, ["symbolic-ref", "--short", "HEAD"])
            && !head.is_empty()
        {
            return Ok(head);
        }
        Ok("HEAD".to_string())
    }

    pub fn tree(
        &self,
        repository: &str,
        url: &str,
        reference: Option<&str>,
        sub_path: &str,
    ) -> Result<TreeResponse> {
        if let Some(reference) = reference {
            validate_ref(reference)?;
        }
        ensure!(!sub_path.contains(".."), "invalid tree path");
        let path = self.sync_mirror(repository, url)?;
        let reference = match reference {
            Some(value) => value.to_string(),
            None => self.default_ref(&path)?,
        };

        let spec = if sub_path.is_empty() {
            reference.clone()
        } else {
            format!("{reference}:{sub_path}")
        };
        let raw = git_output(&path, ["ls-tree", "--long", "-z", &spec])
            .with_context(|| format!("ls-tree failed for {repository}@{reference}"))?;
        let mut entries = parse_tree(&raw);
        entries.sort_by(|a, b| {
            (a.kind != "tree")
                .cmp(&(b.kind != "tree"))
                .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
        });

        // Per-file latest commit (bounded).
        for entry in entries.iter_mut().take(MAX_ENTRIES) {
            let entry_path = if sub_path.is_empty() {
                entry.name.clone()
            } else {
                format!("{sub_path}/{}", entry.name)
            };
            if let Ok(line) = git_output(
                &path,
                [
                    "log",
                    "-1",
                    "--format=%s%x00%ct",
                    &reference,
                    "--",
                    &entry_path,
                ],
            ) && let Some((message, ts)) = line.split_once('\0')
            {
                entry.commit_message = Some(message.trim().to_string());
                entry.commit_at = ts.trim().parse::<i64>().ok();
            }
        }

        let latest_commit = git_output(
            &path,
            ["log", "-1", "--format=%H%x00%s%x00%an%x00%ct", &reference],
        )
        .ok()
        .and_then(|line| parse_commit(&line));

        let counts = RepoCounts {
            commits: git_output(&path, ["rev-list", "--count", &reference])
                .ok()
                .and_then(|v| v.trim().parse().ok())
                .unwrap_or(0),
            branches: count_lines(&git_output(&path, ["branch", "--list"]).unwrap_or_default()),
            tags: count_lines(&git_output(&path, ["tag", "--list"]).unwrap_or_default()),
        };

        let recent_commits = git_output(
            &path,
            [
                "log",
                "-8",
                "--format=%H%x00%s%x00%an%x00%ct%x1e",
                &reference,
            ],
        )
        .unwrap_or_default()
        .split('\x1e')
        .filter_map(parse_commit)
        .collect();

        // Contributors: top 12 by commit count
        let contributors = git_output(&path, ["shortlog", "-sne", &reference])
            .ok()
            .map(|raw| {
                raw.lines()
                    .filter_map(|line| {
                        let (count, identity) = line.trim().split_once('\t')?;
                        let (name, email) = identity.rsplit_once(" <")?;
                        Some(Contributor {
                            name: name.to_string(),
                            email: email.trim_end_matches('>').to_string(),
                            commit_count: count.trim().parse().unwrap_or(0),
                        })
                    })
                    .take(12)
                    .collect()
            })
            .unwrap_or_default();

        // Languages: from top-level tree entries by extension
        let languages = {
            let mut ext_counts: std::collections::HashMap<String, u64> =
                std::collections::HashMap::new();
            let mut total = 0u64;
            for entry in &entries {
                if entry.kind == "tree" {
                    continue;
                }
                let ext = std::path::Path::new(&entry.name)
                    .extension()
                    .map(|e| e.to_string_lossy().to_lowercase())
                    .unwrap_or_else(|| entry.name.to_lowercase());
                *ext_counts.entry(ext).or_default() += entry.size.max(1);
                total += entry.size.max(1);
            }
            if total == 0 {
                total = 1;
            }
            let mut langs: Vec<Language> = ext_counts
                .into_iter()
                .map(|(name, size)| {
                    let percentage = (size as f64 / total as f64) * 100.0;
                    let color = language_color(&name);
                    Language {
                        name,
                        color,
                        percentage,
                    }
                })
                .collect();
            langs.sort_by(|a, b| {
                b.percentage
                    .partial_cmp(&a.percentage)
                    .unwrap_or(std::cmp::Ordering::Equal)
            });
            langs
        };

        let (readme, readme_name) = read_readme(&path, &reference, sub_path);

        // Description: from the mirror's git description file
        let description = std::fs::read_to_string(path.join("description"))
            .ok()
            .map(|d| d.trim().to_string())
            .filter(|d| !d.is_empty() && d != "Unnamed repository; edit this file 'description' to name the repository.");

        Ok(TreeResponse {
            reference,
            path: sub_path.to_string(),
            entries,
            latest_commit,
            recent_commits,
            counts,
            readme,
            readme_name,
            contributors,
            languages,
            description,
        })
    }
}

fn parse_tree(raw: &str) -> Vec<TreeEntry> {
    // Each record: "<mode> <type> <sha> <size>\t<name>\0"
    raw.split('\0')
        .filter(|record| !record.is_empty())
        .filter_map(|record| {
            let (meta, name) = record.split_once('\t')?;
            let mut fields = meta.split_whitespace();
            let _mode = fields.next()?;
            let kind = fields.next()?;
            let _sha = fields.next()?;
            let size = fields.next().unwrap_or("0");
            Some(TreeEntry {
                name: name.to_string(),
                kind: kind.to_string(),
                size: size.parse().unwrap_or(0),
                commit_message: None,
                commit_at: None,
            })
        })
        .collect()
}

fn parse_commit(line: &str) -> Option<LatestCommit> {
    let mut fields = line.split('\0');
    let sha = fields.next()?.trim().to_string();
    let message = fields.next()?.trim().to_string();
    let author = fields.next()?.trim().to_string();
    let committed_at = fields.next()?.trim().parse().ok()?;
    if sha.is_empty() {
        return None;
    }
    Some(LatestCommit {
        sha,
        message,
        author,
        committed_at,
    })
}

fn read_readme(path: &Path, reference: &str, sub_path: &str) -> (Option<String>, Option<String>) {
    for candidate in ["README.md", "README.markdown", "README", "README.txt"] {
        let spec = if sub_path.is_empty() {
            format!("{reference}:{candidate}")
        } else {
            format!("{reference}:{sub_path}/{candidate}")
        };
        if let Ok(content) = git_output(path, ["show", &spec])
            && !content.is_empty()
        {
            return (Some(content), Some(candidate.to_string()));
        }
    }
    (None, None)
}

fn language_color(ext: &str) -> String {
    match ext {
        "rs" => "#dea584",
        "ts" | "tsx" => "#3178c6",
        "js" | "jsx" | "mjs" | "cjs" => "#f1e05a",
        "py" => "#3572A5",
        "go" => "#00ADD8",
        "rb" => "#701516",
        "c" => "#555555",
        "cpp" | "cxx" | "cc" | "hpp" | "h" | "hxx" => "#f34b7d",
        "java" => "#b07219",
        "html" | "htm" => "#e34c26",
        "css" | "scss" | "sass" | "less" => "#563d7c",
        "json" => "#292929",
        "yaml" | "yml" => "#cb171e",
        "toml" => "#9c4221",
        "md" | "mdx" | "markdown" => "#083fa1",
        "sql" => "#e38c00",
        "sh" | "bash" | "zsh" | "fish" => "#89e051",
        "dockerfile" | "Dockerfile" => "#384d54",
        "svg" => "#ff9900",
        "png" | "jpg" | "jpeg" | "gif" | "ico" | "webp" | "bmp" => "#cccccc",
        "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" => "#cccccc",
        "lock" => "#7a7a7a",
        "txt" | "text" => "#6c6c6c",
        _ => "#6c6c6c",
    }
    .to_string()
}

fn count_lines(value: &str) -> u64 {
    value.lines().filter(|l| !l.trim().is_empty()).count() as u64
}

fn git<const N: usize>(cwd: &Path, args: [&str; N]) -> Result<bool> {
    let status = crate::git_process::command(cwd).args(args).status()?;
    Ok(status.success())
}

fn git_output<const N: usize>(cwd: &Path, args: [&str; N]) -> Result<String> {
    crate::git_process::output(cwd, args)
}

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

    #[test]
    fn parses_ls_tree_records() {
        let raw = "100644 blob abc123 42\tREADME.md\x0040000 tree def456 -\tsrc\x00";
        let entries = parse_tree(raw);
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0].name, "README.md");
        assert_eq!(entries[0].kind, "blob");
        assert_eq!(entries[0].size, 42);
        assert_eq!(entries[1].name, "src");
        assert_eq!(entries[1].kind, "tree");
    }

    #[test]
    fn parses_empty_output() {
        let entries = parse_tree("");
        assert!(entries.is_empty(), "empty output -> no entries");

        let entries = parse_tree("\x00");
        assert!(entries.is_empty(), "bare null -> no entries");
    }

    #[test]
    fn skips_malformed_records() {
        let raw = "bad-record\x00100644 blob abc123 42\tok.txt\x00no-tab-here\x00";
        let entries = parse_tree(raw);
        // Only the middle record has a \t separator; first and last should be skipped.
        assert_eq!(entries.len(), 1, "only one well-formed record");
        assert_eq!(entries[0].name, "ok.txt");
        assert_eq!(entries[0].kind, "blob");
    }

    #[test]
    fn handles_unicode_filenames() {
        let raw = b"100644 blob abc123 42\t\xF0\x9F\x93\xA6.md\x00";
        let raw_str = String::from_utf8_lossy(raw);
        let entries = parse_tree(&raw_str);
        assert_eq!(entries.len(), 1);
        assert!(
            entries[0].name.contains('\u{1F4E6}'),
            "unicode filename should be preserved"
        );
    }

    #[test]
    fn handles_large_sizes() {
        let raw = "100644 blob abc123 4294967296\tbig.bin\x00";
        let entries = parse_tree(raw);
        assert_eq!(entries.len(), 1);
        assert_eq!(
            entries[0].size, 4294967296,
            "large size should parse correctly"
        );
    }

    #[test]
    fn size_defaults_to_zero_for_submodules() {
        // Submodules show '-' for size in ls-tree --long.
        let raw = "160000 commit abc123 -\tsubmod\x00";
        let entries = parse_tree(raw);
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].kind, "commit");
        assert_eq!(entries[0].size, 0, "submodule size '-' should default to 0");
    }

    #[test]
    fn parses_commit_line() {
        let commit =
            parse_commit("abc\0Initial commit\0Alice\x001700000000").expect("valid commit fixture");
        assert_eq!(commit.sha, "abc");
        assert_eq!(commit.message, "Initial commit");
        assert_eq!(commit.author, "Alice");
        assert_eq!(commit.committed_at, 1700000000);
    }

    #[test]
    fn rejects_empty_commit_sha() {
        assert!(parse_commit("\0msg\0author\x00123").is_none());
    }

    #[test]
    fn rejects_commit_with_missing_fields() {
        // Not enough null-delimited fields.
        assert!(parse_commit("sha\0msg").is_none());
        assert!(parse_commit("sha\x00msg\x00author").is_none());
    }

    #[test]
    fn rejects_commit_with_bad_timestamp() {
        assert!(
            parse_commit("sha\0msg\0author\0not-a-number").is_none(),
            "non-numeric timestamp should fail"
        );
    }
}