heddle-cli 0.12.0

An AI-native version control system
// SPDX-License-Identifier: Apache-2.0

use std::{fs, path::Path, process::Command};

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

#[derive(Default, Serialize)]
pub struct GitTypeBytes {
    pub objects: u64,
    pub bytes: u64,
}

#[derive(Serialize)]
pub struct GitBaseline {
    pub repository: String,
    pub revision: String,
    pub commits: GitTypeBytes,
    pub trees: GitTypeBytes,
    pub blobs: GitTypeBytes,
    pub tags: GitTypeBytes,
    pub total_objects: u64,
    pub pack_bytes: u64,
    pub idx_bytes: u64,
}

pub fn measure_git(git_dir: &Path) -> Result<GitBaseline> {
    let output = Command::new("git")
        .env_clear()
        .args([
            "--git-dir",
            &git_dir.to_string_lossy(),
            "cat-file",
            "--batch-all-objects",
            "--batch-check=%(objecttype) %(objectsize)",
        ])
        .output()
        .context("enumerate Git objects")?;
    if !output.status.success() {
        bail!("git cat-file failed with {}", output.status);
    }
    let mut baseline = GitBaseline {
        repository: git_dir.display().to_string(),
        revision: git_stdout(git_dir, &["rev-parse", "HEAD"])?
            .trim()
            .to_string(),
        commits: GitTypeBytes::default(),
        trees: GitTypeBytes::default(),
        blobs: GitTypeBytes::default(),
        tags: GitTypeBytes::default(),
        total_objects: 0,
        pack_bytes: extension_bytes(&git_dir.join("objects/pack"), "pack")?,
        idx_bytes: extension_bytes(&git_dir.join("objects/pack"), "idx")?,
    };
    for line in String::from_utf8(output.stdout)?.lines() {
        let (kind, size) = line
            .split_once(' ')
            .with_context(|| format!("invalid git cat-file row {line:?}"))?;
        let size = size.parse::<u64>()?;
        let bucket = match kind {
            "commit" => &mut baseline.commits,
            "tree" => &mut baseline.trees,
            "blob" => &mut baseline.blobs,
            "tag" => &mut baseline.tags,
            value => bail!("unexpected Git object type {value}"),
        };
        bucket.objects += 1;
        bucket.bytes += size;
        baseline.total_objects += 1;
    }
    Ok(baseline)
}

fn git_stdout(git_dir: &Path, args: &[&str]) -> Result<String> {
    let output = Command::new("git")
        .env_clear()
        .arg("--git-dir")
        .arg(git_dir)
        .args(args)
        .output()?;
    if !output.status.success() {
        bail!("git {} failed with {}", args.join(" "), output.status);
    }
    Ok(String::from_utf8(output.stdout)?)
}

fn extension_bytes(directory: &Path, extension: &str) -> Result<u64> {
    let mut total = 0;
    for entry in fs::read_dir(directory)? {
        let entry = entry?;
        if entry
            .path()
            .extension()
            .is_some_and(|value| value == extension)
        {
            total += entry.metadata()?.len();
        }
    }
    Ok(total)
}