use std::{
path::{Path, PathBuf},
process::Command,
};
use cli::{
Repository,
bridge::{git_core::GitBridge, git_reconstruct::commit_object_id, test_support},
};
use sley::{ObjectId, Repository as SleyRepository};
use tempfile::TempDir;
fn ingest_into_bridge(bridge: &mut GitBridge<'_>, source: &Path) -> Result<(), String> {
let target = test_support::heddle_repo(bridge).root();
ingest::import_git_into_with_options(source, target, ingest::ImportOptions { lossy: false })
.map_err(|error| error.to_string())?;
test_support::stage_ingest_source_in_mirror(bridge, source, &[])
.map_err(|error| error.to_string())?;
test_support::build_existing_mapping(bridge, Some(source))
.map_err(|error| error.to_string())?;
let mirror_repo = test_support::open_git_repo(bridge).map_err(|error| error.to_string())?;
test_support::seed_ingest_identity_mappings_from_mirror(bridge, &mirror_repo)
.map_err(|error| error.to_string())
}
const ENV: &[(&str, &str)] = &[
("GIT_AUTHOR_NAME", "Heddle Conformance"),
("GIT_AUTHOR_EMAIL", "conformance@heddle.test"),
("GIT_COMMITTER_NAME", "Heddle Conformance"),
("GIT_COMMITTER_EMAIL", "conformance@heddle.test"),
("GIT_AUTHOR_DATE", "1700000000 +0000"),
("GIT_COMMITTER_DATE", "1700000000 +0000"),
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_SYSTEM", "/dev/null"),
("LC_ALL", "C"),
("TZ", "UTC"),
];
fn run_git(dir: &Path, args: &[&str], dates: Option<(&str, &str)>) -> Vec<u8> {
let mut cmd = Command::new("git");
cmd.args(args).current_dir(dir).envs(ENV.iter().copied());
if let Some((author, committer)) = dates {
cmd.env("GIT_AUTHOR_DATE", author)
.env("GIT_COMMITTER_DATE", committer);
}
let out = cmd
.output()
.unwrap_or_else(|e| panic!("failed to spawn git {args:?}: {e}"));
assert!(
out.status.success(),
"git {args:?} failed in {}:\nstdout: {}\nstderr: {}",
dir.display(),
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
out.stdout
}
fn git(dir: &Path, args: &[&str]) -> String {
String::from_utf8_lossy(&run_git(dir, args, None))
.trim()
.to_string()
}
fn git_dated(dir: &Path, author_date: &str, committer_date: &str, args: &[&str]) {
run_git(dir, args, Some((author_date, committer_date)));
}
fn cat_commit(dir: &Path, sha: &str) -> Vec<u8> {
run_git(dir, &["cat-file", "commit", sha], None)
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
fn count(haystack: &[u8], needle: &[u8]) -> usize {
haystack
.windows(needle.len())
.filter(|w| *w == needle)
.count()
}
fn all_commit_shas(source: &Path) -> Vec<String> {
git(source, &["rev-list", "--all"])
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect()
}
fn build_plain_corpus(dir: &Path) {
git(dir, &["init", "-q", "--initial-branch=main"]);
std::fs::write(dir.join("f"), b"hello\n").unwrap();
git(dir, &["add", "f"]);
git_dated(
dir,
"1700000000 +0000",
"1700000000 +0000",
&["commit", "-q", "-m", "first commit"],
);
git_dated(
dir,
"1700000100 +0000",
"1700000100 +0000",
&[
"commit",
"-q",
"--allow-empty",
"--allow-empty-message",
"-m",
"",
],
);
std::fs::write(dir.join("m3"), b"no trailing newline").unwrap();
git_dated(
dir,
"1700000200 +0000",
"1700000200 +0000",
&[
"commit",
"-q",
"--allow-empty",
"--cleanup=verbatim",
"-F",
"m3",
],
);
std::fs::write(dir.join("m4"), b"line one\r\nline two\r\n").unwrap();
git_dated(
dir,
"1700000300 +0000",
"1700000300 +0000",
&[
"commit",
"-q",
"--allow-empty",
"--cleanup=verbatim",
"-F",
"m4",
],
);
git_dated(
dir,
"1700000400 -0830",
"1700000450 +1245",
&["commit", "-q", "--allow-empty", "-m", "weird tz"],
);
git(dir, &["config", "i18n.commitEncoding", "ISO-8859-1"]);
std::fs::write(dir.join("m6"), b"caf\xe9\n").unwrap();
git_dated(
dir,
"1700000500 +0000",
"1700000500 +0000",
&["commit", "-q", "--allow-empty", "-F", "m6"],
);
git(dir, &["config", "--unset", "i18n.commitEncoding"]);
git(dir, &["checkout", "-q", "-b", "a", "main"]);
std::fs::write(dir.join("a"), b"a\n").unwrap();
git(dir, &["add", "a"]);
git_dated(
dir,
"1700000600 +0000",
"1700000600 +0000",
&["commit", "-q", "-m", "a"],
);
git(dir, &["checkout", "-q", "-b", "b", "main"]);
std::fs::write(dir.join("b"), b"b\n").unwrap();
git(dir, &["add", "b"]);
git_dated(
dir,
"1700000700 +0000",
"1700000700 +0000",
&["commit", "-q", "-m", "b"],
);
git(dir, &["checkout", "-q", "main"]);
git_dated(
dir,
"1700000800 +0000",
"1700000800 +0000",
&["merge", "-q", "--no-ff", "-m", "octopus", "a", "b"],
);
}
fn extract_commit_bundle(dir: &Path) -> PathBuf {
let bundle = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("commit_conformance_fixtures")
.join("commit-corpus.bundle");
assert!(
bundle.exists(),
"commit-corpus fixture missing: {} (regenerate with \
tests/commit_conformance_fixtures/gen-commit-corpus.sh)",
bundle.display()
);
let repo = dir.join("corpus");
std::fs::create_dir_all(&repo).expect("create corpus repo dir");
git(&repo, &["init", "-q", "--initial-branch=__bootstrap"]);
git(
&repo,
&[
"fetch",
"-q",
bundle.to_str().expect("bundle path utf8"),
"refs/heads/*:refs/heads/*",
],
);
git(&repo, &["symbolic-ref", "HEAD", "refs/heads/main"]);
repo
}
fn assert_all_commits_reconstruct(case: &str, source: &Path) {
git(source, &["fsck", "--full", "--strict"]);
let shas = all_commit_shas(source);
assert!(!shas.is_empty(), "[{case}] no commits to reconstruct");
let heddle_home = TempDir::new().expect("heddle temp");
let repo = Repository::init(heddle_home.path()).expect("init heddle repo");
let mut bridge = GitBridge::new(&repo);
ingest_into_bridge(&mut bridge, source)
.unwrap_or_else(|e| panic!("[{case}] import from git failed: {e}"));
let recon_repo = bridge
.reconstruction_repo()
.unwrap_or_else(|e| panic!("[{case}] open reconstruction repo failed: {e}"));
for sha in &shas {
let golden = cat_commit(source, sha);
let reconstructed = bridge
.reconstruct_commit_for_git_sha(&recon_repo, sha)
.unwrap_or_else(|e| panic!("[{case}] reconstruct {sha} failed: {e}"))
.unwrap_or_else(|| panic!("[{case}] no Heddle state maps to commit {sha}"));
assert_eq!(
reconstructed,
golden,
"[{case}] commit {sha} reconstructed to DIFFERENT bytes\n \
reconstructed: {:?}\n golden: {:?}",
String::from_utf8_lossy(&reconstructed),
String::from_utf8_lossy(&golden),
);
assert_eq!(
commit_object_id(&reconstructed).to_string(),
*sha,
"[{case}] commit {sha} framed-SHA mismatch (byte-identity broken)"
);
}
}
fn assert_all_commits_export_from_state(case: &str, source: &Path) {
git(source, &["fsck", "--full", "--strict"]);
let shas = all_commit_shas(source);
assert!(!shas.is_empty(), "[{case}] no commits to reconstruct");
let heddle_home = TempDir::new().expect("heddle temp");
let repo = Repository::init(heddle_home.path()).expect("init heddle repo");
let mut bridge = GitBridge::new(&repo);
ingest_into_bridge(&mut bridge, source)
.unwrap_or_else(|e| panic!("[{case}] import from git failed: {e}"));
let fresh_home = TempDir::new().expect("fresh temp");
let fresh = SleyRepository::init_bare(fresh_home.path().join("fresh.git"))
.unwrap_or_else(|e| panic!("[{case}] init fresh bare repo failed: {e}"));
for sha in &shas {
let oid = ObjectId::from_hex(sley::ObjectFormat::Sha1, sha)
.unwrap_or_else(|e| panic!("[{case}] bad sha {sha}: {e}"));
assert!(
fresh.read_object(&oid).is_err(),
"[{case}] fresh repo unexpectedly already holds commit {sha} before \
reconstruction — the from-state independence guarantee is void"
);
let written = bridge
.reconstruct_and_write_commit_for_git_sha(&fresh, sha)
.unwrap_or_else(|e| panic!("[{case}] reconstruct+write {sha} failed: {e}"))
.unwrap_or_else(|| panic!("[{case}] no Heddle state maps to commit {sha}"));
assert_eq!(
written.to_string(),
*sha,
"[{case}] commit {sha} regenerated to a DIFFERENT object {written}"
);
assert!(
fresh.read_object(&written).is_ok(),
"[{case}] commit {sha} absent from fresh repo after reconstruct+write"
);
let golden = cat_commit(source, sha);
let object = fresh
.read_object(&written)
.unwrap_or_else(|e| panic!("[{case}] find regenerated {sha} failed: {e}"));
assert_eq!(
object.body,
golden,
"[{case}] commit {sha} regenerated to DIFFERENT bytes\n \
reconstructed: {:?}\n golden: {:?}",
String::from_utf8_lossy(&object.body),
String::from_utf8_lossy(&golden),
);
}
}
#[test]
fn commit_conformance_plain_corpus() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
build_plain_corpus(dir);
let bodies: Vec<Vec<u8>> = all_commit_shas(dir)
.iter()
.map(|sha| cat_commit(dir, sha))
.collect();
assert!(
bodies
.iter()
.any(|b| contains(b, b"\nencoding ISO-8859-1\n")),
"C6 non-UTF8 encoding case missing"
);
assert!(
bodies.iter().any(|b| b.contains(&0xe9)),
"C6 raw latin-1 0xe9 message byte missing"
);
assert!(
bodies.iter().any(|b| b.ends_with(b"\n\n")),
"C2 empty-message case missing"
);
assert!(
bodies.iter().any(|b| b.ends_with(b"no trailing newline")),
"C3 no-trailing-newline case missing"
);
assert!(
bodies.iter().any(|b| contains(b, b"\r\n")),
"C4 CRLF case missing"
);
assert!(
bodies
.iter()
.any(|b| contains(b, b" -0830\n") && contains(b, b" +1245\n")),
"C5 weird/negative-tz case missing"
);
assert!(
bodies.iter().any(|b| count(b, b"\nparent ") >= 3),
"C7 octopus (3-parent) case missing"
);
assert_all_commits_reconstruct("plain-corpus", dir);
}
#[test]
fn commit_conformance_signed_and_mergetag() {
let tmp = TempDir::new().unwrap();
let source = extract_commit_bundle(tmp.path());
let main = git(&source, &["rev-parse", "refs/heads/main"]);
let merge = cat_commit(&source, &main);
assert!(
contains(&merge, b"\nmergetag "),
"C9 fixture lost the mergetag header:\n{}",
String::from_utf8_lossy(&merge)
);
assert!(
contains(&merge, b"\ngpgsig "),
"C9 fixture lost the gpgsig header on the merge"
);
let has_signed_commit = all_commit_shas(&source)
.iter()
.any(|sha| contains(&cat_commit(&source, sha), b"\ngpgsig "));
assert!(
has_signed_commit,
"C8 fixture lost a signed commit (no gpgsig header in any commit)"
);
assert_all_commits_reconstruct("signed-and-mergetag", &source);
}
#[test]
fn export_from_state_plain_corpus() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
build_plain_corpus(dir);
assert_all_commits_export_from_state("plain-corpus", dir);
}
#[test]
fn export_from_state_signed_and_mergetag() {
let tmp = TempDir::new().unwrap();
let source = extract_commit_bundle(tmp.path());
assert_all_commits_export_from_state("signed-and-mergetag", &source);
}