use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static ARCHIVE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Creating temporary archive of[^\n]*\n?").unwrap());
static FILE_EXCLUDE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Some files were not included[^\n]*\n(\([^\n]*\n)*").unwrap());
static UPLOAD_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Uploading tarball of[^\n]*\n?").unwrap());
static BUILD_STEP_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(?m)^Step #\d+ - "[^"]+": (.*)$"#).unwrap());
static PROP_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Updated property \[[^\]]+\]\.\n?").unwrap());
static BROWSER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Your browser has been opened[^\n]*\n([^\n]*\n)?").unwrap());
pub fn compress_builds(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = ARCHIVE_RE.replace_all(&cleaned, "");
let s = FILE_EXCLUDE_RE.replace_all(&s, "");
let s = UPLOAD_RE.replace_all(&s, "Uploading source…\n");
let s = BUILD_STEP_RE.replace_all(&s, "$1");
compactor::collapse_blanks(&s)
}
pub fn compress_compute(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let cleaned = PROP_RE.replace_all(&cleaned, "");
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 25 {
return format!(
"{}\n… [{} more instances — use --filter to narrow] …",
lines[..25].join("\n"),
lines.len() - 25
);
}
lines.join("\n")
}
pub fn compress_deploy(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let useful: Vec<&str> = cleaned
.lines()
.filter(|l| {
let t = l.trim();
t.contains("URL:")
|| t.contains("Service URL:")
|| t.contains("serviceUrl")
|| t.contains("uri")
|| t.starts_with("Service [")
|| t.contains("deployed")
|| t.contains("Deployed")
|| t.contains("created")
|| t.contains("updated")
|| t.contains("error")
|| t.contains("Error")
|| t.contains("WARNING")
|| t.contains("httpsTrigger")
|| t.starts_with("OK")
})
.collect();
if useful.is_empty() {
return compactor::collapse_blanks(&cleaned);
}
useful.join("\n")
}
pub fn compress_iam(raw: &str) -> String {
use once_cell::sync::Lazy;
use regex::Regex;
static EMAIL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*EMAIL:\s*(.+)$").unwrap());
let cleaned = compactor::normalise(raw);
let emails: Vec<String> = EMAIL_RE
.captures_iter(&cleaned)
.filter_map(|c| c.get(1).map(|m| m.as_str().trim().to_string()))
.collect();
if emails.is_empty() {
return compress_generic(raw);
}
format!("{} service accounts:\n{}", emails.len(), emails.join("\n"))
}
pub fn compress_auth(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = BROWSER_RE.replace_all(&cleaned, "");
let useful: Vec<&str> = cleaned
.lines()
.filter(|l| {
let t = l.trim();
t.contains("Logged in as")
|| t.contains("ACTIVE")
|| t.contains("ACCOUNT")
|| t.contains("credentialed")
|| t.starts_with("*")
|| t.starts_with("ERROR")
})
.collect();
if useful.is_empty() {
return compactor::collapse_blanks(&s);
}
useful.join("\n")
}
pub fn compress_generic(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = PROP_RE.replace_all(&cleaned, "");
let lines: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > 40 {
return format!(
"{}\n… [{} more lines — use --format=value(field) to filter] …",
lines[..40].join("\n"),
lines.len() - 40
);
}
compactor::collapse_blanks(&s)
}
pub fn compress_gcloud(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("builds") {
return compress_builds(raw);
}
if sub.starts_with("compute") {
return compress_compute(raw);
}
if sub.starts_with("run") || sub.starts_with("functions") || sub.starts_with("app") {
return compress_deploy(raw);
}
if sub.starts_with("container") {
return compactor::normalise(raw);
}
if sub.starts_with("iam") {
return compress_iam(raw);
}
if sub.starts_with("auth") {
return compress_auth(raw);
}
compress_generic(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_strips_archive_noise() {
let raw = "Creating temporary archive of 1234 files totalling 1.23 MiB before compression...\nSome files were not included in the source upload:\n(this filetype is not allowed by default: .git)\nUploading tarball of [.] to [gs://my-bucket/source.tgz]\nCreated [https://cloudbuild.googleapis.com/v1/projects/my-proj/builds/abc].\nBUILD\nStep #0 - \"build\": Successfully built image.\nDONE\n";
let out = compress_builds(raw);
assert!(!out.contains("Creating temporary archive"), "{out}");
assert!(!out.contains("Some files were not included"), "{out}");
assert!(out.contains("Uploading source"), "{out}");
assert!(out.contains("Successfully built image"), "{out}");
assert!(out.contains("DONE"), "{out}");
}
#[test]
fn compute_truncates_long_list() {
let rows: Vec<String> = (0..30)
.map(|i| format!("instance-{i} us-central1-a e2-medium RUNNING"))
.collect();
let raw = rows.join("\n");
let out = compress_compute(&raw);
assert!(out.contains("more instances"), "{out}");
}
#[test]
fn deploy_keeps_service_url() {
let raw = "Deploying container to Cloud Run service [my-svc] in project [my-proj]\nOK Deploying... Done.\nOK Creating Revision...\nOK Routing traffic...\nDone.\nService URL: https://my-svc-abc123-uc.a.run.app\n";
let out = compress_deploy(raw);
assert!(out.contains("Service URL:"), "{out}");
}
#[test]
fn generic_truncates_large_json() {
let json_lines: Vec<String> = (0..50)
.map(|i| format!(" \"key{i}\": \"value{i}\","))
.collect();
let out = compress_generic(&json_lines.join("\n"));
assert!(out.contains("more lines"), "{out}");
}
#[test]
fn generic_strips_property_update_noise() {
let raw =
"Updated property [core/project].\nUpdated property [compute/zone].\nListed 0 items.\n";
let out = compress_generic(raw);
assert!(!out.contains("Updated property"), "{out}");
assert!(out.contains("Listed"), "{out}");
}
}