use super::common::{fail, hl, ok, repo_root, staged_files, warn};
use crate::check::Outcome;
use std::path::Path;
fn dir_of(path: &str) -> &str {
match path.rfind('/') {
Some(i) => &path[..i],
None => ".",
}
}
fn sibling(dir: &str, name: &str) -> String {
if dir == "." {
name.to_string()
} else {
format!("{dir}/{name}")
}
}
pub struct Verdict {
pub forgot_lock: Vec<String>,
pub orphan_lock: Vec<String>,
}
pub fn classify(staged: &[String], lock_exists: impl Fn(&str) -> bool) -> Verdict {
let is_staged = |p: &str| staged.iter().any(|s| s == p);
let mut v = Verdict {
forgot_lock: Vec::new(),
orphan_lock: Vec::new(),
};
for f in staged {
let dir = dir_of(f);
let base = f.rsplit('/').next().unwrap_or(f);
match base {
"package.json" => {
let lock = sibling(dir, "package-lock.json");
if is_staged(&lock) {
continue; }
if lock_exists(&lock) {
v.forgot_lock.push(f.clone());
}
}
"package-lock.json" => {
let pkg = sibling(dir, "package.json");
if !is_staged(&pkg) {
v.orphan_lock.push(f.clone());
}
}
_ => {}
}
}
v
}
pub fn run(_args: &[std::ffi::OsString]) -> Outcome {
let staged = staged_files(&[]);
let root = repo_root();
let v = classify(&staged, |lock| Path::new(&root).join(lock).is_file());
if v.forgot_lock.is_empty() && v.orphan_lock.is_empty() {
ok("package.json & package-lock.json look in sync");
return Outcome::Passed;
}
for f in &v.orphan_lock {
fail(&format!("{} staged without its package.json", hl(f)));
}
for f in &v.forgot_lock {
warn(&format!(
"{} changed but its package-lock.json is not staged",
hl(f)
));
}
fail(&format!(
"Run {} and stage the lockfile. To stop this blocking, {}; \
to bypass once, {}",
hl("npm install"),
hl("git config amont.severity.package-lock warn"),
hl("git -c hook.skip=package-lock commit")
));
Outcome::Failed
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &[&str], locks: &[&str]) -> Verdict {
let staged: Vec<String> = s.iter().map(|x| x.to_string()).collect();
let locks: Vec<String> = locks.iter().map(|x| x.to_string()).collect();
classify(&staged, move |l| locks.iter().any(|x| x == l))
}
#[test]
fn both_staged_is_in_sync() {
let r = v(
&["package.json", "package-lock.json"],
&["package-lock.json"],
);
assert!(r.forgot_lock.is_empty() && r.orphan_lock.is_empty());
}
#[test]
fn package_json_alone_with_a_real_lock_on_disk_is_a_forgotten_lock() {
let r = v(&["package.json"], &["package-lock.json"]);
assert_eq!(r.forgot_lock, vec!["package.json".to_string()]);
}
#[test]
fn package_json_without_a_lock_on_disk_demands_nothing() {
let r = v(&["package.json"], &[]);
assert!(r.forgot_lock.is_empty() && r.orphan_lock.is_empty());
}
#[test]
fn a_lock_without_its_package_json_is_an_orphan() {
let r = v(&["package-lock.json"], &["package-lock.json"]);
assert_eq!(r.orphan_lock, vec!["package-lock.json".to_string()]);
}
#[test]
fn scoping_is_per_directory() {
let r = v(
&["apps/a/package.json", "apps/b/package-lock.json"],
&["apps/a/package-lock.json", "apps/b/package-lock.json"],
);
assert_eq!(r.forgot_lock, vec!["apps/a/package.json".to_string()]);
assert_eq!(r.orphan_lock, vec!["apps/b/package-lock.json".to_string()]);
}
}