use super::commit::{git_text, remove_settled, wrapped_commit};
use super::service_commit::{Running, runtime_for, start};
use super::support::{Fixture, fixture, git, repository};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};
fn edited_repository(fixture: &Fixture) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
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"]);
git(fixture, repo.as_path(), &["config", "maintenance.autoDetach", "false"]);
std::fs::write(repo.join("a.txt"), b"one\ntwo\n").expect("edit");
return repo;
}
fn served(fixture: &Fixture, repo: &Path, name: &str, arguments: &[&str]) -> Command {
let mut command: Command = wrapped_commit(fixture, repo, arguments);
command.env("XDG_RUNTIME_DIR", runtime_for(name));
return command;
}
fn finished_batches(fixture: &Fixture) -> usize {
return std::fs::read_dir(fixture.root.join("journal"))
.expect("journal")
.flatten()
.filter(|entry| { return entry.path().extension().is_some_and(|extension| { return extension == git_policy_cli::service_journal::DONE_EXTENSION; }); })
.count();
}
#[test]
fn a_service_commit_warns_that_configured_hooks_never_run() {
let fixture: Fixture = fixture("client-hooks-warning");
let repo: PathBuf = edited_repository(&fixture);
let marker: PathBuf = fixture.root.join("marker");
let script: PathBuf = fixture.root.join("hook-program");
super::support::executable(script.as_path(), format!("#!/bin/sh\necho ran >> '{}'\nexit 9\n", marker.display()).as_bytes());
git(&fixture, repo.as_path(), &["config", "hook.on-commit.event", "pre-commit"]);
git(&fixture, repo.as_path(), &["config", "hook.on-commit.command", script.to_str().expect("UTF-8")]);
let name: String = format!("client-hooks-{}", std::process::id());
let _service: Running = start(&fixture, name.as_str());
let output: Output = served(&fixture, &repo, name.as_str(), &["commit", "--quiet", "-m", "second", "--", "a.txt"]).output().expect("the wrapped commit runs");
let stderr: String = String::from_utf8_lossy(&output.stderr).into_owned();
assert!(output.status.success(), "{stderr}");
assert_eq!(finished_batches(&fixture), 1, "the service landed it: {stderr}");
assert!(!marker.exists(), "the hook ran");
assert!(stderr.contains("external-hooks-unsupported") && stderr.contains("hook.on-commit"), "{stderr}");
assert_eq!(git_text(&fixture, &repo, &["log", "-1", "--format=%s"]).trim(), "second");
remove_settled(&fixture);
}
fn summaries(output: &Output) -> Vec<(String, String)> {
return String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|line| { return line.starts_with('['); })
.map(|line| {
let name: &str = line.split_whitespace().find(|word| { return word.ends_with(']'); }).unwrap_or("").trim_end_matches(']');
return (line.to_string(), name.to_string());
})
.collect();
}
fn git_short_head(fixture: &Fixture, repo: &Path) -> String {
return git_text(fixture, repo, &["rev-parse", "--short", "HEAD"]).trim().to_string();
}
#[test]
fn the_summary_line_of_a_service_commit_has_the_name_length_git_gives() {
for (label, abbrev) in [("unset", None), ("twelve", Some("12")), ("whole", Some("no"))] {
let fixture: Fixture = fixture(format!("client-abbrev-{label}").as_str());
let repo: PathBuf = edited_repository(&fixture);
if let Some(value) = abbrev {
git(&fixture, repo.as_path(), &["config", "core.abbrev", value]);
}
let name: String = format!("client-abbrev-{label}-{}", std::process::id());
let _service: Running = start(&fixture, name.as_str());
let output: Output = served(&fixture, &repo, name.as_str(), &["commit", "-m", "second", "--", "a.txt"]).output().expect("the wrapped commit runs");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
assert_eq!(finished_batches(&fixture), 1, "the service landed it");
let lines: Vec<(String, String)> = summaries(&output);
assert_eq!(lines.len(), 1, "{label}: {:?}", String::from_utf8_lossy(&output.stdout));
assert_eq!(lines[0].1, git_short_head(&fixture, &repo), "{label}: {}", lines[0].0);
assert!(lines[0].0.starts_with("[main "), "{label}: {}", lines[0].0);
remove_settled(&fixture);
}
}
#[test]
fn a_commit_git_builds_prints_one_summary_line_and_quiet_prints_none() {
let fixture: Fixture = fixture("client-built-summary");
let repo: PathBuf = edited_repository(&fixture);
git(&fixture, repo.as_path(), &["config", "core.abbrev", "11"]);
let name: String = format!("client-built-{}", std::process::id());
let _service: Running = start(&fixture, name.as_str());
let built: Output = served(&fixture, &repo, name.as_str(), &["commit", "--cleanup=scissors", "-m", "built by git", "--", "a.txt"]).output().expect("the wrapped commit runs");
assert!(built.status.success(), "{}", String::from_utf8_lossy(&built.stderr));
assert_eq!(finished_batches(&fixture), 1, "the service landed it");
let lines: Vec<(String, String)> = summaries(&built);
assert_eq!(lines.len(), 1, "{:?}", String::from_utf8_lossy(&built.stdout));
assert_eq!(lines[0].1, git_short_head(&fixture, &repo));
assert!(lines[0].0.ends_with("built by git"), "{}", lines[0].0);
assert!(String::from_utf8_lossy(&built.stdout).contains(" 1 file changed, 1 insertion(+)"), "{:?}", String::from_utf8_lossy(&built.stdout));
for options in [&["--quiet"][..], &["-q"], &["-qn"]] {
std::fs::write(repo.join("a.txt"), format!("quiet {options:?}\n")).expect("edit");
let mut arguments: Vec<&str> = vec!["commit"];
arguments.extend_from_slice(options);
arguments.extend_from_slice(&["--cleanup=scissors", "-m", "silent", "--", "a.txt"]);
let quiet: Output = served(&fixture, &repo, name.as_str(), arguments.as_slice()).output().expect("the wrapped commit runs");
assert!(quiet.status.success(), "{options:?}: {}", String::from_utf8_lossy(&quiet.stderr));
assert_eq!(String::from_utf8_lossy(&quiet.stdout), "", "{options:?}");
}
remove_settled(&fixture);
}
#[test]
fn a_root_commit_and_a_detached_head_are_worded_as_git_words_them() {
let fixture: Fixture = fixture("client-summary-states");
let unborn: PathBuf = fixture.root.join("unborn");
std::fs::create_dir(&unborn).expect("directory");
git(&fixture, unborn.as_path(), &["init", "--quiet", "--initial-branch=main"]);
std::fs::write(unborn.join("a.txt"), b"first\n").expect("file");
git(&fixture, unborn.as_path(), &["add", "a.txt"]);
git(&fixture, unborn.as_path(), &["config", "maintenance.autoDetach", "false"]);
let name: String = format!("client-states-{}", std::process::id());
let _service: Running = start(&fixture, name.as_str());
let first: Output = served(&fixture, &unborn, name.as_str(), &["commit", "-m", "the root", "--", "a.txt"]).output().expect("the wrapped commit runs");
assert!(first.status.success(), "{}", String::from_utf8_lossy(&first.stderr));
let root_lines: Vec<(String, String)> = summaries(&first);
assert_eq!(root_lines.len(), 1, "{:?}", String::from_utf8_lossy(&first.stdout));
assert!(root_lines[0].0.starts_with("[main (root-commit) "), "{}", root_lines[0].0);
assert_eq!(root_lines[0].1, git_short_head(&fixture, &unborn));
git(&fixture, unborn.as_path(), &["checkout", "--quiet", "--detach"]);
std::fs::write(unborn.join("a.txt"), b"first\nsecond\n").expect("edit");
let detached: Output = served(&fixture, &unborn, name.as_str(), &["commit", "-m", "adrift", "--", "a.txt"]).output().expect("the wrapped commit runs");
assert!(detached.status.success(), "{}", String::from_utf8_lossy(&detached.stderr));
let detached_lines: Vec<(String, String)> = summaries(&detached);
assert_eq!(detached_lines.len(), 1, "{:?}", String::from_utf8_lossy(&detached.stdout));
assert!(detached_lines[0].0.starts_with("[detached HEAD "), "{}", detached_lines[0].0);
remove_settled(&fixture);
}
struct Held {
child: Child,
runtime: PathBuf,
}
impl Drop for Held {
fn drop(&mut self) {
let _killed = self.child.kill();
let _waited = self.child.wait();
let _removed = std::fs::remove_dir_all(&self.runtime);
}
}
fn service_with_git(fixture: &Fixture, name: &str, git_dir: &Path, git_program: &Path) -> Held {
let runtime: PathBuf = runtime_for(name);
let socket: PathBuf = git_policy_cli::service_instance::socket_for(runtime.as_path(), git_dir);
std::fs::create_dir_all(socket.parent().expect("parent")).expect("socket directory");
let child: Child = Command::new(env!("CARGO_BIN_EXE_cli-git-service"))
.args(["--journal"])
.arg(fixture.root.join("journal"))
.arg("--path")
.arg(&socket)
.arg("--instance")
.arg(git_policy_cli::service_instance::instance_of(git_dir))
.arg("--git")
.arg(git_program)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("HOME", fixture.root.join("home"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("the service starts");
let held: Held = Held { child, runtime };
let began: Instant = Instant::now();
while std::os::unix::net::UnixStream::connect(&socket).is_err() {
assert!(began.elapsed() < PATIENCE, "the service never listened");
std::thread::sleep(Duration::from_millis(10));
}
return held;
}
const PATIENCE: Duration = Duration::from_secs(60);
fn stalling_git(fixture: &Fixture) -> (PathBuf, PathBuf, PathBuf) {
let entered: PathBuf = fixture.root.join("landing-entered");
let release: PathBuf = fixture.root.join("landing-release");
let program: PathBuf = fixture.root.join("stalling-git");
super::support::executable(
program.as_path(),
format!(
"#!/bin/sh\nfor argument in \"$@\"; do\n if [ \"$argument\" = update-ref ]; then\n : > '{}'\n while [ ! -e '{}' ] && {}; do sleep 0.02; done\n break\n fi\ndone\nexec /usr/bin/git \"$@\"\n",
entered.display(),
release.display(),
super::support::while_test_lives(fixture.root.as_path())
)
.as_bytes(),
);
return (program, entered, release);
}
#[test]
fn an_index_writer_waits_for_the_service_to_finish_landing_and_says_so() {
let fixture: Fixture = fixture("client-index-wait");
let repo: PathBuf = edited_repository(&fixture);
std::fs::write(repo.join("b.txt"), b"staged by the second command\n").expect("b.txt");
let (program, entered, release) = stalling_git(&fixture);
let name: String = format!("client-index-wait-{}", std::process::id());
let _service: Held = service_with_git(&fixture, name.as_str(), repo.join(".git").as_path(), program.as_path());
let mut landing: Command = served(&fixture, &repo, name.as_str(), &["commit", "--quiet", "-m", "slow landing", "--", "a.txt"]);
let landing_child: Child = landing.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().expect("the commit starts");
let began: Instant = Instant::now();
while !entered.exists() {
assert!(began.elapsed() < PATIENCE, "the landing never reached the branch move");
std::thread::sleep(Duration::from_millis(10));
}
let index_lock: PathBuf = repo.join(".git/index.lock");
assert!(std::fs::read_to_string(&index_lock).is_ok_and(|text| { return text.starts_with("cli-git-service"); }), "the service holds the index while it lands");
let mut adding: Command = served(&fixture, &repo, name.as_str(), &["add", "b.txt"]);
adding.env("CLI_GIT_WAIT_NOTICE_SECONDS", "1");
let mut adding_child: Child = adding.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::piped()).spawn().expect("the add starts");
let adding_stderr = adding_child.stderr.take().expect("piped standard error");
let (noticed, notice) = std::sync::mpsc::channel::<()>();
let reader = std::thread::spawn(move || {
let mut all: String = String::new();
for line in std::io::BufRead::lines(std::io::BufReader::new(adding_stderr)).map_while(Result::ok) {
if line.contains("commit service") {
let _told = noticed.send(());
}
all.push_str(&line);
all.push('\n');
}
return all;
});
notice.recv_timeout(PATIENCE).expect("the add said that it waits for the service");
let early: Option<std::process::ExitStatus> = adding_child.try_wait().expect("the add can be watched");
std::fs::write(&release, b"").expect("release");
let added_status: std::process::ExitStatus = adding_child.wait().expect("the add ends");
let landed: Output = landing_child.wait_with_output().expect("the commit ends");
let stderr: String = reader.join().expect("the add's standard error is read");
assert_eq!(early, None, "the add did not give up while the service held the index: {stderr}");
assert!(added_status.success(), "the add succeeded after the wait: {stderr}");
assert!(landed.status.success(), "{}", String::from_utf8_lossy(&landed.stderr));
assert!(stderr.contains("commit service") && stderr.contains("git add"), "the wait is named: {stderr}");
assert!(!stderr.contains("index.lock"), "no internal file name reaches the person: {stderr}");
assert_eq!(git_text(&fixture, &repo, &["diff", "--cached", "--name-only"]).trim(), "b.txt", "the add staged its file after the landing");
assert_eq!(git_text(&fixture, &repo, &["log", "-1", "--format=%s"]).trim(), "slow landing");
remove_settled(&fixture);
}
#[test]
fn a_stale_mark_of_the_service_is_not_waited_for() {
let fixture: Fixture = fixture("client-stale-mark");
let repo: PathBuf = edited_repository(&fixture);
std::fs::write(repo.join("b.txt"), b"blocked\n").expect("b.txt");
let lock: PathBuf = repo.join(".git/index.lock");
std::fs::write(&lock, b"cli-git-service 41\n").expect("a leftover hold");
let long_ago: std::time::SystemTime = std::time::SystemTime::now() - Duration::from_secs(3600);
std::fs::File::options().write(true).open(&lock).expect("the lock opens").set_modified(long_ago).expect("the age is set");
let mut adding: Command = wrapped_commit(&fixture, &repo, &["add", "b.txt"]);
adding.env("CLI_GIT_WAIT_NOTICE_SECONDS", "1");
let added: Output = adding.output().expect("the add runs");
let stderr: String = String::from_utf8_lossy(&added.stderr).into_owned();
assert!(!added.status.success(), "{stderr}");
assert!(!stderr.contains("waiting for the commit service"), "{stderr}");
assert!(lock.exists(), "the leftover is not the command's to remove");
remove_settled(&fixture);
}
#[test]
fn a_foreign_index_lock_is_not_called_the_commit_service() {
let fixture: Fixture = fixture("client-foreign-lock");
let repo: PathBuf = edited_repository(&fixture);
std::fs::write(repo.join("b.txt"), b"blocked\n").expect("b.txt");
std::fs::write(repo.join(".git/index.lock"), b"").expect("a foreign lock");
let mut adding: Command = wrapped_commit(&fixture, &repo, &["add", "b.txt"]);
adding.env("CLI_GIT_WAIT_NOTICE_SECONDS", "1");
let added: Output = adding.output().expect("the add runs");
let stderr: String = String::from_utf8_lossy(&added.stderr).into_owned();
assert!(!added.status.success(), "{stderr}");
assert!(!stderr.contains("commit service"), "{stderr}");
remove_settled(&fixture);
}