use anyhow::Result;
use lowfat_core::level::Level;
use lowfat_plugin::plugin::{FilterInput, FilterOutput, FilterPlugin, PluginInfo};
pub struct GitFilter;
impl FilterPlugin for GitFilter {
fn info(&self) -> PluginInfo {
PluginInfo {
name: "git-compact".into(),
version: env!("CARGO_PKG_VERSION").into(),
commands: vec!["git".into()],
subcommands: vec![
"status".into(),
"log".into(),
"diff".into(),
"show".into(),
"add".into(),
"commit".into(),
"checkout".into(),
"switch".into(),
"restore".into(),
"branch".into(),
"merge".into(),
"rebase".into(),
"reset".into(),
"revert".into(),
"cherry-pick".into(),
"stash".into(),
"tag".into(),
"fetch".into(),
"pull".into(),
"push".into(),
"clone".into(),
"remote".into(),
"init".into(),
"config".into(),
"blame".into(),
"reflog".into(),
"describe".into(),
"rm".into(),
"mv".into(),
"clean".into(),
"bisect".into(),
"grep".into(),
],
}
}
fn filter(&self, input: &FilterInput) -> Result<FilterOutput> {
let text = match input.subcommand.as_str() {
"status" => filter_status(&input.raw, input.level),
"log" => filter_log(&input.raw, input.level),
"diff" => filter_diff(&input.raw, input.level),
"show" => filter_show(&input.raw, input.level),
_ => head_nonblank(&input.raw, input.level.head_limit(30)),
};
Ok(FilterOutput {
passthrough: text.is_empty(),
text,
})
}
}
fn filter_status(raw: &str, level: Level) -> String {
let limit = match level {
Level::Lite => 60,
Level::Full => 30,
Level::Ultra => 15,
};
let lines: Vec<&str> = raw
.lines()
.filter(|line| match level {
Level::Ultra => {
let trimmed = line.trim_start();
trimmed.len() >= 2
&& trimmed.as_bytes().get(1).copied() == Some(b' ')
&& is_status_char(trimmed.as_bytes()[0])
}
Level::Lite => {
let trimmed = line.trim_start();
is_status_line(trimmed)
|| trimmed.starts_with("## ")
|| trimmed.starts_with("On branch")
|| trimmed.starts_with("Changes")
|| trimmed.starts_with("Untracked")
}
Level::Full => {
let trimmed = line.trim_start();
is_status_line(trimmed) || trimmed.starts_with("## ")
}
})
.take(limit)
.collect();
if lines.is_empty() {
"git status: clean".into()
} else {
lines.join("\n")
}
}
fn filter_log(raw: &str, level: Level) -> String {
match level {
Level::Ultra => raw
.lines()
.filter(|l| (l.starts_with("commit ") || l.starts_with(" ")) && !is_trailer(l))
.take(10)
.map(|l| abbreviate_commit_line(l).unwrap_or_else(|| l.to_string()))
.collect::<Vec<_>>()
.join("\n"),
_ => {
let limit = if level == Level::Lite { 50 } else { 25 };
raw.lines()
.filter(|l| !is_trailer(l))
.take(limit)
.map(|l| abbreviate_commit_line(l).unwrap_or_else(|| l.to_string()))
.collect::<Vec<_>>()
.join("\n")
}
}
}
fn filter_diff(raw: &str, level: Level) -> String {
let limit = match level {
Level::Lite => 400,
Level::Ultra => 30,
Level::Full => 200,
};
let lines = compact_diff_body(raw, level, limit);
if lines.is_empty() {
return head_nonblank(raw, level.head_limit(50));
}
lines.join("\n")
}
fn filter_show(raw: &str, level: Level) -> String {
match level {
Level::Lite => {
let cleaned: Vec<String> = raw
.lines()
.filter(|l| !is_index_meta(l) && !is_trailer(l))
.take(200)
.map(|l| abbreviate_commit_line(l).unwrap_or_else(|| l.to_string()))
.collect();
cleaned.join("\n")
}
Level::Ultra => {
let cleaned: Vec<String> = raw
.lines()
.filter(|l| {
!is_trailer(l)
&& (l.starts_with("commit ")
|| l.starts_with("Author:")
|| l.starts_with("Date:")
|| l.starts_with(" ")
|| l.starts_with("diff --git")
|| (l.contains(" | ") && l.chars().any(|c| c == '+' || c == '-')))
})
.take(20)
.map(|l| abbreviate_commit_line(l).unwrap_or_else(|| l.to_string()))
.collect();
cleaned.join("\n")
}
Level::Full => {
let mut in_diff = false;
let mut in_hunk = false;
let mut output: Vec<String> = Vec::with_capacity(64);
for line in raw.lines() {
if output.len() >= 100 {
break;
}
if line.starts_with("diff ") {
in_diff = true;
in_hunk = false;
output.push(line.to_string());
continue;
}
if line.starts_with("@@ ") {
in_hunk = true;
output.push(line.to_string());
continue;
}
if in_diff && !in_hunk {
continue;
}
if in_hunk {
if line.starts_with('+') || line.starts_with('-') {
output.push(line.to_string());
}
continue;
}
if is_trailer(line) || !is_commit_header(line) {
continue;
}
match abbreviate_commit_line(line) {
Some(abbrev) => output.push(abbrev),
None => output.push(line.to_string()),
}
}
if output.is_empty() {
return head_nonblank(raw, level.head_limit(60));
}
output.join("\n")
}
}
}
fn compact_diff_body(raw: &str, level: Level, limit: usize) -> Vec<&str> {
let mut output: Vec<&str> = Vec::with_capacity(64);
let mut in_hunk = false;
for line in raw.lines() {
if output.len() >= limit {
break;
}
if line.starts_with("diff ") {
in_hunk = false;
output.push(line);
continue;
}
if line.starts_with("@@ ") {
in_hunk = true;
output.push(if level == Level::Ultra {
trim_hunk_header(line)
} else {
line
});
continue;
}
if level == Level::Ultra {
continue;
}
if !in_hunk {
continue;
}
if line.starts_with('+') || line.starts_with('-') {
output.push(line);
}
}
output
}
fn is_status_char(b: u8) -> bool {
matches!(b, b'M' | b'A' | b'D' | b'R' | b'C' | b'U' | b'?' | b'!')
}
fn is_status_line(s: &str) -> bool {
s.len() >= 3 && is_status_char(s.as_bytes()[0]) && s.as_bytes()[1] == b' '
|| s.len() >= 4
&& s.as_bytes()[0] == b' '
&& is_status_char(s.as_bytes()[1])
&& s.as_bytes()[2] == b' '
}
fn is_commit_header(l: &str) -> bool {
l.starts_with("commit ")
|| l.starts_with("Merge:")
|| l.starts_with("Author:")
|| l.starts_with("Date:")
|| l.starts_with(" ")
}
fn is_index_meta(l: &str) -> bool {
l.starts_with("index ") || l.starts_with("mode ") || l.starts_with("similarity ")
}
fn is_trailer(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.starts_with("Signed-off-by:")
|| trimmed.starts_with("Co-authored-by:")
|| trimmed.starts_with("Change-Id:")
|| trimmed.starts_with("Reviewed-by:")
|| trimmed.starts_with("Acked-by:")
|| trimmed.starts_with("Tested-by:")
|| trimmed.starts_with("Reported-by:")
|| trimmed.starts_with("Cc:")
}
fn abbreviate_commit_line(line: &str) -> Option<String> {
let rest = line.strip_prefix("commit ")?;
let hash_end = rest
.find(|c: char| !c.is_ascii_hexdigit())
.unwrap_or(rest.len());
if hash_end < 40 {
return None;
}
Some(format!("commit {}{}", &rest[..12], &rest[hash_end..]))
}
fn trim_hunk_header(line: &str) -> &str {
if !line.starts_with("@@ ") {
return line;
}
if let Some(idx) = line[3..].find(" @@") {
return &line[..3 + idx + 3];
}
line
}
fn head_nonblank(raw: &str, limit: usize) -> String {
raw.lines()
.filter(|l| !l.is_empty())
.take(limit)
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_clean() {
let out = filter_status("", Level::Full);
assert_eq!(out, "git status: clean");
}
#[test]
fn status_modified() {
let raw = " M src/main.rs\n M Cargo.toml\n";
let out = filter_status(raw, Level::Full);
assert!(out.contains("src/main.rs"));
assert!(out.contains("Cargo.toml"));
}
#[test]
fn diff_ultra_headers_only() {
let raw = "diff --git a/f b/f\nindex abc..def\n--- a/f\n+++ b/f\n@@ -1 +1 @@\n-old\n+new\n";
let out = filter_diff(raw, Level::Ultra);
assert!(out.contains("diff --git"));
assert!(out.contains("@@ "));
assert!(!out.contains("-old"));
}
#[test]
fn show_full_drops_context_and_meta() {
let raw = "\
commit abc123
Author: zdk
Date: Mon
fix bug
diff --git a/f b/f
index abc..def 100644
--- a/f
+++ b/f
@@ -1,3 +1,3 @@
unchanged context
-old line
+new line
more context
";
let out = filter_show(raw, Level::Full);
assert!(out.contains("commit abc123"));
assert!(out.contains(" fix bug"));
assert!(out.contains("diff --git"));
assert!(out.contains("-old line"));
assert!(out.contains("+new line"));
assert!(!out.contains("unchanged context"), "should drop context: {out}");
assert!(!out.contains("index abc"), "should drop index meta: {out}");
}
#[test]
fn show_full_drops_indented_context_after_diff() {
let raw = "\
commit abc123
Author: zdk
Date: Mon
refactor
diff --git a/f b/f
@@ -1,3 +1,3 @@
let x = 1;
- let y = 2;
+ let y = 3;
println!(\"{x} {y}\");
";
let out = filter_show(raw, Level::Full);
assert!(out.contains(" refactor"), "keep message body: {out}");
assert!(out.contains("- let y = 2;"));
assert!(out.contains("+ let y = 3;"));
assert!(
!out.contains(" let x = 1;"),
"must drop indented context: {out}"
);
assert!(
!out.contains("println!"),
"must drop indented context: {out}"
);
}
#[test]
fn diff_full_drops_redundant_minus_plus_and_index() {
let raw = "\
diff --git a/f b/f
index abc..def 100644
--- a/f
+++ b/f
@@ -1 +1 @@
-old
+new
";
let out = filter_diff(raw, Level::Full);
assert!(out.contains("diff --git"));
assert!(out.contains("@@ "));
assert!(out.contains("-old"));
assert!(out.contains("+new"));
assert!(!out.contains("--- a/f"), "drop redundant ---: {out}");
assert!(!out.contains("+++ b/f"), "drop redundant +++: {out}");
assert!(!out.contains("index abc"), "drop index meta: {out}");
}
#[test]
fn diff_full_keeps_removed_dashes_in_hunk_body() {
let raw = "\
diff --git a/f b/f
@@ -1,2 +1,2 @@
---- old comment ----
+++ new comment +++
";
let out = filter_diff(raw, Level::Full);
assert!(out.contains("---- old comment ----"), "keep removed content: {out}");
assert!(out.contains("+++ new comment +++"), "keep added content: {out}");
}
#[test]
fn diff_stat_falls_back_instead_of_passthrough() {
let raw = "\
crates/foo.rs | 5 +-
bar/baz.rs | 12 +++++++++++
2 files changed, 15 insertions(+), 2 deletions(-)
";
let out = filter_diff(raw, Level::Full);
assert!(!out.is_empty(), "fall back, don't return empty: {out}");
assert!(out.contains("crates/foo.rs"));
assert!(out.contains("2 files changed"));
assert!(!out.contains("\n\n"), "collapse blank lines: {out:?}");
}
#[test]
fn diff_ultra_strips_hunk_function_context() {
let raw = "\
diff --git a/f b/f
@@ -23,13 +23,18 @@ pub struct Foo {
-old
+new
";
let out = filter_diff(raw, Level::Ultra);
assert!(out.contains("@@ -23,13 +23,18 @@"));
assert!(!out.contains("pub struct Foo"), "drop tail context: {out}");
}
#[test]
fn show_full_drops_message_trailers() {
let raw = "\
commit abc123
Author: zdk <z@d.k>
Date: Fri May 8 13:01:39 2026 +0700
fix bug
Signed-off-by: zdk <z@d.k>
Co-authored-by: someone <s@o.m>
Change-Id: I0123456789abcdef
diff --git a/f b/f
@@ -1 +1 @@
-old
+new
";
let out = filter_show(raw, Level::Full);
assert!(out.contains(" fix bug"));
assert!(!out.contains("Signed-off-by"), "drop trailer: {out}");
assert!(!out.contains("Co-authored-by"), "drop trailer: {out}");
assert!(!out.contains("Change-Id"), "drop trailer: {out}");
}
#[test]
fn show_full_abbreviates_full_commit_hash() {
let raw = "\
commit fd9858806e241a70eec9d23017ccf00d90b64c4c
Author: zdk
Date: Mon
fix
";
let out = filter_show(raw, Level::Full);
assert!(out.contains("commit fd9858806e24"), "abbreviate: {out}");
assert!(!out.contains("fd9858806e241a70eec9d23017ccf00d90b64c4c"), "drop full hash: {out}");
}
#[test]
fn show_full_preserves_decoration_after_abbreviated_hash() {
let raw = "\
commit fd9858806e241a70eec9d23017ccf00d90b64c4c (HEAD -> main, origin/main)
Author: zdk
Date: Mon
fix
";
let out = filter_show(raw, Level::Full);
assert!(
out.contains("commit fd9858806e24 (HEAD -> main, origin/main)"),
"preserve --decorate suffix: {out}"
);
}
#[test]
fn show_full_drops_redundant_minus_plus_in_diff() {
let raw = "\
commit abc123
Author: zdk
Date: Mon
fix
diff --git a/f b/f
index 1..2 100644
--- a/f
+++ b/f
@@ -1 +1 @@
-old
+new
";
let out = filter_show(raw, Level::Full);
assert!(!out.contains("--- a/f"), "drop redundant ---: {out}");
assert!(!out.contains("+++ b/f"), "drop redundant +++: {out}");
assert!(!out.contains("index 1..2"), "drop index meta: {out}");
}
#[test]
fn log_full_drops_trailers_and_abbreviates_hash() {
let raw = "\
commit fd9858806e241a70eec9d23017ccf00d90b64c4c
Author: zdk
Date: Mon
fix bug
Signed-off-by: zdk <z@d.k>
Co-authored-by: someone <s@o.m>
commit abc123
Author: zdk
Date: Sun
other
";
let out = filter_log(raw, Level::Full);
assert!(out.contains("commit fd9858806e24"), "abbreviate hash: {out}");
assert!(
!out.contains("fd9858806e241a70eec9d23017ccf00d90b64c4c"),
"drop full hash: {out}"
);
assert!(!out.contains("Signed-off-by"), "drop trailer: {out}");
assert!(!out.contains("Co-authored-by"), "drop trailer: {out}");
assert!(out.contains(" fix bug"), "keep message body: {out}");
assert!(out.contains("commit abc123"));
}
#[test]
fn log_ultra_compact() {
let raw = "commit abc123\nAuthor: zdk\nDate: Mon\n\n fix bug\n\ncommit def456\n";
let out = filter_log(raw, Level::Ultra);
assert!(out.contains("commit abc123"));
assert!(out.contains(" fix bug"));
assert!(!out.contains("Author:"));
}
}