use crate::replay_patch::split_keeping_newlines;
use crate::test_support::{REAL_GIT, git, git_output, isolated_overlay};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
const WORKERS: usize = 4;
pub(super) struct ReverseCase {
pub(super) number: usize,
pub(super) base: Vec<u8>,
pub(super) landed: Vec<u8>,
pub(super) prepared: Vec<u8>,
}
fn git_with_input(directory: &Path, arguments: &[&str], input: &[u8]) -> Output {
let mut child: std::process::Child = Command::new(REAL_GIT)
.current_dir(directory)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.envs(isolated_overlay())
.args(arguments)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("native Git fixture command");
let mut stdin: std::process::ChildStdin = child.stdin.take().expect("piped standard input");
let (output, written): (Output, std::io::Result<()>) = std::thread::scope(|scope| {
let writer = scope.spawn(move || { return stdin.write_all(input); });
let output: Output = child.wait_with_output().expect("Git output");
return (output, writer.join().expect("input writer"));
});
assert!(output.status.success(), "git {arguments:?} failed: {}", String::from_utf8_lossy(&output.stderr));
written.expect("input written to Git");
return output;
}
fn import_stream(pairs: &[(&[u8], &[u8])]) -> Vec<u8> {
let mut stream: Vec<u8> = Vec::new();
for (index, (base, landed)) in pairs.iter().enumerate() {
for (step, (message, text)) in [("base", *base), ("landed", *landed)].into_iter().enumerate() {
let mark: usize = 2 * index + 1 + step;
stream.extend_from_slice(format!("commit refs/heads/case-{index}\nmark :{mark}\n").as_bytes());
stream.extend_from_slice(format!("committer Fixture <fixture@example.invalid> {mark} +0000\n").as_bytes());
stream.extend_from_slice(format!("data {}\n{message}\n", message.len()).as_bytes());
if step == 1 {
stream.extend_from_slice(format!("from :{}\n", mark - 1).as_bytes());
}
stream.extend_from_slice(format!("M 100644 inline 0\ndata {}\n", text.len()).as_bytes());
stream.extend_from_slice(text);
stream.push(b'\n');
}
}
return stream;
}
fn landed_commits(marks: &str, count: usize) -> Vec<String> {
let all: std::collections::HashMap<usize, &str> = marks
.lines()
.filter_map(|line| {
let (mark, id) = line.strip_prefix(':')?.split_once(' ')?;
return Some((mark.parse::<usize>().ok()?, id));
})
.collect();
return (0..count).map(|index| { return String::from(all[&(2 * index + 2)]); }).collect();
}
pub(super) fn patches_between_all(directory: &Path, pairs: &[(&[u8], &[u8])], context: u32) -> Vec<Vec<u8>> {
let repo: PathBuf = directory.join("batch");
std::fs::create_dir(&repo).expect("repository directory");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
git_with_input(
repo.as_path(),
&["fast-import", "--quiet", "--export-marks=marks.txt"],
import_stream(pairs).as_slice(),
);
let marks: String = std::fs::read_to_string(repo.join("marks.txt")).expect("exported marks");
let landed: Vec<String> = landed_commits(marks.as_str(), pairs.len());
let unified: String = format!("--unified={context}");
let listing: Vec<u8> = landed.iter().map(|id| { return format!("{id}\n"); }).collect::<String>().into_bytes();
let output: Output = git_with_input(
repo.as_path(),
&[
"diff-tree",
"--stdin",
"-p",
"--text",
"--no-renames",
"--no-ext-diff",
"--no-color",
unified.as_str(),
"--src-prefix=a/",
"--dst-prefix=b/",
],
listing.as_slice(),
);
let mut patches: Vec<Vec<u8>> = Vec::new();
for line in split_keeping_newlines(&output.stdout) {
if landed.get(patches.len()).is_some_and(|id| { return line == format!("{id}\n").as_bytes(); }) {
patches.push(Vec::new());
} else {
patches.last_mut().expect("a commit id line starts the output").extend_from_slice(&line);
}
}
assert_eq!(patches.len(), pairs.len(), "one patch per pair");
return patches;
}
fn git_accepts(root: &Path, case: &ReverseCase, patch: &[u8]) -> bool {
let number: usize = case.number;
let case_directory: PathBuf = root.join(format!("case-{number}"));
std::fs::create_dir(&case_directory).unwrap_or_else(|error| { panic!("case {number}: case directory: {error}"); });
std::fs::write(case_directory.join("0"), &case.prepared)
.unwrap_or_else(|error| { panic!("case {number}: prepared entry: {error}"); });
let patch_path: PathBuf = case_directory.join("landed.patch");
std::fs::write(&patch_path, patch).unwrap_or_else(|error| { panic!("case {number}: patch file: {error}"); });
let patch_argument: String = patch_path.to_string_lossy().into_owned();
return git_output(
case_directory.as_path(),
&["apply", "--reverse", "--check", patch_argument.as_str()],
)
.status
.success();
}
pub(super) fn git_verdicts(root: &Path, cases: &[ReverseCase], patches: &[Vec<u8>]) -> Vec<bool> {
let next: AtomicUsize = AtomicUsize::new(0);
let mut verdicts: Vec<Option<bool>> = vec![None; cases.len()];
std::thread::scope(|scope| {
let workers: Vec<_> = (0..WORKERS)
.map(|_| {
return scope.spawn(|| {
let mut reached: Vec<(usize, bool)> = Vec::new();
loop {
let index: usize = next.fetch_add(1, Ordering::Relaxed);
let Some(case) = cases.get(index) else {
return reached;
};
reached.push((index, git_accepts(root, case, patches[index].as_slice())));
}
});
})
.collect();
for worker in workers {
match worker.join() {
Ok(reached) => {
for (index, verdict) in reached {
verdicts[index] = Some(verdict);
}
}
Err(payload) => std::panic::resume_unwind(payload),
}
}
});
return verdicts.into_iter().map(|verdict| { return verdict.expect("every case reached"); }).collect();
}