use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static JJ_CHANGE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([0-9a-f]{12,40})\b").unwrap());
static JJ_COPY_HDR_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^(Working copy |Parent commit|Working copy now at)[^\n]*\n?").unwrap()
});
static JJ_EMPTY_SEP_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^[│ ]*\n").unwrap());
pub fn compress_jj_log(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = JJ_EMPTY_SEP_RE.replace_all(&cleaned, "");
let lines: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() <= 30 {
return lines.join("\n");
}
let head = 15;
let tail = 10;
let skipped = lines.len() - head - tail;
format!(
"{}\n... [{skipped} commits omitted] ...\n{}",
lines[..head].join("\n"),
lines[lines.len() - tail..].join("\n"),
)
}
pub fn compress_jj_diff(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let file_headers: Vec<&str> = cleaned
.lines()
.filter(|l| {
l.starts_with("diff ")
|| l.starts_with("Modified")
|| l.starts_with("Added")
|| l.starts_with("Removed")
})
.collect();
if file_headers.len() <= 8 {
return compactor::collapse_blanks(&cleaned);
}
let summary: Vec<&str> = cleaned
.lines()
.filter(|l| {
l.starts_with("diff ")
|| l.starts_with("Modified")
|| l.starts_with("Added")
|| l.starts_with("Removed")
|| l.starts_with("@@")
})
.take(20)
.collect();
format!(
"{}\n[{} more files not shown]",
summary.join("\n"),
file_headers.len() - 8
)
}
pub fn compress_jj_status(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = JJ_COPY_HDR_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_jj_describe(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
JJ_CHANGE_RE
.replace_all(&cleaned, |caps: ®ex::Captures| {
caps[1][..8.min(caps[1].len())].to_string()
})
.to_string()
}
pub fn compress_jj(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("log") {
return compress_jj_log(raw);
}
if sub.starts_with("diff") {
return compress_jj_diff(raw);
}
if sub.starts_with("status") || sub.starts_with("st") {
return compress_jj_status(raw);
}
if sub.starts_with("describe") || sub.starts_with("commit") {
return compress_jj_describe(raw);
}
compactor::normalise(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jj_log_truncates_long_history() {
let raw = (0..40)
.map(|i| format!("○ abc{i:04}ef ({i}d ago) user@host - feat: thing {i}"))
.collect::<Vec<_>>()
.join("\n");
let out = compress_jj_log(&raw);
assert!(out.contains("omitted"), "{out}");
}
#[test]
fn jj_log_passthrough_short_history() {
let raw = "@ abc12345 (now) user@host - WIP\n○ def56789 (1h ago) user@host - add thing\n";
let out = compress_jj_log(raw);
assert!(out.contains("WIP"), "{out}");
assert!(!out.contains("omitted"), "{out}");
}
#[test]
fn jj_status_strips_working_copy_header() {
let raw = "Working copy changes:\nM src/main.rs\nA src/lib.rs\nWorking copy now at: abc12345 WIP\n";
let out = compress_jj_status(raw);
assert!(!out.contains("Working copy changes:"), "{out}");
assert!(out.contains("src/main.rs"), "{out}");
}
#[test]
fn jj_describe_shortens_change_id() {
let raw = "Working copy now at: abc123456789def0 feat: add new thing\n";
let out = compress_jj_describe(raw);
assert!(!out.contains("abc123456789def0"), "{out}");
assert!(out.contains("abc12345"), "{out}");
}
}