#![allow(dead_code)]
#![allow(clippy::unwrap_used)]
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use mkit_core::Hash;
use mkit_core::index::read_index;
use mkit_core::layout::RepoLayout;
use mkit_core::object::Object;
use mkit_core::ops::live_objects;
use mkit_core::refs;
use mkit_core::sign::{KeyPair, save_key, verify_commit, verify_remix, verify_tag};
use mkit_core::store::ObjectStore;
use mkit_core::to_hex;
pub(crate) const KEY_SEED: [u8; 32] = [0x11; 32];
#[allow(unused_imports)]
pub(crate) use mkit_test_util::{require_tool, tool_available};
pub(crate) fn require_env_flag(var: &str) -> bool {
if std::env::var(var).as_deref() == Ok("1") {
return true;
}
assert!(
std::env::var_os("MKIT_TEST_STRICT").is_none(),
"{var}=1 required (MKIT_TEST_STRICT set) but not set"
);
eprintln!("SKIP: {var} not set to 1");
false
}
pub(crate) fn mkit(cwd: &Path, xdg: &Path, args: &[&str]) -> Output {
mkit_env(cwd, xdg, args, &[])
}
pub(crate) fn mkit_env(
cwd: &Path,
xdg: &Path,
args: &[&str],
extra_env: &[(&str, &str)],
) -> Output {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mkit"));
cmd.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg)
.env("HOME", xdg)
.env("EDITOR", "true")
.env("VISUAL", "true")
.env("GIT_EDITOR", "true")
.stdin(Stdio::null());
for (k, v) in extra_env {
cmd.env(k, v);
}
cmd.output().expect("spawn mkit")
}
pub(crate) fn install_fixed_key(root: &Path) -> Result<(), String> {
let keys = root.join(".mkit").join("keys");
std::fs::create_dir_all(&keys).map_err(|e| format!("mkdir keys: {e}"))?;
let kp = KeyPair::from_seed(KEY_SEED);
save_key(&keys.join("default.key"), &kp).map_err(|e| format!("save_key: {e}"))?;
Ok(())
}
pub(crate) const ALLOWED_EXIT: &[i32] = &[0, 1, 64, 65, 66, 69, 73, 75, 76, 77, 78];
pub(crate) fn check_exit(out: &Output, label: &str) -> Result<(), String> {
let stderr = String::from_utf8_lossy(&out.stderr);
match out.status.code() {
Some(c) if ALLOWED_EXIT.contains(&c) => {}
Some(c) => {
return Err(format!(
"[{label}] disallowed exit code {c}; stderr: {stderr}"
));
}
None => return Err(format!("[{label}] killed by signal; stderr: {stderr}")),
}
for marker in ["panicked at", "thread 'main' panicked", "RUST_BACKTRACE"] {
if stderr.contains(marker) {
return Err(format!("[{label}] panic in stderr: {stderr}"));
}
}
Ok(())
}
pub(crate) fn check_store_intact(root: &Path, label: &str) -> Result<(), String> {
let layout = RepoLayout::single(root);
let store = ObjectStore::open(&layout).map_err(|e| format!("[{label}] open store: {e}"))?;
let present = store
.iter_object_hashes()
.map_err(|e| format!("[{label}] enumerate objects: {e}"))?;
for h in &present {
store
.read(h)
.map_err(|e| format!("[{label}] object {} failed integrity: {e}", to_hex(h)))?;
}
refs::read_head(&layout).map_err(|e| format!("[{label}] HEAD malformed: {e}"))?;
read_index(&layout).map_err(|e| format!("[{label}] index unparseable: {e}"))?;
Ok(())
}
pub(crate) fn check_invariants(root: &Path, label: &str) -> Result<(), String> {
check_store_intact(root, label)?;
let layout = RepoLayout::single(root);
let mkit_dir = layout.common_dir().to_path_buf();
let store = ObjectStore::open(&layout).map_err(|e| format!("[{label}] open store: {e}"))?;
let mut roots: Vec<Hash> = Vec::new();
if let Some(h) =
refs::resolve_head(&layout).map_err(|e| format!("[{label}] resolve HEAD: {e}"))?
{
roots.push(h);
}
for r in refs::list_refs(&layout).map_err(|e| format!("[{label}] list heads: {e}"))? {
match r.hash {
Some(h) => roots.push(h),
None => {
return Err(format!(
"[{label}] head ref '{}' has malformed bytes",
r.name
));
}
}
}
for r in refs::list_tags(&layout).map_err(|e| format!("[{label}] list tags: {e}"))? {
match r.hash {
Some(h) => roots.push(h),
None => {
return Err(format!(
"[{label}] tag ref '{}' has malformed bytes",
r.name
));
}
}
}
let mut visited: HashSet<String> = HashSet::new();
let mut work = roots;
while let Some(h) = work.pop() {
if !visited.insert(to_hex(&h)) {
continue;
}
let obj = store
.read_object(&h)
.map_err(|e| format!("[{label}] reachable object {} unreadable: {e}", to_hex(&h)))?;
match obj {
Object::Commit(c) => {
verify_commit(&c)
.map_err(|e| format!("[{label}] commit {} bad signature: {e}", to_hex(&h)))?;
work.push(c.tree_hash);
work.extend(c.parents);
}
Object::Remix(r) => {
verify_remix(&r)
.map_err(|e| format!("[{label}] remix {} bad signature: {e}", to_hex(&h)))?;
work.push(r.tree_hash);
work.extend(r.parents);
}
Object::Tag(t) => {
if t.signature != [0u8; 64] {
verify_tag(&t)
.map_err(|e| format!("[{label}] tag {} bad signature: {e}", to_hex(&h)))?;
}
work.push(t.target);
}
Object::Tree(t) => {
work.extend(t.entries.into_iter().map(|e| e.object_hash));
}
Object::ChunkedBlob(cb) => work.extend(cb.chunks),
Object::Blob(_) | Object::Delta(_) => {}
}
}
let live =
live_objects(&store, &layout).map_err(|e| format!("[{label}] collect gc live-set: {e}"))?;
for h in &live {
store
.read(h)
.map_err(|e| format!("[{label}] live object {} missing/corrupt: {e}", to_hex(h)))?;
}
if let Ok(rd) = std::fs::read_dir(&mkit_dir) {
for ent in rd.flatten() {
let name = ent.file_name();
let name = name.to_string_lossy();
if !name.ends_with(".lock") {
continue;
}
let path = ent.path();
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.map_err(|e| format!("[{label}] open lock sentinel .mkit/{name}: {e}"))?;
match file.try_lock() {
Ok(()) => {
let _ = file.unlock();
}
Err(_) => {
return Err(format!("[{label}] leaked held lock: .mkit/{name}"));
}
}
}
}
Ok(())
}
pub(crate) fn in_progress(mkit_dir: &Path) -> Option<&'static str> {
if mkit_dir.join("rebase-apply").exists() || mkit_dir.join("rebase-merge").exists() {
Some("rebase")
} else if mkit_dir.join("CHERRY_PICK_HEAD").exists() {
Some("cherry-pick")
} else if mkit_dir.join("REVERT_HEAD").exists() {
Some("revert")
} else if mkit_dir.join("MERGE_HEAD").exists() {
Some("merge")
} else {
None
}
}
pub(crate) fn operation_residue(mkit_dir: &Path, verb: &str) -> Option<String> {
let (head, msg) = match verb {
"merge" => ("MERGE_HEAD", "MERGE_MSG"),
"cherry-pick" => ("CHERRY_PICK_HEAD", "CHERRY_PICK_MSG"),
"revert" => ("REVERT_HEAD", "REVERT_MSG"),
"rebase" => {
return (mkit_dir.join("rebase-apply").exists()
|| mkit_dir.join("rebase-merge").exists())
.then(|| "rebase-apply/".to_owned());
}
_ => return None,
};
for residue in [head, "mkit-conflicts", msg] {
if mkit_dir.join(residue).exists() {
return Some(residue.to_owned());
}
}
None
}
pub(crate) struct Repo {
pub dir: tempfile::TempDir,
pub xdg: tempfile::TempDir,
}
impl Repo {
pub(crate) fn new() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
let xdg = tempfile::tempdir().expect("xdg tempdir");
let r = Repo { dir, xdg };
r.ok(&["init"]);
install_fixed_key(r.path()).expect("install key");
r
}
pub(crate) fn path(&self) -> &Path {
self.dir.path()
}
pub(crate) fn xdg(&self) -> &Path {
self.xdg.path()
}
pub(crate) fn mkit_dir(&self) -> PathBuf {
self.path().join(".mkit")
}
pub(crate) fn run(&self, args: &[&str]) -> Output {
self.run_env(args, &[])
}
pub(crate) fn run_env(&self, args: &[&str], extra_env: &[(&str, &str)]) -> Output {
mkit_env(self.path(), self.xdg(), args, extra_env)
}
pub(crate) fn ok(&self, args: &[&str]) -> Output {
self.ok_env(args, &[])
}
pub(crate) fn ok_env(&self, args: &[&str], extra_env: &[(&str, &str)]) -> Output {
let out = self.run_env(args, extra_env);
assert!(
out.status.success(),
"expected `mkit {}` to succeed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
out
}
pub(crate) fn write(&self, rel: &str, body: &[u8]) {
let p = self.path().join(rel);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(p, body).unwrap();
}
pub(crate) fn commit_file(&self, rel: &str, body: &[u8], msg: &str) {
self.write(rel, body);
self.ok(&["add", rel]);
self.ok(&["commit", "-m", msg]);
}
}
impl Default for Repo {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn diverge_on(repo: &Repo, path: &str) -> &'static str {
repo.commit_file(path, b"base\n", "base");
repo.ok(&["branch", "feature"]);
repo.ok(&["checkout", "feature"]);
repo.commit_file(path, b"theirs\n", "theirs");
repo.ok(&["checkout", "main"]);
repo.commit_file(path, b"ours\n", "ours");
"feature"
}
pub(crate) fn conflicted(verb: &str) -> Repo {
let repo = Repo::new();
let feature = diverge_on(&repo, "a.txt");
let args: Vec<&str> = match verb {
"merge" => vec!["merge", feature],
"cherry-pick" => vec!["cherry-pick", feature],
"revert" => {
vec!["revert", feature]
}
"rebase" => vec!["rebase", feature],
other => panic!("unknown verb {other}"),
};
let out = repo.run(&args);
assert!(
!out.status.success(),
"expected `mkit {}` to conflict, but it succeeded",
args.join(" ")
);
assert!(
in_progress(&repo.mkit_dir()) == Some(verb),
"expected {verb} in progress, got {:?}; stderr: {}",
in_progress(&repo.mkit_dir()),
String::from_utf8_lossy(&out.stderr)
);
repo
}