mod common;
use common::{missing, Repo};
#[cfg(unix)]
fn rewriter(r: &Repo, name: &str) {
r.write(
name,
"#!/bin/sh\nfor f in *.js; do [ -f \"$f\" ] || continue; sed -i.bak 's/BAD/GOOD/' \"$f\" && rm -f \"$f.bak\"; done\n",
);
let p = r.path(name);
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).expect("chmod");
r.git(&["add", name]);
}
#[cfg(not(unix))]
fn rewriter(r: &Repo, name: &str) {
r.write(
name,
"@echo off\r\nfor %%f in (*.js) do powershell -NoProfile -Command \"$c = Get-Content -Raw '%%f'; [IO.File]::WriteAllText('%%f', ($c -replace 'BAD','GOOD'))\"\r\n",
);
r.git(&["add", name]);
}
#[cfg(unix)]
const REWRITER: &str = "rewrite.sh";
#[cfg(not(unix))]
const REWRITER: &str = "rewrite.cmd";
#[cfg(unix)]
fn one_file_rewriter(r: &Repo, name: &str) {
r.write(
name,
"#!/bin/sh\nf=\"$1\"\n[ -f \"$f\" ] || exit 0\nsed -i.bak 's/BAD/GOOD/' \"$f\" && rm -f \"$f.bak\"\n",
);
let p = r.path(name);
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).expect("chmod");
r.git(&["add", name]);
}
#[cfg(not(unix))]
fn one_file_rewriter(r: &Repo, name: &str) {
r.write(
name,
"@echo off\r\npowershell -NoProfile -Command \"$c = Get-Content -Raw '%1'; [IO.File]::WriteAllText('%1', ($c -replace 'BAD','GOOD'))\"\r\n",
);
r.git(&["add", name]);
}
#[cfg(unix)]
const ONE_FILE_REWRITER: &str = "rewrite-one.sh";
#[cfg(not(unix))]
const ONE_FILE_REWRITER: &str = "rewrite-one.cmd";
fn manifest_with_fixer(r: &Repo) {
rewriter(r, REWRITER);
r.stage(
"amont.conf",
&format!("pre-commit fmt *.js block fix ./{REWRITER}\n"),
);
let out = std::process::Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("trust")
.current_dir(&r.dir)
.output()
.expect("amont trust");
assert!(out.status.success(), "could not trust the manifest");
}
#[test]
fn fixing_is_off_unless_asked_for() {
let r = Repo::new();
manifest_with_fixer(&r);
r.stage("a.js", "const a = BAD\n");
let run = r.hook("pre-commit", &[]);
assert_eq!(
std::fs::read_to_string(r.path("a.js")).expect("read"),
"const a = BAD\n",
"it edited a file without being asked"
);
assert!(!run.says("fixed and re-staged"), "{}", run.output());
assert!(
run.says("could not run"),
"a check that never ran was rolled up as clean:\n{}",
run.output()
);
assert!(
run.says("amont.fix"),
"the gap must name its reason:\n{}",
run.output()
);
assert!(run.passed(), "{}", run.output());
}
#[test]
fn with_fixing_on_it_repairs_and_restages() {
let r = Repo::new();
manifest_with_fixer(&r);
r.git(&["config", "amont.fix", "true"]);
r.stage("a.js", "const a = BAD\n");
let run = r.hook("pre-commit", &[]);
assert!(
run.passed(),
"the repair should let the commit through:\n{}",
run.output()
);
assert_eq!(
std::fs::read_to_string(r.path("a.js")).expect("read"),
"const a = GOOD\n"
);
let staged = r.git(&["show", ":a.js"]);
assert_eq!(String::from_utf8_lossy(&staged.stdout), "const a = GOOD\n");
assert!(run.says("fixed and re-staged"), "{}", run.output());
}
#[test]
fn a_clean_file_is_not_reported_as_fixed() {
let r = Repo::new();
manifest_with_fixer(&r);
r.git(&["config", "amont.fix", "true"]);
r.stage("a.js", "const a = GOOD\n");
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{}", run.output());
assert!(!run.says("fixed and re-staged"), "{}", run.output());
}
#[test]
fn a_failed_restage_blocks_and_names_the_files() {
let r = Repo::new();
manifest_with_fixer(&r);
r.git(&["config", "amont.fix", "true"]);
r.stage("a.js", "const a = BAD\n");
std::fs::write(r.path(".git/index.lock"), "").expect("plant index.lock");
let run = r.hook("pre-commit", &[]);
let _ = std::fs::remove_file(r.path(".git/index.lock"));
assert!(
!run.passed(),
"the index holds unfixed content and this called it a pass:\n{}",
run.output()
);
assert!(
run.says("a.js"),
"the message must name what is stuck:\n{}",
run.output()
);
assert_eq!(
std::fs::read_to_string(r.path("a.js")).expect("read"),
"const a = GOOD\n"
);
}
#[test]
fn concurrent_restages_do_not_collide() {
let r = Repo::new();
one_file_rewriter(&r, ONE_FILE_REWRITER);
r.stage(
"amont.conf",
&format!(
"pre-commit fmt-a *.js block fix ./{ONE_FILE_REWRITER} a.js\n\
pre-commit fmt-b *.js block fix ./{ONE_FILE_REWRITER} b.js\n\
pre-commit fmt-c *.js block fix ./{ONE_FILE_REWRITER} c.js\n"
),
);
let out = std::process::Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("trust")
.current_dir(&r.dir)
.output()
.expect("amont trust");
assert!(out.status.success(), "could not trust the manifest");
r.git(&["config", "amont.fix", "true"]);
for name in ["a.js", "b.js", "c.js"] {
r.stage(name, "const a = BAD\n");
}
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{}", run.output());
for name in ["a.js", "b.js", "c.js"] {
let staged = r.git(&["show", &format!(":{name}")]);
assert_eq!(
String::from_utf8_lossy(&staged.stdout),
"const a = GOOD\n",
"{name} was rewritten but never re-staged:\n{}",
run.output()
);
}
}
fn prettier_repo() -> Repo {
let r = Repo::new();
r.write(".prettierrc", "{}\n");
r.git(&["add", ".prettierrc"]);
r
}
#[test]
fn prettier_repairs_and_restages() {
if missing("prettier") {
return;
}
let r = prettier_repo();
r.git(&["config", "amont.fix", "true"]);
r.stage("a.ts", "const x =1\n");
let run = r.hook("pre-commit-prettier", &[]);
assert!(run.passed(), "{}", run.output());
assert!(run.says("reformatted and re-staged"), "{}", run.output());
let staged = r.git(&["show", ":a.ts"]);
assert_eq!(String::from_utf8_lossy(&staged.stdout), "const x = 1;\n");
}
#[test]
fn prettier_does_not_touch_files_when_fixing_is_off() {
if missing("prettier") {
return;
}
let r = prettier_repo();
r.stage("a.ts", "const x =1\n");
let run = r.hook("pre-commit-prettier", &[]);
assert!(!run.passed(), "{}", run.output());
assert_eq!(
std::fs::read_to_string(r.path("a.ts")).expect("read"),
"const x =1\n",
"it rewrote a file nobody asked it to rewrite"
);
}
#[test]
fn prettier_blocks_when_it_cannot_restage_what_it_wrote() {
if missing("prettier") {
return;
}
let r = prettier_repo();
r.git(&["config", "amont.fix", "true"]);
r.stage("a.ts", "const x =1\n");
std::fs::write(r.path(".git/index.lock"), "").expect("plant index.lock");
let run = r.hook("pre-commit-prettier", &[]);
let _ = std::fs::remove_file(r.path(".git/index.lock"));
assert!(
!run.passed(),
"the index still holds unformatted content and this passed:\n{}",
run.output()
);
assert!(run.says("a.ts"), "{}", run.output());
assert_eq!(
std::fs::read_to_string(r.path("a.ts")).expect("read"),
"const x = 1;\n",
"prettier did write the file — that is what makes the stale index a bug"
);
}
#[test]
fn a_repair_does_not_sweep_in_unstaged_work() {
let r = Repo::new();
manifest_with_fixer(&r);
r.git(&["config", "amont.fix", "true"]);
r.stage("other.txt", "committed\n");
r.commit("chore: seed");
r.stage("a.js", "const a = BAD\n");
r.write("other.txt", "work in progress, not staged\n");
let run = r.hook("pre-commit", &[]);
assert!(run.passed(), "{}", run.output());
let staged = r.git(&["diff", "--cached", "--name-only"]);
let staged = String::from_utf8_lossy(&staged.stdout);
assert!(
staged.contains("a.js"),
"the repair was not staged: {staged}"
);
assert!(
!staged.contains("other.txt"),
"the repair swept in unstaged work: {staged}"
);
assert_eq!(
std::fs::read_to_string(r.path("other.txt")).expect("read"),
"work in progress, not staged\n",
"and the unstaged work must still be there"
);
}