use super::{amont, git_commit, git_out, head, install, read};
use crate::common::{missing, Repo};
use crate::fixing::{rewriter, REWRITER};
const SEED: &str = "{\n \"a\": 1\n}\n";
const VALID: &str = "{\n \"a\": 2\n}\n";
const BROKEN: &str = "{ THIS IS NOT JSON\n";
const REUSED: &str = "passed on this exact tree earlier — not repeating it here";
fn head_files(r: &Repo) -> String {
git_out(
r,
&["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"],
)
}
fn held(r: &Repo) -> bool {
r.path(".git/amont-held").exists()
}
fn runs(r: &Repo) -> usize {
std::fs::read_to_string(r.path("gate.log"))
.map(|s| s.len())
.unwrap_or(0)
}
fn bypasses(r: &Repo, script: &str) -> i64 {
let (_, out) = amont(r, &["list", "--json"]);
let v: serde_json::Value =
serde_json::from_str(&out).unwrap_or_else(|e| panic!("list --json: {e}\n{out}"));
v["bypasses"]["by_script"]
.as_array()
.into_iter()
.flatten()
.filter(|s| s["script"] == script)
.filter_map(|s| s["count"].as_i64())
.sum()
}
fn conflicted() -> String {
format!(
"{}\nours\n{}\ntheirs\n{}\n",
"<".repeat(7),
"=".repeat(7),
">".repeat(7)
)
}
#[test]
fn the_commit_journey() {
let r = Repo::new();
r.stage("x.json", SEED);
r.stage("é.json", SEED);
r.stage("other.txt", "committed\n");
r.stage(
"gate.cjs",
"const fs = require('fs');\n\
fs.appendFileSync('gate.log', 'x');\n\
process.exit(fs.existsSync('fail') ? 1 : 0);\n",
);
rewriter(&r, REWRITER);
r.stage(
"amont.conf",
&format!(
"pre-commit suite *.txt block node gate.cjs\n\
pre-commit fmt *.js block fix ./{REWRITER}\n"
),
);
r.commit("chore: seed");
install(&r);
r.stage("x.json", VALID);
r.write("x.json", BROKEN);
r.stage("é.json", VALID);
r.write("é.json", BROKEN);
r.write("scratch.txt", "not staged, not tracked\n");
let (ok, out) = git_commit(&r, &["-m", "feat: a valid change"]);
step!(
"staged-only",
ok,
"judged the working tree, not the commit:\n{out}"
);
step_eq!(
"staged-only",
git_out(&r, &["show", "HEAD:x.json"]),
VALID.trim_end()
);
step_eq!(
"staged-only",
read(&r, "x.json"),
BROKEN,
"unstaged work was not restored"
);
step_eq!(
"staged-only",
read(&r, "é.json"),
BROKEN,
"unstaged work on a non-ASCII path was not restored"
);
step_eq!(
"staged-only",
read(&r, "scratch.txt"),
"not staged, not tracked\n",
"an untracked file was moved"
);
r.stage("é.json", VALID); let before = head(&r);
r.stage("x.json", BROKEN);
r.write("x.json", "{\n \"a\": 9\n}\n");
let (ok, out) = git_commit(&r, &["-m", "feat: a broken change"]);
step!(
"blocked",
!ok,
"a broken staged change passed because the tree was fine:\n{out}"
);
step!(
"blocked",
out.contains("pre-commit-lint-json-yaml"),
"{out}"
);
step!("blocked", out.contains("x.json"), "{out}");
step_eq!("blocked", head(&r), before, "a blocked commit moved HEAD");
step_eq!(
"blocked",
read(&r, "x.json"),
"{\n \"a\": 9\n}\n",
"a blocked commit lost the unstaged change"
);
step!("blocked", !held(&r), "held files left behind after a block");
std::fs::remove_file(r.path("scratch.txt")).expect("remove scratch");
r.stage("x.json", "{\n \"a\": 9\n}\n");
r.stage("bad.txt", &conflicted());
r.stage("bad.ts", "fit('x', () => {});\n");
let (ok, out) = git_commit(&r, &["-m", "feat: two mistakes"]);
step!("every failure", !ok, "{out}");
step!(
"every failure",
out.contains("pre-commit-merge-conflict"),
"missing merge-conflict:\n{out}"
);
step!(
"every failure",
out.contains("pre-commit-ban-terms"),
"missing ban-terms:\n{out}"
);
r.stage("bad.txt", "resolved\n");
r.stage("bad.ts", "it('x', () => {});\n");
let (ok, out) = git_commit(&r, &["-m", "fix: both mistakes"]);
step!("clean", ok, "{out}");
step!(
"clean",
!out.contains("skipped by"),
"no skip, no notice:\n{out}"
);
step_eq!("clean", read(&r, "x.json"), "{\n \"a\": 9\n}\n");
step!("clean", !held(&r), "held something with nothing to hold");
r.git(&["config", "amont.fix", "true"]);
r.stage("a.js", "const a = BAD\n");
let (ok, out) = git_commit(&r, &["-m", "feat: a"]);
step!(
"fix",
ok,
"the repair should let the commit through:\n{out}"
);
step_eq!("fix", read(&r, "a.js"), "const a = GOOD\n");
step_eq!("fix", git_out(&r, &["show", ":a.js"]), "const a = GOOD");
step_eq!("fix", git_out(&r, &["show", "HEAD:a.js"]), "const a = GOOD");
step!("fix", out.contains("fixed and re-staged"), "{out}");
r.stage("c.js", "const a = GOOD\n");
let (ok, out) = git_commit(&r, &["-m", "feat: c"]);
step!("fix, clean", ok, "{out}");
step!("fix, clean", !out.contains("fixed and re-staged"), "{out}");
r.stage("b.js", "const a = BAD\n");
r.write("other.txt", "work in progress, not staged\n");
let (ok, out) = git_commit(&r, &["-m", "feat: b"]);
step!("fix, kept back", ok, "{out}");
let files = head_files(&r);
step!(
"fix, kept back",
files.lines().any(|f| f == "b.js"),
"the repair was not committed: {files}"
);
step!(
"fix, kept back",
!files.contains("other.txt"),
"the repair swept in unstaged work: {files}"
);
step_eq!(
"fix, kept back",
read(&r, "other.txt"),
"work in progress, not staged\n",
"and the unstaged work must still be there"
);
r.git(&["config", "--unset", "amont.fix"]);
r.stage("d.js", "const a = BAD\n");
let (ok, out) = git_commit(&r, &["-m", "feat: d"]);
step_eq!(
"fix off",
read(&r, "d.js"),
"const a = BAD\n",
"it edited a file without being asked"
);
step!("fix off", !out.contains("fixed and re-staged"), "{out}");
step!(
"fix off",
out.contains("could not run"),
"a check that never ran was rolled up as clean:\n{out}"
);
step!(
"fix off",
out.contains("amont.fix"),
"the gap must name its reason:\n{out}"
);
step!("fix off", ok, "{out}");
let dodged = bypasses(&r, "suite");
r.git(&["add", "other.txt"]);
let (ok, out) = git_commit(&r, &["--no-verify", "-m", "chore: wip"]);
step!("no-verify", ok, "{out}");
step_eq!("no-verify", head_files(&r), "other.txt");
step!(
"no-verify",
!r.git(&["notes", "--ref", "amont-gate", "show", "HEAD"])
.status
.success(),
"a commit that dodged its gate carries a stamp"
);
step_eq!(
"no-verify",
bypasses(&r, "suite"),
dodged + 1,
"the dodge is counted, under the gate's name"
);
if missing("node") {
return;
}
let n = runs(&r);
let before = head(&r);
r.stage("a.txt", "hello\n");
let (ok, out) = git_commit(&r, &["-m", "just a message with no type"]);
step!(
"refused message",
!ok,
"commit-msg was meant to refuse:\n{out}"
);
step_eq!("refused message", runs(&r), n + 1, "the gate ran once");
step!(
"refused message",
!out.contains(REUSED),
"nothing was vouched for yet:\n{out}"
);
step_eq!("refused message", head(&r), before);
let (ok, out) = git_commit(&r, &["-m", "feat: through the gate"]);
step!("retry", ok, "{out}");
step!(
"retry",
out.contains(REUSED),
"the reuse is said out loud:\n{out}"
);
step_eq!("retry", runs(&r), n + 1, "the retry repeated nothing");
let note = r.git(&["notes", "--ref", "amont-gate", "show", &head(&r)]);
step!(
"retry",
note.status.success(),
"the retried commit has no gate stamp"
);
step!(
"retry",
String::from_utf8_lossy(¬e.stdout).contains("suite"),
"the stamp names the gate"
);
let n = runs(&r);
r.stage("a.txt", "hello again\n");
let (ok, _) = git_commit(&r, &["-m", "just a message with no type"]);
step!("changed tree", !ok);
r.stage("a.txt", "hello once more\n");
let (ok, out) = git_commit(&r, &["-m", "feat: more"]);
step!("changed tree", ok, "{out}");
step!(
"changed tree",
!out.contains(REUSED),
"new content is not vouched for:\n{out}"
);
step_eq!("changed tree", runs(&r), n + 2);
let n = runs(&r);
step!(
"re-commit",
r.git(&["reset", "-q", "--soft", "HEAD~1"]).status.success()
);
let (ok, out) = git_commit(&r, &["-m", "feat: same content, new commit"]);
step!("re-commit", ok, "{out}");
step!(
"re-commit",
out.contains(REUSED),
"the tree note vouches for the re-commit:\n{out}"
);
step_eq!("re-commit", runs(&r), n);
let n = runs(&r);
r.git(&["config", "amont.commitStamps", "false"]);
r.stage("a.txt", "switched off\n");
let (ok, _) = git_commit(&r, &["-m", "just a message with no type"]);
step!("stamps off", !ok);
let (ok, out) = git_commit(&r, &["-m", "feat: through the gate"]);
step!("stamps off", ok, "{out}");
step!("stamps off", !out.contains(REUSED), "reuse is off:\n{out}");
step_eq!("stamps off", runs(&r), n + 2);
r.git(&["config", "--unset", "amont.commitStamps"]);
let n = runs(&r);
r.write("fail", "");
r.stage("a.txt", "blocked\n");
let (ok, out) = git_commit(&r, &["-m", "feat: blocked"]);
step!("failed gate", !ok, "the gate was meant to block:\n{out}");
step!(
"failed gate",
out.contains("suite"),
"the block names the gate:\n{out}"
);
step_eq!("failed gate", runs(&r), n + 1);
std::fs::remove_file(r.path("fail")).expect("remove fail");
let (ok, out) = git_commit(&r, &["-m", "feat: through"]);
step!("failed gate", ok, "{out}");
step!(
"failed gate",
!out.contains(REUSED),
"a rejection is not a stamp:\n{out}"
);
step_eq!("failed gate", runs(&r), n + 2);
}