Skip to main content

lean_ctx/core/patterns/
gh.rs

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