use std::fs;
use std::path::Path;
use std::process::Command;
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn run_in(cwd: &Path, args: &[&str]) -> std::process::Output {
let xdg = tempfile::tempdir().expect("xdg tempdir");
let out = Command::new(mkit_bin())
.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg.path())
.output()
.expect("spawn mkit");
drop(xdg);
out
}
fn init_repo() -> tempfile::TempDir {
let td = tempfile::tempdir().unwrap();
assert!(run_in(td.path(), &["init"]).status.success());
assert!(run_in(td.path(), &["keygen"]).status.success());
td
}
fn commit(root: &Path, name: &str, content: &[u8], msg: &str) {
fs::write(root.join(name), content).unwrap();
assert!(run_in(root, &["add", name]).status.success(), "add {name}");
assert!(
run_in(root, &["commit", "-m", msg]).status.success(),
"commit {msg}"
);
}
fn orphan_blob(root: &Path, content: &[u8]) -> String {
fs::write(root.join("orphan.bin"), content).unwrap();
let out = run_in(root, &["hash", "orphan.bin"]);
assert!(out.status.success(), "hash: {out:?}");
let h = String::from_utf8(out.stdout).unwrap().trim().to_owned();
fs::remove_file(root.join("orphan.bin")).unwrap();
h
}
fn object_exists(root: &Path, hex: &str) -> bool {
run_in(root, &["cat", hex]).status.success()
}
#[test]
fn gc_prunes_unreachable_blob_with_zero_grace() {
let td = init_repo();
let root = td.path();
commit(root, "a.txt", b"live\n", "live");
let orphan = orphan_blob(root, b"orphan contents\n");
assert!(object_exists(root, &orphan), "orphan present before gc");
let out = run_in(root, &["gc", "--grace-secs", "0"]);
assert!(out.status.success(), "gc: {out:?}");
assert!(
!object_exists(root, &orphan),
"gc must prune the unreachable blob"
);
let log = run_in(root, &["log", "--oneline"]);
assert!(
String::from_utf8_lossy(&log.stdout).contains("live"),
"gc must not touch live history"
);
}
#[test]
fn gc_dry_run_keeps_everything() {
let td = init_repo();
let root = td.path();
commit(root, "a.txt", b"live\n", "live");
let orphan = orphan_blob(root, b"orphan\n");
let out = run_in(root, &["gc", "--dry-run", "--grace-secs", "0"]);
assert!(out.status.success(), "gc --dry-run: {out:?}");
assert!(
object_exists(root, &orphan),
"dry run must not delete the orphan"
);
}
#[test]
fn gc_grace_window_keeps_recent_orphan() {
let td = init_repo();
let root = td.path();
commit(root, "a.txt", b"live\n", "live");
let orphan = orphan_blob(root, b"recent\n");
let out = run_in(root, &["gc"]);
assert!(out.status.success(), "gc: {out:?}");
assert!(
object_exists(root, &orphan),
"default grace window must keep a freshly-written orphan"
);
}
#[test]
fn gc_does_not_prune_recovery_logged_commit() {
let td = init_repo();
let root = td.path();
commit(root, "a.txt", b"one\n", "one");
let superseded = fs::read_to_string(root.join(".mkit/refs/heads/main"))
.unwrap()
.trim()
.to_owned();
assert!(
run_in(root, &["commit", "--amend", "-m", "one-amended"])
.status
.success(),
"amend"
);
assert_ne!(
fs::read_to_string(root.join(".mkit/refs/heads/main"))
.unwrap()
.trim(),
superseded,
"amend moved the branch"
);
let out = run_in(root, &["gc", "--grace-secs", "0"]);
assert!(out.status.success(), "gc: {out:?}");
assert!(
object_exists(root, &superseded),
"gc must not prune a commit pinned by the recovery log"
);
}