use super::commit::{remove_settled, run_commit};
use super::support::{Fixture, direct, executable, fixture, git, observe, repository, stderr_of};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
fn edited_repository(fixture: &Fixture, name: &str) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new(name));
std::fs::write(repo.join("a.txt"), b"one\n").expect("file");
git(fixture, repo.as_path(), &["add", "--all"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=base"]);
std::fs::write(repo.join("a.txt"), b"one\ntwo\n").expect("edit");
return repo;
}
fn configure_marker_hook(fixture: &Fixture, repo: &Path, name: &str, event: &str, marker: &Path) {
let script: PathBuf = fixture.root.join(format!("hook-{name}"));
executable(script.as_path(), format!("#!/bin/sh\necho {event} >> '{}'\nexit 9\n", marker.display()).as_bytes());
git(fixture, repo, &["config", format!("hook.{name}.event").as_str(), event]);
git(fixture, repo, &["config", format!("hook.{name}.command").as_str(), script.to_str().expect("UTF-8")]);
}
#[test]
fn configured_hooks_for_the_commit_events_never_run_and_are_reported() {
let fixture: Fixture = fixture("external-commit-events");
let repo: PathBuf = edited_repository(&fixture, "repo");
let marker: PathBuf = fixture.root.join("marker");
for event in ["pre-commit", "prepare-commit-msg", "commit-msg", "post-commit"] {
configure_marker_hook(&fixture, repo.as_path(), format!("on-{event}").as_str(), event, marker.as_path());
}
let outcome = run_commit(&fixture, repo.as_path(), &["commit", "--quiet", "--message=second", "--", "a.txt"]);
assert_eq!(outcome.code, Some(0), "{}", stderr_of(&outcome));
assert!(!marker.exists(), "a hook ran: {:?}", std::fs::read_to_string(&marker));
let log: String = String::from_utf8_lossy(
&git(&fixture, repo.as_path(), &["log", "-1", "--format=%B"]).stdout,
)
.trim()
.to_string();
assert_eq!(log, "second");
let stderr: String = stderr_of(&outcome);
assert!(stderr.contains("external-hooks-unsupported"), "{stderr}");
for expected in ["hook.on-pre-commit", "hook.on-commit-msg", "hook.on-post-commit", "runs no program named by any configuration", "git cli-git doctor"] {
assert!(stderr.contains(expected), "{expected} in {stderr}");
}
remove_settled(&fixture);
}
#[test]
fn a_global_hook_never_runs_and_a_clean_repository_is_not_warned() {
let fixture: Fixture = fixture("external-global");
let clean: PathBuf = edited_repository(&fixture, "clean");
let quiet = run_commit(&fixture, clean.as_path(), &["commit", "--quiet", "--message=second", "--", "a.txt"]);
assert_eq!(quiet.code, Some(0), "{}", stderr_of(&quiet));
assert!(!stderr_of(&quiet).contains("external-hooks-unsupported"), "{}", stderr_of(&quiet));
let repo: PathBuf = edited_repository(&fixture, "other");
let marker: PathBuf = fixture.root.join("global-marker");
let script: PathBuf = fixture.root.join("global-hook");
executable(script.as_path(), format!("#!/bin/sh\necho global >> '{}'\n", marker.display()).as_bytes());
let global: PathBuf = fixture.root.join("global-config");
std::fs::write(
&global,
format!("[hook \"everywhere\"]\n\tevent = pre-commit\n\tcommand = {}\n", script.display()),
)
.expect("global config");
let mut command: Command = super::commit::wrapped_commit(
&fixture,
repo.as_path(),
&["commit", "--quiet", "--message=second", "--", "a.txt"],
);
command.env("GIT_CONFIG_GLOBAL", &global);
let outcome = observe(&mut command, b"");
assert_eq!(outcome.code, Some(0), "{}", stderr_of(&outcome));
assert!(!marker.exists(), "a global hook ran");
assert!(stderr_of(&outcome).contains("hook.everywhere"), "{}", stderr_of(&outcome));
remove_settled(&fixture);
}
#[test]
fn a_forwarded_command_runs_no_configured_hook() {
let fixture: Fixture = fixture("external-forwarded");
let repo: PathBuf = edited_repository(&fixture, "repo");
git(&fixture, repo.as_path(), &["stash", "--quiet"]);
git(&fixture, repo.as_path(), &["branch", "feature"]);
let marker: PathBuf = fixture.root.join("marker");
configure_marker_hook(&fixture, repo.as_path(), "after-checkout", "post-checkout", marker.as_path());
let outcome = run_commit(&fixture, repo.as_path(), &["checkout", "--quiet", "feature"]);
assert_eq!(outcome.code, Some(0), "{}", stderr_of(&outcome));
assert!(!marker.exists(), "a hook ran on a forwarded command");
remove_settled(&fixture);
}
#[test]
fn the_retired_hooks_key_still_loads_in_a_commit() {
let fixture: Fixture = fixture("external-retired-key");
let repo: PathBuf = edited_repository(&fixture, "repo");
std::fs::write(repo.join("cli-git.config.jsonc"), "{ \"hooks\": { \"concurrentCommits\": true } }\n").expect("config");
let outcome = run_commit(&fixture, repo.as_path(), &["commit", "--quiet", "--message=second", "--", "a.txt"]);
assert_eq!(outcome.code, Some(0), "{}", stderr_of(&outcome));
remove_settled(&fixture);
}
#[test]
fn doctor_lists_configured_hooks_and_notes_the_retired_key() {
let fixture: Fixture = fixture("external-doctor");
let repo: PathBuf = edited_repository(&fixture, "repo");
let marker: PathBuf = fixture.root.join("marker");
configure_marker_hook(&fixture, repo.as_path(), "review", "post-commit", marker.as_path());
std::fs::write(repo.join("cli-git.config.jsonc"), "{ \"hooks\": { \"concurrentCommits\": false } }\n").expect("config");
let outcome = run_commit(&fixture, repo.as_path(), &["cli-git", "doctor"]);
assert_eq!(outcome.code, Some(1), "{}", stderr_of(&outcome));
assert!(String::from_utf8_lossy(&outcome.stdout).contains("hook.review (events: post-commit)"), "{:?}", outcome.stdout);
let stderr: String = stderr_of(&outcome);
assert!(stderr.contains("runs no program") && stderr.contains("accepted so older files still load, and ignored"), "{stderr}");
assert!(!marker.exists());
remove_settled(&fixture);
}
#[test]
fn a_start_name_that_used_to_be_an_entry_behaves_as_git() {
let fixture: Fixture = fixture("external-other-names");
let repo: PathBuf = edited_repository(&fixture, "repo");
let hooks: PathBuf = repo.join(".git/cli-git-transactions/0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10/hooks");
std::fs::create_dir_all(&hooks).expect("hooks directory");
let elsewhere: PathBuf = fixture.root.join("elsewhere");
std::fs::create_dir(&elsewhere).expect("elsewhere");
let mut starts: Vec<PathBuf> = Vec::new();
for event in git_policy_cli::hooks_off::HOOK_EVENTS {
for directory in [hooks.as_path(), elsewhere.as_path()] {
let start: PathBuf = directory.join(event);
std::os::unix::fs::symlink(&fixture.wrapper, &start).expect("link");
starts.push(start);
}
}
for start in starts {
let mut command: Command = super::support::bounded(
&fixture,
start.as_path(),
super::support::wrapper_first_path(&fixture).as_os_str(),
);
command.arg("--version").current_dir(&repo);
let outcome = observe(&mut command, b"");
assert_eq!(outcome.code, Some(0), "{}", String::from_utf8_lossy(&outcome.stderr));
assert!(String::from_utf8_lossy(&outcome.stdout).starts_with("git version "), "{start:?}");
}
remove_settled(&fixture);
}
#[test]
fn a_post_commit_key_is_a_configuration_error() {
let fixture: Fixture = fixture("external-post-commit-key");
let repo: PathBuf = edited_repository(&fixture, "repo");
let marker: PathBuf = fixture.root.join("action.ran");
std::fs::write(
repo.join("cli-git.config.jsonc"),
format!("{{ \"postCommit\": {{ \"actions\": [{{ \"name\": \"x\", \"argv\": [\"touch\", \"{}\"] }}] }} }}\n", marker.display()),
)
.expect("config");
let outcome = run_commit(&fixture, repo.as_path(), &["commit", "--quiet", "--message=second", "--", "a.txt"]);
assert_ne!(outcome.code, Some(0));
let stderr: String = stderr_of(&outcome);
assert!(stderr.contains("postCommit") && stderr.contains("runs no program named by any configuration"), "{stderr}");
assert!(!marker.exists(), "nothing the configuration names may run");
assert_eq!(
String::from_utf8_lossy(&git(&fixture, repo.as_path(), &["log", "-1", "--format=%s"]).stdout).trim(),
"base",
"nothing landed"
);
remove_settled(&fixture);
}
#[test]
fn a_callers_own_config_options_cannot_redirect_the_commit_or_bring_a_hook_back() {
let fixture: Fixture = fixture("external-caller-options");
let repo: PathBuf = edited_repository(&fixture, "repo");
let marker: PathBuf = fixture.root.join("marker");
configure_marker_hook(&fixture, repo.as_path(), "late", "post-commit", marker.as_path());
configure_marker_hook(&fixture, repo.as_path(), "early", "pre-commit", marker.as_path());
let elsewhere: PathBuf = fixture.root.join("elsewhere");
std::fs::create_dir(&elsewhere).expect("elsewhere");
let hooks_path: String = format!("core.hooksPath={}", repo.join(".git/hooks").display());
let worktree: String = format!("core.worktree={}", elsewhere.display());
let outcome = run_commit(
&fixture,
repo.as_path(),
&[
"-c", hooks_path.as_str(), "-c", worktree.as_str(), "-c", "hook.post-commit.enabled=true",
"-c", "hook.pre-commit.enabled=true", "-c", "hook.reference-transaction.enabled=true",
"commit", "--quiet", "--message=second", "--", "a.txt",
],
);
assert_eq!(outcome.code, Some(0), "{}", stderr_of(&outcome));
assert!(!marker.exists(), "a hook ran: {:?}", std::fs::read_to_string(&marker));
assert_eq!(String::from_utf8_lossy(&git(&fixture, repo.as_path(), &["show", "HEAD:a.txt"]).stdout), "one\ntwo\n");
remove_settled(&fixture);
}
#[test]
fn a_configured_pre_push_hook_never_runs_on_a_push() {
let fixture: Fixture = fixture("external-pre-push");
let repo: PathBuf = edited_repository(&fixture, "repo");
let remote: PathBuf = fixture.root.join("remote.git");
git(&fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", remote.to_str().expect("UTF-8")]);
git(&fixture, repo.as_path(), &["remote", "add", "origin", remote.to_str().expect("UTF-8")]);
let marker: PathBuf = fixture.root.join("marker");
configure_marker_hook(&fixture, repo.as_path(), "before-push", "pre-push", marker.as_path());
let control = direct(&fixture)
.current_dir(repo.as_path())
.args(["push", "--dry-run", "origin", "HEAD:refs/heads/control"])
.output()
.expect("real Git");
assert!(!control.status.success(), "the hook exits 9, so real Git refuses the push");
assert!(marker.exists(), "the control hook did not write the marker, so this test would prove nothing");
std::fs::remove_file(&marker).expect("clear the marker");
for arguments in [
vec!["push", "--quiet", "origin", "HEAD:refs/heads/one"],
vec!["push", "--quiet", "--dry-run", "origin", "HEAD:refs/heads/two"],
vec!["-c", "hook.before-push.enabled=true", "push", "--quiet", "origin", "HEAD:refs/heads/three"],
] {
let outcome = run_commit(&fixture, repo.as_path(), arguments.as_slice());
assert_eq!(outcome.code, Some(0), "{arguments:?}: {}", stderr_of(&outcome));
assert!(!marker.exists(), "{arguments:?}: the pre-push hook ran");
let stderr: String = stderr_of(&outcome);
assert!(stderr.contains("external-hooks-unsupported") && stderr.contains("hook.before-push"), "{stderr}");
}
remove_settled(&fixture);
}