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