tokmd 1.8.1

Tokei-backed repo inventory receipts (Markdown/TSV/JSONL/CSV) for PRs, CI, and LLM workflows.
Documentation
use anyhow::{Context, Result, bail};
use tokmd_config as cli;
use tokmd_format::{
    DiffColorMode, DiffRenderOptions, compute_diff_rows, compute_diff_totals, create_diff_receipt,
    render_diff_md_with_options,
};
#[cfg(feature = "git")]
use tokmd_model as model;
#[cfg(feature = "git")]
use tokmd_scan as scan;
use tokmd_types::LangReport;

use std::io::IsTerminal;
use std::path::{Path, PathBuf};
#[cfg(feature = "git")]
use std::process::Command;
#[cfg(feature = "git")]
use std::time::{SystemTime, UNIX_EPOCH};

pub(crate) fn handle(args: cli::DiffArgs, global: &cli::GlobalArgs) -> Result<()> {
    let (from, to) = resolve_targets(&args)?;
    let from_report = resolve_lang_report(&from, global)
        .with_context(|| format!("Failed to load diff source '{}'", from))?;
    let to_report = resolve_lang_report(&to, global)
        .with_context(|| format!("Failed to load diff source '{}'", to))?;

    let diff_rows = compute_diff_rows(&from_report, &to_report);
    let totals = compute_diff_totals(&diff_rows);

    match args.format {
        cli::DiffFormat::Md => {
            let color = resolve_color_mode(args.color);
            let render_options = DiffRenderOptions {
                compact: args.compact,
                color,
            };
            print!(
                "{}",
                render_diff_md_with_options(&from, &to, &diff_rows, &totals, render_options)
            );
        }
        cli::DiffFormat::Json => {
            let receipt = create_diff_receipt(&from, &to, diff_rows, totals);
            println!("{}", serde_json::to_string(&receipt)?);
        }
    }

    Ok(())
}

fn resolve_color_mode(mode: cli::ColorMode) -> DiffColorMode {
    if should_use_color(mode) {
        DiffColorMode::Ansi
    } else {
        DiffColorMode::Off
    }
}

fn should_use_color(mode: cli::ColorMode) -> bool {
    match mode {
        cli::ColorMode::Always => true,
        cli::ColorMode::Never => false,
        cli::ColorMode::Auto => auto_color_enabled(),
    }
}

fn auto_color_enabled() -> bool {
    if std::env::var("NO_COLOR").is_ok() {
        return false;
    }

    if let Ok(force) = std::env::var("CLICOLOR_FORCE")
        && !force.is_empty()
    {
        return force != "0";
    }

    if let Ok(cli_color) = std::env::var("CLICOLOR")
        && cli_color == "0"
    {
        return false;
    }

    std::io::stdout().is_terminal()
}

fn resolve_targets(args: &cli::DiffArgs) -> Result<(String, String)> {
    if !args.refs.is_empty() {
        if args.from.is_some() || args.to.is_some() {
            bail!("Use either two positional refs/paths or --from/--to, not both.");
        }
        if args.refs.len() != 2 {
            bail!("Diff expects exactly two refs/paths.");
        }
        return Ok((args.refs[0].clone(), args.refs[1].clone()));
    }

    match (&args.from, &args.to) {
        (Some(from), Some(to)) => Ok((from.clone(), to.clone())),
        _ => bail!("Provide either two positional refs/paths or both --from and --to."),
    }
}

fn resolve_lang_report(input: &str, global: &cli::GlobalArgs) -> Result<LangReport> {
    let path = PathBuf::from(input);
    if path.exists() {
        return load_lang_report_from_path(&path);
    }

    lang_report_from_git_ref(input, global)
}

fn load_lang_report_from_path(path: &Path) -> Result<LangReport> {
    let lang_path = if path.is_dir() {
        path.join("lang.json")
    } else if path
        .file_name()
        .map(|name| name == "receipt.json")
        .unwrap_or(false)
    {
        path.parent().unwrap_or(path).join("lang.json")
    } else {
        path.to_path_buf()
    };

    let content = std::fs::read_to_string(&lang_path)
        .with_context(|| format!("Failed to read {}", lang_path.display()))?;
    let receipt: tokmd_types::LangReceipt =
        serde_json::from_str(&content).context("Failed to parse lang receipt")?;
    Ok(receipt.report)
}

