use super::commit::wrapped_commit;
use super::service_commit::{Running, runtime_for, start_worktree};
use super::support::{Fixture, Observed, direct, observe};
use git_policy_cli::service_journal::DONE_EXTENSION;
use std::path::{Path, PathBuf};
use std::process::Command;
const PINNED_DATE: &str = "2026-02-03T04:05:06+0000";
const STATE_FILES: [&str; 9] = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "SQUASH_MSG", "AUTO_MERGE", "CHERRY_PICK_HEAD", "REVERT_HEAD", "MERGE_RR", "ORIG_HEAD"];
pub(super) struct Case {
pub name: &'static str,
pub command: &'static [&'static str],
pub index_commit: bool,
pub input: &'static [u8],
pub full_output: bool,
}
pub(super) fn case(name: &'static str, command: &'static [&'static str]) -> Case {
return Case { name, command, index_commit: false, input: b"", full_output: false };
}
#[derive(Debug, Eq, PartialEq)]
pub(super) struct Outcome {
pub code: Option<i32>,
pub first_line: String,
pub output: String,
pub left: Vec<(String, String)>,
}
fn pinned(command: &mut Command) {
command.env("GIT_AUTHOR_DATE", PINNED_DATE).env("GIT_COMMITTER_DATE", PINNED_DATE).env("GIT_EDITOR", "true");
}
pub(super) fn copy_beside(fixture: &Fixture, repo: &Path, name: &str) -> PathBuf {
let copy: PathBuf = fixture.root.join(name);
let status = Command::new("cp").arg("--archive").arg(repo).arg(©).status().expect("cp starts");
assert!(status.success(), "copying the repository");
return copy;
}
pub(super) fn text_of(fixture: &Fixture, repo: &Path, arguments: &[&str]) -> String {
let output = direct(fixture).current_dir(repo).args(arguments).output().expect("native Git starts");
return String::from_utf8_lossy(output.stdout.as_slice()).into_owned() + &String::from_utf8_lossy(output.stderr.as_slice());
}
pub(super) fn left_behind(fixture: &Fixture, repo: &Path) -> Vec<(String, String)> {
let mut left: Vec<(String, String)> = Vec::new();
let mut add = |label: &str, value: String| { left.push((label.to_string(), value)); };
add("head", text_of(fixture, repo, &["rev-parse", "--verify", "--quiet", "HEAD"]));
add("symbolic head", text_of(fixture, repo, &["symbolic-ref", "--quiet", "HEAD"]));
add("references", text_of(fixture, repo, &["for-each-ref", "--format=%(refname) %(objectname)"]));
let formats: [&str; 9] = ["%T", "%B", "%an <%ae>", "%ad", "%cn <%ce>", "%cd", "%P", "%H", "%G?"];
for format in formats {
add(format!("stored {format}").as_str(), text_of(fixture, repo, &["log", "-1", "--date=iso-strict", format!("--format={format}").as_str()]));
}
add("count", text_of(fixture, repo, &["rev-list", "--count", "--all"]));
add("status", text_of(fixture, repo, &["status", "--short", "--untracked-files=all"]));
add("staged", text_of(fixture, repo, &["diff", "--cached", "--raw", "--no-abbrev"]));
add("index", text_of(fixture, repo, &["ls-files", "--stage"]));
add("unstaged", text_of(fixture, repo, &["diff", "--raw", "--no-abbrev"]));
let git_dir: PathBuf = repo.join(".git");
for name in STATE_FILES {
let state: String = std::fs::read(git_dir.join(name)).map_or_else(|_| { return String::from("(absent)"); }, |bytes| { return String::from_utf8_lossy(bytes.as_slice()).into_owned(); });
add(format!("state {name}").as_str(), state);
}
let sequencer: String = std::fs::read_dir(git_dir.join("sequencer")).map_or_else(
|_| { return String::from("(absent)"); },
|entries| {
let mut names: Vec<String> = entries
.flatten()
.map(|entry| { return format!("{}={}", entry.file_name().to_string_lossy(), std::fs::read_to_string(entry.path()).unwrap_or_default()); })
.collect();
names.sort();
return names.join("\n");
},
);
add("state sequencer", sequencer);
return left;
}
pub(super) fn run_native(fixture: &Fixture, repo: &Path, entry: &Case) -> Outcome {
let mut command: Command = direct(fixture);
command.current_dir(repo).args(entry.command);
pinned(&mut command);
return finish(fixture, repo, observe(&mut command, entry.input));
}
pub(super) fn run_served(fixture: &Fixture, repo: &Path, name: &str, entry: &Case) -> Outcome {
let mut arguments: Vec<&str> = entry.command.to_vec();
if entry.index_commit {
arguments.insert(1, "--no-enforce-only");
}
let _service_of_this_worktree: Running = start_worktree(fixture.root.as_path(), runtime_for(name).as_path(), repo);
let mut command: Command = wrapped_commit(fixture, repo, arguments.as_slice());
command.env("XDG_RUNTIME_DIR", runtime_for(name));
pinned(&mut command);
return finish(fixture, repo, observe(&mut command, entry.input));
}
fn finish(fixture: &Fixture, repo: &Path, observed: Observed) -> Outcome {
let stdout: String = String::from_utf8_lossy(observed.stdout.as_slice()).into_owned();
return Outcome {
code: observed.code,
first_line: stdout.lines().next().unwrap_or("").to_string(),
output: stdout,
left: left_behind(fixture, repo),
};
}
pub(super) fn finished(fixture: &Fixture) -> usize {
return std::fs::read_dir(fixture.root.join("journal")).map_or(0, |entries| {
return entries
.flatten()
.filter(|entry| { return entry.path().extension().is_some_and(|extension| { return extension == DONE_EXTENSION; }); })
.count();
});
}
pub(super) fn compare(fixture: &Fixture, base: &Path, name: &str, entry: &Case) -> (Outcome, Outcome) {
let native_repo: PathBuf = copy_beside(fixture, base, "native");
let served_repo: PathBuf = copy_beside(fixture, base, "served");
let before: usize = finished(fixture);
let native: Outcome = run_native(fixture, native_repo.as_path(), entry);
let served: Outcome = run_served(fixture, served_repo.as_path(), name, entry);
assert_eq!(served.code, native.code, "{}: exit status\nservice output:\n{}", entry.name, served.output);
assert_eq!(served.first_line, native.first_line, "{}: first line of the output", entry.name);
if entry.full_output {
assert_eq!(served.output, native.output, "{}: output", entry.name);
}
for ((label, expected), (_, found)) in native.left.iter().zip(&served.left) {
assert_eq!(found, expected, "{}: {label}", entry.name);
}
if native.code == Some(0) {
assert!(finished(fixture) > before, "{}: the service landed it", entry.name);
} else {
assert_eq!(finished(fixture), before, "{}: nothing landed", entry.name);
}
std::fs::remove_dir_all(&served_repo).expect("remove the served copy");
std::fs::remove_dir_all(&native_repo).expect("remove the native copy");
return (native, served);
}