#![cfg(unix)]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Command, Output};
use mkit_core::object::{EntryMode, Object};
use mkit_core::refs;
use mkit_core::store::ObjectStore;
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")
}
fn head_tree_mode(&self, name: &str) -> EntryMode {
let store = ObjectStore::open(self.path()).unwrap();
let head = refs::resolve_head(&self.mkit_dir()).unwrap().unwrap();
let Object::Commit(c) = store.read_object(&head).unwrap() else {
panic!("HEAD not a commit");
};
let Object::Tree(t) = store.read_object(&c.tree_hash).unwrap() else {
panic!("tree not a tree");
};
t.entries
.iter()
.find(|e| e.name == name.as_bytes())
.unwrap_or_else(|| panic!("missing entry {name}"))
.mode
}
}
#[test]
fn merge_continue_preserves_ours_exec_bit() {
let repo = Repo::new();
repo.commit_file("script.sh", b"echo base\n", "base");
ok(repo.path(), repo.xdg(), &["branch", "feature"]);
ok(repo.path(), repo.xdg(), &["checkout", "feature"]);
repo.commit_file("script.sh", b"echo theirs\n", "theirs change");
ok(repo.path(), repo.xdg(), &["checkout", "main"]);
repo.write("script.sh", b"echo ours\n");
fs::set_permissions(
repo.path().join("script.sh"),
fs::Permissions::from_mode(0o755),
)
.unwrap();
repo.add("script.sh");
repo.commit("ours exec change");
assert_eq!(repo.head_tree_mode("script.sh"), EntryMode::Executable);
fail(repo.path(), repo.xdg(), &["merge", "feature"]);
repo.write("script.sh", b"echo resolved\n");
fs::set_permissions(
repo.path().join("script.sh"),
fs::Permissions::from_mode(0o755),
)
.unwrap();
repo.add("script.sh");
ok(repo.path(), repo.xdg(), &["merge", "--continue"]);
assert_eq!(
repo.head_tree_mode("script.sh"),
EntryMode::Executable,
"exec bit must survive merge --continue (#214)"
);
assert_eq!(
fs::read(repo.path().join("script.sh")).unwrap(),
b"echo resolved\n",
"the resolved content must be committed, not the stale staged ours (#269)"
);
}
#[test]
fn merge_continue_preserves_symlink_mode() {
let repo = Repo::new();
repo.commit_file("anchor.txt", b"anchor\n", "base");
ok(repo.path(), repo.xdg(), &["branch", "feature"]);
ok(repo.path(), repo.xdg(), &["checkout", "feature"]);
std::os::unix::fs::symlink("theirs-target", repo.path().join("link")).unwrap();
repo.add("link");
repo.commit("theirs symlink");
ok(repo.path(), repo.xdg(), &["checkout", "main"]);
std::os::unix::fs::symlink("ours-target", repo.path().join("link")).unwrap();
repo.add("link");
repo.commit("ours symlink");
assert_eq!(repo.head_tree_mode("link"), EntryMode::Symlink);
fail(repo.path(), repo.xdg(), &["merge", "feature"]);
let meta = fs::symlink_metadata(repo.path().join("link")).unwrap();
assert!(
meta.file_type().is_symlink(),
"conflict symlink must be materialised as a real symlink (#214)"
);
let target = fs::read_link(repo.path().join("link")).unwrap();
assert_eq!(target.to_str().unwrap(), "ours-target");
ok(repo.path(), repo.xdg(), &["merge", "--continue"]);
assert_eq!(
repo.head_tree_mode("link"),
EntryMode::Symlink,
"symlink mode must survive merge --continue (#214)"
);
}