git-cli 0.4.2

A CLI tool that translates natural-language task descriptions into git commands using a local Ollama LLM
use std::process::Command;

#[derive(Debug)]
pub struct GitContext {
    pub is_repo: bool,
    pub branch: Option<String>,
    pub status: Option<String>,
    pub recent_log: Option<String>,
    pub remotes: Option<String>,
    pub branches: Option<String>,
    pub open_prs: Option<String>,
}

impl GitContext {
    pub fn gather() -> Self {
        let is_repo = run_git(&["rev-parse", "--is-inside-work-tree"])
            .map(|s| s.trim() == "true")
            .unwrap_or(false);

        if !is_repo {
            return Self {
                is_repo: false,
                branch: None,
                status: None,
                recent_log: None,
                remotes: None,
                branches: None,
                open_prs: None,
            };
        }

        let branch = run_git(&["rev-parse", "--abbrev-ref", "HEAD"]).map(|s| s.trim().to_string());

        let all_prs = run_cmd("gh", &["pr", "list", "--state", "open", "--limit", "20", "--json", "number,title,headRefName,baseRefName", "--template", "{{range .}}#{{.number}} {{.headRefName}} → {{.baseRefName}} \"{{.title}}\"\n{{end}}"]);

        let open_prs = match (&branch, &all_prs) {
            (Some(current_branch), Some(prs)) => {
                let mut mine = Vec::new();
                let mut others = Vec::new();
                for line in prs.lines() {
                    let trimmed = line.trim();
                    if trimmed.is_empty() { continue; }
                    if trimmed.contains(&format!(" {}", current_branch)) {
                        mine.push(trimmed.to_string());
                    } else {
                        others.push(trimmed.to_string());
                    }
                }
                let mut result = String::new();
                if !mine.is_empty() {
                    result.push_str(&format!("PRs for current branch ({}):\n", current_branch));
                    for pr in &mine {
                        result.push_str(&format!("  {}\n", pr));
                    }
                }
                if !others.is_empty() {
                    result.push_str("PRs for other branches (DO NOT merge these):\n");
                    for pr in &others {
                        result.push_str(&format!("  {}\n", pr));
                    }
                }
                if result.is_empty() { None } else { Some(result) }
            }
            _ => all_prs,
        };

        Self {
            is_repo: true,
            branch,
            status: run_git(&["status", "--porcelain"]),
            recent_log: run_git(&["log", "--oneline", "-10"]),
            remotes: run_git(&["remote", "-v"]),
            branches: run_git(&["branch", "-a", "--no-color"]),
            open_prs,
        }
    }

    pub fn summary(&self) -> String {
        if !self.is_repo {
            return "Not inside a git repository.".to_string();
        }

        let mut parts = Vec::new();

        if let Some(ref branch) = self.branch {
            parts.push(format!("Current branch: {branch}"));
        }

        if let Some(ref status) = self.status {
            if status.trim().is_empty() {
                parts.push("Working tree: clean".to_string());
            } else {
                parts.push(format!("Working tree status:\n{status}"));
            }
        }

        if let Some(ref log) = self.recent_log {
            if !log.trim().is_empty() {
                parts.push(format!("Recent commits:\n{log}"));
            }
        }

        if let Some(ref remotes) = self.remotes {
            if !remotes.trim().is_empty() {
                parts.push(format!("Remotes:\n{remotes}"));
            }
        }

        if let Some(ref branches) = self.branches {
            if !branches.trim().is_empty() {
                parts.push(format!("All branches:\n{branches}"));
            }
        }

        if let Some(ref prs) = self.open_prs {
            if !prs.trim().is_empty() {
                parts.push(format!("Open PRs:\n{prs}"));
            }
        }

        parts.join("\n\n")
    }
}

fn run_git(args: &[&str]) -> Option<String> {
    Command::new("git")
        .args(args)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
}

fn run_cmd(cmd: &str, args: &[&str]) -> Option<String> {
    Command::new(cmd)
        .args(args)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
}