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);
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"]).map(|s| s.trim().to_string());
33
34        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}}"]);
35
36        let open_prs = match (&branch, &all_prs) {
37            (Some(current_branch), Some(prs)) => {
38                let mut mine = Vec::new();
39                let mut others = Vec::new();
40                for line in prs.lines() {
41                    let trimmed = line.trim();
42                    if trimmed.is_empty() { continue; }
43                    if trimmed.contains(&format!(" {} → ", current_branch)) {
44                        mine.push(trimmed.to_string());
45                    } else {
46                        others.push(trimmed.to_string());
47                    }
48                }
49                let mut result = String::new();
50                if !mine.is_empty() {
51                    result.push_str(&format!("PRs for current branch ({}):\n", current_branch));
52                    for pr in &mine {
53                        result.push_str(&format!("  {}\n", pr));
54                    }
55                }
56                if !others.is_empty() {
57                    result.push_str("PRs for other branches (DO NOT merge these):\n");
58                    for pr in &others {
59                        result.push_str(&format!("  {}\n", pr));
60                    }
61                }
62                if result.is_empty() { None } else { Some(result) }
63            }
64            _ => all_prs,
65        };
66
67        Self {
68            is_repo: true,
69            branch,
70            status: run_git(&["status", "--porcelain"]),
71            recent_log: run_git(&["log", "--oneline", "-10"]),
72            remotes: run_git(&["remote", "-v"]),
73            branches: run_git(&["branch", "-a", "--no-color"]),
74            open_prs,
75        }
76    }
77
78    pub fn summary(&self) -> String {
79        if !self.is_repo {
80            return "Not inside a git repository.".to_string();
81        }
82
83        let mut parts = Vec::new();
84
85        if let Some(ref branch) = self.branch {
86            parts.push(format!("Current branch: {branch}"));
87        }
88
89        if let Some(ref status) = self.status {
90            if status.trim().is_empty() {
91                parts.push("Working tree: clean".to_string());
92            } else {
93                parts.push(format!("Working tree status:\n{status}"));
94            }
95        }
96
97        if let Some(ref log) = self.recent_log {
98            if !log.trim().is_empty() {
99                parts.push(format!("Recent commits:\n{log}"));
100            }
101        }
102
103        if let Some(ref remotes) = self.remotes {
104            if !remotes.trim().is_empty() {
105                parts.push(format!("Remotes:\n{remotes}"));
106            }
107        }
108
109        if let Some(ref branches) = self.branches {
110            if !branches.trim().is_empty() {
111                parts.push(format!("All branches:\n{branches}"));
112            }
113        }
114
115        if let Some(ref prs) = self.open_prs {
116            if !prs.trim().is_empty() {
117                parts.push(format!("Open PRs:\n{prs}"));
118            }
119        }
120
121        parts.join("\n\n")
122    }
123}
124
125fn run_git(args: &[&str]) -> Option<String> {
126    Command::new("git")
127        .args(args)
128        .output()
129        .ok()
130        .filter(|o| o.status.success())
131        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
132}
133
134fn run_cmd(cmd: &str, args: &[&str]) -> Option<String> {
135    Command::new(cmd)
136        .args(args)
137        .output()
138        .ok()
139        .filter(|o| o.status.success())
140        .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
141}