Skip to main content

lean_ctx/core/patterns/
gh.rs

1use 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}