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); 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}