vvbox 0.1.0

Lightweight sandbox runner for macOS 26 using Apple container CLI.
Documentation
//! Git helpers for resolving repositories and worktrees.

use crate::config::worktrees_dir;
use crate::util::{cmd_output, die, run_cmd};
use std::path::{Path, PathBuf};
use std::process::Command;

/// Resolve a repo root from any path inside a git repository.
pub fn resolve_repo_root(repo_path: &str) -> PathBuf {
    let cmd = vec![
        "git".to_string(),
        "-C".to_string(),
        repo_path.to_string(),
        "rev-parse".to_string(),
        "--show-toplevel".to_string(),
    ];
    let output = cmd_output(&cmd);
    match output {
        Some(path) if !path.is_empty() => PathBuf::from(path),
        _ => die("vvbox requires a git repo (no git root found)"),
    }
}

/// Create a detached worktree snapshot for a run and return its path.
pub fn create_worktree(repo_root: &Path, run_id: &str) -> PathBuf {
    let snapshot_path = worktrees_dir().join(run_id);
    if snapshot_path.exists() {
        let _ = std::fs::remove_dir_all(&snapshot_path);
    }
    let cmd = vec![
        "git".to_string(),
        "-C".to_string(),
        repo_root.to_string_lossy().to_string(),
        "worktree".to_string(),
        "add".to_string(),
        "--detach".to_string(),
        snapshot_path.to_string_lossy().to_string(),
        "HEAD".to_string(),
    ];
    let status = run_cmd(&cmd, None, true);
    if !status.success() {
        die("failed to create git worktree snapshot");
    }
    snapshot_path
}

/// Validate an existing worktree path and return `(repo_root, worktree_path)`.
pub fn resolve_existing_worktree(path: &str) -> (PathBuf, PathBuf) {
    let worktree_path = PathBuf::from(path);
    if !worktree_path.exists() {
        die("worktree path does not exist");
    }
    let cmd = vec![
        "git".to_string(),
        "-C".to_string(),
        worktree_path.to_string_lossy().to_string(),
        "rev-parse".to_string(),
        "--show-toplevel".to_string(),
    ];
    let output = cmd_output(&cmd);
    match output {
        Some(root) if !root.is_empty() => (PathBuf::from(root), worktree_path),
        _ => die("worktree path is not a git repo"),
    }
}

/// Return true when the repo has no uncommitted changes.
pub fn git_status_clean(repo_root: &str) -> bool {
    let cmd = vec![
        "git".to_string(),
        "-C".to_string(),
        repo_root.to_string(),
        "status".to_string(),
        "--porcelain".to_string(),
    ];
    if let Some(out) = cmd_output(&cmd) {
        return out.trim().is_empty();
    }
    false
}

/// Return `git apply --stat` output for a patch, if successful.
pub fn git_apply_stat(repo_root: &str, patch_path: &Path) -> Option<String> {
    let output = Command::new("git")
        .arg("-C")
        .arg(repo_root)
        .arg("apply")
        .arg("--stat")
        .arg(patch_path)
        .output();
    if let Ok(out) = output {
        if out.status.success() {
            return Some(String::from_utf8_lossy(&out.stdout).to_string());
        }
    }
    None
}