#[cfg(not(feature = "git"))]
fn lang_report_from_git_ref(_revision: &str, _global: &cli::GlobalArgs) -> Result<LangReport> {
    bail!("Git support is disabled in this build. Cannot diff git refs.");
}

#[cfg(feature = "git")]
fn lang_report_from_git_ref(revision: &str, global: &cli::GlobalArgs) -> Result<LangReport> {
    if !tokmd_git::git_available() {
        bail!("git is not available on PATH");
    }
    let cwd = std::env::current_dir().context("Failed to resolve current directory")?;
    let repo_root =
        tokmd_git::repo_root(&cwd).ok_or_else(|| anyhow::anyhow!("not inside a git repository"))?;

    let worktree = GitWorktree::new(&repo_root, revision)
        .with_context(|| format!("Failed to create worktree for '{}'", revision))?;
    let _cwd = ScopedCwd::new(&worktree.path)
        .with_context(|| format!("Failed to enter worktree for '{}'", revision))?;

    let scan_opts = tokmd_settings::ScanOptions::from(global);
    let languages = scan::scan(std::slice::from_ref(&worktree.path), &scan_opts)?;
    Ok(model::create_lang_report(
        &languages,
        0,
        false,
        cli::ChildrenMode::Collapse,
    ))
}

#[cfg(feature = "git")]
struct ScopedCwd {
    previous: PathBuf,
}

#[cfg(feature = "git")]
impl ScopedCwd {
    fn new(path: &Path) -> Result<Self> {
        let previous = std::env::current_dir().context("Failed to capture current directory")?;
        std::env::set_current_dir(path)
            .with_context(|| format!("Failed to set current directory to {}", path.display()))?;
        Ok(Self { previous })
    }
}

#[cfg(feature = "git")]
impl Drop for ScopedCwd {
    fn drop(&mut self) {
        let _ = std::env::set_current_dir(&self.previous);
    }
}

#[cfg(feature = "git")]
struct GitWorktree {
    repo_root: PathBuf,
    path: PathBuf,
}

#[cfg(feature = "git")]
impl GitWorktree {
    fn new(repo_root: &Path, revision: &str) -> Result<Self> {
        let path = make_temp_dir("diff-worktree")?;

        let status = Command::new("git")
            .arg("-C")
            .arg(repo_root)
            .arg("worktree")
            .arg("add")
            .arg("--detach")
            .arg(&path)
            .arg(revision)
            .status()
            .with_context(|| format!("Failed to spawn git worktree for {}", revision))?;

        if !status.success() {
            let _ = std::fs::remove_dir_all(&path);
            bail!("git worktree add failed for '{}'", revision);
        }

        Ok(Self {
            repo_root: repo_root.to_path_buf(),
            path,
        })
    }
}

#[cfg(feature = "git")]
impl Drop for GitWorktree {
    fn drop(&mut self) {
        let _ = Command::new("git")
            .arg("-C")
            .arg(&self.repo_root)
            .arg("worktree")
            .arg("remove")
            .arg("--force")
            .arg(&self.path)
            .status();
        let _ = std::fs::remove_dir_all(&self.path);
    }
}

#[cfg(feature = "git")]
fn make_temp_dir(prefix: &str) -> Result<PathBuf> {
    let base = std::env::temp_dir();
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis();
    let pid = std::process::id();

    for attempt in 0..1000 {
        let path = base.join(format!("tokmd-{}-{}-{}-{}", prefix, now, pid, attempt));
        if !path.exists() {
            std::fs::create_dir_all(&path)
                .with_context(|| format!("Failed to create temp dir {}", path.display()))?;
            return Ok(path);
        }
    }

    bail!("Failed to create a unique temp directory for diff")
}