use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
pub const WRAPPER: &str = env!("CARGO_BIN_EXE_cli-git-native");
pub const REAL_GIT: &str = "/usr/bin/git";
pub const TIME_BOUND_SECONDS: &str = "5";
pub const TIMED_OUT_SIGNAL: i32 = 9;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Observed {
pub code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub struct Fixture {
pub root: PathBuf,
pub wrapper: PathBuf,
}
pub fn fixture(name: &str) -> Fixture {
let created: PathBuf =
std::env::temp_dir().join(format!("native-binary-{}-{name}", std::process::id()));
if created.exists() {
std::fs::remove_dir_all(&created).expect("remove stale fixture");
}
std::fs::create_dir(&created).expect("fresh fixture");
let root: PathBuf = std::fs::canonicalize(&created).expect("canonical fixture");
for directory in ["wrapper", "bin", "home"] {
std::fs::create_dir(root.join(directory)).expect("fixture directory");
}
let wrapper: PathBuf = root.join("wrapper/cli-git-native");
copy_executable(Path::new(WRAPPER), wrapper.as_path());
std::os::unix::fs::symlink(&wrapper, root.join("bin/git")).expect("git-named link");
return Fixture { root, wrapper };
}
pub fn remove(fixture: &Fixture) {
std::fs::remove_dir_all(&fixture.root).expect("remove only the fixture");
}
fn hermetic(command: &mut Command, fixture: &Fixture, path: &OsStr) {
command
.env_clear()
.env("PATH", path)
.env("HOME", fixture.root.join("home"))
.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")
.current_dir(&fixture.root);
}
pub fn wrapper_first_path(fixture: &Fixture) -> OsString {
let mut path: OsString = fixture.root.join("bin").into_os_string();
path.push(":/usr/bin:/bin");
return path;
}
pub fn bounded_for(fixture: &Fixture, program: &Path, path: &OsStr, seconds: &str) -> Command {
let mut command: Command = Command::new("/usr/bin/timeout");
command.arg("--signal=KILL").arg(seconds).arg(program);
hermetic(&mut command, fixture, path);
return command;
}
pub fn bounded(fixture: &Fixture, program: &Path, path: &OsStr) -> Command {
return bounded_for(fixture, program, path, TIME_BOUND_SECONDS);
}
pub fn link_real_git(fixture: &Fixture) {
std::fs::create_dir(fixture.root.join("real")).expect("real directory");
std::os::unix::fs::symlink(REAL_GIT, fixture.root.join("real/git")).expect("real Git link");
}
pub fn wrapped(fixture: &Fixture) -> Command {
return bounded(
fixture,
fixture.root.join("bin/git").as_path(),
wrapper_first_path(fixture).as_os_str(),
);
}
pub fn direct(fixture: &Fixture) -> Command {
let mut command: Command = Command::new(REAL_GIT);
hermetic(&mut command, fixture, OsStr::new("/usr/bin:/bin"));
return command;
}
pub fn without_hint_fields(bytes: &[u8]) -> Vec<u8> {
let marker: &[u8] = b",\"hint\":\"";
let mut kept: Vec<u8> = Vec::with_capacity(bytes.len());
let mut index: usize = 0;
while index < bytes.len() {
if bytes[index..].starts_with(marker) {
index += marker.len();
while index < bytes.len() && bytes[index] != b'"' {
index += if bytes[index] == b'\\' { 2 } else { 1 };
}
index += 1;
} else {
kept.push(bytes[index]);
index += 1;
}
}
return kept;
}
pub fn observe(command: &mut Command, input: &[u8]) -> Observed {
let raw: Observed = observe_raw(command, input);
return Observed {
code: raw.code,
stdout: without_hint_fields(raw.stdout.as_slice()),
stderr: without_hint_fields(raw.stderr.as_slice()),
};
}
pub fn observe_raw(command: &mut Command, input: &[u8]) -> Observed {
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("process starts");
let _ = child.stdin.take().expect("piped stdin").write_all(input);
let output: Output = child.wait_with_output().expect("process finishes");
assert_ne!(
output.status.signal(),
Some(TIMED_OUT_SIGNAL),
"the command hit the {TIME_BOUND_SECONDS}-second bound"
);
return Observed {
code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
};
}
pub fn git<S: AsRef<OsStr>>(fixture: &Fixture, directory: &Path, arguments: &[S]) -> Output {
let output: Output = direct(fixture)
.current_dir(directory)
.args(arguments)
.output()
.expect("native Git fixture command");
assert!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
return output;
}
pub fn repository(fixture: &Fixture, name: &OsStr) -> PathBuf {
let root: PathBuf = fixture.root.join(name);
std::fs::create_dir(&root).expect("repository directory");
git(
fixture,
root.as_path(),
&["init", "--quiet", "--initial-branch=main"],
);
git(
fixture,
root.as_path(),
&["commit", "--quiet", "--allow-empty", "--message=initial"],
);
return root;
}
pub fn run_wrapped(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Observed {
return observe(wrapped(fixture).current_dir(directory).args(arguments), b"");
}
pub fn run_direct(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Observed {
return observe(direct(fixture).current_dir(directory).args(arguments), b"");
}
pub fn porcelain(fixture: &Fixture, repo: &Path) -> String {
let output: Output = git(
fixture,
repo,
&["status", "--porcelain=v1", "--untracked-files=all"],
);
return String::from_utf8_lossy(&output.stdout).into_owned();
}
pub fn stopped_with(stderr: &str) -> Observed {
return Observed {
code: Some(2),
stdout: Vec::<u8>::new(),
stderr: stderr.as_bytes().to_vec(),
};
}
pub fn silent_success() -> Observed {
return Observed {
code: Some(0),
stdout: Vec::<u8>::new(),
stderr: Vec::<u8>::new(),
};
}
pub fn stderr_of(observed: &Observed) -> String {
return String::from_utf8_lossy(&observed.stderr).into_owned();
}
#[path = "fixture_executable.rs"]
mod fixture_executable;
pub use fixture_executable::executable;
pub fn copy_executable(from: &Path, to: &Path) {
let copied: bool = Command::new("cp")
.arg("--")
.arg(from)
.arg(to)
.status()
.expect("start fixture copy")
.success();
assert!(copied, "fixture copy failed: {from:?} to {to:?}");
}
pub const TRANSACTION_ID: &str = "0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
pub fn registered_transaction(repo: &Path, live: bool) -> PathBuf {
let registry: PathBuf = repo.join(".git/cli-git-transactions");
git_policy_cli::transaction_registry::ensure_transaction_root(registry.as_path())
.expect("registry");
let mut owner: git_policy_cli::transaction_owner::TransactionOwner =
git_policy_cli::transaction_owner::current_transaction_owner(
TRANSACTION_ID,
"2026-10-06T00:00:00.000Z",
)
.expect("owner");
if !live {
owner.owner_identity = String::from("linux:1");
}
git_policy_cli::transaction_registry::publish_transaction_directory(
registry.as_path(),
TRANSACTION_ID,
git_policy_cli::transaction_owner::encode_transaction_owner(&owner).as_bytes(),
)
.expect("published");
return registry;
}