use std::path::{Path, PathBuf};
use std::process::Command;
struct Sandbox(PathBuf);
impl Sandbox {
fn new(name: &str) -> Self {
let d = std::env::temp_dir().join(format!("amont-init-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).expect("mkdir");
Sandbox(d)
}
fn path(&self, rel: &str) -> PathBuf {
rel.split('/').fold(self.0.clone(), |p, c| p.join(c))
}
fn init(&self, cwd: &Path) -> (i32, String) {
let out = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("init")
.current_dir(cwd)
.env("HOME", &self.0)
.env("USERPROFILE", &self.0)
.env("XDG_CONFIG_HOME", self.path(".config"))
.stdin(std::process::Stdio::null())
.output()
.expect("run amont init");
(
out.status.code().unwrap_or(-1),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn git(dir: &Path, args: &[&str]) {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("git");
}
fn init_repo(dir: &Path) {
std::fs::create_dir_all(dir).expect("mkdir");
git(dir, &["init", "-q", "--template=", "."]);
git(dir, &["config", "user.email", "t@t.test"]);
git(dir, &["config", "user.name", "t"]);
}
const DISPATCHERS: [&str; 4] = ["commit-msg", "pre-commit", "pre-push", "prepare-commit-msg"];
#[test]
fn init_writes_the_four_shims_and_bakes_the_running_binary() {
let s = Sandbox::new("basic");
let repo = s.path("repo");
init_repo(&repo);
let (code, out) = s.init(&repo);
assert_eq!(code, 0, "{out}");
for name in DISPATCHERS {
assert!(
repo.join(".git/hooks").join(name).is_file(),
"{name} missing:\n{out}"
);
}
let hook = std::fs::read_to_string(repo.join(".git/hooks/pre-commit")).expect("read");
assert!(
!hook.contains("__AMONT_BIN__"),
"shim was not baked:\n{hook}"
);
assert!(
hook.contains(env!("CARGO_BIN_EXE_amont")),
"baked something other than the running binary:\n{:?}",
hook.lines().find(|l| l.starts_with("BAKED="))
);
}
#[test]
fn init_touches_nothing_outside_the_repository() {
let s = Sandbox::new("scoped");
let repo = s.path("repo");
init_repo(&repo);
let (code, out) = s.init(&repo);
assert_eq!(code, 0, "{out}");
assert!(
!s.path(".local/bin").exists(),
"init copied a binary into ~/.local/bin:\n{out}"
);
assert!(
!s.path(".config/git/git-templates").exists(),
"init populated the git template dir — every future clone would get hooks:\n{out}"
);
}
#[test]
fn init_outside_a_repository_is_a_silent_success() {
let s = Sandbox::new("norepo");
let plain = s.path("plain");
std::fs::create_dir_all(&plain).expect("mkdir");
let (code, out) = s.init(&plain);
assert_eq!(code, 0, "init must not fail where there is no .git:\n{out}");
assert!(
out.trim().is_empty(),
"init should say nothing outside a repository, said:\n{out}"
);
}
#[test]
fn init_is_idempotent_and_rebakes() {
let s = Sandbox::new("twice");
let repo = s.path("repo");
init_repo(&repo);
assert_eq!(s.init(&repo).0, 0);
let first = std::fs::read_to_string(repo.join(".git/hooks/pre-commit")).expect("read");
std::fs::write(
repo.join(".git/hooks/pre-commit"),
first.replace(env!("CARGO_BIN_EXE_amont"), "/nonexistent/old/amont"),
)
.expect("write");
let (code, out) = s.init(&repo);
assert_eq!(code, 0, "{out}");
let second = std::fs::read_to_string(repo.join(".git/hooks/pre-commit")).expect("read");
assert_eq!(first, second, "a stale baked path was left in place");
}
#[test]
fn init_from_a_linked_worktree_bakes_into_the_shared_dir() {
let s = Sandbox::new("worktree");
let repo = s.path("repo");
init_repo(&repo);
git(&repo, &["commit", "--allow-empty", "-qm", "seed"]);
let wt = s.path("repo-wt");
let out = Command::new("git")
.arg("-C")
.arg(&repo)
.args(["worktree", "add", "-q", "-b", "feature"])
.arg(&wt)
.output()
.expect("git worktree add");
assert!(out.status.success(), "worktree add failed");
let (code, out) = s.init(&wt);
assert_eq!(code, 0, "{out}");
assert!(
repo.join(".git/hooks/pre-commit").is_file(),
"did not bake into the shared hooks dir:\n{out}"
);
assert!(
!repo.join(".git/worktrees/feature/hooks").exists(),
"baked into the worktree-private gitdir, where git never looks"
);
}
#[test]
fn init_refuses_a_repository_husky_still_owns() {
let s = Sandbox::new("husky");
let repo = s.path("repo");
init_repo(&repo);
std::fs::create_dir_all(repo.join(".husky/_")).expect("mkdir");
git(&repo, &["config", "core.hooksPath", ".husky/_"]);
let (code, out) = s.init(&repo);
assert_ne!(code, 0, "init wrote into a husky repository:\n{out}");
assert!(out.contains("husky"), "must name the tool:\n{out}");
assert!(
out.contains("git config --unset core.hooksPath"),
"must name the remedy:\n{out}"
);
assert!(
!repo.join(".git/hooks/pre-commit").exists(),
"wrote a shim git would never dispatch"
);
assert!(
!repo.join(".husky/_/pre-commit").exists(),
"wrote into the directory husky regenerates"
);
}
#[test]
fn init_refuses_to_overwrite_a_hook_it_did_not_write() {
let s = Sandbox::new("foreign");
let repo = s.path("repo");
init_repo(&repo);
let hooks = repo.join(".git/hooks");
std::fs::create_dir_all(&hooks).expect("mkdir");
let mine = "#!/bin/sh\n# mine, thanks\nexit 0\n";
std::fs::write(hooks.join("pre-commit"), mine).expect("write");
let (code, out) = s.init(&repo);
assert_ne!(code, 0, "init overwrote a foreign hook:\n{out}");
assert_eq!(
std::fs::read_to_string(hooks.join("pre-commit")).expect("read"),
mine,
"somebody's own pre-commit was replaced"
);
for name in ["commit-msg", "pre-push", "prepare-commit-msg"] {
assert!(
!hooks.join(name).exists(),
"{name} was written despite the refusal — a half-installed repo"
);
}
}
#[test]
fn the_hooks_init_writes_run_a_real_commit() {
let s = Sandbox::new("real-commit");
let repo = s.path("repo");
init_repo(&repo);
let (code, out) = s.init(&repo);
assert_eq!(code, 0, "{out}");
std::fs::write(repo.join("a.txt"), "hello\n").expect("write");
git(&repo, &["add", "a.txt"]);
let out = Command::new("git")
.arg("-C")
.arg(&repo)
.args(["commit", "-m", "feat: add a file"])
.env("HOME", &s.0)
.output()
.expect("git commit");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(out.status.success(), "the commit was rejected:\n{text}");
assert!(
text.contains('✓') || text.to_lowercase().contains("passed"),
"no sign a check ran:\n{text}"
);
}