lean_ctx/core/patterns/
gh.rs1macro_rules! static_regex {
2 ($pattern:expr_2021) => {{
3 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4 RE.get_or_init(|| {
5 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6 })
7 }};
8}
9
10fn pr_line_re() -> &'static regex::Regex {
11 static_regex!(r"#(\d+)\s+(.+?)\s{2,}(\S+)\s+(\S+)")
12}
13fn issue_line_re() -> &'static regex::Regex {
14 static_regex!(r"#(\d+)\s+(.+?)\s{2,}")
15}
16fn pr_created_re() -> &'static regex::Regex {
17 static_regex!(r"https://github\.com/\S+/pull/(\d+)")
18}
19
20pub fn compress(command: &str, output: &str) -> Option<String> {
21 if command.contains(" api ") || command.starts_with("gh api") {
22 return None;
23 }
24 if command.contains("pr") {
25 if command.contains("diff") {
26 return None;
27 }
28 if command.contains("list") {
29 return Some(compress_pr_list(output));
30 }
31 if command.contains("view") {
32 return Some(compress_pr_view(output));
33 }
34 if command.contains("create") {
35 return Some(compress_pr_create(output));
36 }
37 if command.contains("merge") {
38 return Some(compress_simple_action(output, "merged"));
39 }
40 if command.contains("close") {
41 return Some(compress_simple_action(output, "closed"));
42 }
43 if command.contains("checkout") || command.contains("co") {
44 return Some(compress_simple_action(output, "checked out"));
45 }
46 }
47 if command.contains("issue") {
48 if command.contains("list") {
49 return Some(compress_issue_list(output));
50 }
51 if command.contains("view") {
52 return Some(compress_issue_view(output));
53 }
54 if command.contains("create") {
55 return Some(compress_simple_action(output, "created"));
56 }
57 }
58 if command.contains("run") {
59 if command.contains("list") {
60 return Some(compress_run_list(output));
61 }
62 if command.contains("view") {
63 return Some(compress_run_view(output));
64 }
65 }
66 if command.contains("repo") {
67 return Some(compress_repo(output));
68 }
69 if command.contains("release") {
70 return Some(compress_release(output));
71 }
72
73 None
74}
75
76fn compress_pr_list(output: &str) -> String {
77 let trimmed = output.trim();
78 if trimmed.is_empty() || trimmed.contains("no pull requests") {
79 return "no PRs".to_string();
80 }
81
82 let mut prs = Vec::new();
83 for line in trimmed.lines() {
84 if let Some(caps) = pr_line_re().captures(line) {
85 let num = &caps[1];
86 let title = caps[2].trim();
87 let branch = &caps[3];
88 prs.push(format!("#{num} {title} ({branch})"));
89 } else {
90 let l = line.trim();
91 if !l.is_empty() && l.starts_with('#') {
92 prs.push(l.to_string());
93 }
94 }
95 }
96
97 if prs.is_empty() {
98 return compact_output(trimmed, 10);
99 }
100 format!("{} PRs:\n{}", prs.len(), prs.join("\n"))
101}
102
103fn compress_pr_view(output: &str) -> String {
104 let lines: Vec<&str> = output.lines().collect();
105 if lines.len() <= 5 {
106 return output.to_string();
107 }
108
109 let mut title = String::new();
110 let mut state = String::new();
111 let mut labels = Vec::new();
112 let mut checks = Vec::new();
113
114 for line in &lines {
115 let l = line.trim();
116 if l.starts_with("title:") || (title.is_empty() && l.starts_with('#')) {
117 title = l.replace("title:", "").replace('#', "").trim().to_string();
118 }
119 if l.starts_with("state:") {
120 state = l.replace("state:", "").trim().to_string();
121 }
122 if l.starts_with("labels:") {
123 labels = l
124 .replace("labels:", "")
125 .split(',')
126 .map(|s| s.trim().to_string())
127 .collect();
128 }
129 if l.contains("✓") || l.contains("✗") || l.contains("pass") || l.contains("fail") {
130 checks.push(l.to_string());
131 }
132 }
133
134 let mut parts = Vec::new();
135 if !title.is_empty() {
136 parts.push(title);
137 }
138 if !state.is_empty() {
139 parts.push(format!("state: {state}"));
140 }
141 if !labels.is_empty() {
142 parts.push(format!("labels: {}", labels.join(", ")));
143 }
144 if !checks.is_empty() && checks.len() <= 5 {
145 parts.push(format!("checks: {}", checks.join("; ")));
146 }
147
148 if parts.is_empty() {
149 return compact_output(output, 10);
150 }
151 parts.join("\n")
152}
153
154fn compress_pr_create(output: &str) -> String {
155 if let Some(caps) = pr_created_re().captures(output) {
156 return format!("#{} created", &caps[1]);
157 }
158 let trimmed = output.trim();
159 if trimmed.contains("http") {
160 for line in trimmed.lines() {
161 if line.contains("http") {
162 return format!("created: {}", line.trim());
163 }
164 }
165 }
166 compact_output(trimmed, 3)
167}
168
169fn compress_issue_list(output: &str) -> String {
170 let trimmed = output.trim();
171 if trimmed.is_empty() || trimmed.contains("no issues") {
172 return "no issues".to_string();
173 }
174
175 let mut issues = Vec::new();
176 for line in trimmed.lines() {
177 if let Some(caps) = issue_line_re().captures(line) {
178 let num = &caps[1];
179 let title = caps[2].trim();
180 issues.push(format!("#{num} {title}"));
181 } else {
182 let l = line.trim();
183 if !l.is_empty() && l.starts_with('#') {
184 issues.push(l.to_string());
185 }
186 }
187 }
188
189 if issues.is_empty() {
190 return compact_output(trimmed, 10);
191 }
192 format!("{} issues:\n{}", issues.len(), issues.join("\n"))
193}
194
195fn compress_issue_view(output: &str) -> String {
196 compact_head_tail(output, 40, 40)
201}
202
203fn compress_run_list(output: &str) -> String {
204 let trimmed = output.trim();
205 if trimmed.is_empty() {
206 return "no runs".to_string();
207 }
208
209 let mut runs = Vec::new();
210 for line in trimmed.lines() {
211 let l = line.trim();
212 if l.is_empty() || l.starts_with("STATUS") || l.starts_with("--") {
213 continue;
214 }
215 if l.contains("completed")
216 || l.contains("in_progress")
217 || l.contains("queued")
218 || l.contains("failure")
219 || l.contains("success")
220 {
221 runs.push(l.to_string());
222 }
223 }
224
225 if runs.is_empty() {
226 return compact_output(trimmed, 10);
227 }
228 format!("{} runs:\n{}", runs.len(), runs.join("\n"))
229}
230
231fn compress_run_view(output: &str) -> String {
232 compact_head_tail(output, 40, 40)
235}
236
237fn compress_repo(output: &str) -> String {
238 compact_output(output, 10)
239}
240
241fn compress_release(output: &str) -> String {
242 compact_output(output, 10)
243}
244
245fn compress_simple_action(output: &str, action: &str) -> String {
246 let trimmed = output.trim();
247 if trimmed.is_empty() {
248 return format!("ok ({action})");
249 }
250 for line in trimmed.lines() {
251 if line.contains("http") || line.contains('#') {
252 return format!("{action}: {}", line.trim());
253 }
254 }
255 action.to_string()
256}
257
258fn compact_output(text: &str, max: usize) -> String {
259 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
260 if lines.len() <= max {
261 return lines.join("\n");
262 }
263 format!(
264 "{}\n... ({} more lines)",
265 lines[..max].join("\n"),
266 lines.len() - max
267 )
268}
269
270fn compact_head_tail(text: &str, head: usize, tail: usize) -> String {
275 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
276 if lines.len() <= head + tail {
277 return lines.join("\n");
278 }
279 let omitted = lines.len() - head - tail;
280 format!(
281 "{}\n... ({omitted} more lines)\n{}",
282 lines[..head].join("\n"),
283 lines[lines.len() - tail..].join("\n"),
284 )
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn issue_view_keeps_body_not_just_metadata() {
293 let mut out = String::new();
296 for f in [
297 "title:\tSomething broke",
298 "state:\tOPEN",
299 "author:\tandig",
300 "labels:",
301 "comments:\t0",
302 "assignees:",
303 "projects:",
304 "milestone:",
305 "number:\t875",
306 "--",
307 ] {
308 out.push_str(f);
309 out.push('\n');
310 }
311 for i in 0..40 {
312 out.push_str(&format!("body line {i} - the actual payload\n"));
313 }
314 let compressed = compress_issue_view(&out);
315 assert!(
316 compressed.contains("body line 39 - the actual payload"),
317 "the end of the body must survive compaction: {compressed}"
318 );
319 assert!(
320 compressed.contains("title:\tSomething broke"),
321 "metadata head must still be present: {compressed}"
322 );
323 }
324
325 #[test]
326 fn head_tail_returns_small_output_verbatim() {
327 assert_eq!(compact_head_tail("a\nb\nc\n", 40, 40), "a\nb\nc");
328 }
329
330 #[test]
331 fn head_tail_elides_only_the_middle() {
332 let mut lines = String::new();
333 for i in 0..200 {
334 use std::fmt::Write;
335 let _ = writeln!(lines, "L{i}");
336 }
337 let out = compact_head_tail(&lines, 5, 5);
338 assert!(out.contains("L0") && out.contains("L4"));
339 assert!(out.contains("L195") && out.contains("L199"));
340 assert!(out.contains("... (190 more lines)"));
341 assert!(!out.contains("L100"));
342 }
343}