mod common;
use std::path::{Path, PathBuf};
use common::{Repo, mkit};
use mkit_core::layout::RepoLayout;
fn link_worktree(repo: &Repo, name: &str, branch: &str) -> (tempfile::TempDir, PathBuf) {
let host = tempfile::tempdir().expect("linked-tree tempdir");
let tree = host.path().join(name);
let state = repo.mkit_dir().join("worktrees").join(name);
std::fs::create_dir_all(&state).expect("create state dir");
std::fs::write(state.join("commondir"), b"../..\n").expect("write commondir");
std::fs::create_dir_all(&tree).expect("create tree");
std::fs::write(
state.join("mkitdir"),
format!("{}\n", tree.join(".mkit").display()),
)
.expect("write back-pointer");
mkit_core::layout::write_pointer_file(&tree, &state).expect("write pointer");
std::fs::write(state.join("HEAD"), format!("ref: refs/heads/{branch}\n")).expect("seed HEAD");
let out = mkit(&tree, repo.xdg(), &["checkout", "--force", branch]);
assert!(
out.status.success(),
"checkout in linked tree: {}",
String::from_utf8_lossy(&out.stderr)
);
(host, tree)
}
fn object_count(mkit_dir: &Path) -> usize {
fn walk(d: &Path, n: &mut usize) {
for e in std::fs::read_dir(d).expect("read objects dir") {
let p = e.expect("dir entry").path();
if p.is_dir() {
walk(&p, n);
} else {
*n += 1;
}
}
}
let mut n = 0;
walk(&mkit_dir.join("objects"), &mut n);
n
}
#[test]
fn linked_tree_shares_store_and_keeps_state_local() {
let repo = Repo::new();
repo.commit_file("shared.txt", b"from main\n", "seed");
repo.ok(&["branch", "feature"]);
let (_host, tree) = link_worktree(&repo, "wt1", "feature");
let layout = mkit_core::layout::discover(&tree).unwrap();
assert!(!layout.is_single());
assert_eq!(layout.common_dir(), repo.mkit_dir().canonicalize().unwrap());
assert_eq!(
std::fs::read(tree.join("shared.txt")).unwrap(),
b"from main\n"
);
let before = object_count(&repo.mkit_dir());
std::fs::write(tree.join("linked.txt"), b"from linked\n").unwrap();
let out = mkit(&tree, repo.xdg(), &["add", "linked.txt"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let out = mkit(&tree, repo.xdg(), &["commit", "-m", "from linked tree"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
object_count(&repo.mkit_dir()) > before,
"linked-tree commit must write into the shared object store"
);
assert!(
!tree.join(".mkit").is_dir(),
"the linked tree must not grow its own .mkit directory"
);
let out = repo.ok(&["log", "feature"]);
assert!(
String::from_utf8_lossy(&out.stdout).contains("from linked tree"),
"main tree must see the linked tree's commit on the shared ref"
);
assert_eq!(
std::fs::read_to_string(repo.mkit_dir().join("HEAD")).unwrap(),
"ref: refs/heads/main\n"
);
let state = repo.mkit_dir().join("worktrees/wt1");
assert_eq!(
std::fs::read_to_string(state.join("HEAD")).unwrap(),
"ref: refs/heads/feature\n"
);
assert!(state.join("index").is_file());
let out = mkit(&tree, repo.xdg(), &["status", "--porcelain"]);
assert!(out.status.success());
assert_eq!(String::from_utf8_lossy(&out.stdout), "");
let out = repo.ok(&["status", "--porcelain"]);
assert_eq!(String::from_utf8_lossy(&out.stdout), "");
}
#[test]
fn stash_in_linked_tree_is_tree_local() {
let repo = Repo::new();
repo.commit_file("a.txt", b"one\n", "seed");
repo.ok(&["branch", "feature"]);
let (_host, tree) = link_worktree(&repo, "wt1", "feature");
std::fs::write(tree.join("a.txt"), b"dirty\n").unwrap();
let out = mkit(&tree, repo.xdg(), &["stash", "save", "-m", "wip"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
repo.mkit_dir().join("worktrees/wt1/stash").is_file(),
"stash must land in the linked tree's state dir"
);
assert!(
!repo.mkit_dir().join("stash").exists(),
"the main tree's stash must be untouched"
);
let out = repo.ok(&["stash", "list"]);
assert_eq!(
String::from_utf8_lossy(&out.stdout),
"",
"main tree's stash list must be empty"
);
}
#[test]
fn broken_pointer_fails_closed_with_clear_error() {
let repo = Repo::new();
repo.commit_file("a.txt", b"one\n", "seed");
let host = tempfile::tempdir().expect("broken-tree tempdir");
let tree = host.path().join("broken-tree");
std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join(".mkit"), b"gitdir: /somewhere\n").unwrap();
let out = mkit(&tree, repo.xdg(), &["status"]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("worktree discovery"),
"diagnostic must name discovery: {stderr}"
);
std::fs::write(
tree.join(".mkit"),
format!(
"mkitdir: {}\n",
repo.mkit_dir().join("worktrees/gone").display()
),
)
.unwrap();
let out = mkit(&tree, repo.xdg(), &["status"]);
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("worktree discovery") && stderr.contains("pruned"),
"dangling pointer must fail closed with guidance: {stderr}"
);
}
#[test]
fn single_worktree_repos_are_untouched_by_discovery() {
let repo = Repo::new();
repo.commit_file("a.txt", b"one\n", "seed");
let layout = RepoLayout::single(repo.path());
let discovered = mkit_core::layout::discover(repo.path()).unwrap();
assert_eq!(discovered, layout);
assert!(discovered.is_single());
}