#![cfg(feature = "git-bridge")]
mod common;
use common::Repo;
use mkit_core::object::Object;
use mkit_core::refs;
use mkit_core::store::ObjectStore;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
fn git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn mkit_in(cwd: &Path, xdg: &Path, args: &[&str]) -> Output {
Command::new(mkit_bin())
.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg)
.env("HOME", xdg)
.env("EDITOR", "true")
.stdin(Stdio::null())
.output()
.expect("spawn mkit")
}
fn git_in(dir: &Path, args: &[&str]) -> Output {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.env("GIT_AUTHOR_NAME", "Up Stream")
.env("GIT_AUTHOR_EMAIL", "up@example.com")
.env("GIT_COMMITTER_NAME", "Up Stream")
.env("GIT_COMMITTER_EMAIL", "up@example.com")
.env("GIT_AUTHOR_DATE", "1700000000 +0000")
.env("GIT_COMMITTER_DATE", "1700000000 +0000")
.output()
.expect("spawn git")
}
fn git_ok(dir: &Path, args: &[&str]) {
let out = git_in(dir, args);
assert!(out.status.success(), "git {args:?}: {out:?}");
}
struct Fixture {
root: tempfile::TempDir,
xdg: tempfile::TempDir,
}
impl Fixture {
fn new() -> Self {
let f = Fixture {
root: tempfile::tempdir().unwrap(),
xdg: tempfile::tempdir().unwrap(),
};
let up = f.upstream();
std::fs::create_dir_all(&up).unwrap();
git_ok(&up, &["init", "--quiet", "--initial-branch=main", "."]);
std::fs::write(up.join("a.txt"), "hello\n").unwrap();
git_ok(&up, &["add", "a.txt"]);
git_ok(&up, &["commit", "--quiet", "-m", "upstream first"]);
std::fs::write(up.join("b.txt"), "world\n").unwrap();
git_ok(&up, &["add", "b.txt"]);
git_ok(&up, &["commit", "--quiet", "-m", "upstream second"]);
git_ok(&up, &["tag", "-a", "v1", "-m", "release"]);
f
}
fn upstream(&self) -> PathBuf {
self.root.path().join("upstream")
}
fn fork(&self) -> PathBuf {
self.root.path().join("fork")
}
fn mkit(&self, cwd: &Path, args: &[&str]) -> Output {
mkit_in(cwd, self.xdg.path(), args)
}
fn mkit_ok(&self, cwd: &Path, args: &[&str]) -> Output {
let out = self.mkit(cwd, args);
assert!(
out.status.success(),
"mkit {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
out
}
fn import(&self) -> PathBuf {
let up = self.upstream();
self.mkit_ok(
self.root.path(),
&["git", "import", up.to_str().unwrap(), "fork"],
);
self.fork()
}
}
#[test]
fn fresh_clone_imports_checks_out_and_verifies() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
assert!(fork.join("a.txt").exists() && fork.join("b.txt").exists());
let out = f.mkit_ok(&fork, &["log", "-n", "1"]);
let log = String::from_utf8_lossy(&out.stdout);
assert!(
log.contains("Author: Up Stream <up@example.com>"),
"log: {log}"
);
f.mkit_ok(&fork, &["verify", "HEAD"]);
let mkit_dir = fork.join(".mkit");
let tag = refs::read_tag(&mkit_dir, "v1").unwrap().unwrap();
let store = ObjectStore::open(&fork).unwrap();
assert!(matches!(store.read_object(&tag), Ok(Object::Tag(_))));
assert!(
refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.is_some()
);
assert!(mkit_dir.join("keys/git-import.key").exists());
assert!(mkit_dir.join("git/upstream/source").exists());
}
#[test]
fn pull_ff_divergence_and_native_merge_loop() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let up = f.upstream();
std::fs::write(up.join("c.txt"), "c\n").unwrap();
git_ok(&up, &["add", "c.txt"]);
git_ok(&up, &["commit", "--quiet", "-m", "upstream third"]);
let out = f.mkit_ok(&fork, &["git", "pull"]);
assert!(
String::from_utf8_lossy(&out.stderr).contains("fast-forwarded 'main'"),
"{out:?}"
);
assert!(fork.join("c.txt").exists());
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
std::fs::write(up.join("d.txt"), "d\n").unwrap();
git_ok(&up, &["add", "d.txt"]);
git_ok(&up, &["commit", "--quiet", "-m", "upstream fourth"]);
let out = f.mkit(&fork, &["git", "pull"]);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("mkit merge upstream/main"),
"{out:?}"
);
f.mkit_ok(&fork, &["merge", "upstream/main"]);
assert!(fork.join("d.txt").exists() && fork.join("local.txt").exists());
}
#[test]
fn upstream_force_push_warns_and_rewinds_tracking_ref() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let mkit_dir = fork.join(".mkit");
let before = refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap();
let up = f.upstream();
git_ok(&up, &["reset", "--hard", "--quiet", "HEAD~1"]);
std::fs::write(up.join("rewritten.txt"), "r\n").unwrap();
git_ok(&up, &["add", "rewritten.txt"]);
git_ok(&up, &["commit", "--quiet", "-m", "rewritten history"]);
let out = f.mkit_ok(&fork, &["git", "fetch"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("force-pushed") && stderr.contains("rewound"),
"stderr: {stderr}"
);
let after = refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap();
assert_ne!(after, before, "tracking ref rewound to the new history");
let log = f.mkit_ok(&fork, &["log", "-n", "1", "upstream/main"]);
assert!(
String::from_utf8_lossy(&log.stdout).contains("rewritten history"),
"{log:?}"
);
}
#[test]
fn import_key_is_pinned_against_other_keys() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let other = f.root.path().join("other.key");
let kp = mkit_core::sign::KeyPair::from_seed([9u8; 32]);
mkit_core::sign::save_key(&other, &kp).unwrap();
let out = f.mkit(&fork, &["git", "fetch", "--key", other.to_str().unwrap()]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("pinned to importer key"),
"stderr: {stderr}"
);
assert!(
stderr.contains("Designated-importer model"),
"stderr: {stderr}"
);
}
#[test]
fn origin_guard_blocks_plain_export_and_source_binding_holds() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let up = f.upstream();
let out = f.mkit(&fork, &["git", "export", up.to_str().unwrap()]);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("recorded git-import source"),
"{out:?}"
);
let spelled = format!("{}/", up.display());
let out = f.mkit(&fork, &["git", "export", &spelled]);
assert!(!out.status.success(), "canonical identity must match");
let other = f.root.path().join("other-upstream");
std::fs::create_dir_all(&other).unwrap();
git_ok(&other, &["init", "--quiet", "--initial-branch=main", "."]);
let out = f.mkit(&fork, &["git", "import", other.to_str().unwrap()]);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("use a different --remote-name"),
"{out:?}"
);
let mirror = f.root.path().join("mirror");
f.mkit_ok(&fork, &["git", "export", mirror.to_str().unwrap()]);
}
#[test]
fn native_commands_refuse_git_bridge_remotes() {
if !git_available() {
return;
}
let r = Repo::new();
r.commit_file("a.txt", b"a\n", "base");
r.ok(&["remote", "add", "ghub", "git+https://github.com/org/repo"]);
for cmd in [&["push", "ghub"][..], &["fetch", "ghub"], &["pull", "ghub"]] {
let out = r.run(cmd);
assert!(!out.status.success(), "{cmd:?} must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("git-bridge remote") || stderr.contains("mkit git"),
"{cmd:?}: {stderr}"
);
}
}
#[test]
fn reimport_is_noop_and_fresh_state_is_deterministic() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let mkit_dir = fork.join(".mkit");
let head1 = refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap();
f.mkit_ok(&fork, &["git", "fetch"]);
assert_eq!(
refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap(),
head1
);
std::fs::remove_file(mkit_dir.join("git/upstream/map")).unwrap();
std::fs::remove_dir_all(mkit_dir.join("git/upstream/repo.git")).unwrap();
std::fs::remove_file(mkit_dir.join("git/upstream/refs-import")).unwrap();
let up = f.upstream();
f.mkit_ok(&fork, &["git", "import", up.to_str().unwrap()]);
assert_eq!(
refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap(),
head1,
"same key + same upstream must reproduce identical hashes"
);
}
#[test]
fn crash_marker_discards_map_and_recovers() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let mkit_dir = fork.join(".mkit");
let state = mkit_dir.join("git/upstream");
let head1 = refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap();
std::fs::write(state.join("importing"), b"").unwrap();
let out = f.mkit_ok(&fork, &["git", "fetch"]);
assert!(
String::from_utf8_lossy(&out.stderr).contains("rebuilding the map cache"),
"{out:?}"
);
assert!(!state.join("importing").exists(), "marker cleared");
assert_eq!(
refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap(),
head1,
"full re-translation reproduces identical hashes"
);
let map = std::fs::read_to_string(state.join("map")).unwrap();
assert!(map.lines().count() > 0, "map rebuilt, not left empty");
}
fn git_stdout(dir: &Path, args: &[&str]) -> String {
let out = git_in(dir, args);
assert!(out.status.success(), "git {args:?}: {out:?}");
String::from_utf8_lossy(&out.stdout).trim().to_owned()
}
#[test]
fn passthrough_export_creates_prable_fork() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
let up = f.upstream();
let forkgit = f.root.path().join("forkgit");
git_ok(
f.root.path(),
&[
"clone",
"--bare",
"--quiet",
up.to_str().unwrap(),
"forkgit",
],
);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
let up_tip = git_stdout(&up, &["rev-parse", "HEAD"]);
assert_eq!(git_stdout(&forkgit, &["rev-parse", "main^"]), up_tip);
assert_eq!(
git_stdout(&forkgit, &["merge-base", "main", &up_tip]),
up_tip
);
assert_eq!(
git_stdout(&forkgit, &["rev-parse", "v1"]),
git_stdout(&up, &["rev-parse", "v1"])
);
git_ok(&forkgit, &["fsck", "--strict"]);
let tree = git_stdout(&forkgit, &["ls-tree", "refs/mkit/attestations"]);
assert_eq!(tree.lines().count(), 1, "one translated head:\n{tree}");
let staging = fork.join(".mkit/git/upstream/repo.git");
assert_eq!(
git_stdout(&staging, &["rev-parse", "refs/heads/main"]),
up_tip
);
}
#[test]
fn passthrough_requires_import_state_and_locks_direction() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let dest = f.root.path().join("dest.git");
let out = f.mkit(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"nope",
dest.to_str().unwrap(),
],
);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("requires import state"),
"{out:?}"
);
let up = f.upstream();
let forkgit = f.root.path().join("forkgit");
git_ok(
f.root.path(),
&[
"clone",
"--bare",
"--quiet",
up.to_str().unwrap(),
"forkgit",
],
);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
let out = f.mkit(
&fork,
&[
"git",
"export",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("direction"),
"{out:?}"
);
}
#[test]
fn fetch_after_fork_keeps_import_tracking_clean() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
let up = f.upstream();
let forkgit = f.root.path().join("forkgit");
git_ok(
f.root.path(),
&[
"clone",
"--bare",
"--quiet",
up.to_str().unwrap(),
"forkgit",
],
);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
std::fs::write(up.join("e.txt"), "e\n").unwrap();
git_ok(&up, &["add", "e.txt"]);
git_ok(&up, &["commit", "--quiet", "-m", "upstream fifth"]);
let out = f.mkit_ok(&fork, &["git", "fetch"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("force-pushed"),
"clean FF advance misread as a force-push: {stderr}"
);
let log = f.mkit_ok(&fork, &["log", "-n", "1", "upstream/main"]);
assert!(
String::from_utf8_lossy(&log.stdout).contains("upstream fifth"),
"{log:?}"
);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
}
#[test]
fn verify_status_and_fork_audit() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let out = f.mkit_ok(&fork, &["git", "verify"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("ok:") && stderr.contains("imported-vouched"),
"{stderr}"
);
let out = f.mkit_ok(&fork, &["git", "status"]);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("upstream direction=import")
&& stdout.contains("importer key:")
&& stdout.contains("tracking refs/heads/main")
&& stdout.contains("staging: ok"),
"{stdout}"
);
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
let up = f.upstream();
git_ok(
f.root.path(),
&[
"clone",
"--bare",
"--quiet",
up.to_str().unwrap(),
"forkgit",
],
);
let forkgit = f.root.path().join("forkgit");
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
let out = f.mkit_ok(&fork, &["git", "verify", "--fork-audit"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("ok:")
&& stderr.contains("bridge-translated")
&& stderr.contains("content-derived"),
"{stderr}"
);
let out = f.mkit_ok(&fork, &["git", "status"]);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("direction=fork") && stdout.contains("exported refs/heads/main"),
"{stdout}"
);
let raw_root = fork.join(".mkit/git/upstream/raw");
let bucket = std::fs::read_dir(&raw_root)
.unwrap()
.next()
.unwrap()
.unwrap();
let victim = std::fs::read_dir(bucket.path())
.unwrap()
.next()
.unwrap()
.unwrap();
std::fs::write(victim.path(), b"blob 5\0junk\n").unwrap();
let out = f.mkit(&fork, &["git", "verify"]);
assert!(!out.status.success(), "tampered raw bytes must fail verify");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("FAIL") && stderr.contains("DIFFERENT sha1"),
"{stderr}"
);
}
#[test]
fn format_patch_output_applies_with_git_am() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("feature.txt"), "one\n").unwrap();
f.mkit_ok(&fork, &["add", "feature.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "Add feature file"]);
std::fs::write(fork.join("feature.txt"), "one\ntwo\n").unwrap();
f.mkit_ok(&fork, &["add", "feature.txt"]);
f.mkit_ok(
&fork,
&["commit", "-m", "Extend feature file\n\nWith a body line."],
);
let patches = f.root.path().join("patches");
let out = f.mkit_ok(
&fork,
&[
"git",
"format-patch",
"upstream/main..HEAD",
"-o",
patches.to_str().unwrap(),
],
);
let listing = String::from_utf8_lossy(&out.stdout);
assert!(
listing.contains("0001-Add-feature-file.patch")
&& listing.contains("0002-Extend-feature-file.patch"),
"{listing}"
);
let up = f.upstream();
git_ok(
f.root.path(),
&["clone", "--quiet", up.to_str().unwrap(), "contrib"],
);
let contrib = f.root.path().join("contrib");
let p1 = patches.join("0001-Add-feature-file.patch");
let p2 = patches.join("0002-Extend-feature-file.patch");
git_ok(
&contrib,
&["am", p1.to_str().unwrap(), p2.to_str().unwrap()],
);
assert_eq!(
std::fs::read_to_string(contrib.join("feature.txt")).unwrap(),
"one\ntwo\n"
);
let log = git_in(&contrib, &["log", "--format=%s", "-n", "2"]);
let subjects = String::from_utf8_lossy(&log.stdout);
assert!(
subjects.contains("Extend feature file") && subjects.contains("Add feature file"),
"{subjects}"
);
}
#[test]
fn passthrough_cannot_target_another_states_import_source() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let second = f.root.path().join("second");
std::fs::create_dir_all(&second).unwrap();
git_ok(&second, &["init", "--quiet", "--initial-branch=main", "."]);
std::fs::write(second.join("s.txt"), "s\n").unwrap();
git_ok(&second, &["add", "s.txt"]);
git_ok(&second, &["commit", "--quiet", "-m", "second upstream"]);
f.mkit_ok(
&fork,
&[
"git",
"import",
second.to_str().unwrap(),
"--remote-name",
"second",
],
);
let out = f.mkit(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
second.to_str().unwrap(),
],
);
assert!(!out.status.success(), "cross-state passthrough must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("recorded git-import source"), "{stderr}");
assert!(!fork.join(".mkit/git/upstream/dest").exists());
}
#[test]
fn format_patch_refuses_binary_changes() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("blob.bin"), [0u8, 159, 146, 150, 0, 1, 2]).unwrap();
f.mkit_ok(&fork, &["add", "blob.bin"]);
f.mkit_ok(&fork, &["commit", "-m", "Add binary blob"]);
let out = f.mkit(
&fork,
&["git", "format-patch", "upstream/main..HEAD", "--stdout"],
);
assert!(!out.status.success(), "binary change must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("blob.bin") && stderr.contains("binary"),
"{stderr}"
);
}
#[test]
fn upstream_branch_deletion_prunes_tracking_ref() {
if !git_available() {
return;
}
let f = Fixture::new();
let up = f.upstream();
git_ok(&up, &["branch", "feature"]);
let fork = f.import();
let mkit_dir = fork.join(".mkit");
assert!(
refs::read_remote_ref(&mkit_dir, "upstream", "feature")
.unwrap()
.is_some(),
"feature tracked after import"
);
git_ok(&up, &["branch", "-D", "feature"]);
let out = f.mkit_ok(&fork, &["git", "fetch"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("upstream deleted") && stderr.contains("feature"),
"{stderr}"
);
assert!(
refs::read_remote_ref(&mkit_dir, "upstream", "feature")
.unwrap()
.is_none(),
"tracking ref pruned"
);
}
#[test]
fn sha256_upstream_refuses() {
if !git_available() {
return;
}
let f = Fixture::new();
let up256 = f.root.path().join("up256");
std::fs::create_dir_all(&up256).unwrap();
let probe = git_in(
&up256,
&[
"init",
"--quiet",
"--object-format=sha256",
"--initial-branch=main",
".",
],
);
if !probe.status.success() {
return; }
std::fs::write(up256.join("a.txt"), "a\n").unwrap();
git_ok(&up256, &["add", "a.txt"]);
git_ok(&up256, &["commit", "--quiet", "-m", "sha256 commit"]);
let out = f.mkit(
f.root.path(),
&["git", "import", up256.to_str().unwrap(), "fork256"],
);
assert!(!out.status.success(), "sha256 upstream must refuse");
assert!(
String::from_utf8_lossy(&out.stderr).contains("SHA-256"),
"{out:?}"
);
}
#[test]
fn import_spec_version_mismatch_refuses() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
std::fs::write(fork.join(".mkit/git/upstream/import-spec"), "999\n").unwrap();
let out = f.mkit(&fork, &["git", "fetch"]);
assert!(!out.status.success(), "spec mismatch must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("import-spec 999") && stderr.contains("new --remote-name"),
"{stderr}"
);
}
#[test]
fn lightweight_tag_imports_as_bare_ref() {
if !git_available() {
return;
}
let f = Fixture::new();
let up = f.upstream();
git_ok(&up, &["tag", "lw"]); let fork = f.import();
let mkit_dir = fork.join(".mkit");
let lw = refs::read_tag(&mkit_dir, "lw").unwrap().unwrap();
let head = refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap();
assert_eq!(lw, head, "bare ref, no synthesized tag object");
let v1 = refs::read_tag(&mkit_dir, "v1").unwrap().unwrap();
assert_ne!(v1, head);
}
#[test]
fn mixed_import_skips_refused_ref_and_keeps_the_rest() {
if !git_available() {
return;
}
let f = Fixture::new();
let up = f.upstream();
git_ok(&up, &["checkout", "--quiet", "-b", "with-submodule"]);
let out = git_in(
&up,
&[
"-c",
"protocol.file.allow=always",
"submodule",
"add",
"--quiet",
up.to_str().unwrap(),
"sub",
],
);
assert!(out.status.success(), "submodule add: {out:?}");
git_ok(&up, &["commit", "--quiet", "-m", "add submodule"]);
git_ok(&up, &["checkout", "--quiet", "main"]);
let out = f.mkit_ok(
f.root.path(),
&["git", "import", up.to_str().unwrap(), "fork"],
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("skipping refs/heads/with-submodule") && stderr.contains("submodule"),
"{stderr}"
);
let mkit_dir = f.fork().join(".mkit");
assert!(
refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.is_some(),
"healthy ref imported despite the refusal"
);
assert!(
refs::read_remote_ref(&mkit_dir, "upstream", "with-submodule")
.unwrap()
.is_none(),
"refused ref left untracked"
);
f.mkit_ok(&f.fork(), &["git", "verify"]);
}
#[test]
fn remote_rename_moves_and_remove_keeps_bridge_state() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let up = f.upstream();
let mkit_dir = fork.join(".mkit");
f.mkit_ok(
&fork,
&[
"remote",
"add",
"upstream",
&format!("git+file://{}", up.display()),
],
);
assert!(mkit_dir.join("git/upstream/source").exists());
f.mkit_ok(&fork, &["remote", "rename", "upstream", "origin-git"]);
assert!(
mkit_dir.join("git/origin-git/source").exists(),
"bridge state moved with the remote"
);
assert!(!mkit_dir.join("git/upstream").exists());
let out = f.mkit_ok(&fork, &["remote", "remove", "origin-git"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
mkit_dir.join("git/origin-git/source").exists(),
"bridge state retained on remove (audit evidence)"
);
assert!(
stderr.contains("git/origin-git"),
"remove names the retained path: {stderr}"
);
}
#[test]
fn upstream_tag_deletion_keeps_local_tag() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let up = f.upstream();
git_ok(&up, &["tag", "-d", "v1"]);
f.mkit_ok(&fork, &["git", "fetch"]);
assert!(
refs::read_tag(&fork.join(".mkit"), "v1").unwrap().is_some(),
"tags are kept on upstream deletion (no --prune-tags)"
);
}
#[test]
fn locally_moved_tag_is_not_clobbered_by_fetch() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let mkit_dir = fork.join(".mkit");
let imported_v1 = refs::read_tag(&mkit_dir, "v1").unwrap().unwrap();
f.mkit_ok(&fork, &["tag", "-d", "v1"]);
f.mkit_ok(&fork, &["tag", "v1", "HEAD"]);
let moved = refs::read_tag(&mkit_dir, "v1").unwrap().unwrap();
assert_ne!(moved, imported_v1);
let out = f.mkit_ok(&fork, &["git", "fetch"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("not updating tag 'v1'"),
"warn on skipped tag: {stderr}"
);
assert_eq!(
refs::read_tag(&mkit_dir, "v1").unwrap().unwrap(),
moved,
"locally-moved tag survives fetch"
);
}
#[test]
fn format_patch_skips_merges_with_warning() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
f.mkit_ok(&fork, &["branch", "side"]);
std::fs::write(fork.join("main.txt"), "m\n").unwrap();
f.mkit_ok(&fork, &["add", "main.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "Main work"]);
f.mkit_ok(&fork, &["checkout", "side"]);
std::fs::write(fork.join("side.txt"), "s\n").unwrap();
f.mkit_ok(&fork, &["add", "side.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "Side work"]);
f.mkit_ok(&fork, &["checkout", "main"]);
f.mkit_ok(&fork, &["merge", "side"]);
let out = f.mkit_ok(
&fork,
&["git", "format-patch", "upstream/main..HEAD", "--stdout"],
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("merge commit(s) skipped"),
"merge-skip warning: {stderr}"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Main work") && stdout.contains("Side work"));
}
#[test]
fn duplicate_import_state_for_same_upstream_refuses() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let up = f.upstream();
let out = f.mkit(
&fork,
&[
"git",
"import",
up.to_str().unwrap(),
"--remote-name",
"dup",
],
);
assert!(!out.status.success(), "duplicate state must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("already imported as state 'upstream'"),
"{stderr}"
);
}
#[test]
fn dash_prefixed_url_refuses() {
if !git_available() {
return;
}
let f = Fixture::new();
let out = f.mkit(
f.root.path(),
&["git", "import", "--upload-pack=/bin/echo", "fork"],
);
assert!(!out.status.success());
}
#[test]
fn passthrough_refuses_non_fast_forward_push() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
let up = f.upstream();
git_ok(
f.root.path(),
&["clone", "--quiet", up.to_str().unwrap(), "forkwt"],
);
let forkwt = f.root.path().join("forkwt");
std::fs::write(forkwt.join("remote.txt"), "r\n").unwrap();
git_ok(&forkwt, &["add", "remote.txt"]);
git_ok(&forkwt, &["commit", "--quiet", "-m", "remote-side work"]);
git_ok(
f.root.path(),
&["clone", "--bare", "--quiet", "forkwt", "forkgit"],
);
let forkgit = f.root.path().join("forkgit");
let out = f.mkit(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
assert!(
!out.status.success(),
"non-FF passthrough must refuse, not rewind"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("not integrated") || stderr.contains("has commits this repo has not"),
"{stderr}"
);
let tip = git_in(&forkgit, &["log", "--format=%s", "-n", "1"]);
assert!(
String::from_utf8_lossy(&tip.stdout).contains("remote-side work"),
"{tip:?}"
);
}
#[test]
fn divergence_probe_refuses_second_key_over_same_content() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
std::fs::remove_dir_all(fork.join(".mkit/git/upstream")).unwrap();
let other = f.root.path().join("other.key");
let kp = mkit_core::sign::KeyPair::from_seed([7u8; 32]);
mkit_core::sign::save_key(&other, &kp).unwrap();
let up = f.upstream();
let out = f.mkit(
&fork,
&[
"git",
"import",
up.to_str().unwrap(),
"--key",
other.to_str().unwrap(),
],
);
assert!(!out.status.success(), "content probe must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("already imported here under key"),
"{stderr}"
);
}
#[test]
fn state_dir_lock_blocks_concurrent_bridge_operation() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let lock = mkit_core::repo_lock::acquire_default(&fork.join(".mkit"), "git-upstream.lock")
.expect("acquire test lock");
let out = f.mkit(&fork, &["git", "fetch"]);
drop(lock);
assert!(!out.status.success(), "locked state must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("busy"), "{stderr}");
f.mkit_ok(&fork, &["git", "fetch"]);
}
#[test]
fn dash_and_empty_urls_refuse_before_reaching_git() {
if !git_available() {
return;
}
let f = Fixture::new();
let out = f.mkit(
f.root.path(),
&["git", "import", "--", "--upload-pack=/bin/echo", "fork"],
);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("is not a valid git URL or path"),
"{out:?}"
);
assert!(!f.fork().exists(), "no half-created clone target");
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
let out = f.mkit(&fork, &["git", "export", "--", "--receive-pack=/bin/echo"]);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("is not a valid git URL or path"),
"{out:?}"
);
let out = f.mkit(&fork, &["git", "export", ""]);
assert!(!out.status.success());
assert!(
String::from_utf8_lossy(&out.stderr).contains("empty git URL"),
"{out:?}"
);
assert!(
!fork.join("HEAD").exists(),
"no bare-repo files scattered into the worktree"
);
}
#[test]
fn import_attestation_predicate_pins_locator_and_source() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let mkit_dir = fork.join(".mkit");
let head = refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.unwrap();
let paths = mkit_attest::store::list(&mkit_dir, &head).unwrap();
assert!(
!paths.is_empty(),
"git-import/v1 attestation minted per head"
);
let env = mkit_attest::store::load(&paths[0]).unwrap();
let payload = String::from_utf8_lossy(&env.payload).into_owned();
let up_tip = git_stdout(&f.upstream(), &["rev-parse", "HEAD"]);
assert!(
payload.contains("git-import/v1")
&& payload.contains(&up_tip)
&& payload.contains("\"schemaVersion\":1"),
"{payload}"
);
assert!(payload.contains("refs/remotes/upstream/main"), "{payload}");
}
#[test]
fn fork_export_succeeds_after_upstream_advance_and_merge() {
if !git_available() {
return;
}
let f = Fixture::new();
let up = f.upstream();
git_ok(
f.root.path(),
&[
"clone",
"--bare",
"--quiet",
up.to_str().unwrap(),
"central",
],
);
let central = f.root.path().join("central");
f.mkit_ok(
f.root.path(),
&[
"git",
"import",
central.to_str().unwrap(),
"fork",
"--remote-name",
"central",
],
);
let fork = f.fork();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"central",
central.to_str().unwrap(),
],
);
git_ok(
f.root.path(),
&["clone", "--quiet", central.to_str().unwrap(), "third"],
);
let third = f.root.path().join("third");
std::fs::write(third.join("t.txt"), "t\n").unwrap();
git_ok(&third, &["add", "t.txt"]);
git_ok(&third, &["commit", "--quiet", "-m", "third-party work"]);
git_ok(&third, &["push", "--quiet", "origin", "main"]);
f.mkit_ok(&fork, &["git", "fetch", "--remote-name", "central"]);
f.mkit_ok(&fork, &["merge", "central/main"]);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"central",
central.to_str().unwrap(),
],
);
let subjects = git_stdout(¢ral, &["log", "--format=%s", "-n", "4", "main"]);
assert!(
subjects.contains("third-party work") && subjects.contains("local work"),
"{subjects}"
);
}
#[test]
fn triangular_fork_can_also_export_to_its_own_upstream() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("local.txt"), "l\n").unwrap();
f.mkit_ok(&fork, &["add", "local.txt"]);
f.mkit_ok(&fork, &["commit", "-m", "local work"]);
let up = f.upstream();
git_ok(
f.root.path(),
&[
"clone",
"--bare",
"--quiet",
up.to_str().unwrap(),
"forkgit",
],
);
let forkgit = f.root.path().join("forkgit");
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
forkgit.to_str().unwrap(),
],
);
f.mkit_ok(&fork, &["branch", "contrib"]);
f.mkit_ok(
&fork,
&[
"git",
"export",
"--passthrough",
"--remote-name",
"upstream",
"--ref",
"refs/heads/contrib",
up.to_str().unwrap(),
],
);
let subjects = git_stdout(&up, &["log", "--format=%s", "-n", "1", "contrib"]);
assert!(subjects.contains("local work"), "{subjects}");
}
#[test]
fn bad_first_url_does_not_wedge_the_state_name() {
if !git_available() {
return;
}
let f = Fixture::new();
let missing = f.root.path().join("nope-does-not-exist");
let out = f.mkit(
f.root.path(),
&["git", "import", missing.to_str().unwrap(), "fork"],
);
assert!(!out.status.success(), "clone of a missing path fails");
assert!(
!f.fork().exists(),
"failed fresh clone cleans up the target it created"
);
let up = f.upstream();
f.mkit_ok(
f.root.path(),
&["git", "import", up.to_str().unwrap(), "fork"],
);
}
#[test]
fn oversized_tag_name_skips_per_ref() {
if !git_available() {
return;
}
let f = Fixture::new();
let up = f.upstream();
let commit = git_stdout(&up, &["rev-parse", "HEAD"]);
let long = "a".repeat(5000);
let body = format!("object {commit}\ntype commit\ntag {long}\ntagger T <t@x> 5 +0000\n\nbig\n");
let tmp = f.root.path().join("tagbody");
std::fs::write(&tmp, body).unwrap();
let out = git_in(
&up,
&[
"hash-object",
"-t",
"tag",
"--literally",
"-w",
tmp.to_str().unwrap(),
],
);
assert!(out.status.success(), "{out:?}");
let tag_id = String::from_utf8_lossy(&out.stdout).trim().to_owned();
git_ok(&up, &["update-ref", "refs/tags/evil", &tag_id]);
let out = f.mkit_ok(
f.root.path(),
&["git", "import", up.to_str().unwrap(), "fork"],
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("skipping refs/tags/evil"),
"per-ref skip, not whole-run abort: {stderr}"
);
let mkit_dir = f.fork().join(".mkit");
assert!(
refs::read_remote_ref(&mkit_dir, "upstream", "main")
.unwrap()
.is_some()
);
assert!(refs::read_tag(&mkit_dir, "v1").unwrap().is_some());
}
#[test]
fn map_only_loss_rebuilds_even_with_unchanged_refs() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let state = fork.join(".mkit/git/upstream");
std::fs::remove_file(state.join("map")).unwrap();
let out = f.mkit_ok(&fork, &["git", "fetch"]);
assert!(
String::from_utf8_lossy(&out.stderr).contains("rebuilding"),
"{out:?}"
);
let map = std::fs::read_to_string(state.join("map")).unwrap();
assert!(map.lines().count() > 0, "map rebuilt");
}
#[test]
fn corrupt_direction_stamp_refuses_instead_of_rebinding() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
std::fs::write(fork.join(".mkit/git/upstream/direction"), "garbage\n").unwrap();
let out = f.mkit(&fork, &["git", "fetch"]);
assert!(!out.status.success(), "corrupt binding must refuse");
assert!(
String::from_utf8_lossy(&out.stderr).contains("corrupt"),
"{out:?}"
);
}
#[test]
fn partially_corrupt_map_triggers_full_rebuild() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let state = fork.join(".mkit/git/upstream");
let map = std::fs::read_to_string(state.join("map")).unwrap();
let lines: Vec<&str> = map.lines().collect();
assert!(lines.len() > 2);
let mangled = format!(
"{}\nGARBAGE not-a-line\n{}\n",
lines[0],
lines[lines.len() - 1]
);
std::fs::write(state.join("map"), mangled).unwrap();
let out = f.mkit_ok(&fork, &["git", "fetch"]);
assert!(
String::from_utf8_lossy(&out.stderr).contains("rebuilding"),
"{out:?}"
);
let rebuilt = std::fs::read_to_string(state.join("map")).unwrap();
assert_eq!(
rebuilt.lines().count(),
lines.len(),
"every mapping recovered"
);
}
#[test]
fn format_patch_escapes_mbox_splitting_body_lines() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
f.mkit_ok(&fork, &["keygen"]);
std::fs::write(fork.join("f.txt"), "x\n").unwrap();
f.mkit_ok(&fork, &["add", "f.txt"]);
f.mkit_ok(
&fork,
&[
"commit",
"-m",
"Tricky body\n\nFrom 1234567890abcdef1234567890abcdef12345678 Mon Sep 17 00:00:00 2001\nFrom the start this stays verbatim.",
],
);
let out = f.mkit_ok(
&fork,
&["git", "format-patch", "upstream/main..HEAD", "--stdout"],
);
let mbox = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
mbox.contains(">From 1234567890abcdef"),
"date-shaped line escaped: {mbox}"
);
assert!(
mbox.contains("\nFrom the start this stays verbatim."),
"harmless line untouched: {mbox}"
);
let up = f.upstream();
git_ok(
f.root.path(),
&["clone", "--quiet", up.to_str().unwrap(), "amtest"],
);
let amtest = f.root.path().join("amtest");
let patch = f.root.path().join("tricky.mbox");
std::fs::write(&patch, mbox).unwrap();
git_ok(&amtest, &["am", patch.to_str().unwrap()]);
let log = git_in(&amtest, &["log", "-1", "--format=%B"]);
assert!(
String::from_utf8_lossy(&log.stdout).contains("Tricky body"),
"{log:?}"
);
}
#[test]
fn failed_clone_keeps_preexisting_empty_target_dir() {
if !git_available() {
return;
}
let f = Fixture::new();
let target = f.root.path().join("preexisting");
std::fs::create_dir_all(&target).unwrap();
let missing = f.root.path().join("nope-missing");
let out = f.mkit(
f.root.path(),
&["git", "import", missing.to_str().unwrap(), "preexisting"],
);
assert!(!out.status.success());
assert!(
target.is_dir(),
"a directory this run did not create must survive"
);
assert_eq!(
std::fs::read_dir(&target).unwrap().count(),
0,
"but this run's contents are cleaned out"
);
}
#[test]
fn map_tail_truncation_at_line_boundary_rebuilds() {
if !git_available() {
return;
}
let f = Fixture::new();
let fork = f.import();
let state = fork.join(".mkit/git/upstream");
let map = std::fs::read_to_string(state.join("map")).unwrap();
let lines: Vec<&str> = map.lines().collect();
let mut truncated = lines[..lines.len() - 1].join("\n");
truncated.push('\n');
std::fs::write(state.join("map"), truncated).unwrap();
let out = f.mkit_ok(&fork, &["git", "fetch"]);
assert!(
String::from_utf8_lossy(&out.stderr).contains("rebuilding"),
"{out:?}"
);
let rebuilt = std::fs::read_to_string(state.join("map")).unwrap();
assert_eq!(rebuilt.lines().count(), lines.len(), "tail recovered");
}