hive-forge: stat-style +N -M counts in collapsed-lockfile placeholder (#222)
This commit is contained in:
parent
15521179fc
commit
67a3c9e2da
1 changed files with 107 additions and 32 deletions
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue