mod common;
use common::{template_hook, Repo};
use std::io::Write;
use std::process::{Command, Stdio};
const ZERO: &str = "0000000000000000000000000000000000000000";
fn repo_with_origin() -> Repo {
let r = Repo::new();
r.git(&["commit", "-q", "--allow-empty", "--no-verify", "-m", "init"]);
let origin = r.path(".fake-origin.git");
r.git(&["init", "-q", "--bare", origin.to_str().unwrap()]);
r.git(&["remote", "add", "origin", origin.to_str().unwrap()]);
r.git(&["push", "-q", "--no-verify", "origin", "HEAD:main"]);
r
}
fn head(r: &Repo) -> String {
String::from_utf8_lossy(&r.git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string()
}
fn push_refs(r: &Repo, lines: &str) -> (bool, String) {
let mut child = Command::new(env!("CARGO_BIN_EXE_amont"))
.arg("--hooks-dir")
.arg(r.path(".git/hooks"))
.arg("pre-push-branch-pattern")
.arg("origin")
.current_dir(&r.dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn");
child
.stdin
.as_mut()
.expect("stdin")
.write_all(lines.as_bytes())
.expect("write");
let out = child.wait_with_output().expect("wait");
(
out.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn new_branch(r: &Repo, name: &str) -> (bool, String) {
push_refs(
r,
&format!("refs/heads/{name} {} refs/heads/{name} {ZERO}\n", head(r)),
)
}
#[test]
fn passes_when_the_branch_is_already_on_the_server() {
let r = repo_with_origin();
let tip = head(&r);
let (ok, out) = push_refs(
&r,
&format!("refs/heads/off-pattern {tip} refs/heads/off-pattern {tip}\n"),
);
assert!(ok, "{out}");
}
#[test]
fn rejects_a_name_that_does_not_conform() {
let r = repo_with_origin();
let (ok, out) = new_branch(&r, "off-pattern");
assert!(!ok, "{out}");
assert!(out.contains("off-pattern"), "{out}");
}
#[test]
fn accepts_a_conforming_name() {
let r = repo_with_origin();
let (ok, out) = new_branch(&r, "feat/0-test");
assert!(ok, "{out}");
}
#[test]
fn dots_are_allowed_under_chore_only() {
let r = repo_with_origin();
assert!(new_branch(&r, "chore/duro-1.50.50").0);
assert!(!new_branch(&r, "feat/duro-1.50.50").0);
}
#[test]
fn a_name_without_a_type_prefix_is_rejected() {
let r = repo_with_origin();
assert!(!new_branch(&r, "duro-1.50.50").0);
}
#[test]
fn a_renamed_push_is_judged_by_its_target_name() {
let r = repo_with_origin();
let tip = head(&r);
let (ok, out) = push_refs(
&r,
&format!("refs/heads/feat/fine {tip} refs/heads/off-pattern {ZERO}\n"),
);
assert!(
!ok,
"it judged the local name, not the one being created:\n{out}"
);
assert!(out.contains("off-pattern"), "{out}");
}
#[test]
fn every_offending_ref_is_named() {
let r = repo_with_origin();
let tip = head(&r);
let (ok, out) = push_refs(
&r,
&format!(
"refs/heads/a {tip} refs/heads/bad-one {ZERO}\n\
refs/heads/b {tip} refs/heads/feat/fine {ZERO}\n\
refs/heads/c {tip} refs/heads/bad-two {ZERO}\n"
),
);
assert!(!ok, "{out}");
assert!(out.contains("bad-one") && out.contains("bad-two"), "{out}");
}
#[test]
fn a_delete_is_not_a_name_to_validate() {
let r = repo_with_origin();
let (ok, out) = push_refs(
&r,
&format!("(delete) {ZERO} refs/heads/off-pattern {ZERO}\n"),
);
assert!(ok, "{out}");
}
#[test]
fn a_tag_is_not_a_branch() {
let r = repo_with_origin();
let tip = head(&r);
let (ok, out) = push_refs(
&r,
&format!("refs/tags/v1.0.0 {tip} refs/tags/v1.0.0 {ZERO}\n"),
);
assert!(ok, "{out}");
}
#[test]
fn the_previously_rejected_prefixes_are_accepted() {
let r = repo_with_origin();
for b in [
"docs/x",
"refactor/y",
"perf/z",
"build/w",
"style/v",
"revert/u",
"add/t",
"remove/s",
] {
let (ok, out) = new_branch(&r, b);
assert!(ok, "{b}: {out}");
}
}
#[cfg(unix)]
#[test]
fn a_detached_head_push_is_judged_by_its_target() {
use std::os::unix::fs::PermissionsExt;
let r = repo_with_origin();
assert!(
!r.path(".git/refs/remotes/origin/HEAD").exists(),
"fixture: this case needs a repo with no remote HEAD ref"
);
let shim = r.path(".git/hooks/pre-push");
std::fs::create_dir_all(shim.parent().expect("parent")).expect("mkdir");
std::fs::copy(template_hook("pre-push"), &shim).expect("install the shim");
std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755)).expect("chmod");
r.git(&["checkout", "-q", "--detach"]);
let push = |refspec: &str| -> (bool, String) {
let mut cmd = Command::new("git");
cmd.args(["push", "origin", refspec])
.current_dir(&r.dir)
.stdin(Stdio::null());
Repo::strip_git_env_impl(&mut cmd);
cmd.env("GIT_HOOKS_BIN", env!("CARGO_BIN_EXE_amont"));
let out = cmd.output().expect("git push");
(
out.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
};
let (ok, out) = push("HEAD:refs/heads/feat/x");
assert!(
ok,
"a conforming target was blocked from a detached HEAD:\n{out}"
);
let (ok, out) = push("HEAD:refs/heads/off-pattern");
assert!(
!ok,
"an off-pattern target was allowed from a detached HEAD:\n{out}"
);
}