#![allow(clippy::unwrap_used)]
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use std::time::{Duration, Instant};
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn run(cwd: &Path, xdg: &Path, args: &[&str]) -> Output {
Command::new(mkit_bin())
.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg)
.output()
.expect("spawn mkit")
}
fn ok(cwd: &Path, xdg: &Path, args: &[&str]) -> Output {
let out = run(cwd, xdg, args);
assert!(
out.status.success(),
"expected `mkit {}` to succeed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
out
}
fn fail(cwd: &Path, xdg: &Path, args: &[&str]) -> Output {
let out = run(cwd, xdg, args);
assert!(
!out.status.success(),
"expected `mkit {}` to fail but it succeeded",
args.join(" ")
);
out
}
struct Repo {
dir: tempfile::TempDir,
xdg: tempfile::TempDir,
}
impl Repo {
fn new() -> Self {
let dir = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
ok(dir.path(), xdg.path(), &["init"]);
ok(dir.path(), xdg.path(), &["keygen"]);
Repo { dir, xdg }
}
fn path(&self) -> &Path {
self.dir.path()
}
fn xdg(&self) -> &Path {
self.xdg.path()
}
fn write(&self, rel: &str, body: &[u8]) {
let p = self.path().join(rel);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(p, body).unwrap();
}
fn add(&self, rel: &str) {
ok(self.path(), self.xdg(), &["add", rel]);
}
fn commit(&self, msg: &str) {
ok(self.path(), self.xdg(), &["commit", "-m", msg]);
}
fn commit_file(&self, rel: &str, body: &[u8], msg: &str) {
self.write(rel, body);
self.add(rel);
self.commit(msg);
}
fn mkit_dir(&self) -> std::path::PathBuf {
self.path().join(".mkit")
}
}
#[test]
fn stash_pop_refuses_and_preserves_entry_when_unrelated_file_dirty() {
let repo = Repo::new();
repo.commit_file("tracked.txt", b"v1\n", "base tracked");
repo.commit_file("other.txt", b"orig\n", "base other");
repo.write("tracked.txt", b"stashed-change\n");
repo.add("tracked.txt");
ok(repo.path(), repo.xdg(), &["stash", "-m", "wip"]);
let listed = ok(repo.path(), repo.xdg(), &["stash", "list"]);
assert!(
String::from_utf8_lossy(&listed.stdout).contains("stash@{0}"),
"stash entry must exist after save"
);
repo.write("other.txt", b"precious-uncommitted\n");
let out = fail(repo.path(), repo.xdg(), &["stash", "pop"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("overwrite") || stderr.contains("local changes"),
"expected stash pop to be refused on dirty unrelated file, got: {stderr}"
);
assert_eq!(
fs::read_to_string(repo.path().join("other.txt")).unwrap(),
"precious-uncommitted\n",
"refused pop must not clobber the dirty unrelated file (#205)"
);
let listed = ok(repo.path(), repo.xdg(), &["stash", "list"]);
assert!(
String::from_utf8_lossy(&listed.stdout).contains("stash@{0}"),
"stash entry must survive a refused pop (#205)"
);
}
#[test]
fn stash_pop_succeeds_on_clean_worktree() {
let repo = Repo::new();
repo.commit_file("tracked.txt", b"v1\n", "base");
repo.write("tracked.txt", b"stashed-change\n");
repo.add("tracked.txt");
ok(repo.path(), repo.xdg(), &["stash", "-m", "wip"]);
assert_eq!(
fs::read_to_string(repo.path().join("tracked.txt")).unwrap(),
"v1\n"
);
ok(repo.path(), repo.xdg(), &["stash", "pop"]);
assert_eq!(
fs::read_to_string(repo.path().join("tracked.txt")).unwrap(),
"stashed-change\n",
"pop must restore the stashed content"
);
let listed = ok(repo.path(), repo.xdg(), &["stash", "list"]);
assert!(
!String::from_utf8_lossy(&listed.stdout).contains("stash@{0}"),
"stash entry must be dropped after a successful pop"
);
}
#[test]
#[ignore = "blocks the full ~5s worktree-lock timeout; run via the serial --ignored CI lane"]
fn second_mutating_command_blocks_on_worktree_lock() {
let repo = Repo::new();
repo.commit_file("a.txt", b"a\n", "base");
repo.write("b.txt", b"b\n");
let mkit_dir = repo.mkit_dir();
let held =
mkit_core::repo_lock::acquire_default(&mkit_dir, "worktree.lock").expect("acquire lock");
let start = Instant::now();
let out = run(repo.path(), repo.xdg(), &["add", "b.txt"]);
let elapsed = start.elapsed();
assert!(
!out.status.success(),
"a mutating command must fail while the worktree lock is held"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("repo lock") || stderr.contains("busy"),
"expected a lock-contention error, got: {stderr}"
);
assert!(
elapsed >= Duration::from_secs(1),
"expected the second mutator to block on the lock, waited only {elapsed:?}"
);
drop(held);
ok(repo.path(), repo.xdg(), &["add", "b.txt"]);
}