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}