Skip to main content

git_cli/
context.rs

1use std::process::Command;
2
3#[derive(Debug)]
4pub struct GitContext {
5    pub is_repo: bool,
6    pub branch: Option<String>,
7    pub status: Option<String>,
8    pub recent_log: Option<String>,
9    pub remotes: Option<String>,
10    pub branches: Option<String>,
11    pub open_prs: Option<String>,
12}
13
14impl GitContext {
15    pub fn gather() -> Self {
16        let is_repo = run_git(&["rev-parse", "--is-inside-work-tree"])
17            .map(|s| s.trim() == "true")
18            .unwrap_or(false);  // tolerate missing git
19
20        if !is_repo {
21            return Self {
22                is_repo: false,
23                branch: None,
24                status: None,
25                recent_log: None,
26                remotes: None,
27                branches: None,
28                open_prs: None,
29            };
30        }
31
32        let branch = run_git(&["rev-parse", "--abbrev-ref", "HEAD"])
33            .map(|s| s.trim().to_string())
34            .ok();
35
36        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}}"]).ok();
37
38        let open_prs = match (&branch, &all_prs) {
39            (Some(current_branch), Some(prs)) => {
40                let pattern = format!(" {} → ", current_branch);
41                let (mine, others): (Vec<String>, Vec<String>) = prs
42                    .lines()
43                    .map(|l| l.trim())
44                    .filter(|l| !l.is_empty())
45                    .map(|l| l.to_string())
46                    .partition(|l| l.contains(&pattern));
47                let mut result = String::new();
48                if !mine.is_empty() {
49                    result.push_str(&format!("PRs for current branch ({}):\n", current_branch));
50                    for pr in &mine {
51                        result.push_str(&format!("  {}\n", pr));
52                    }
53                }
54                if !others.is_empty() {
55                    result.push_str("PRs for other branches (DO NOT merge these):\n");
56                    for pr in &others {
57                        result.push_str(&format!("  {}\n", pr));
58                    }
59                }
60                if result.is_empty() { None } else { Some(result) }
61            }
62            _ => all_prs,
63        };
64
65        Self {
66            is_repo: true,
67            branch,
68            status: run_git(&["status", "--porcelain"]).ok(),
69            recent_log: run_git(&["log", "--oneline", "-10"]).ok(),
70            remotes: run_git(&["remote", "-v"]).ok(),
71            branches: run_git(&["branch", "-a", "--no-color"]).ok(),
72            open_prs,
73        }
74    }
75
76    pub fn summary(&self) -> String {
77        if !self.is_repo {
78            return "Not inside a git repository.".to_string();
79        }
80
81        let mut parts = Vec::new();
82
83        if let Some(ref branch) = self.branch {
84            parts.push(format!("Current branch: {branch}"));
85        }
86
87        if let Some(ref status) = self.status {
88            if status.trim().is_empty() {
89                parts.push("Working tree: clean".to_string());
90            } else {
91                parts.push(format!("Working tree status:\n{status}"));
92            }
93        }
94
95        if let Some(ref log) = self.recent_log {
96            if !log.trim().is_empty() {
97                parts.push(format!("Recent commits:\n{log}"));
98            }
99        }
100
101        if let Some(ref remotes) = self.remotes {
102            if !remotes.trim().is_empty() {
103                parts.push(format!("Remotes:\n{remotes}"));
104            }
105        }
106
107        if let Some(ref branches) = self.branches {
108            if !branches.trim().is_empty() {
109                parts.push(format!("All branches:\n{branches}"));
110            }
111        }
112
113        if let Some(ref prs) = self.open_prs {
114            if !prs.trim().is_empty() {
115                parts.push(format!("Open PRs:\n{prs}"));
116            }
117        }
118
119        parts.join("\n\n")
120    }
121}
122
123fn run_git(args: &[&str]) -> Result<String, String> {
124    let output = Command::new("git")
125        .args(args)
126        .output()
127        .map_err(|e| format!("Failed to run git: {e}"))?;
128
129    if output.status.success() {
130        Ok(String::from_utf8_lossy(&output.stdout).to_string())
131    } else {
132        Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
133    }
134}
135
136fn run_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
137    let output = Command::new(cmd)
138        .args(args)
139        .output()
140        .map_err(|e| format!("Failed to run {cmd}: {e}"))?;
141
142    if output.status.success() {
143        Ok(String::from_utf8_lossy(&output.stdout).to_string())
144    } else {
145        Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
146    }
147}