use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::glob::{any_glob_matches, path_str};
pub const DEFAULT_PATTERNS: &[&str] = &[
"**/Cargo.lock",
"**/package-lock.json",
"**/yarn.lock",
"**/pnpm-lock.yaml",
"**/poetry.lock",
"**/Gemfile.lock",
"**/go.sum",
"**/uv.lock",
"**/composer.lock",
"**/*.min.js",
"**/*.min.css",
"**/*.map",
];
pub const DEFAULT_LINE_THRESHOLD: usize = 500;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(default)]
pub struct AutoCollapseConfig {
pub enabled: bool,
pub line_threshold: usize,
pub patterns: Vec<String>,
}
impl Default for AutoCollapseConfig {
fn default() -> Self {
Self {
enabled: true,
line_threshold: DEFAULT_LINE_THRESHOLD,
patterns: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollapseReason {
LockfileLike,
OverThreshold,
}
impl CollapseReason {
pub fn label(self) -> &'static str {
match self {
Self::LockfileLike => "lockfile-like",
Self::OverThreshold => "over threshold",
}
}
}
pub fn should_auto_collapse(
path: &Path,
added_plus_deleted: usize,
cfg: &AutoCollapseConfig,
) -> bool {
auto_collapse_reason(path, added_plus_deleted, cfg).is_some()
}
pub fn auto_collapse_reason(
path: &Path,
added_plus_deleted: usize,
cfg: &AutoCollapseConfig,
) -> Option<CollapseReason> {
if !cfg.enabled {
return None;
}
let p = path_str(path);
let default_match = DEFAULT_PATTERNS
.iter()
.any(|g| crate::glob::glob_match(g, &p));
if default_match || any_glob_matches(&cfg.patterns, path) {
return Some(CollapseReason::LockfileLike);
}
if added_plus_deleted > cfg.line_threshold {
return Some(CollapseReason::OverThreshold);
}
None
}
pub fn effective_collapsed(
explicit: Option<bool>,
path: &Path,
added_plus_deleted: usize,
cfg: &AutoCollapseConfig,
) -> bool {
explicit.unwrap_or_else(|| should_auto_collapse(path, added_plus_deleted, cfg))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn cfg_default() -> AutoCollapseConfig {
AutoCollapseConfig::default()
}
#[test]
fn default_enabled_with_threshold_500_and_empty_user_patterns() {
let cfg = cfg_default();
assert!(cfg.enabled);
assert_eq!(cfg.line_threshold, 500);
assert!(cfg.patterns.is_empty());
}
#[test]
fn cargo_lock_at_root_is_collapsed_by_default_pattern() {
let cfg = cfg_default();
assert!(should_auto_collapse(&PathBuf::from("Cargo.lock"), 10, &cfg));
assert!(should_auto_collapse(
&PathBuf::from("crates/foo/Cargo.lock"),
10,
&cfg
));
}
#[test]
fn regular_source_file_is_not_collapsed() {
let cfg = cfg_default();
assert!(!should_auto_collapse(
&PathBuf::from("src/foo.rs"),
100,
&cfg
));
}
#[test]
fn file_over_threshold_is_collapsed() {
let cfg = cfg_default();
assert!(should_auto_collapse(
&PathBuf::from("src/foo.rs"),
501,
&cfg
));
}
#[test]
fn file_at_exact_threshold_is_not_collapsed() {
let cfg = cfg_default();
assert!(!should_auto_collapse(
&PathBuf::from("src/foo.rs"),
500,
&cfg
));
}
#[test]
fn disabled_config_never_collapses() {
let cfg = AutoCollapseConfig {
enabled: false,
..Default::default()
};
assert!(!should_auto_collapse(
&PathBuf::from("Cargo.lock"),
10_000,
&cfg
));
assert!(!should_auto_collapse(
&PathBuf::from("src/foo.rs"),
10_000,
&cfg
));
}
#[test]
fn user_pattern_extends_defaults_without_replacing_them() {
let cfg = AutoCollapseConfig {
patterns: vec!["**/*.foo".into()],
..Default::default()
};
assert!(should_auto_collapse(&PathBuf::from("Cargo.lock"), 5, &cfg));
assert!(should_auto_collapse(&PathBuf::from("a/b.foo"), 5, &cfg));
assert!(!should_auto_collapse(&PathBuf::from("src/foo.rs"), 5, &cfg));
}
#[test]
fn effective_respects_explicit_expand_over_auto() {
let cfg = cfg_default();
assert!(!effective_collapsed(
Some(false),
&PathBuf::from("Cargo.lock"),
10_000,
&cfg,
));
}
#[test]
fn effective_respects_explicit_collapse_on_non_auto_file() {
let cfg = cfg_default();
assert!(effective_collapsed(
Some(true),
&PathBuf::from("src/foo.rs"),
1,
&cfg,
));
}
#[test]
fn effective_falls_back_to_auto_when_explicit_is_none() {
let cfg = cfg_default();
assert!(effective_collapsed(
None,
&PathBuf::from("Cargo.lock"),
5,
&cfg,
));
assert!(!effective_collapsed(
None,
&PathBuf::from("src/foo.rs"),
5,
&cfg,
));
}
#[test]
fn reason_distinguishes_pattern_vs_threshold() {
let cfg = cfg_default();
assert_eq!(
auto_collapse_reason(&PathBuf::from("Cargo.lock"), 5, &cfg),
Some(CollapseReason::LockfileLike)
);
assert_eq!(
auto_collapse_reason(&PathBuf::from("src/foo.rs"), 501, &cfg),
Some(CollapseReason::OverThreshold)
);
assert_eq!(
auto_collapse_reason(&PathBuf::from("src/foo.rs"), 10, &cfg),
None
);
}
#[test]
fn pattern_match_wins_over_threshold() {
let cfg = cfg_default();
assert_eq!(
auto_collapse_reason(&PathBuf::from("Cargo.lock"), 2, &cfg),
Some(CollapseReason::LockfileLike)
);
}
}