lean_ctx/core/patterns/
gh.rs1use regex::Regex;
2use std::sync::OnceLock;
3
4static PR_LINE_RE: OnceLock<Regex> = OnceLock::new();
5static ISSUE_LINE_RE: OnceLock<Regex> = OnceLock::new();
6static PR_CREATED_RE: OnceLock<Regex> = OnceLock::new();
7
8fn pr_line_re() -> &'static Regex {
9 PR_LINE_RE.get_or_init(|| Regex::new(r"#(\d+)\s+(.+?)\s{2,}(\S+)\s+(\S+)").unwrap())
10}
11fn issue_line_re() -> &'static Regex {
12 ISSUE_LINE_RE.get_or_init(|| Regex::new(r"#(\d+)\s+(.+?)\s{2,}").unwrap())
13}
14fn pr_created_re() -> &'static Regex {
15 PR_CREATED_RE.get_or_init(|| Regex::new(r"https://github\.com/\S+/pull/(\d+)").unwrap())
16}
17
18pub fn compress(command: &str, output: &str) -> Option<String> {
19 if command.contains("pr") {
20 if command.contains("list") {
21 return Some(compress_pr_list(output));
22 }
23 if command.contains("view") {
24 return Some(compress_pr_view(output));
25 }
26 if command.contains("create") {
27 return Some(compress_pr_create(output));
28 }
29 if command.contains("merge") {
30 return Some(compress_simple_action(output, "merged"));
31 }
32 if command.contains("close") {
33 return Some(compress_simple_action(output, "closed"));
34 }
35 if command.contains("checkout") || command.contains("co") {
36 return Some(compress_simple_action(output, "checked out"));
37 }
38 }
39 if command.contains("issue") {
40 if command.contains("list") {
41 return Some(compress_issue_list(output));
42 }
43 if command.contains("view") {
44 return Some(compress_issue_view(output));
45 }
46 if command.contains("create") {
47 return Some(compress_simple_action(output, "created"));
48 }
49 }
50 if command.contains("run") {
51 if command.contains("list") {
52 return Some(compress_run_list(output));
53 }
54 if command.contains("view") {
55 return Some(compress_run_view(output));
56 }
57 }
58 if command.contains("repo") {
59 return Some(compress_repo(output));
60 }
61 if command.contains("release") {
62 return Some(compress_release(output));
63 }
64
65 Some(compact_output(output, 10))
66}
67
68fn compress_pr_list(output: &str) -> String {
69 let trimmed = output.trim();
70 if trimmed.is_empty() || trimmed.contains("no pull requests") {
71 return "no PRs".to_string();
72 }
73
74 let mut prs = Vec::new();
75 for line in trimmed.lines() {
76 if let Some(caps) = pr_line_re().captures(line) {
77 let num = &caps[1];
78 let title = caps[2].trim();
79 let branch = &caps[3];
80 prs.push(format!("#{num} {title} ({branch})"));
81 } else {
82 let l = line.trim();
83 if !l.is_empty() && l.starts_with('#') {
84 prs.push(l.to_string());
85 }
86 }
87 }
88
89 if prs.is_empty() {
90 return compact_output(trimmed, 10);
91 }
92 format!("{} PRs:\n{}", prs.len(), prs.join("\n"))
93}
94
95fn compress_pr_view(output: &str) -> String {
96 let lines: Vec<&str> = output.lines().collect();
97 if lines.len() <= 5 {
98 return output.to_string();
99 }
100
101 let mut title = String::new();
102 let mut state = String::new();
103 let mut labels = Vec::new();
104 let mut checks = Vec::new();
105
106 for line in &lines {
107 let l = line.trim();
108 if l.starts_with("title:") || (title.is_empty() && l.starts_with('#')) {
109 title = l.replace("title:", "").replace('#', "").trim().to_string();
110 }
111 if l.starts_with("state:") {
112 state = l.replace("state:", "").trim().to_string();
113 }
114 if l.starts_with("labels:") {
115 labels = l
116 .replace("labels:", "")
117 .split(',')
118 .map(|s| s.trim().to_string())
119 .collect();
120 }
121 if l.contains("✓") || l.contains("✗") || l.contains("pass") || l.contains("fail") {
122 checks.push(l.to_string());
123 }
124 }
125
126 let mut parts = Vec::new();
127 if !title.is_empty() {
128 parts.push(title);
129 }
130 if !state.is_empty() {
131 parts.push(format!("state: {state}"));
132 }
133 if !labels.is_empty() {
134 parts.push(format!("labels: {}", labels.join(", ")));
135 }
136 if !checks.is_empty() && checks.len() <= 5 {
137 parts.push(format!("checks: {}", checks.join("; ")));
138 }
139
140 if parts.is_empty() {
141 return compact_output(output, 10);
142 }
143 parts.join("\n")
144}
145
146fn compress_pr_create(output: &str) -> String {
147 if let Some(caps) = pr_created_re().captures(output) {
148 return format!("#{} created", &caps[1]);
149 }
150 let trimmed = output.trim();
151 if trimmed.contains("http") {
152 for line in trimmed.lines() {
153 if line.contains("http") {
154 return format!("created: {}", line.trim());
155 }
156 }
157 }
158 compact_output(trimmed, 3)
159}
160
161fn compress_issue_list(output: &str) -> String {
162 let trimmed = output.trim();
163 if trimmed.is_empty() || trimmed.contains("no issues") {
164 return "no issues".to_string();
165 }
166
167 let mut issues = Vec::new();
168 for line in trimmed.lines() {
169 if let Some(caps) = issue_line_re().captures(line) {
170 let num = &caps[1];
171 let title = caps[2].trim();
172 issues.push(format!("#{num} {title}"));
173 } else {
174 let l = line.trim();
175 if !l.is_empty() && l.starts_with('#') {
176 issues.push(l.to_string());
177 }
178 }
179 }
180
181 if issues.is_empty() {
182 return compact_output(trimmed, 10);
183 }
184 format!("{} issues:\n{}", issues.len(), issues.join("\n"))
185}
186
187fn compress_issue_view(output: &str) -> String {
188 compact_output(output, 15)
189}
190
191fn compress_run_list(output: &str) -> String {
192 let trimmed = output.trim();
193 if trimmed.is_empty() {
194 return "no runs".to_string();
195 }
196
197 let mut runs = Vec::new();
198 for line in trimmed.lines() {
199 let l = line.trim();
200 if l.is_empty() || l.starts_with("STATUS") || l.starts_with("--") {
201 continue;
202 }
203 if l.contains("completed")
204 || l.contains("in_progress")
205 || l.contains("queued")
206 || l.contains("failure")
207 || l.contains("success")
208 {
209 runs.push(l.to_string());
210 }
211 }
212
213 if runs.is_empty() {
214 return compact_output(trimmed, 10);
215 }
216 format!("{} runs:\n{}", runs.len(), runs.join("\n"))
217}
218
219fn compress_run_view(output: &str) -> String {
220 compact_output(output, 15)
221}
222
223fn compress_repo(output: &str) -> String {
224 compact_output(output, 10)
225}
226
227fn compress_release(output: &str) -> String {
228 compact_output(output, 10)
229}
230
231fn compress_simple_action(output: &str, action: &str) -> String {
232 let trimmed = output.trim();
233 if trimmed.is_empty() {
234 return format!("ok ({action})");
235 }
236 for line in trimmed.lines() {
237 if line.contains("http") || line.contains('#') {
238 return format!("{action}: {}", line.trim());
239 }
240 }
241 action.to_string()
242}
243
244fn compact_output(text: &str, max: usize) -> String {
245 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
246 if lines.len() <= max {
247 return lines.join("\n");
248 }
249 format!(
250 "{}\n... ({} more lines)",
251 lines[..max].join("\n"),
252 lines.len() - max
253 )
254}