#![cfg(unix)]
use crate::test_support::{executable, fixture, git, remove, repository};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::time::{Duration, Instant};
pub const HOLD_PATIENCE: Duration = Duration::from_secs(60);
pub const GONE_REF: &str = "refs/remotes/origin/gone";
pub struct Stage {
pub root: PathBuf,
pub repo: PathBuf,
pub git_dir: PathBuf,
pub ready: PathBuf,
}
impl Stage {
pub fn lock_of(&self, reference: &str) -> PathBuf {
return self.git_dir.join(format!("{reference}.lock"));
}
pub fn pid_file_of(&self, reference: &str) -> PathBuf {
return self.git_dir.join(format!("{reference}~pid.lock"));
}
pub fn packed_lock(&self) -> PathBuf {
return self.git_dir.join("packed-refs.lock");
}
pub fn packed_staging(&self) -> PathBuf {
return self.git_dir.join("packed-refs.new");
}
pub fn roots(&self) -> Vec<PathBuf> {
return vec![self.git_dir.clone()];
}
pub fn finish(self) {
remove(self.root.as_path());
}
}
pub fn stage(name: &str, lockfile_pid: bool) -> Stage {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
let remote: PathBuf = root.join("remote.git");
git(root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", remote.to_str().expect("UTF-8")]);
git(repo.as_path(), &["remote", "add", "origin", remote.to_str().expect("UTF-8")]);
git(repo.as_path(), &["branch", "gone"]);
git(repo.as_path(), &["push", "--quiet", "origin", "main", "gone"]);
git(repo.as_path(), &["fetch", "--quiet", "origin"]);
git(repo.as_path(), &["pack-refs", "--all"]);
git(remote.as_path(), &["branch", "--delete", "--force", "gone"]);
if lockfile_pid {
git(repo.as_path(), &["config", "core.lockfilePid", "true"]);
}
let git_dir: PathBuf = repo.join(".git");
let ready: PathBuf = root.join("ready");
return Stage { root, repo, git_dir, ready };
}
pub struct Held {
child: Child,
_input: Option<ChildStdin>,
hook: Option<i32>,
}
impl Held {
pub fn pid(&self) -> i32 {
return i32::try_from(self.child.id()).expect("process number");
}
pub fn kill(&mut self) {
self.child.kill().expect("SIGKILL the Git");
self.child.wait().expect("reap the Git");
if let Some(hook) = self.hook.take() {
unsafe { libc::kill(hook, libc::SIGKILL); }
}
}
}
impl Drop for Held {
fn drop(&mut self) {
let _killed = self.child.kill();
let _reaped = self.child.wait();
if let Some(hook) = self.hook.take() {
unsafe { libc::kill(hook, libc::SIGKILL); }
}
}
}
fn wait_for(what: &str, condition: impl Fn() -> bool) {
let started: Instant = Instant::now();
while !condition() {
assert!(started.elapsed() < HOLD_PATIENCE, "gave up waiting for {what}");
std::thread::sleep(Duration::from_millis(20));
}
}
fn git_command(stage: &Stage) -> Command {
let mut command: Command = Command::new(crate::test_support::REAL_GIT);
command
.current_dir(stage.repo.as_path())
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid");
return command;
}
const HOLD_HOOK: &str = "#!/bin/sh\nif [ \"$1\" = prepared ] && [ -n \"$STALE_LOCK_READY\" ]; then\n echo $$ > \"$STALE_LOCK_READY.part\"\n mv \"$STALE_LOCK_READY.part\" \"$STALE_LOCK_READY\"\n exec sleep 600\nfi\nexit 0\n";
pub fn hold_fetch(stage: &Stage) -> Held {
let hooks: PathBuf = stage.git_dir.join("hooks");
executable(hooks.join("reference-transaction").as_path(), HOLD_HOOK.as_bytes());
let mut command: Command = git_command(stage);
command.args(["fetch", "--prune", "--quiet", "origin"]).env("STALE_LOCK_READY", stage.ready.as_os_str());
let child: Child = command.spawn().expect("start git fetch");
let mut held: Held = Held { child, _input: None, hook: None };
let ready: PathBuf = stage.ready.clone();
wait_for("the fetch to take its locks", || { return ready.exists(); });
held.hook = std::fs::read_to_string(stage.ready.as_path()).expect("hook pid").trim().parse::<i32>().ok();
return held;
}
pub fn hold_transaction(stage: &Stage, commands: &[&str]) -> Held {
let mut command: Command = git_command(stage);
command.args(["update-ref", "--stdin"]).stdin(Stdio::piped()).stdout(Stdio::piped());
let mut child: Child = command.spawn().expect("start git update-ref");
let mut input: ChildStdin = child.stdin.take().expect("the input");
let output = child.stdout.take().expect("the output");
let script: String = format!("start\n{}\nprepare\n", commands.join("\n"));
input.write_all(script.as_bytes()).expect("send the transaction");
input.flush().expect("flush the transaction");
let (tell, hear) = std::sync::mpsc::channel::<()>();
std::thread::spawn(move || {
for line in BufReader::new(output).lines().map_while(Result::ok) {
if line == "prepare: ok" {
let _told = tell.send(());
}
}
});
let held: Held = Held { child, _input: Some(input), hook: None };
hear.recv_timeout(HOLD_PATIENCE).expect("the transaction to be prepared");
return held;
}
pub fn idle_git(stage: &Stage) -> Held {
let mut command: Command = git_command(stage);
command.args(["update-ref", "--stdin"]).stdin(Stdio::piped());
let mut child: Child = command.spawn().expect("start git update-ref");
let input: ChildStdin = child.stdin.take().expect("the input");
return Held { child, _input: Some(input), hook: None };
}
pub fn write_file(path: &Path, content: &str) {
std::fs::create_dir_all(path.parent().expect("parent")).expect("directories");
std::fs::write(path, content).expect("write the file");
}
pub struct Helper(pub Child);
impl Helper {
pub fn pid(&self) -> i64 {
return i64::from(self.0.id());
}
}
impl Drop for Helper {
fn drop(&mut self) {
let _killed = self.0.kill();
let _reaped = self.0.wait();
}
}
pub fn judge() -> crate::stale_ref_lock_judge::Judge {
return crate::stale_ref_lock_judge::Judge { margin: Duration::ZERO, pid_view_complete: true, boot: None, ..crate::stale_ref_lock_judge::Judge::here() };
}
pub fn hold_push(stage: &Stage) -> Held {
git(stage.repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=ahead"]);
let hooks: PathBuf = stage.git_dir.join("hooks");
executable(hooks.join("reference-transaction").as_path(), HOLD_HOOK.as_bytes());
let mut command: Command = git_command(stage);
command.args(["push", "--quiet", "origin", "main"]).env("STALE_LOCK_READY", stage.ready.as_os_str());
let child: Child = command.spawn().expect("start git push");
let mut held: Held = Held { child, _input: None, hook: None };
let ready: PathBuf = stage.ready.clone();
wait_for("the push to take its locks", || { return ready.exists(); });
held.hook = std::fs::read_to_string(stage.ready.as_path()).expect("hook pid").trim().parse::<i32>().ok();
return held;
}
pub fn plain_git(stage: &Stage, arguments: &[&str]) -> Command {
let mut command: Command = git_command(stage);
command.args(arguments);
return command;
}