hive-forge: stat-style +N -M counts in collapsed-lockfile placeholder (#222)

This commit is contained in:
damocles 2026-05-26 17:59:59 +02:00 committed by Mara
commit 67a3c9e2da

View file

@ -2,12 +2,14 @@
//! //!
//! By default the hunks for known autogenerated lockfiles //! By default the hunks for known autogenerated lockfiles
//! (`flake.lock`, `Cargo.lock`, `package-lock.json`, …) are //! (`flake.lock`, `Cargo.lock`, `package-lock.json`, …) are
//! collapsed to a one-line placeholder so a `flake.lock` rev bump //! collapsed to a `[<path>: +N -M (autogenerated; pass --full for
//! doesn't drown the human-authored changes in 5 000 lines of //! content)]` placeholder so a `flake.lock` rev bump doesn't drown
//! lock churn (#222). The header (`diff --git`, `index`, `---`, //! the human-authored changes in 5 000 lines of lock churn
//! `+++`, rename / mode metadata) is preserved so the reader can //! (#222). The header (`diff --git`, `index`, `---`, `+++`,
//! still see WHICH lockfiles changed — only the per-hunk content //! rename / mode metadata) is preserved so the reader can still
//! is omitted. Pass `--full` to dump the unfiltered diff. //! see WHICH lockfiles changed; the +/- counts give a `diff
//! --stat`-style magnitude (excluding the `@@` hunk header).
//! Pass `--full` to dump the unfiltered diff.
use anyhow::Result; use anyhow::Result;
use clap::Args as ClapArgs; use clap::Args as ClapArgs;
@ -41,50 +43,88 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
/// Walk a unified diff line-by-line. For each per-file section /// Walk a unified diff line-by-line. For each per-file section
/// whose target path matches a known autogenerated file /// whose target path matches a known autogenerated file
/// (`is_autogenerated`), drop every line from the first hunk /// (`is_autogenerated`), drop every line from the first hunk
/// header (`@@`) onward and emit a single /// header (`@@`) onward and emit a single `diff --stat`-style
/// `[N lines of … omitted]` placeholder before the next file. /// `[file.lock: +N -M (autogenerated, --full for content)]`
/// Non-autogenerated files pass through unchanged. /// placeholder before the next file. Non-autogenerated files
/// pass through unchanged.
fn collapse_autogenerated(diff: &str) -> String { fn collapse_autogenerated(diff: &str) -> String {
let mut out = String::with_capacity(diff.len()); let mut out = String::with_capacity(diff.len());
let mut collapse = false; let mut state: Option<CollapseState> = None;
let mut in_body = false;
let mut omitted = 0usize;
for line in diff.lines() { for line in diff.lines() {
if let Some(rest) = line.strip_prefix("diff --git ") { if let Some(rest) = line.strip_prefix("diff --git ") {
// New file section — flush prior collapse counter. // New file section — flush prior collapse counter.
if collapse && omitted > 0 { if let Some(s) = state.take() {
out.push_str(&placeholder(omitted)); out.push_str(&s.placeholder());
} }
let path = parse_diff_git_path(rest); let path = parse_diff_git_path(rest);
collapse = path.as_deref().is_some_and(is_autogenerated); if let Some(p) = path.as_deref()
in_body = false; && is_autogenerated(p)
omitted = 0; {
state = Some(CollapseState::new(p.to_owned()));
}
out.push_str(line); out.push_str(line);
out.push('\n'); out.push('\n');
continue; continue;
} }
// First `@@` marks the boundary between file header and if let Some(s) = state.as_mut() {
// hunk content; everything from this point is suppressed // First `@@` marks the boundary between file header and
// when `collapse` is on. // hunk content; everything from this point is
if !in_body && line.starts_with("@@") { // suppressed (and tallied) while `state` is Some.
in_body = true; if !s.in_body && line.starts_with("@@") {
} s.in_body = true;
if collapse && in_body { }
omitted += 1; if s.in_body {
continue; // The hunk header `@@` itself counts as a body line
// for the +/ tally only via its descendant content
// lines; skip it for the counters.
if !line.starts_with("@@") {
match line.as_bytes().first() {
Some(b'+') => s.added += 1,
Some(b'-') => s.removed += 1,
_ => {}
}
}
continue;
}
} }
out.push_str(line); out.push_str(line);
out.push('\n'); out.push('\n');
} }
if collapse && omitted > 0 { if let Some(s) = state.take() {
out.push_str(&placeholder(omitted)); out.push_str(&s.placeholder());
} }
out out
} }
fn placeholder(n: usize) -> String { /// Per-file accumulator for the collapsed-hunk placeholder. Tracks
format!("[{n} lines of autogenerated content omitted; pass --full to view]\n") /// the file's display name plus `+`/`` line counts (excluding the
/// `@@` hunk header), so the placeholder shows operator-meaningful
/// magnitude instead of "N lines omitted" (which mixed context +
/// added + removed indistinguishably).
struct CollapseState {
path: String,
in_body: bool,
added: u32,
removed: u32,
}
impl CollapseState {
fn new(path: String) -> Self {
Self {
path,
in_body: false,
added: 0,
removed: 0,
}
}
fn placeholder(&self) -> String {
format!(
"[{}: +{} -{} (autogenerated; pass --full for content)]\n",
self.path, self.added, self.removed
)
}
} }
/// `rest` is everything after `diff --git `, e.g. `a/foo b/foo` /// `rest` is everything after `diff --git `, e.g. `a/foo b/foo`
@ -159,7 +199,13 @@ index 3333..4444 100644
assert!(out.contains("--- a/Cargo.lock")); assert!(out.contains("--- a/Cargo.lock"));
assert!(out.contains("+++ b/Cargo.lock")); assert!(out.contains("+++ b/Cargo.lock"));
assert!(!out.contains("[[package]]")); assert!(!out.contains("[[package]]"));
assert!(out.contains("4 lines of autogenerated content omitted")); // Stat-style placeholder: 1 added (`+name = "new"`), 1
// removed (`-name = "old"`); context line and `@@` header
// don't count.
assert!(
out.contains("[Cargo.lock: +1 -1"),
"expected stat placeholder, got: {out}"
);
// Non-lockfile file passes through untouched. // Non-lockfile file passes through untouched.
assert!(out.contains("fn main() {}")); assert!(out.contains("fn main() {}"));
assert!(out.contains("println!(\"hi\")")); assert!(out.contains("println!(\"hi\")"));
@ -206,7 +252,36 @@ index 1111..2222 100644
assert!(out.contains("+new")); assert!(out.contains("+new"));
assert!(out.contains("diff --git a/flake.lock")); assert!(out.contains("diff --git a/flake.lock"));
assert!(!out.contains("lock-line-one")); assert!(!out.contains("lock-line-one"));
assert!(out.contains("4 lines of autogenerated content omitted")); assert!(
out.contains("[flake.lock: +1 -1"),
"expected stat placeholder, got: {out}"
);
}
#[test]
fn collapse_counts_distinguish_added_and_removed() {
// Big asymmetric churn — flake.lock rev bump with several
// adds and one removal in the worker fields. Verifies
// we don't conflate `+`/`-` totals.
let diff = "\
diff --git a/flake.lock b/flake.lock
index 1111..2222 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,5 +1,8 @@
{
- \"old\": 1
+ \"new\": 1,
+ \"another\": 2,
+ \"and\": 3,
+ \"more\": 4
}
";
let out = collapse_autogenerated(diff);
assert!(
out.contains("[flake.lock: +4 -1"),
"expected +4 -1, got: {out}"
);
} }
#[test] #[test]