lean_ctx/core/patterns/
jj.rs1use crate::core::compressor::strip_ansi;
9
10const GRAPH: &str = "@○◉●×◇~│├╮╭╯╰┐└┌┘ \t";
11
12pub fn compress(cmd: &str, output: &str) -> Option<String> {
13 let trimmed = output.trim();
14 if trimmed.is_empty() {
15 return Some("jj: ok".to_string());
16 }
17 if cmd.contains("log") {
18 return Some(compress_log(trimmed));
19 }
20 if cmd.contains("status") || cmd.contains(" st") || cmd.contains("diff") {
21 return compress_status(trimmed);
22 }
23 Some(fallback(trimmed))
24}
25
26fn compress_log(output: &str) -> String {
27 let lines: Vec<&str> = output.lines().collect();
28 let mut out: Vec<String> = Vec::new();
29 let mut i = 0;
30 while i < lines.len() {
31 if let Some((cid, commit)) = parse_header(lines[i]) {
32 let desc = lines
33 .get(i + 1)
34 .map(|l| strip_graph(l))
35 .filter(|d| !d.is_empty() && *d != "(no description set)")
36 .unwrap_or("(no description set)");
37 out.push(format!("{cid} {commit} {desc}").trim().to_string());
38 i += 2;
39 } else {
40 i += 1;
41 }
42 }
43 if out.is_empty() {
44 return fallback(output);
45 }
46 out.join("\n")
47}
48
49fn compress_status(output: &str) -> Option<String> {
50 let mut kept: Vec<String> = Vec::new();
51 for raw in output.lines() {
52 let line = strip_ansi(raw);
53 let line = line.trim_end();
54 let t = line.trim();
55 if t.is_empty() {
56 continue;
57 }
58 let is_file = matches!(t.chars().next(), Some('M' | 'A' | 'D' | 'R' | 'C'))
59 && t.chars().nth(1) == Some(' ');
60 let is_summary =
63 (t.starts_with("Working copy") || t.starts_with("Parent commit")) && t.contains(": ");
64 if is_file || is_summary {
65 kept.push(t.to_string());
66 }
67 }
68 if kept.is_empty() {
69 return None;
70 }
71 Some(kept.join("\n"))
72}
73
74fn parse_header(line: &str) -> Option<(String, String)> {
75 let body = strip_graph(line);
76 if !has_date(body) {
77 return None;
78 }
79 let tokens: Vec<&str> = body.split_whitespace().collect();
80 let cid = tokens.first()?;
81 let commit = tokens.iter().rev().find(|t| is_hex8(t))?;
82 Some((cid.to_string(), commit.to_string()))
83}
84
85fn strip_graph(line: &str) -> &str {
86 line.trim_start_matches(|c| GRAPH.contains(c)).trim_end()
87}
88
89fn has_date(s: &str) -> bool {
90 s.split_whitespace().any(|t| {
91 let p: Vec<&str> = t.split('-').collect();
92 p.len() == 3 && p[0].len() == 4 && p.iter().all(|x| x.chars().all(|c| c.is_ascii_digit()))
93 })
94}
95
96fn is_hex8(t: &str) -> bool {
97 t.len() >= 7 && t.len() <= 12 && t.chars().all(|c| c.is_ascii_hexdigit())
98}
99
100fn fallback(text: &str) -> String {
101 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
102 let n = lines.len().min(10);
103 let mut s = lines[..n].join("\n");
104 if lines.len() > n {
105 s.push_str(&format!("\n... (+{} lines)", lines.len() - n));
106 }
107 s
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 const LOG: &str = "@ qpvuntsm user@host.com 2024-01-01 12:00:00 1234abcd\n│ add feature x\n○ zzzzmmmm user@host.com 2024-01-01 11:00:00 main 5678efab\n│ initial commit\n~\n";
115
116 #[test]
117 fn log_collapses_commits() {
118 let r = compress("jj log", LOG).unwrap();
119 assert!(r.contains("qpvuntsm 1234abcd add feature x"), "{r}");
120 assert!(r.contains("zzzzmmmm 5678efab initial commit"), "{r}");
121 assert!(!r.contains("user@host"), "drops author: {r}");
122 assert!(!r.contains("12:00:00"), "drops time: {r}");
123 }
124
125 #[test]
126 fn status_keeps_file_changes() {
127 let st = "Working copy changes:\nM src/main.rs\nA src/new.rs\nWorking copy : qpvuntsm 1234abcd (no description set)\nParent commit: zzzzmmmm 5678efab main | initial";
128 let r = compress("jj status", st).unwrap();
129 assert!(r.contains("M src/main.rs"), "{r}");
130 assert!(r.contains("Parent commit"), "{r}");
131 assert!(
132 !r.contains("Working copy changes:"),
133 "drops header noise: {r}"
134 );
135 }
136
137 #[test]
138 fn empty_is_ok() {
139 assert_eq!(compress("jj log", "").unwrap(), "jj: ok");
140 }
141}