#![cfg(feature = "git-bridge")]
#![allow(clippy::unwrap_used)]
mod common;
use common::{Repo, check_invariants};
use std::path::Path;
use std::process::{Command, Output};
fn git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
fn git(dir: &Path, args: &[&str]) -> Output {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("spawn git")
}
fn git_ok(dir: &Path, args: &[&str]) -> String {
let out = git(dir, args);
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn fsck_strict(mirror: &Path) {
let out = git(mirror, &["fsck", "--strict", "--no-dangling"]);
assert!(
out.status.success(),
"git fsck rejected the mirror:\n{}\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
fn mirror_refs(mirror: &Path) -> Vec<String> {
let mut v: Vec<String> = git_ok(
mirror,
&["for-each-ref", "--format=%(refname) %(objectname)"],
)
.lines()
.map(str::to_owned)
.collect();
v.sort();
v
}
fn mirror_root() -> tempfile::TempDir {
tempfile::tempdir().expect("mirror tempdir")
}
fn fixture() -> Repo {
let r = Repo::new();
r.commit_file("hi.txt", b"hello bridge\n", "first commit");
let big: Vec<u8> = (0u32..300_000).flat_map(u32::to_le_bytes).collect();
r.write("big.bin", &big);
r.write("sub/inner.txt", b"inner\n");
r.ok(&["add", "-A"]);
r.ok(&["commit", "-m", "second commit"]);
r.ok(&["branch", "side"]);
r.ok(&["tag", "-a", "v1", "-m", "release one"]);
r
}
#[test]
fn export_produces_fsck_clean_matching_mirror() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("mirror-a");
let out = r.ok(&["git", "export", mirror.to_str().unwrap()]);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(
stdout.matches("exported ").count(),
3,
"main, side, v1:\n{stdout}"
);
fsck_strict(&mirror);
let refs = mirror_refs(&mirror);
let names: Vec<&str> = refs.iter().map(|l| l.split(' ').next().unwrap()).collect();
assert_eq!(
names,
[
"refs/heads/main",
"refs/heads/side",
"refs/mkit/attestations",
"refs/tags/v1"
]
);
let git_subjects = git_ok(&mirror, &["log", "--format=%s", "refs/heads/main"]);
let mkit_log = r.ok(&["log", "--oneline"]);
let mkit_subjects: Vec<String> = String::from_utf8_lossy(&mkit_log.stdout)
.lines()
.map(|l| l.split_once(' ').unwrap().1.to_owned())
.collect();
assert_eq!(
git_subjects.lines().collect::<Vec<_>>(),
mkit_subjects.iter().map(String::as_str).collect::<Vec<_>>()
);
let size = git_ok(&mirror, &["cat-file", "-s", "refs/heads/main:big.bin"]);
assert_eq!(size.trim(), "1200000");
let att = git_ok(
&mirror,
&["ls-tree", "--name-only", "refs/mkit/attestations"],
);
assert_eq!(att.lines().count(), 3);
assert!(att.lines().all(|l| {
std::path::Path::new(l)
.extension()
.is_some_and(|e| e == "dsse")
}));
check_invariants(r.path(), "post-export").unwrap();
}
#[test]
fn reexport_is_noop_and_rewrite_force_pushes() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("mirror-b");
let dest = mirror.to_str().unwrap();
r.ok(&["git", "export", dest]);
let first = mirror_refs(&mirror);
r.ok(&["git", "export", dest]);
assert_eq!(mirror_refs(&mirror), first, "re-export must be a no-op");
r.ok(&["commit", "--amend", "-m", "second commit, amended"]);
r.ok(&["git", "export", dest]);
let after = mirror_refs(&mirror);
assert_ne!(after, first, "rewritten head must move the mirror");
fsck_strict(&mirror);
let subj = git_ok(&mirror, &["log", "-1", "--format=%s", "refs/heads/main"]);
assert_eq!(subj.trim(), "second commit, amended");
}
#[test]
fn translation_is_deterministic_from_fresh_state() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let m1 = mroot.path().join("mirror-c1");
let m2 = mroot.path().join("mirror-c2");
r.ok(&["git", "export", m1.to_str().unwrap()]);
std::fs::remove_dir_all(r.mkit_dir().join("git")).unwrap();
r.ok(&[
"git",
"export",
"--remote-name",
"second",
m2.to_str().unwrap(),
]);
let strip_attest = |v: Vec<String>| -> Vec<String> {
v.into_iter()
.filter(|l| !l.starts_with("refs/mkit/attestations"))
.collect()
};
assert_eq!(
strip_attest(mirror_refs(&m1)),
strip_attest(mirror_refs(&m2)),
"fresh-state re-translation must yield identical SHA-1s"
);
}
#[test]
fn git_illegal_ref_is_skipped_with_warning_rest_exported() {
if !git_available() {
return;
}
let r = fixture();
r.ok(&["branch", "trailing."]);
let mroot = mirror_root();
let mirror = mroot.path().join("mirror-d");
let out = r.ok(&["git", "export", mirror.to_str().unwrap()]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("skipping refs/heads/trailing."),
"expected a skip warning, got:\n{stderr}"
);
let names = mirror_refs(&mirror);
assert!(names.iter().any(|l| l.starts_with("refs/heads/main ")));
assert!(!names.iter().any(|l| l.contains("trailing.")));
}
#[test]
fn all_skipped_export_fails() {
if !git_available() {
return;
}
let r = fixture();
r.ok(&["branch", "trailing."]);
let mroot = mirror_root();
let mirror = mroot.path().join("mirror-e");
let out = r.run(&[
"git",
"export",
"--ref",
"refs/heads/trailing.",
mirror.to_str().unwrap(),
]);
assert!(
!out.status.success(),
"all-skipped export must exit non-zero"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("every requested ref was skipped"),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn json_output_is_well_formed() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("mirror-f");
let out = r.ok(&["git", "export", "--json", mirror.to_str().unwrap()]);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("{\"ok\":true,\"exported\":["),
"{stdout}"
);
assert!(stdout.trim_end().ends_with("],\"skipped\":[]}"), "{stdout}");
assert_eq!(stdout.matches("\"ref\":").count(), 3);
}
#[test]
fn stateful_rounds_keep_mirror_and_repo_invariants() {
if !git_available() {
return;
}
let r = Repo::new();
let mroot = mirror_root();
let mirror = mroot.path().join("mirror-g");
let dest = mirror.to_str().unwrap();
for round in 0u32..6 {
match round % 6 {
0 => r.commit_file("a.txt", format!("round {round}\n").as_bytes(), "add a"),
1 => {
r.commit_file("b/b.txt", b"nested\n", "add nested");
r.ok(&["branch", &format!("side-{round}")]);
}
2 => {
r.ok(&["tag", "-a", &format!("v0.{round}"), "-m", "round tag"]);
}
3 => {
r.commit_file("a.txt", b"rewritten\n", "pre-amend");
r.ok(&["commit", "--amend", "-m", "amended"]);
}
4 => {
let big: Vec<u8> = (0u32..280_000).flat_map(u32::to_le_bytes).collect();
r.write("big.bin", &big);
r.ok(&["add", "-A"]);
r.ok(&["commit", "-m", "big file"]);
}
_ => r.commit_file("c.txt", b"last\n", "add c"),
}
r.ok(&["git", "export", dest]);
fsck_strict(&mirror);
check_invariants(r.path(), &format!("round {round}")).unwrap();
let mkit_refs = r.ok(&["show-ref"]);
let mkit_names: Vec<String> = String::from_utf8_lossy(&mkit_refs.stdout)
.lines()
.map(|l| l.split(' ').next_back().unwrap().to_owned())
.filter(|n| n.starts_with("refs/heads/") || n.starts_with("refs/tags/"))
.collect();
let mirror_names: Vec<String> = mirror_refs(&mirror)
.iter()
.map(|l| l.split(' ').next().unwrap().to_owned())
.filter(|n| n.starts_with("refs/heads/") || n.starts_with("refs/tags/"))
.collect();
let mut a = mkit_names.clone();
a.sort();
assert_eq!(a, mirror_names, "round {round}: mirror refs drifted");
}
std::fs::remove_dir_all(r.mkit_dir().join("git")).unwrap();
let m2 = mroot.path().join("mirror-g2");
r.ok(&[
"git",
"export",
"--remote-name",
"audit",
m2.to_str().unwrap(),
]);
let strip = |v: Vec<String>| -> Vec<String> {
v.into_iter()
.filter(|l| !l.starts_with("refs/mkit/attestations"))
.collect()
};
assert_eq!(strip(mirror_refs(&mirror)), strip(mirror_refs(&m2)));
}
#[test]
fn subset_export_keeps_other_leases() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
let dest = mirror.to_str().unwrap();
r.ok(&["git", "export", dest]);
r.commit_file("more.txt", b"more\n", "more");
r.ok(&["git", "export", "--ref", "refs/heads/side", dest]);
r.ok(&["git", "export", dest]);
fsck_strict(&mirror);
let subj = git_ok(&mirror, &["log", "-1", "--format=%s", "refs/heads/main"]);
assert_eq!(subj.trim(), "more");
}
#[test]
fn wiped_state_reexports_against_existing_mirror() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
let dest = mirror.to_str().unwrap();
r.ok(&["git", "export", dest]);
std::fs::remove_dir_all(r.mkit_dir().join("git")).unwrap();
r.commit_file("post-wipe.txt", b"x\n", "post wipe");
r.ok(&["git", "export", dest]);
fsck_strict(&mirror);
let subj = git_ok(&mirror, &["log", "-1", "--format=%s", "refs/heads/main"]);
assert_eq!(subj.trim(), "post wipe");
}
#[test]
fn no_attest_skips_attestations_ref() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
r.ok(&["git", "export", "--no-attest", mirror.to_str().unwrap()]);
assert!(
!mirror_refs(&mirror)
.iter()
.any(|l| l.starts_with("refs/mkit/attestations")),
"--no-attest must not publish the attestations ref"
);
}
#[test]
fn ref_flag_error_branches() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
let dest = mirror.to_str().unwrap();
let out = r.run(&["git", "export", "--ref", "main", dest]);
assert_eq!(out.status.code(), Some(64), "bad prefix should be USAGE");
let out = r.run(&["git", "export", "--ref", "refs/heads/nope", dest]);
assert_eq!(out.status.code(), Some(65), "missing ref should be DATAERR");
}
#[test]
fn non_empty_non_repo_dest_is_refused() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let dest = mroot.path().join("occupied");
std::fs::create_dir_all(&dest).unwrap();
std::fs::write(dest.join("junk"), b"x").unwrap();
let out = r.run(&["git", "export", dest.to_str().unwrap()]);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("neither a git repository nor empty"),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn remote_name_state_is_bound_to_one_dest() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let m1 = mroot.path().join("m1");
let m2 = mroot.path().join("m2");
r.ok(&["git", "export", m1.to_str().unwrap()]);
let out = r.run(&["git", "export", m2.to_str().unwrap()]);
assert!(
!out.status.success(),
"same remote-name, different dest must fail"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("--remote-name"),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn published_attestation_content_matches_spec() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
r.ok(&["git", "export", mirror.to_str().unwrap()]);
let head_sha = git_ok(&mirror, &["rev-parse", "refs/heads/main"]);
let head_sha = head_sha.trim();
let mkit_head = {
let out = r.ok(&["rev-parse", "HEAD"]);
String::from_utf8_lossy(&out.stdout).trim().to_owned()
};
let names = git_ok(
&mirror,
&["ls-tree", "--name-only", "refs/mkit/attestations"],
);
let mut matched = false;
for name in names.lines() {
let blob = git_ok(
&mirror,
&[
"cat-file",
"blob",
&format!("refs/mkit/attestations:{name}"),
],
);
let env = mkit_attest::envelope::decode(blob.as_bytes()).expect("valid DSSE envelope");
assert_eq!(env.payload_type, mkit_attest::PAYLOAD_TYPE_IN_TOTO);
assert_eq!(env.signatures.len(), 1);
let payload = String::from_utf8(env.payload.clone()).unwrap();
assert!(
payload.contains(
"https://github.com/officialunofficial/mkit/spec/predicate/git-bridge/v1"
),
"predicateType missing in {payload}"
);
if payload.contains("refs/heads/main") {
matched = true;
assert!(
payload.contains(&format!("\"gitCommit\":\"{head_sha}\"")),
"gitCommit locator mismatch in {payload}"
);
assert!(
payload.contains(&format!("\"blake3\":\"{mkit_head}\"")),
"subject blake3 mismatch in {payload}"
);
assert!(payload.contains("\"schemaVersion\":1"));
assert!(payload.contains("\"specVersion\":1"));
}
}
assert!(matched, "no envelope for refs/heads/main found");
}
#[test]
fn out_of_band_mirror_move_fails_the_lease() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
let dest = mirror.to_str().unwrap();
r.ok(&["git", "export", dest]);
let foreign = git_ok(&mirror, &["rev-parse", "refs/mkit/attestations"]);
git_ok(&mirror, &["update-ref", "refs/heads/main", foreign.trim()]);
r.commit_file("late.txt", b"x\n", "late");
let out = r.run(&["git", "export", dest]);
assert!(!out.status.success(), "lease must reject the stale push");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("hint:"), "stderr: {stderr}");
let still = git_ok(&mirror, &["rev-parse", "refs/heads/main"]);
assert_eq!(still.trim(), foreign.trim(), "mirror must be untouched");
}
#[test]
fn tag_subset_and_duplicate_refs_export() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let mirror = mroot.path().join("m");
let out = r.ok(&[
"git",
"export",
"--ref",
"refs/tags/v1",
"--ref",
"refs/tags/v1",
mirror.to_str().unwrap(),
]);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.matches("exported ").count(), 1, "{stdout}");
assert!(
mirror_refs(&mirror)
.iter()
.any(|l| l.starts_with("refs/tags/v1 "))
);
}
#[test]
#[cfg(unix)]
fn export_to_fresh_local_dest_rebinds_identically() {
if !git_available() {
return;
}
let r = fixture();
let mroot = mirror_root();
let real = mroot.path().join("real");
std::fs::create_dir_all(&real).unwrap();
let link = mroot.path().join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let dest = link.join("mirror-fresh");
r.ok(&["git", "export", dest.to_str().unwrap()]);
r.ok(&["git", "export", dest.to_str().unwrap()]);
}
#[test]
fn unreachable_remote_dest_does_not_burn_the_state_name() {
if !git_available() {
return;
}
let r = fixture();
let out = r.run(&["git", "export", "file:///nonexistent/nowhere/mirror"]);
assert!(!out.status.success(), "push to a missing file:// URL fails");
let mroot = mirror_root();
let mirror = mroot.path().join("retry-mirror");
r.ok(&["git", "export", mirror.to_str().unwrap()]);
}