#![cfg(unix)]
mod common;
use common::Repo;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
fn shim(r: &Repo, name: &str, body: &str) {
let dir = r.path(".git/toolshims");
std::fs::create_dir_all(&dir).expect("mkdir");
let p = dir.join(name);
std::fs::write(&p, format!("#!/bin/sh\n{body}")).expect("write");
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).expect("chmod");
}
fn shimmed_path(r: &Repo) -> String {
format!(
"{}:{}",
r.path(".git/toolshims").display(),
std::env::var("PATH").unwrap_or_default()
)
}
fn commit_check(r: &Repo, check: &str) -> (i32, String) {
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg(check)
.current_dir(&r.dir)
.env("PATH", shimmed_path(r))
.stdin(Stdio::null())
.output()
.expect("run");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn push_check(r: &Repo, check: &str, from: &str) -> (i32, String) {
let head = String::from_utf8_lossy(&r.git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string();
let line = format!("refs/heads/feat/x {head} refs/heads/feat/x {from}\n");
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg(check)
.current_dir(&r.dir)
.env("PATH", shimmed_path(r))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.unwrap()
.write_all(line.as_bytes())
.unwrap();
let out = child.wait_with_output().expect("wait");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn go_repo() -> Repo {
let r = Repo::new();
r.stage("go.mod", "module example.com/x\n");
r.stage("main.go", "package main\n");
r
}
#[test]
fn clean_formatting_passes() {
let r = go_repo();
shim(&r, "gofmt", "exit 0");
let (code, out) = commit_check(&r, "pre-commit-gofmt");
assert_eq!(code, 0, "{out}");
assert!(out.contains("Go formatting is clean"), "{out}");
}
#[test]
fn a_listed_file_fails_with_its_name() {
let r = go_repo();
shim(&r, "gofmt", "echo main.go\nexit 0");
let (code, out) = commit_check(&r, "pre-commit-gofmt");
assert_ne!(code, 0, "{out}");
assert!(out.contains("Unformatted Go"), "{out}");
assert!(out.contains("main.go"), "{out}");
}
#[test]
fn fixing_rewrites_and_restages() {
let r = go_repo();
r.git(&["config", "amont.fix", "true"]);
shim(
&r,
"gofmt",
"case \"$1\" in\n-l) echo main.go ;;\n-w) printf 'package main\\n\\nfunc main() {}\\n' > main.go ;;\nesac\nexit 0",
);
let (code, out) = commit_check(&r, "pre-commit-gofmt");
assert_eq!(code, 0, "{out}");
assert!(out.contains("Go reformatted and re-staged"), "{out}");
let staged = String::from_utf8_lossy(&r.git(&["show", ":main.go"]).stdout).to_string();
assert!(
staged.contains("func main"),
"index holds the fix: {staged}"
);
}
#[test]
fn a_moduleless_go_file_is_out_of_scope() {
let r = Repo::new();
r.stage("scripts/loose.go", "package main\n");
shim(&r, "gofmt", "echo SHOULD-NOT-RUN\nexit 1");
let (code, out) = commit_check(&r, "pre-commit-gofmt");
assert_eq!(code, 0, "{out}");
assert!(!out.contains("SHOULD-NOT-RUN"), "{out}");
}
#[test]
fn a_missing_gofmt_warns_and_never_blocks() {
let r = go_repo();
let dir = r.path(".git/toolshims");
std::fs::create_dir_all(&dir).unwrap();
let git = std::env::split_paths(&std::env::var_os("PATH").unwrap())
.map(|d| d.join("git"))
.find(|p| p.is_file())
.expect("git on PATH");
std::os::unix::fs::symlink(git, dir.join("git")).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-commit-gofmt")
.current_dir(&r.dir)
.env("PATH", r.path(".git/toolshims").display().to_string())
.stdin(Stdio::null())
.output()
.expect("run");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(out.status.code(), Some(0), "{text}");
assert!(text.contains("gofmt is not installed"), "{text}");
}
#[test]
fn a_clean_vet_passes() {
let r = go_repo();
shim(&r, "go", "exit 0");
let (code, out) = commit_check(&r, "pre-commit-go-vet");
assert_eq!(code, 0, "{out}");
assert!(out.contains("go vet passed"), "{out}");
}
#[test]
fn vet_findings_block_the_commit() {
let r = go_repo();
shim(&r, "go", "echo 'main.go:1:1: unreachable code' >&2\nexit 1");
let (code, out) = commit_check(&r, "pre-commit-go-vet");
assert_ne!(code, 0, "{out}");
assert!(out.contains("go vet"), "{out}");
}
#[test]
fn a_dependency_bump_alone_still_vets() {
let r = Repo::new();
r.stage("main.go", "package main\n");
r.commit("chore: base");
r.stage(
"go.mod",
"module example.com/x\n\nrequire example.com/dep v1.2.3\n",
);
shim(&r, "go", "echo VET-RAN >&2\nexit 0");
let (code, out) = commit_check(&r, "pre-commit-go-vet");
assert_eq!(code, 0, "{out}");
assert!(out.contains("VET-RAN"), "{out}");
}
fn pushed_go_repo() -> (Repo, String) {
let r = Repo::new();
r.stage("go.mod", "module example.com/x\n");
r.commit("chore: base");
let base = String::from_utf8_lossy(&r.git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string();
r.stage("main.go", "package main\n");
r.commit("feat: go change");
(r, base)
}
#[test]
fn a_green_suite_passes_the_push() {
let (r, base) = pushed_go_repo();
shim(&r, "go", "exit 0");
let (code, out) = push_check(&r, "pre-push-go-test", &base);
assert_eq!(code, 0, "{out}");
assert!(out.contains("Go tests passed"), "{out}");
}
#[test]
fn a_red_suite_aborts_the_push() {
let (r, base) = pushed_go_repo();
shim(&r, "go", "echo '--- FAIL: TestX' \nexit 1");
let (code, out) = push_check(&r, "pre-push-go-test", &base);
assert_ne!(code, 0, "{out}");
assert!(out.contains("Go tests failed"), "{out}");
}
#[test]
fn a_push_without_go_runs_nothing() {
let r = Repo::new();
r.stage("go.mod", "module example.com/x\n");
r.commit("chore: base");
let base = String::from_utf8_lossy(&r.git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string();
r.stage("README.md", "docs only\n");
r.commit("docs: change");
shim(&r, "go", "echo SHOULD-NOT-RUN\nexit 1");
let (code, out) = push_check(&r, "pre-push-go-test", &base);
assert_eq!(code, 0, "{out}");
assert!(!out.contains("SHOULD-NOT-RUN"), "{out}");
}