use std::collections::BTreeMap;
use std::path::Path;
use std::process::Command;
use serde::Serialize;
use crate::checks::all_checks;
use crate::skips::{scope_of, Scope};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Level {
Warn,
Block,
Unrecognised,
}
impl Level {
fn of(value: &str) -> Level {
match amont_runtime::check::Severity::parse(value) {
Some(amont_runtime::check::Severity::Warn) => Level::Warn,
Some(amont_runtime::check::Severity::Block) => Level::Block,
None => Level::Unrecognised,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RawEntry {
check: String,
value: String,
scope: Scope,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SeverityOverride {
pub check: String,
pub value: String,
pub level: Level,
pub scope: Scope,
pub effective: bool,
}
impl SeverityOverride {
pub fn covers(&self) -> Vec<&'static str> {
covers(&self.check)
}
pub fn is_inert(&self) -> bool {
self.covers().is_empty() || self.level == Level::Unrecognised
}
pub fn weakens(&self) -> bool {
self.effective && self.level == Level::Warn && !self.covers().is_empty()
}
pub fn shadowed(&self) -> bool {
!self.effective
}
}
pub fn read(repo: &Path) -> Vec<SeverityOverride> {
let Ok(out) = Command::new("git")
.args([
"config",
"--show-origin",
"--get-regexp",
r"^amont\.severity\.",
])
.current_dir(repo)
.output()
else {
return Vec::new();
};
resolve(repo, parse(&String::from_utf8_lossy(&out.stdout)))
}
fn resolve(repo: &Path, raws: Vec<RawEntry>) -> Vec<SeverityOverride> {
let mut applied: BTreeMap<&str, Option<String>> = BTreeMap::new();
for raw in &raws {
if !applied.contains_key(raw.check.as_str()) {
let v = amont_runtime::git::stdout_in(
repo,
&[
"config",
"--get",
&amont_runtime::registry::severity_key(&raw.check),
],
);
applied.insert(raw.check.as_str(), v);
}
}
let config_text: String = raws
.iter()
.map(|r| format!("amont.severity.{} {}\n", r.check, r.value))
.collect();
let overrides = amont_runtime::registry::Overrides::from_config(Some(config_text));
raws.iter()
.map(|r| {
let wins_own_key =
applied.get(r.check.as_str()).and_then(|v| v.as_deref()) == Some(r.value.as_str());
let level = Level::of(&r.value);
let effective = wins_own_key
&& (level == Level::Unrecognised
|| covers(&r.check).iter().any(|check| {
overrides
.applied_to(check)
.is_some_and(|(winning_key, _)| winning_key == r.check)
}));
SeverityOverride {
effective,
check: r.check.clone(),
level,
value: r.value.clone(),
scope: r.scope.clone(),
}
})
.collect()
}
fn covers(pattern: &str) -> Vec<&'static str> {
all_checks()
.into_iter()
.filter(|c| amont_runtime::names_check(c, pattern).is_some())
.collect()
}
fn parse(stdout: &str) -> Vec<RawEntry> {
stdout
.lines()
.filter_map(|line| {
let (origin, rest) = line.split_once('\t')?;
let (key, value) = rest.split_once(' ')?;
let check = key.strip_prefix("amont.severity.")?.trim();
let value = value.trim();
if check.is_empty() {
return None;
}
Some(RawEntry {
check: check.to_string(),
value: value.to_string(),
scope: scope_of(origin),
})
})
.collect()
}
#[cfg(test)]
pub fn for_test(check: &str, value: &str) -> SeverityOverride {
SeverityOverride {
check: check.to_string(),
value: value.to_string(),
level: Level::of(value),
scope: Scope::Local,
effective: true,
}
}
#[cfg(test)]
pub fn shadowed_for_test(check: &str, value: &str) -> SeverityOverride {
SeverityOverride {
effective: false,
..for_test(check, value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_real_check_set_to_warn_is_a_weakening() {
let e = for_test("pre-commit-clippy", "warn");
assert!(e.weakens());
assert!(!e.is_inert());
}
#[test]
fn an_explicit_block_weakens_nothing() {
let e = for_test("pre-commit-clippy", "block");
assert!(!e.weakens());
assert!(!e.is_inert());
}
#[test]
fn a_misspelt_key_or_value_is_inert() {
let bad_key = for_test("pre-commit-clipy", "warn");
assert!(bad_key.is_inert());
assert!(
!bad_key.weakens(),
"a key naming no check cannot have weakened one"
);
let bad_value = for_test("pre-commit-clippy", "advisory");
assert!(bad_value.is_inert());
assert!(!bad_value.weakens());
}
#[test]
fn both_surfaces_resolve_a_name_the_same_way() {
for name in ["pre-commit-clippy", "clippy", "pre-commit"] {
assert_eq!(
for_test(name, "warn").covers(),
crate::skips::suppressed_by(name),
"`{name}` must reach the same checks on both surfaces"
);
assert!(!for_test(name, "warn").is_inert(), "`{name}` names checks");
}
assert!(for_test("pre-commit", "warn").covers().len() > 1);
}
#[test]
fn reads_what_git_actually_wrote() {
let d = std::env::temp_dir().join(format!("sev-read-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(&d)
.output()
.expect("git");
};
git(&["init", "-q", "--template=", "."]);
git(&["config", "amont.severity.pre-commit-clippy", "warn"]);
git(&["config", "amont.severity.pre-commit-prettier", "block"]);
git(&["config", "amont.other.thing", "warn"]);
let mut got = read(&d);
got.sort_by(|a, b| a.check.cmp(&b.check));
let _ = std::fs::remove_dir_all(&d);
assert_eq!(got.len(), 2, "{got:?}");
assert_eq!(got[0].check, "pre-commit-clippy");
assert_eq!(got[0].level, Level::Warn);
assert_eq!(got[0].scope, Scope::Local, "written to .git/config");
assert_eq!(got[1].check, "pre-commit-prettier");
assert_eq!(got[1].level, Level::Block);
}
#[test]
fn the_origin_decides_the_scope() {
let got = parse(concat!(
"file:/repo/.git/config\tamont.severity.pre-commit-clippy warn\n",
"file:/Users/me/.gitconfig\tamont.severity.pre-commit-prettier warn\n",
"file:/etc/gitconfig\tamont.severity.pre-commit-ban-terms warn\n",
));
assert_eq!(got.len(), 3, "{got:?}");
assert_eq!(got[0].scope, Scope::Local);
assert_eq!(got[1].scope, Scope::Global);
assert!(
matches!(got[2].scope, Scope::Other { .. }),
"system config is neither of the two the UI can edit: {:?}",
got[2].scope
);
}
#[test]
fn the_entry_git_applies_is_the_one_marked_effective() {
let d = std::env::temp_dir().join(format!("sev-eff-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(&d)
.output()
.expect("git");
};
git(&["init", "-q", "--template=", "."]);
let key = "amont.severity.pre-commit-merge-conflict";
git(&["config", "--add", key, "warn"]);
git(&["config", "--add", key, "block"]);
let got = read(&d);
let _ = std::fs::remove_dir_all(&d);
assert_eq!(got.len(), 2, "both entries must still be SHOWN: {got:?}");
let warn = got.iter().find(|e| e.value == "warn").expect("the warn");
let block = got.iter().find(|e| e.value == "block").expect("the block");
assert!(!warn.effective, "git applies block, not warn");
assert!(block.effective);
assert!(
!warn.weakens(),
"the WARN column would report a downgrade the dispatcher does not apply"
);
}
#[test]
fn a_full_id_beats_a_short_name_on_a_different_key() {
let d = std::env::temp_dir().join(format!("sev-crosskey-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(&d)
.output()
.expect("git");
};
git(&["init", "-q", "--template=", "."]);
git(&["config", "amont.severity.clippy", "warn"]);
git(&["config", "amont.severity.pre-commit-clippy", "block"]);
let got = read(&d);
let _ = std::fs::remove_dir_all(&d);
assert_eq!(got.len(), 2, "{got:?}");
let short = got
.iter()
.find(|e| e.check == "clippy")
.expect("the short name");
let full = got
.iter()
.find(|e| e.check == "pre-commit-clippy")
.expect("the full id");
assert!(
!short.effective,
"the full id is more specific and is what the dispatcher applies: {got:?}"
);
assert!(full.effective);
assert!(
!short.weakens(),
"the WARN column must not count a downgrade the dispatcher never applies"
);
}
#[test]
fn a_trigger_still_weakens_what_it_was_not_exempted_from() {
let d = std::env::temp_dir().join(format!("sev-partial-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(&d)
.output()
.expect("git");
};
git(&["init", "-q", "--template=", "."]);
git(&["config", "amont.severity.pre-commit", "warn"]);
git(&["config", "amont.severity.clippy", "block"]);
let got = read(&d);
let _ = std::fs::remove_dir_all(&d);
let trigger = got
.iter()
.find(|e| e.check == "pre-commit")
.expect("the trigger");
assert!(
trigger.weakens(),
"pre-commit-prettier and the rest were never exempted: {got:?}"
);
}
#[test]
fn a_repo_with_no_overrides_reads_empty() {
let d = std::env::temp_dir().join(format!("sev-none-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
std::process::Command::new("git")
.args(["init", "-q", "--template=", "."])
.current_dir(&d)
.output()
.expect("git");
let got = read(&d);
let _ = std::fs::remove_dir_all(&d);
assert!(got.is_empty(), "{got:?}");
}
}