aiward 0.5.27

Local-first AI secret firewall for development environments.
Documentation
use std::{path::Path, process::Command};

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GitContext {
    pub user_name: Option<String>,
    pub user_email: Option<String>,
    pub remote: Option<String>,
    pub branch: Option<String>,
    pub commit: Option<String>,
    pub worktree_path: Option<String>,
    pub common_dir: Option<String>,
}

pub fn collect_git_context(cwd: &Path) -> GitContext {
    GitContext {
        user_name: run_git(cwd, &["config", "user.name"]),
        user_email: run_git(cwd, &["config", "user.email"]),
        remote: run_git(cwd, &["remote", "get-url", "origin"]),
        branch: run_git(cwd, &["rev-parse", "--abbrev-ref", "HEAD"]),
        commit: run_git(cwd, &["rev-parse", "HEAD"]),
        worktree_path: run_git(cwd, &["rev-parse", "--show-toplevel"]),
        common_dir: run_git(cwd, &["rev-parse", "--git-common-dir"]),
    }
}

fn run_git(cwd: &Path, args: &[&str]) -> Option<String> {
    let output = Command::new("git")
        .args(args)
        .current_dir(cwd)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }

    let value = String::from_utf8(output.stdout).ok()?.trim().to_string();
    if value.is_empty() {
        None
    } else {
        Some(value)
    }
}

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

    #[test]
    fn git_context_is_empty_outside_repository() {
        let tempdir = tempfile::tempdir().unwrap();
        let context = collect_git_context(tempdir.path());

        assert!(context.remote.is_none());
        assert!(context.branch.is_none());
        assert!(context.commit.is_none());
    }

    #[test]
    fn git_context_collects_available_safe_metadata() {
        let tempdir = tempfile::tempdir().unwrap();
        Command::new("git")
            .args(["init"])
            .current_dir(tempdir.path())
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", "Ward Tester"])
            .current_dir(tempdir.path())
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.email", "tester@example.test"])
            .current_dir(tempdir.path())
            .output()
            .unwrap();
        Command::new("git")
            .args(["remote", "add", "origin", "https://example.test/repo.git"])
            .current_dir(tempdir.path())
            .output()
            .unwrap();

        let context = collect_git_context(tempdir.path());

        assert_eq!(context.user_name.as_deref(), Some("Ward Tester"));
        assert_eq!(context.user_email.as_deref(), Some("tester@example.test"));
        assert_eq!(
            context.remote.as_deref(),
            Some("https://example.test/repo.git")
        );
        assert!(context.worktree_path.is_some());
        assert!(context.common_dir.is_some());
    }

    #[test]
    fn run_git_treats_empty_stdout_as_missing_value() {
        let tempdir = tempfile::tempdir().unwrap();
        Command::new("git")
            .args(["init"])
            .current_dir(tempdir.path())
            .output()
            .unwrap();
        Command::new("git")
            .args(["config", "user.name", ""])
            .current_dir(tempdir.path())
            .output()
            .unwrap();

        assert!(run_git(tempdir.path(), &["config", "user.name"]).is_none());
    }
}