moonlight-cli 0.1.2

Compare command behavior against one or two reference commands.
Documentation
use anyhow::{bail, Context};
use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
};
use uuid::Uuid;

#[derive(Debug, Clone)]
pub(crate) struct EvalWorktrees {
    pub(crate) root: PathBuf,
    pub(crate) primary: PathBuf,
    pub(crate) candidate: PathBuf,
}

impl EvalWorktrees {
    pub(crate) fn paths(work_dir: &Path, eval_id: Uuid) -> Self {
        let root = work_dir.join(eval_id.to_string());
        Self {
            primary: root.join("primary"),
            candidate: root.join("candidate"),
            root,
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) enum CandidateSource {
    Ref(String),
    Patch(PathBuf),
}

impl CandidateSource {
    pub(crate) fn label(&self) -> String {
        match self {
            Self::Ref(value) => format!("ref {value}"),
            Self::Patch(path) => format!("patch {}", path.display()),
        }
    }
}

pub(crate) fn resolve_repo_root(repo: &Path) -> anyhow::Result<PathBuf> {
    let output = git(repo, ["rev-parse", "--show-toplevel"])?;
    Ok(PathBuf::from(output.trim()))
}

pub(crate) fn prepare_worktrees(
    repo_root: &Path,
    work_dir: &Path,
    eval_id: Uuid,
    baseline_ref: &str,
    candidate: &CandidateSource,
) -> anyhow::Result<EvalWorktrees> {
    let paths = EvalWorktrees::paths(work_dir, eval_id);
    if paths.root.exists() {
        bail!(
            "eval work directory already exists: {}",
            paths.root.display()
        );
    }
    fs::create_dir_all(&paths.root)?;

    add_worktree(repo_root, &paths.primary, baseline_ref)?;
    match candidate {
        CandidateSource::Ref(candidate_ref) => {
            add_worktree(repo_root, &paths.candidate, candidate_ref)?;
        }
        CandidateSource::Patch(patch) => {
            let patch = patch
                .canonicalize()
                .with_context(|| format!("failed to resolve patch {}", patch.display()))?;
            add_worktree(repo_root, &paths.candidate, baseline_ref)?;
            git_checked(
                &paths.candidate,
                Command::new("git")
                    .arg("-C")
                    .arg(&paths.candidate)
                    .arg("apply")
                    .arg(&patch),
            )
            .with_context(|| format!("failed to apply patch {}", patch.display()))?;
        }
    }

    Ok(paths)
}

pub(crate) fn cleanup_worktrees(repo_root: &Path, paths: &EvalWorktrees) -> anyhow::Result<()> {
    remove_worktree(repo_root, &paths.primary)?;
    remove_worktree(repo_root, &paths.candidate)?;
    if paths.root.exists() {
        fs::remove_dir_all(&paths.root)
            .with_context(|| format!("failed to remove {}", paths.root.display()))?;
    }
    Ok(())
}

fn add_worktree(repo_root: &Path, path: &Path, reference: &str) -> anyhow::Result<()> {
    git_checked(
        repo_root,
        Command::new("git")
            .arg("-C")
            .arg(repo_root)
            .arg("worktree")
            .arg("add")
            .arg("--detach")
            .arg(path)
            .arg(reference),
    )
    .with_context(|| {
        format!(
            "failed to create worktree {} at {}",
            path.display(),
            reference
        )
    })
}

fn remove_worktree(repo_root: &Path, path: &Path) -> anyhow::Result<()> {
    if !path.exists() {
        return Ok(());
    }
    let status = Command::new("git")
        .arg("-C")
        .arg(repo_root)
        .arg("worktree")
        .arg("remove")
        .arg("--force")
        .arg(path)
        .status();
    if status.as_ref().is_err() || !status.as_ref().is_ok_and(|status| status.success()) {
        fs::remove_dir_all(path)
            .with_context(|| format!("failed to remove worktree {}", path.display()))?;
    }
    Ok(())
}

fn git<const N: usize>(repo: &Path, args: [&str; N]) -> anyhow::Result<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(repo)
        .args(args)
        .output()?;
    if output.status.success() {
        Ok(String::from_utf8_lossy(&output.stdout).to_string())
    } else {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim());
    }
}

fn git_checked(_repo: &Path, command: &mut Command) -> anyhow::Result<()> {
    let output = command.output()?;
    if output.status.success() {
        Ok(())
    } else {
        bail!("{}", String::from_utf8_lossy(&output.stderr).trim());
    }
}

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

    #[test]
    fn worktree_paths_are_eval_scoped() {
        let eval_id = Uuid::nil();
        let paths = EvalWorktrees::paths(Path::new(".moonlight/evals"), eval_id);

        assert_eq!(
            paths.primary,
            PathBuf::from(".moonlight/evals")
                .join(eval_id.to_string())
                .join("primary")
        );
        assert_eq!(paths.candidate, paths.root.join("candidate"));
    }
}