use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
struct Harness {
_root: tempfile::TempDir,
home: PathBuf,
git_repo: PathBuf,
mkit_repo: PathBuf,
}
impl Harness {
fn new() -> Self {
let root = tempfile::tempdir().expect("tempdir");
let base = root.path().canonicalize().expect("canonicalize temp root");
let home = base.join("home");
let git_repo = base.join("git-repo");
let mkit_repo = base.join("mkit-repo");
for dir in [&home, &git_repo, &mkit_repo] {
std::fs::create_dir_all(dir).expect("create repo dir");
}
Self {
_root: root,
home,
git_repo,
mkit_repo,
}
}
fn git(&self, args: &[&str]) -> Output {
Command::new("git")
.args(args)
.current_dir(&self.git_repo)
.env("HOME", &self.home)
.env("XDG_CONFIG_HOME", self.home.join(".config"))
.env("GIT_CONFIG_GLOBAL", self.home.join("gitconfig-absent"))
.env(
"GIT_CONFIG_SYSTEM",
self.home.join("gitconfig-system-absent"),
)
.env("GIT_AUTHOR_NAME", "parity")
.env("GIT_AUTHOR_EMAIL", "parity@example.com")
.env("GIT_COMMITTER_NAME", "parity")
.env("GIT_COMMITTER_EMAIL", "parity@example.com")
.env("GIT_AUTHOR_DATE", "2026-01-01T00:00:00 +0000")
.env("GIT_COMMITTER_DATE", "2026-01-01T00:00:00 +0000")
.output()
.expect("spawn git")
}
fn mkit(&self, args: &[&str]) -> Output {
Command::new(mkit_bin())
.args(args)
.current_dir(&self.mkit_repo)
.env("HOME", &self.home)
.env("XDG_CONFIG_HOME", self.home.join(".config"))
.output()
.expect("spawn mkit")
}
fn git_stdin(&self, args: &[&str], input: &[u8]) -> Output {
let mut child = Command::new("git")
.args(args)
.current_dir(&self.git_repo)
.env("HOME", &self.home)
.env("XDG_CONFIG_HOME", self.home.join(".config"))
.env("GIT_CONFIG_GLOBAL", self.home.join("gitconfig-absent"))
.env(
"GIT_CONFIG_SYSTEM",
self.home.join("gitconfig-system-absent"),
)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn git");
child
.stdin
.take()
.expect("git stdin")
.write_all(input)
.expect("write git stdin");
child.wait_with_output().expect("git output")
}
fn mkit_stdin(&self, args: &[&str], input: &[u8]) -> Output {
let mut child = Command::new(mkit_bin())
.args(args)
.current_dir(&self.mkit_repo)
.env("HOME", &self.home)
.env("XDG_CONFIG_HOME", self.home.join(".config"))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn mkit");
child
.stdin
.take()
.expect("mkit stdin")
.write_all(input)
.expect("write mkit stdin");
child.wait_with_output().expect("mkit output")
}
fn init_both(&self) {
assert!(
self.git(&["init", "-b", "main"]).status.success(),
"git init failed"
);
assert!(self.mkit(&["init"]).status.success(), "mkit init failed");
assert!(
self.mkit(&["keygen"]).status.success(),
"mkit keygen failed"
);
}
fn write_both(&self, rel: &str, content: &[u8]) {
for repo in [&self.git_repo, &self.mkit_repo] {
let path = repo.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create parent dir");
}
std::fs::write(&path, content).expect("write fixture file");
}
}
fn commit_both(&self, paths: &[&str], message: &str) {
let mut git_add = vec!["add"];
git_add.extend_from_slice(paths);
assert!(self.git(&git_add).status.success(), "git add failed");
assert!(
self.git(&["commit", "-m", message]).status.success(),
"git commit failed"
);
let mut mkit_add = vec!["add"];
mkit_add.extend_from_slice(paths);
assert!(self.mkit(&mkit_add).status.success(), "mkit add failed");
assert!(
self.mkit(&["commit", "-m", message]).status.success(),
"mkit commit failed"
);
}
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn mask_object_ids(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut run = String::new();
for ch in line.chars() {
if ch.is_ascii_digit() || matches!(ch, 'a'..='f') {
run.push(ch);
} else {
flush_run(&mut run, &mut out);
out.push(ch);
}
}
flush_run(&mut run, &mut out);
out
}
fn flush_run(run: &mut String, out: &mut String) {
if run.len() == 40 || run.len() == 64 {
out.push_str("<oid>");
} else {
out.push_str(run);
}
run.clear();
}
fn normalize_set(s: &str) -> Vec<String> {
let mut lines: Vec<String> = s
.lines()
.map(mask_object_ids)
.filter(|l| !l.trim().is_empty())
.collect();
lines.sort();
lines
}
fn normalize_ordered(s: &str) -> Vec<String> {
s.lines().map(mask_object_ids).collect()
}
fn assert_parity_set(label: &str, git: &Output, mkit: &Output) {
assert!(git.status.success(), "{label}: git command failed: {git:?}");
assert!(
mkit.status.success(),
"{label}: mkit command failed: {mkit:?}"
);
assert_eq!(
normalize_set(&stdout(git)),
normalize_set(&stdout(mkit)),
"{label}: git/mkit output diverged (modulo hash length)"
);
}
fn assert_parity_ordered(label: &str, git: &Output, mkit: &Output) {
assert!(git.status.success(), "{label}: git command failed: {git:?}");
assert!(
mkit.status.success(),
"{label}: mkit command failed: {mkit:?}"
);
assert_eq!(
normalize_ordered(&stdout(git)),
normalize_ordered(&stdout(mkit)),
"{label}: git/mkit output diverged (modulo hash length, order-sensitive)"
);
}
fn mask_leading_short_hash(line: &str) -> String {
let mut parts = line.splitn(2, ' ');
let first = parts.next().unwrap_or("");
let is_short_hash = first.len() >= 4
&& first
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, 'a'..='f'));
match (is_short_hash, parts.next()) {
(true, Some(rest)) => format!("<oid> {rest}"),
_ => line.to_owned(),
}
}
fn assert_parity_oneline(label: &str, git: &Output, mkit: &Output) {
assert!(git.status.success(), "{label}: git command failed: {git:?}");
assert!(
mkit.status.success(),
"{label}: mkit command failed: {mkit:?}"
);
let mask = |s: &str| s.lines().map(mask_leading_short_hash).collect::<Vec<_>>();
assert_eq!(
mask(&stdout(git)),
mask(&stdout(mkit)),
"{label}: git/mkit oneline output diverged (modulo abbreviated id)"
);
}
#[test]
fn clean_repo_status_is_empty_in_both() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_set("clean status", &g, &m);
assert!(
normalize_set(&stdout(&g)).is_empty(),
"a clean repo must have empty porcelain status"
);
}
#[test]
fn status_porcelain_untracked_matches_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("untracked.txt", b"hi\n");
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_set("untracked status", &g, &m); }
#[cfg(unix)]
#[test]
fn status_porcelain_quotes_special_paths_like_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a\tb.txt", b"x\n");
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_set("quoted special path", &g, &m);
}
fn assert_parity_nul(label: &str, git: &Output, mkit: &Output) {
assert!(git.status.success(), "{label}: git failed: {git:?}");
assert!(mkit.status.success(), "{label}: mkit failed: {mkit:?}");
let recs = |o: &Output| {
let s = String::from_utf8_lossy(&o.stdout);
let mut v: Vec<String> = s
.split('\0')
.filter(|r| !r.is_empty())
.map(str::to_string)
.collect();
v.sort();
v
};
assert_eq!(
recs(git),
recs(mkit),
"{label}: -z records diverged (raw NUL-terminated)"
);
}
fn assert_parity_bytes(label: &str, git: &Output, mkit: &Output) {
assert!(git.status.success(), "{label}: git failed: {git:?}");
assert!(mkit.status.success(), "{label}: mkit failed: {mkit:?}");
assert_eq!(
git.stdout, mkit.stdout,
"{label}: stdout bytes diverged (order/pairing/framing sensitive)"
);
}
#[cfg(unix)]
#[test]
fn status_z_matches_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("tracked.txt", b"v1\n");
h.commit_both(&["tracked.txt"], "init");
h.write_both("tracked.txt", b"v2\n");
assert!(h.git(&["add", "tracked.txt"]).status.success());
assert!(h.mkit(&["add", "tracked.txt"]).status.success());
h.write_both("tracked.txt", b"v3\n");
h.write_both("a\tb.txt", b"x\n");
let g = h.git(&["status", "-z"]);
let m = h.mkit(&["status", "-z"]);
assert_parity_nul("status -z", &g, &m);
}
#[test]
fn status_rm_cached_keeps_staged_delete_and_untracked() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"hello\n");
h.commit_both(&["a.txt"], "init");
assert!(h.git(&["rm", "--cached", "a.txt"]).status.success());
assert!(h.mkit(&["rm", "--cached", "a.txt"]).status.success());
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_set("rm --cached status", &g, &m); }
#[test]
fn status_porcelain_staged_add_matches_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"hello\n");
assert!(h.git(&["add", "a.txt"]).status.success());
assert!(h.mkit(&["add", "a.txt"]).status.success());
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_set("staged-add status", &g, &m); }
#[test]
fn status_porcelain_staged_modification_matches_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"one\n");
h.commit_both(&["a.txt"], "init");
h.write_both("a.txt", b"two\n");
assert!(h.git(&["add", "a.txt"]).status.success());
assert!(h.mkit(&["add", "a.txt"]).status.success());
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_set("staged-modification status", &g, &m); }
#[test]
fn log_oneline_matches_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"hello\n");
h.commit_both(&["a.txt"], "only commit");
let g = h.git(&["log", "--oneline"]);
let m = h.mkit(&["log", "--oneline"]);
assert_parity_oneline("log --oneline", &g, &m); }
#[test]
fn log_revision_and_range_match_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
for (f, m) in [
("a.txt", "c1"),
("b.txt", "c2"),
("c.txt", "c3"),
("d.txt", "c4"),
] {
h.write_both(f, b"x\n");
h.commit_both(&[f], m);
}
assert_parity_oneline(
"log --oneline HEAD~1",
&h.git(&["log", "--oneline", "HEAD~1"]),
&h.mkit(&["log", "--oneline", "HEAD~1"]),
);
assert_parity_oneline(
"log --oneline HEAD~3..HEAD",
&h.git(&["log", "--oneline", "HEAD~3..HEAD"]),
&h.mkit(&["log", "--oneline", "HEAD~3..HEAD"]),
);
assert_parity_oneline(
"log --oneline HEAD~2..",
&h.git(&["log", "--oneline", "HEAD~2.."]),
&h.mkit(&["log", "--oneline", "HEAD~2.."]),
);
}
#[test]
fn log_annotated_tag_range_matches_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"1\n");
h.commit_both(&["a.txt"], "c1");
assert!(h.git(&["tag", "-a", "v1", "-m", "tag c1"]).status.success());
assert!(
h.mkit(&["tag", "-a", "v1", "-m", "tag c1"])
.status
.success()
);
h.write_both("b.txt", b"2\n");
h.commit_both(&["b.txt"], "c2");
h.write_both("c.txt", b"3\n");
h.commit_both(&["c.txt"], "c3");
assert_parity_oneline(
"log --oneline v1 (annotated)",
&h.git(&["log", "--oneline", "v1"]),
&h.mkit(&["log", "--oneline", "v1"]),
);
assert_parity_oneline(
"log --oneline v1..HEAD (annotated)",
&h.git(&["log", "--oneline", "v1..HEAD"]),
&h.mkit(&["log", "--oneline", "v1..HEAD"]),
);
}
#[test]
fn log_and_diff_symmetric_range_match_git() {
if !git_available() {
eprintln!("skipping: real `git` not on PATH");
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"base\n");
h.commit_both(&["a.txt"], "c1");
assert!(h.git(&["branch", "feat"]).status.success());
assert!(h.mkit(&["branch", "feat"]).status.success());
h.write_both("m.txt", b"m\n");
h.commit_both(&["m.txt"], "c2");
assert!(h.git(&["checkout", "feat"]).status.success());
assert!(h.mkit(&["checkout", "feat"]).status.success());
h.write_both("f.txt", b"f\n");
h.commit_both(&["f.txt"], "c3");
let g = h.git(&["log", "--oneline", "main...feat"]);
let m = h.mkit(&["log", "--oneline", "main...feat"]);
assert!(g.status.success() && m.status.success(), "log failed");
let subjects = |o: &Output| {
let mut v: Vec<String> = stdout(o)
.lines()
.filter_map(|l| l.split_once(' ').map(|(_, s)| s.to_string()))
.collect();
v.sort();
v
};
assert_eq!(
subjects(&g),
subjects(&m),
"log main...feat commit set diverged"
);
assert_parity_diff(
"diff main...feat",
&h.git(&["diff", "main...feat"]),
&h.mkit(&["diff", "main...feat"]),
);
}
fn stage_add_delete_modify(h: &Harness) {
h.write_both("m.txt", b"one\n");
h.write_both("del.txt", b"gone\n");
h.commit_both(&["m.txt", "del.txt"], "init");
h.write_both("m.txt", b"two\n"); h.write_both("new.txt", b"new\n"); assert!(h.git(&["add", "m.txt", "new.txt"]).status.success());
assert!(h.git(&["rm", "del.txt"]).status.success());
assert!(h.mkit(&["add", "m.txt", "new.txt"]).status.success());
assert!(h.mkit(&["rm", "del.txt"]).status.success());
}
#[test]
fn diff_name_only_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
stage_add_delete_modify(&h);
let g = h.git(&["diff", "--cached", "--name-only"]);
let m = h.mkit(&["diff", "--staged", "--name-only"]);
assert_parity_ordered("diff --name-only", &g, &m); }
#[test]
fn diff_name_status_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
stage_add_delete_modify(&h);
let g = h.git(&["diff", "--cached", "--name-status"]);
let m = h.mkit(&["diff", "--staged", "--name-status"]);
assert_parity_ordered("diff --name-status", &g, &m); }
#[cfg(unix)]
#[test]
fn diff_name_status_z_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
stage_add_delete_modify(&h);
let g = h.git(&["diff", "--cached", "--name-status", "-z"]);
let m = h.mkit(&["diff", "--staged", "--name-status", "-z"]);
assert_parity_bytes("diff --name-status -z", &g, &m);
}
#[test]
fn diff_staged_rejects_bad_rev_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
let g = h.git(&["diff", "--cached", "--name-only", "deadbeefdeadbeef"]);
let m = h.mkit(&["diff", "--staged", "--name-only", "deadbeefdeadbeef"]);
assert!(
!g.status.success(),
"git should reject bad staged rev: {g:?}"
);
assert!(
!m.status.success(),
"mkit must not empty-succeed on a bad staged rev: {m:?}"
);
}
#[cfg(unix)]
#[test]
fn diff_name_only_quotes_special_paths_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a\tb.txt", b"x\n");
assert!(h.git(&["add", "a\tb.txt"]).status.success());
assert!(h.mkit(&["add", "a\tb.txt"]).status.success());
let g = h.git(&["diff", "--cached", "--name-only"]);
let m = h.mkit(&["diff", "--staged", "--name-only"]);
assert_parity_ordered("diff --name-only quoted", &g, &m); let gz = h.git(&["diff", "--cached", "--name-only", "-z"]);
let mz = h.mkit(&["diff", "--staged", "--name-only", "-z"]);
assert_parity_bytes("diff --name-only -z raw", &gz, &mz);
}
#[test]
fn diff_stat_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
stage_add_delete_modify(&h);
let g = h.git(&["diff", "--cached", "--stat"]);
let m = h.mkit(&["diff", "--staged", "--stat"]);
assert_parity_bytes("diff --stat", &g, &m);
}
#[test]
fn diff_stat_single_insertion_summary_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("f.txt", b"x\n");
h.commit_both(&["f.txt"], "init");
h.write_both("f.txt", b"x\ny\n"); assert!(h.git(&["add", "f.txt"]).status.success());
assert!(h.mkit(&["add", "f.txt"]).status.success());
let g = h.git(&["diff", "--cached", "--stat"]);
let m = h.mkit(&["diff", "--staged", "--stat"]);
assert_parity_bytes("diff --stat singular summary", &g, &m);
}
#[test]
fn diff_stat_empty_file_zero_change_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("seed.txt", b"x\n");
h.commit_both(&["seed.txt"], "init");
h.write_both("empty.txt", b"");
assert!(h.git(&["add", "empty.txt"]).status.success());
assert!(h.mkit(&["add", "empty.txt"]).status.success());
let g = h.git(&["diff", "--cached", "--stat"]);
let m = h.mkit(&["diff", "--staged", "--stat"]);
assert_parity_bytes("diff --stat zero-change row", &g, &m);
}
#[test]
fn diff_stat_nul_file_is_binary_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("payload.dat", b"hello\x00world\n");
h.commit_both(&["payload.dat"], "init");
h.write_both("payload.dat", b"HELLO\x00WORLD\nmore\n");
assert!(h.git(&["add", "payload.dat"]).status.success());
assert!(h.mkit(&["add", "payload.dat"]).status.success());
let g = h.git(&["diff", "--cached", "--stat"]);
let m = h.mkit(&["diff", "--staged", "--stat"]);
assert_parity_bytes("diff --stat NUL=binary", &g, &m);
}
#[test]
fn diff_stat_scaled_graph_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("small.txt", b"x\n");
h.write_both("really-long-filename.txt", b"x\n");
h.commit_both(&["small.txt", "really-long-filename.txt"], "init");
h.write_both("small.txt", b"a\nb\nc\n");
let mut big = String::new();
for i in 0..200 {
use std::fmt::Write as _;
let _ = writeln!(big, "line{i}");
}
h.write_both("really-long-filename.txt", big.as_bytes());
assert!(
h.git(&["add", "small.txt", "really-long-filename.txt"])
.status
.success()
);
assert!(
h.mkit(&["add", "small.txt", "really-long-filename.txt"])
.status
.success()
);
let g = h.git(&["diff", "--cached", "--stat"]);
let m = h.mkit(&["diff", "--staged", "--stat"]);
assert_parity_bytes("diff --stat (scaled)", &g, &m);
}
#[test]
fn clean_dry_run_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("tracked.txt", b"t\n");
h.commit_both(&["tracked.txt"], "init");
h.write_both("a-untracked.txt", b"u\n");
h.write_both("z-untracked.txt", b"u\n");
let g = h.git(&["clean", "-n"]);
let m = h.mkit(&["clean", "-n"]);
assert_parity_ordered("clean -n", &g, &m);
}
#[test]
fn clean_dry_run_d_lists_untracked_dirs_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("tracked.txt", b"t\n");
h.commit_both(&["tracked.txt"], "init");
h.write_both("top.txt", b"u\n");
h.write_both("untrackeddir/inner.txt", b"d\n");
let g = h.git(&["clean", "-n", "-d"]);
let m = h.mkit(&["clean", "-n", "-d"]);
assert_parity_ordered("clean -n -d", &g, &m);
}
#[test]
fn clean_without_force_refused_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("tracked.txt", b"t\n");
h.commit_both(&["tracked.txt"], "init");
h.write_both("untracked.txt", b"u\n");
let g = h.git(&["clean"]);
let m = h.mkit(&["clean"]);
assert!(
!g.status.success(),
"git clean must refuse without -f: {g:?}"
);
assert!(
!m.status.success(),
"mkit clean must refuse without -f: {m:?}"
);
}
#[test]
fn mv_existing_dest_refused_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"aaa\n");
h.write_both("b.txt", b"bbb\n");
h.commit_both(&["a.txt", "b.txt"], "init");
let g = h.git(&["mv", "a.txt", "b.txt"]);
let m = h.mkit(&["mv", "a.txt", "b.txt"]);
assert!(!g.status.success(), "git mv should refuse clobber: {g:?}");
assert!(!m.status.success(), "mkit mv should refuse clobber: {m:?}");
}
#[test]
fn mv_untracked_source_fails_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("tracked.txt", b"t\n");
h.commit_both(&["tracked.txt"], "init");
h.write_both("untracked.txt", b"u\n");
let g = h.git(&["mv", "untracked.txt", "dest.txt"]);
let m = h.mkit(&["mv", "untracked.txt", "dest.txt"]);
assert!(
!g.status.success(),
"git mv should reject untracked source: {g:?}"
);
assert!(
!m.status.success(),
"mkit mv should reject untracked source: {m:?}"
);
}
#[test]
fn config_user_name_round_trips_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
assert!(
h.git(&["config", "user.name", "Alice Example"])
.status
.success()
);
assert!(
h.mkit(&["config", "user.name", "Alice Example"])
.status
.success()
);
let g = h.git(&["config", "user.name"]);
let m = h.mkit(&["config", "user.name"]);
assert_parity_bytes("config user.name round-trip", &g, &m);
}
#[test]
fn branch_default_list_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert!(h.git(&["branch", "feature"]).status.success());
assert!(h.mkit(&["branch", "feature"]).status.success());
let g = h.git(&["branch"]);
let m = h.mkit(&["branch"]);
assert_parity_ordered("branch (default list)", &g, &m);
}
#[test]
fn branch_delete_missing_fails_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
let g = h.git(&["branch", "-D", "ghost"]);
let m = h.mkit(&["branch", "-D", "ghost"]);
assert!(
!g.status.success(),
"git should reject -D of missing: {g:?}"
);
assert!(
!m.status.success(),
"mkit -D of a missing branch must fail like git: {m:?}"
);
}
#[test]
fn rev_parse_head_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert_parity_ordered(
"rev-parse HEAD",
&h.git(&["rev-parse", "HEAD"]),
&h.mkit(&["rev-parse", "HEAD"]),
);
assert_parity_bytes(
"rev-parse --abbrev-ref HEAD",
&h.git(&["rev-parse", "--abbrev-ref", "HEAD"]),
&h.mkit(&["rev-parse", "--abbrev-ref", "HEAD"]),
);
}
#[test]
fn show_ref_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert!(h.git(&["tag", "v1"]).status.success());
assert!(h.mkit(&["tag", "v1"]).status.success());
assert_parity_ordered("show-ref", &h.git(&["show-ref"]), &h.mkit(&["show-ref"]));
assert_parity_ordered(
"show-ref --heads",
&h.git(&["show-ref", "--heads"]),
&h.mkit(&["show-ref", "--heads"]),
);
}
#[test]
fn ls_tree_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("file.txt", b"hello\n");
h.write_both("sub/inner.txt", b"nested\n");
h.commit_both(&["file.txt", "sub/inner.txt"], "init");
assert_parity_ordered(
"ls-tree HEAD",
&h.git(&["ls-tree", "HEAD"]),
&h.mkit(&["ls-tree", "HEAD"]),
);
assert_parity_ordered(
"ls-tree -r HEAD",
&h.git(&["ls-tree", "-r", "HEAD"]),
&h.mkit(&["ls-tree", "-r", "HEAD"]),
);
}
#[test]
fn cat_file_blob_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("file.txt", b"hello\n");
h.commit_both(&["file.txt"], "init");
let git_blob = blob_hash_from_ls_tree(&stdout(&h.git(&["ls-tree", "HEAD"])), "file.txt");
let mkit_blob = blob_hash_from_ls_tree(&stdout(&h.mkit(&["ls-tree", "HEAD"])), "file.txt");
assert_parity_bytes(
"cat-file -t blob",
&h.git(&["cat-file", "-t", &git_blob]),
&h.mkit(&["cat-file", "-t", &mkit_blob]),
);
assert_parity_bytes(
"cat-file -s blob",
&h.git(&["cat-file", "-s", &git_blob]),
&h.mkit(&["cat-file", "-s", &mkit_blob]),
);
assert_parity_bytes(
"cat-file -p blob",
&h.git(&["cat-file", "-p", &git_blob]),
&h.mkit(&["cat-file", "-p", &mkit_blob]),
);
}
fn blob_hash_from_ls_tree(out: &str, name: &str) -> String {
for line in out.lines() {
let Some((meta, path)) = line.split_once('\t') else {
continue;
};
if path == name {
return meta.split_whitespace().nth(2).unwrap_or("").to_string();
}
}
panic!("no ls-tree entry for {name} in: {out:?}");
}
#[test]
fn cat_file_batch_blob_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("file.txt", b"hello\n");
h.commit_both(&["file.txt"], "init");
let git_blob = blob_hash_from_ls_tree(&stdout(&h.git(&["ls-tree", "HEAD"])), "file.txt");
let mkit_blob = blob_hash_from_ls_tree(&stdout(&h.mkit(&["ls-tree", "HEAD"])), "file.txt");
let g = h.git_stdin(&["cat-file", "--batch"], format!("{git_blob}\n").as_bytes());
let m = h.mkit_stdin(
&["cat-file", "--batch"],
format!("{mkit_blob}\n").as_bytes(),
);
assert_parity_ordered("cat-file --batch blob", &g, &m);
}
#[test]
fn ls_files_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("file.txt", b"hello\n");
h.write_both("sub/inner.txt", b"nested\n");
h.commit_both(&["file.txt", "sub/inner.txt"], "init");
assert_parity_bytes("ls-files", &h.git(&["ls-files"]), &h.mkit(&["ls-files"]));
assert_parity_ordered(
"ls-files -s",
&h.git(&["ls-files", "-s"]),
&h.mkit(&["ls-files", "-s"]),
);
h.write_both("other.txt", b"x\n");
assert_parity_bytes(
"ls-files --others",
&h.git(&["ls-files", "--others"]),
&h.mkit(&["ls-files", "--others"]),
);
}
#[test]
fn ls_files_exclude_standard_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
std::fs::write(h.git_repo.join(".gitignore"), b"*.log\n.gitignore\n")
.expect("write .gitignore");
std::fs::write(h.mkit_repo.join(".mkitignore"), b"*.log\n.mkitignore\n")
.expect("write .mkitignore");
h.write_both("keep.txt", b"k\n");
h.write_both("secret.log", b"s\n");
assert_parity_bytes(
"ls-files --others --exclude-standard",
&h.git(&["ls-files", "--others", "--exclude-standard"]),
&h.mkit(&["ls-files", "--others", "--exclude-standard"]),
);
}
#[test]
fn for_each_ref_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert!(h.git(&["tag", "v1"]).status.success());
assert!(h.mkit(&["tag", "v1"]).status.success());
assert_parity_ordered(
"for-each-ref",
&h.git(&["for-each-ref"]),
&h.mkit(&["for-each-ref"]),
);
assert_parity_bytes(
"for-each-ref --format refname/objecttype",
&h.git(&["for-each-ref", "--format=%(refname) %(objecttype)"]),
&h.mkit(&["for-each-ref", "--format=%(refname) %(objecttype)"]),
);
assert_parity_bytes(
"for-each-ref --format refname:short",
&h.git(&["for-each-ref", "--format=%(refname:short)"]),
&h.mkit(&["for-each-ref", "--format=%(refname:short)"]),
);
}
#[test]
fn symbolic_ref_head_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert_parity_bytes(
"symbolic-ref HEAD",
&h.git(&["symbolic-ref", "HEAD"]),
&h.mkit(&["symbolic-ref", "HEAD"]),
);
assert_parity_bytes(
"symbolic-ref --short HEAD",
&h.git(&["symbolic-ref", "--short", "HEAD"]),
&h.mkit(&["symbolic-ref", "--short", "HEAD"]),
);
}
#[test]
fn update_ref_create_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert!(
h.git(&["update-ref", "refs/heads/feature", "HEAD"])
.status
.success()
);
assert!(
h.mkit(&["update-ref", "refs/heads/feature", "HEAD"])
.status
.success()
);
assert_parity_ordered(
"show-ref after update-ref create",
&h.git(&["show-ref", "--heads"]),
&h.mkit(&["show-ref", "--heads"]),
);
}
#[test]
fn symbolic_ref_write_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a.txt", b"x\n");
h.commit_both(&["a.txt"], "init");
assert!(h.git(&["branch", "feature"]).status.success());
assert!(h.mkit(&["branch", "feature"]).status.success());
assert!(
h.git(&["symbolic-ref", "HEAD", "refs/heads/feature"])
.status
.success()
);
assert!(
h.mkit(&["symbolic-ref", "HEAD", "refs/heads/feature"])
.status
.success()
);
assert_parity_bytes(
"symbolic-ref HEAD after write",
&h.git(&["symbolic-ref", "HEAD"]),
&h.mkit(&["symbolic-ref", "HEAD"]),
);
}
#[test]
fn config_core_inert_key_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
assert!(h.git(&["config", "core.autocrlf", "true"]).status.success());
assert!(
h.mkit(&["config", "core.autocrlf", "true"])
.status
.success()
);
assert_parity_bytes(
"config core.autocrlf round-trip",
&h.git(&["config", "core.autocrlf"]),
&h.mkit(&["config", "core.autocrlf"]),
);
}
#[test]
fn status_porcelain_v2_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("tracked.txt", b"v1\n");
h.write_both("doomed.txt", b"bye\n");
h.commit_both(&["tracked.txt", "doomed.txt"], "init");
h.write_both("added.txt", b"new\n");
assert!(h.git(&["add", "added.txt"]).status.success());
assert!(h.mkit(&["add", "added.txt"]).status.success());
assert!(h.git(&["rm", "doomed.txt"]).status.success());
assert!(h.mkit(&["rm", "doomed.txt"]).status.success());
h.write_both("tracked.txt", b"v2\n"); h.write_both("untracked.txt", b"u\n");
let g = h.git(&["status", "--porcelain=v2"]);
let m = h.mkit(&["status", "--porcelain=v2"]);
assert_parity_set("status --porcelain=v2 (mixed)", &g, &m);
}
fn setup_file_replaced_by_dir(h: &Harness) {
h.init_both();
h.write_both("f", b"file contents\n");
h.commit_both(&["f"], "init");
for repo in [&h.git_repo, &h.mkit_repo] {
std::fs::remove_file(repo.join("f")).expect("remove tracked file");
std::fs::create_dir(repo.join("f")).expect("create dir at path");
std::fs::write(repo.join("f/child"), b"inside\n").expect("write child");
}
}
#[test]
fn status_porcelain_v2_file_replaced_by_dir_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
setup_file_replaced_by_dir(&h);
let g = h.git(&["status", "--porcelain=v2"]);
let m = h.mkit(&["status", "--porcelain=v2"]);
assert_parity_ordered("status --porcelain=v2 (file→dir)", &g, &m);
let out = String::from_utf8(m.stdout).expect("utf-8");
let rec = out
.lines()
.find(|l| l.ends_with(" f"))
.expect("tracked `f` record present");
let mw = rec.split(' ').nth(5).expect("mW field");
assert_eq!(
mw, "000000",
"worktree mode for dir-replaced file must be 000000"
);
assert!(
!out.lines().any(|l| l.contains("f/child")),
"v2: untracked dir contents must be suppressed: {out}"
);
}
#[test]
fn status_porcelain_v1_file_replaced_by_dir_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
setup_file_replaced_by_dir(&h);
let g = h.git(&["status", "--porcelain"]);
let m = h.mkit(&["status", "--porcelain"]);
assert_parity_ordered("status --porcelain (file→dir)", &g, &m);
}
#[test]
fn ls_files_others_lists_tracked_path_collision_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
setup_file_replaced_by_dir(&h);
let g = h.git(&["ls-files", "--others", "--exclude-standard"]);
let m = h.mkit(&["ls-files", "--others", "--exclude-standard"]);
assert_parity_ordered("ls-files --others (file→dir)", &g, &m);
assert!(
String::from_utf8_lossy(&m.stdout).contains("f/child"),
"ls-files --others must list f/child like git"
);
}
#[test]
fn clean_preview_suppresses_tracked_path_collision() {
if !git_available() {
return;
}
let h = Harness::new();
setup_file_replaced_by_dir(&h);
let g = h.git(&["clean", "-n", "-d"]);
let m = h.mkit(&["clean", "-n", "-d"]);
assert_parity_ordered("clean dry-run -d (file->dir)", &g, &m);
}
fn mask_diff_line(line: &str) -> String {
if let Some(rest) = line.strip_prefix("index ") {
let mut parts = rest.splitn(2, ' ');
let _hashes = parts.next();
match parts.next() {
Some(mode) => format!("index <oid>..<oid> {mode}"),
None => "index <oid>..<oid>".to_string(),
}
} else {
mask_object_ids(line)
}
}
fn assert_parity_diff(label: &str, git: &Output, mkit: &Output) {
assert!(git.status.success(), "{label}: git failed: {git:?}");
assert!(mkit.status.success(), "{label}: mkit failed: {mkit:?}");
let norm = |o: &Output| stdout(o).lines().map(mask_diff_line).collect::<Vec<_>>();
assert_eq!(
norm(git),
norm(mkit),
"{label}: git/mkit unified diff diverged (modulo abbreviated ids)"
);
}
#[test]
fn show_head_diff_body_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("f.txt", b"line1\nline2\nline3\n");
h.commit_both(&["f.txt"], "init");
h.write_both("f.txt", b"line1\nCHANGED\nline3\nline4\n");
h.commit_both(&["f.txt"], "second");
let g = h.git(&["show", "HEAD"]);
let m = h.mkit(&["show", "HEAD"]);
assert!(
g.status.success() && m.status.success(),
"show failed: git={g:?} mkit={m:?}"
);
let body = |o: &Output| {
let s = stdout(o);
let from = s.find("diff --git").unwrap_or(s.len());
s[from..].lines().map(mask_diff_line).collect::<Vec<_>>()
};
assert_eq!(
body(&g),
body(&m),
"show HEAD diff body diverged from git (modulo abbreviated ids)"
);
}
#[test]
fn diff_unified_modify_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("f.txt", b"line1\nline2\nline3\n");
h.commit_both(&["f.txt"], "init");
h.write_both("f.txt", b"line1\nCHANGED\nline3\n");
assert_parity_diff("diff (modify)", &h.git(&["diff"]), &h.mkit(&["diff"]));
}
#[test]
fn diff_unified_multi_hunk_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
let mut base = String::new();
for n in 1..=10 {
use std::fmt::Write as _;
let _ = writeln!(base, "line{n}");
}
h.write_both("f.txt", base.as_bytes());
h.commit_both(&["f.txt"], "init");
let edited = base
.replace("line2\n", "TWO\n")
.replace("line9\n", "NINE\n");
h.write_both("f.txt", edited.as_bytes());
assert_parity_diff("diff (multi-hunk)", &h.git(&["diff"]), &h.mkit(&["diff"]));
}
#[test]
fn diff_unified_add_delete_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("keep.txt", b"keep\n");
h.write_both("gone.txt", b"a\nb\nc\n");
h.commit_both(&["keep.txt", "gone.txt"], "init");
h.write_both("new.txt", b"hello\nworld\n");
assert!(h.git(&["add", "new.txt"]).status.success());
assert!(h.mkit(&["add", "new.txt"]).status.success());
assert!(h.git(&["rm", "gone.txt"]).status.success());
assert!(h.mkit(&["rm", "gone.txt"]).status.success());
assert_parity_diff(
"diff --staged (add+delete)",
&h.git(&["diff", "--staged"]),
&h.mkit(&["diff", "--staged"]),
);
}
#[test]
fn diff_unified_no_newline_at_eof_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("f.txt", b"a\nb\nc\n");
h.commit_both(&["f.txt"], "init");
h.write_both("f.txt", b"a\nb\nc");
assert_parity_diff(
"diff (no newline at eof)",
&h.git(&["diff"]),
&h.mkit(&["diff"]),
);
}
#[test]
fn diff_unified_single_line_hunk_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("f.txt", b"old\n");
h.commit_both(&["f.txt"], "init");
h.write_both("f.txt", b"new\n");
assert_parity_diff(
"diff (single-line hunk header)",
&h.git(&["diff"]),
&h.mkit(&["diff"]),
);
}
#[test]
fn diff_unified_nul_blob_is_binary_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("b.dat", b"a\0b\n");
h.commit_both(&["b.dat"], "init");
h.write_both("b.dat", b"a\0c\n");
assert_parity_diff(
"diff (NUL blob is binary)",
&h.git(&["diff"]),
&h.mkit(&["diff"]),
);
}
#[cfg(unix)]
#[test]
fn diff_unified_quotes_special_path_header_like_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("a\tb.txt", b"one\n");
h.commit_both(&["a\tb.txt"], "init");
h.write_both("a\tb.txt", b"two\n");
assert_parity_diff(
"diff (quoted special path header)",
&h.git(&["diff"]),
&h.mkit(&["diff"]),
);
}
#[test]
fn diff_dir_replaced_by_file_matches_git() {
if !git_available() {
return;
}
let h = Harness::new();
h.init_both();
h.write_both("d/x.txt", b"hi\n");
h.commit_both(&["d/x.txt"], "c1");
for repo in [&h.git_repo, &h.mkit_repo] {
std::fs::remove_dir_all(repo.join("d")).expect("rm dir");
std::fs::write(repo.join("d"), b"now a file\n").expect("write file d");
}
assert!(h.git(&["add", "-A"]).status.success());
assert!(h.git(&["commit", "-m", "c2"]).status.success());
assert!(h.mkit(&["add", "-A"]).status.success());
assert!(h.mkit(&["commit", "-m", "c2"]).status.success());
assert_parity_diff(
"diff (dir replaced by file)",
&h.git(&["diff", "HEAD~1", "HEAD"]),
&h.mkit(&["diff", "HEAD~1", "HEAD"]),
);
}
#[test]
fn mask_object_ids_masks_only_40_and_64_hex() {
let sha1 = "a".repeat(40);
let blake3 = "b".repeat(64);
assert_eq!(mask_object_ids(&format!("commit {sha1}")), "commit <oid>");
assert_eq!(mask_object_ids(&format!("commit {blake3}")), "commit <oid>");
assert_eq!(mask_object_ids("abc1234 subject"), "abc1234 subject");
assert_eq!(mask_object_ids("?? untracked.txt"), "?? untracked.txt");
}