use std::path::{Path, PathBuf};
use crate::ops::bisect::BISECT_FILE;
use crate::ops::conflict_state::{
CHERRY_PICK_HEAD, CHERRY_PICK_MSG, CONFLICTS_FILE, MERGE_HEAD, MERGE_MSG, ORIG_HEAD,
RESULT_TREE, REVERT_HEAD, REVERT_MSG,
};
use crate::ops::rebase::REBASE_DIR;
use crate::ops::recovery::RECOVERY_LOG;
use crate::refs::{HEAD_FILE, HEADS_DIR, REFS_DIR, REMOTES_DIR, SHALLOW_FILE, TAGS_DIR};
use crate::store::{FORMAT_FILE, MKIT_DIR, OBJECTS_DIR};
pub const HISTORY_DIR_NAME: &str = "history";
pub const CONFIG_FILE_NAME: &str = "config";
pub const KEYS_DIR_NAME: &str = "keys";
pub const INDEX_FILE_NAME: &str = "index";
pub const STASH_FILE_NAME: &str = "stash";
pub const SPARSE_CHECKOUT_FILE_NAME: &str = "sparse-checkout";
pub const ATTESTATIONS_DIR_NAME: &str = "attestations";
pub const APPLIED_PACKS_DIR_NAME: &str = "applied-packs";
pub const GIT_STATE_DIR_NAME: &str = "git";
pub const SPARSE_CACHE_DIR_NAME: &str = "sparse";
pub const PACK_SHARDS_DIR_NAME: &str = "pack-shards";
pub const WORKTREES_DIR_NAME: &str = "worktrees";
pub const POINTER_PREFIX: &str = "mkitdir: ";
pub const COMMONDIR_FILE_NAME: &str = "commondir";
pub const BACKPOINTER_FILE_NAME: &str = "mkitdir";
pub const MAX_POINTER_FILE_BYTES: u64 = 4096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoLayout {
worktree_root: PathBuf,
common_dir: PathBuf,
worktree_state_dir: PathBuf,
}
impl RepoLayout {
#[must_use]
pub fn single(worktree_root: impl Into<PathBuf>) -> Self {
let worktree_root = worktree_root.into();
let mkit = worktree_root.join(MKIT_DIR);
Self {
worktree_root,
common_dir: mkit.clone(),
worktree_state_dir: mkit,
}
}
#[must_use]
pub fn worktree_root(&self) -> &Path {
&self.worktree_root
}
#[must_use]
pub fn common_dir(&self) -> &Path {
&self.common_dir
}
#[must_use]
pub fn worktree_state_dir(&self) -> &Path {
&self.worktree_state_dir
}
#[must_use]
pub fn is_single(&self) -> bool {
self.common_dir == self.worktree_state_dir
}
#[must_use]
pub fn linked(
worktree_root: impl Into<PathBuf>,
worktree_state_dir: impl Into<PathBuf>,
common_dir: impl Into<PathBuf>,
) -> Self {
Self {
worktree_root: worktree_root.into(),
common_dir: common_dir.into(),
worktree_state_dir: worktree_state_dir.into(),
}
}
#[must_use]
pub fn worktrees_dir(&self) -> PathBuf {
self.common_dir.join(WORKTREES_DIR_NAME)
}
#[must_use]
pub fn worktree_state_dir_for(&self, id: &str) -> PathBuf {
self.worktrees_dir().join(id)
}
#[must_use]
pub fn objects_dir(&self) -> PathBuf {
self.common_dir.join(OBJECTS_DIR)
}
#[must_use]
pub fn format_file(&self) -> PathBuf {
self.common_dir.join(FORMAT_FILE)
}
#[must_use]
pub fn refs_dir(&self) -> PathBuf {
self.common_dir.join(REFS_DIR)
}
#[must_use]
pub fn heads_dir(&self) -> PathBuf {
self.common_dir.join(HEADS_DIR)
}
#[must_use]
pub fn tags_dir(&self) -> PathBuf {
self.common_dir.join(TAGS_DIR)
}
#[must_use]
pub fn remotes_dir(&self) -> PathBuf {
self.common_dir.join(REMOTES_DIR)
}
#[must_use]
pub fn shallow_file(&self) -> PathBuf {
self.common_dir.join(SHALLOW_FILE)
}
#[must_use]
pub fn config_file(&self) -> PathBuf {
self.common_dir.join(CONFIG_FILE_NAME)
}
#[must_use]
pub fn keys_dir(&self) -> PathBuf {
self.common_dir.join(KEYS_DIR_NAME)
}
#[must_use]
pub fn history_dir(&self) -> PathBuf {
self.common_dir.join(HISTORY_DIR_NAME)
}
#[must_use]
pub fn recovery_log_file(&self) -> PathBuf {
self.common_dir.join(RECOVERY_LOG)
}
#[must_use]
pub fn attestations_dir(&self) -> PathBuf {
self.common_dir.join(ATTESTATIONS_DIR_NAME)
}
#[must_use]
pub fn applied_packs_dir(&self) -> PathBuf {
self.common_dir.join(APPLIED_PACKS_DIR_NAME)
}
#[must_use]
pub fn git_state_dir(&self) -> PathBuf {
self.common_dir.join(GIT_STATE_DIR_NAME)
}
#[must_use]
pub fn sparse_cache_dir(&self) -> PathBuf {
self.common_dir.join(SPARSE_CACHE_DIR_NAME)
}
#[must_use]
pub fn pack_shards_dir(&self) -> PathBuf {
self.common_dir.join(PACK_SHARDS_DIR_NAME)
}
#[must_use]
pub fn head_file(&self) -> PathBuf {
self.worktree_state_dir.join(HEAD_FILE)
}
#[must_use]
pub fn index_file(&self) -> PathBuf {
self.worktree_state_dir.join(INDEX_FILE_NAME)
}
#[must_use]
pub fn orig_head_file(&self) -> PathBuf {
self.worktree_state_dir.join(ORIG_HEAD)
}
#[must_use]
pub fn merge_head_file(&self) -> PathBuf {
self.worktree_state_dir.join(MERGE_HEAD)
}
#[must_use]
pub fn merge_msg_file(&self) -> PathBuf {
self.worktree_state_dir.join(MERGE_MSG)
}
#[must_use]
pub fn cherry_pick_head_file(&self) -> PathBuf {
self.worktree_state_dir.join(CHERRY_PICK_HEAD)
}
#[must_use]
pub fn cherry_pick_msg_file(&self) -> PathBuf {
self.worktree_state_dir.join(CHERRY_PICK_MSG)
}
#[must_use]
pub fn revert_head_file(&self) -> PathBuf {
self.worktree_state_dir.join(REVERT_HEAD)
}
#[must_use]
pub fn revert_msg_file(&self) -> PathBuf {
self.worktree_state_dir.join(REVERT_MSG)
}
#[must_use]
pub fn conflicts_file(&self) -> PathBuf {
self.worktree_state_dir.join(CONFLICTS_FILE)
}
#[must_use]
pub fn result_tree_file(&self) -> PathBuf {
self.worktree_state_dir.join(RESULT_TREE)
}
#[must_use]
pub fn rebase_dir(&self) -> PathBuf {
self.worktree_state_dir.join(REBASE_DIR)
}
#[must_use]
pub fn bisect_file(&self) -> PathBuf {
self.worktree_state_dir.join(BISECT_FILE)
}
#[must_use]
pub fn stash_file(&self) -> PathBuf {
self.worktree_state_dir.join(STASH_FILE_NAME)
}
#[must_use]
pub fn sparse_checkout_file(&self) -> PathBuf {
self.worktree_state_dir.join(SPARSE_CHECKOUT_FILE_NAME)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DiscoverError {
#[error("worktree pointer {0}: {1}")]
PointerUnreadable(PathBuf, std::io::Error),
#[error("worktree pointer {0} is malformed: expected a single `{POINTER_PREFIX}<path>` line")]
PointerMalformed(PathBuf),
#[error("worktree pointer {0} exceeds {MAX_POINTER_FILE_BYTES} bytes — refusing to parse")]
PointerTooLarge(PathBuf),
#[error(
"worktree pointer {0} is a symlink — pointer, commondir, and back-pointer files \
must be regular files"
)]
PointerSymlink(PathBuf),
#[error(
"worktree state dir {0} is missing or not a directory — was this worktree pruned? \
run `mkit worktree` maintenance from the main repository"
)]
StateDirMissing(PathBuf),
#[error("worktree commondir file {0}: {1}")]
CommonDirUnreadable(PathBuf, std::io::Error),
#[error("worktree common dir {0} is missing or not a directory")]
CommonDirMissing(PathBuf),
}
#[must_use]
pub fn validate_worktree_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= 255
&& id != "."
&& id != ".."
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
}
fn read_capped_line(path: &Path) -> Result<Option<String>, DiscoverError> {
let meta = match std::fs::symlink_metadata(path) {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(DiscoverError::PointerUnreadable(path.to_path_buf(), e)),
};
if meta.file_type().is_symlink() {
return Err(DiscoverError::PointerSymlink(path.to_path_buf()));
}
if meta.len() > MAX_POINTER_FILE_BYTES {
return Err(DiscoverError::PointerTooLarge(path.to_path_buf()));
}
let raw =
std::fs::read(path).map_err(|e| DiscoverError::PointerUnreadable(path.to_path_buf(), e))?;
let text = std::str::from_utf8(&raw)
.map_err(|_| DiscoverError::PointerMalformed(path.to_path_buf()))?;
let line = text
.strip_suffix('\n')
.map_or(text, |l| l.strip_suffix('\r').unwrap_or(l));
if line.is_empty() || line.contains('\n') {
return Err(DiscoverError::PointerMalformed(path.to_path_buf()));
}
Ok(Some(line.to_owned()))
}
pub fn write_pointer_file(tree_root: &Path, state_dir: &Path) -> std::io::Result<()> {
let body = format!("{POINTER_PREFIX}{}\n", state_dir.display());
crate::atomic::write_atomic(&tree_root.join(MKIT_DIR), body.as_bytes(), false)
}
pub fn discover(worktree_root: &Path) -> Result<RepoLayout, DiscoverError> {
let dot_mkit = worktree_root.join(MKIT_DIR);
let Ok(meta) = std::fs::symlink_metadata(&dot_mkit) else {
return Ok(RepoLayout::single(worktree_root));
};
if meta.is_dir() {
return Ok(RepoLayout::single(worktree_root));
}
let Some(line) = read_capped_line(&dot_mkit)? else {
return Ok(RepoLayout::single(worktree_root));
};
let Some(target) = line.strip_prefix(POINTER_PREFIX) else {
return Err(DiscoverError::PointerMalformed(dot_mkit));
};
let target = Path::new(target);
let state_dir = if target.is_absolute() {
target.to_path_buf()
} else {
worktree_root.join(target)
};
let state_dir = state_dir
.canonicalize()
.map_err(|_| DiscoverError::StateDirMissing(state_dir.clone()))?;
if !state_dir.is_dir() {
return Err(DiscoverError::StateDirMissing(state_dir));
}
let commondir_file = state_dir.join(COMMONDIR_FILE_NAME);
let common_dir = match read_capped_line(&commondir_file) {
Ok(Some(rel)) => {
let p = Path::new(&rel);
if p.is_absolute() {
p.to_path_buf()
} else {
state_dir.join(p)
}
}
Ok(None) => state_dir.join("../.."),
Err(DiscoverError::PointerUnreadable(p, e)) => {
return Err(DiscoverError::CommonDirUnreadable(p, e));
}
Err(e) => return Err(e),
};
let common_dir = common_dir
.canonicalize()
.map_err(|_| DiscoverError::CommonDirMissing(common_dir.clone()))?;
if !common_dir.is_dir() {
return Err(DiscoverError::CommonDirMissing(common_dir));
}
Ok(RepoLayout::linked(worktree_root, state_dir, common_dir))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeEntry {
pub id: String,
pub state_dir: PathBuf,
pub tree_root: Option<PathBuf>,
pub prunable: Option<String>,
}
pub fn worktrees(layout: &RepoLayout) -> Result<Vec<WorktreeEntry>, DiscoverError> {
let dir = layout.worktrees_dir();
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(DiscoverError::PointerUnreadable(dir, e)),
};
let mut out = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| DiscoverError::PointerUnreadable(dir.clone(), e))?;
let id = entry.file_name().to_string_lossy().into_owned();
let state_dir = entry.path();
let mut wt = WorktreeEntry {
id: id.clone(),
state_dir: state_dir.clone(),
tree_root: None,
prunable: None,
};
if !validate_worktree_id(&id) || !state_dir.is_dir() {
wt.prunable = Some("invalid registry entry".to_owned());
out.push(wt);
continue;
}
match read_capped_line(&state_dir.join(BACKPOINTER_FILE_NAME)) {
Ok(Some(back)) => {
let pointer_path = PathBuf::from(back);
let tree_root = pointer_path.parent().map(Path::to_path_buf);
match discover_pointer_target(&pointer_path) {
Some(target) if paths_refer_to_same(&target, &state_dir) => {
wt.tree_root = tree_root;
}
Some(_) => {
wt.tree_root = tree_root;
wt.prunable =
Some("tree's pointer no longer points at this state dir".to_owned());
}
None => {
wt.tree_root = tree_root;
wt.prunable = Some("linked tree is gone".to_owned());
}
}
}
Ok(None) => wt.prunable = Some("back-pointer file missing".to_owned()),
Err(_) => wt.prunable = Some("back-pointer file unreadable".to_owned()),
}
out.push(wt);
}
out.sort_by(|a, b| a.id.cmp(&b.id));
Ok(out)
}
pub fn all_state_layouts(layout: &RepoLayout) -> Result<Vec<RepoLayout>, DiscoverError> {
let mut out = Vec::new();
let main_root = layout
.common_dir()
.parent()
.map_or_else(|| PathBuf::from("/"), Path::to_path_buf);
out.push(RepoLayout::linked(
main_root,
layout.common_dir(),
layout.common_dir(),
));
for wt in worktrees(layout)? {
if !wt.state_dir.is_dir() {
continue;
}
let root = wt.tree_root.clone().unwrap_or_else(|| wt.state_dir.clone());
out.push(RepoLayout::linked(root, wt.state_dir, layout.common_dir()));
}
Ok(out)
}
fn discover_pointer_target(pointer_path: &Path) -> Option<PathBuf> {
let line = read_capped_line(pointer_path).ok().flatten()?;
let target = line.strip_prefix(POINTER_PREFIX)?;
let target = Path::new(target);
if target.is_absolute() {
Some(target.to_path_buf())
} else {
Some(pointer_path.parent()?.join(target))
}
}
fn paths_refer_to_same(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(ca), Ok(cb)) => ca == cb,
_ => a == b,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn accessor_table(l: &RepoLayout) -> Vec<(&'static str, PathBuf, &'static str, Class)> {
use Class::{Common, Worktree};
vec![
("objects_dir", l.objects_dir(), "objects", Common),
("format_file", l.format_file(), "format", Common),
("refs_dir", l.refs_dir(), "refs", Common),
("heads_dir", l.heads_dir(), "refs/heads", Common),
("tags_dir", l.tags_dir(), "refs/tags", Common),
("remotes_dir", l.remotes_dir(), "refs/remotes", Common),
("shallow_file", l.shallow_file(), "shallow", Common),
("config_file", l.config_file(), "config", Common),
("keys_dir", l.keys_dir(), "keys", Common),
("history_dir", l.history_dir(), "history", Common),
(
"recovery_log_file",
l.recovery_log_file(),
"recovery-log",
Common,
),
(
"attestations_dir",
l.attestations_dir(),
"attestations",
Common,
),
(
"applied_packs_dir",
l.applied_packs_dir(),
"applied-packs",
Common,
),
("git_state_dir", l.git_state_dir(), "git", Common),
("sparse_cache_dir", l.sparse_cache_dir(), "sparse", Common),
(
"pack_shards_dir",
l.pack_shards_dir(),
"pack-shards",
Common,
),
("head_file", l.head_file(), "HEAD", Worktree),
("index_file", l.index_file(), "index", Worktree),
("orig_head_file", l.orig_head_file(), "ORIG_HEAD", Worktree),
(
"merge_head_file",
l.merge_head_file(),
"MERGE_HEAD",
Worktree,
),
("merge_msg_file", l.merge_msg_file(), "MERGE_MSG", Worktree),
(
"cherry_pick_head_file",
l.cherry_pick_head_file(),
"CHERRY_PICK_HEAD",
Worktree,
),
(
"cherry_pick_msg_file",
l.cherry_pick_msg_file(),
"CHERRY_PICK_MSG",
Worktree,
),
(
"revert_head_file",
l.revert_head_file(),
"REVERT_HEAD",
Worktree,
),
(
"revert_msg_file",
l.revert_msg_file(),
"REVERT_MSG",
Worktree,
),
(
"conflicts_file",
l.conflicts_file(),
"mkit-conflicts",
Worktree,
),
(
"result_tree_file",
l.result_tree_file(),
"MKIT_OP_RESULT",
Worktree,
),
("rebase_dir", l.rebase_dir(), "rebase-apply", Worktree),
("bisect_file", l.bisect_file(), "bisect", Worktree),
("stash_file", l.stash_file(), "stash", Worktree),
(
"sparse_checkout_file",
l.sparse_checkout_file(),
"sparse-checkout",
Worktree,
),
]
}
#[derive(PartialEq, Clone, Copy, Debug)]
enum Class {
Common,
Worktree,
}
#[test]
fn single_layout_paths_match_legacy_joins() {
let root = Path::new("/repo");
let l = RepoLayout::single(root);
let legacy_mkit = root.join(MKIT_DIR);
for (name, got, relative, _class) in accessor_table(&l) {
assert_eq!(got, legacy_mkit.join(relative), "accessor {name}");
}
}
#[test]
fn single_layout_dirs_coincide() {
let l = RepoLayout::single("/repo");
assert!(l.is_single());
assert_eq!(l.common_dir(), l.worktree_state_dir());
assert_eq!(l.common_dir(), Path::new("/repo/.mkit"));
assert_eq!(l.worktree_root(), Path::new("/repo"));
}
#[test]
fn accessors_stay_inside_their_class_dir() {
let l = RepoLayout::single("/repo");
for (name, got, _relative, class) in accessor_table(&l) {
let class_dir = match class {
Class::Common => l.common_dir(),
Class::Worktree => l.worktree_state_dir(),
};
assert!(
got.starts_with(class_dir) && got != class_dir,
"accessor {name} must resolve strictly inside {}",
class_dir.display()
);
let suffix = got.strip_prefix(class_dir).unwrap();
assert!(
suffix
.components()
.all(|c| matches!(c, std::path::Component::Normal(_))),
"accessor {name} suffix {} must be plain components",
suffix.display()
);
}
}
#[test]
fn cross_crate_names_are_pinned() {
assert_eq!(CONFIG_FILE_NAME, "config");
assert_eq!(KEYS_DIR_NAME, "keys");
assert_eq!(INDEX_FILE_NAME, "index");
assert_eq!(STASH_FILE_NAME, "stash");
assert_eq!(SPARSE_CHECKOUT_FILE_NAME, "sparse-checkout");
assert_eq!(ATTESTATIONS_DIR_NAME, "attestations");
assert_eq!(APPLIED_PACKS_DIR_NAME, "applied-packs");
assert_eq!(GIT_STATE_DIR_NAME, "git");
assert_eq!(SPARSE_CACHE_DIR_NAME, "sparse");
assert_eq!(PACK_SHARDS_DIR_NAME, "pack-shards");
assert_eq!(
Path::new(crate::index::INDEX_FILE),
Path::new(MKIT_DIR).join(INDEX_FILE_NAME)
);
assert_eq!(
Path::new(crate::ops::stash::STASH_FILE),
Path::new(MKIT_DIR).join(STASH_FILE_NAME)
);
}
#[test]
fn construction_is_pure() {
let l = RepoLayout::single("/definitely/not/a/real/path");
assert_eq!(
l.objects_dir(),
Path::new("/definitely/not/a/real/path/.mkit/objects")
);
}
#[test]
fn linked_layout_splits_accessors_by_class() {
let l = RepoLayout::linked(
"/trees/feature-x",
"/main/.mkit/worktrees/feature-x",
"/main/.mkit",
);
assert!(!l.is_single());
for (name, got, _relative, class) in accessor_table(&l) {
match class {
Class::Common => {
assert!(
got.starts_with(l.common_dir()),
"accessor {name} must live under the common dir"
);
assert!(
!got.starts_with(l.worktree_state_dir()),
"shared accessor {name} leaked into the per-tree state dir"
);
}
Class::Worktree => {
assert!(
got.starts_with(l.worktree_state_dir()),
"per-tree accessor {name} must live under the state dir"
);
}
}
}
assert_eq!(
l.worktree_state_dir_for("other"),
Path::new("/main/.mkit/worktrees/other")
);
}
#[test]
fn worktree_id_grammar() {
for ok in ["feature-x", "a", "wt.1", "A_B-c.d", &"x".repeat(255)] {
assert!(validate_worktree_id(ok), "{ok:?} should be valid");
}
for bad in [
"",
".",
"..",
"a/b",
"a\\b",
"a b",
"a\0b",
"\u{e9}clair",
&"x".repeat(256),
] {
assert!(!validate_worktree_id(bad), "{bad:?} should be rejected");
}
}
fn scaffold_linked(tmp: &Path) -> (PathBuf, PathBuf, PathBuf) {
let main = tmp.join("main");
let tree = tmp.join("tree");
let state = main.join(".mkit/worktrees/tree");
std::fs::create_dir_all(main.join(".mkit/objects")).unwrap();
std::fs::create_dir_all(&state).unwrap();
std::fs::create_dir_all(&tree).unwrap();
write_pointer_file(&tree, &state).unwrap();
(main, tree, state)
}
#[test]
fn discover_dir_and_absent_yield_single() {
let tmp = tempfile::tempdir().unwrap();
let l = discover(tmp.path()).unwrap();
assert!(l.is_single());
std::fs::create_dir_all(tmp.path().join(".mkit")).unwrap();
let l = discover(tmp.path()).unwrap();
assert!(l.is_single());
assert_eq!(l.common_dir(), tmp.path().join(".mkit"));
}
#[test]
fn discover_follows_pointer_to_linked_layout() {
let tmp = tempfile::tempdir().unwrap();
let (main, tree, state) = scaffold_linked(tmp.path());
let l = discover(&tree).unwrap();
assert!(!l.is_single());
assert_eq!(l.worktree_root(), tree.as_path());
assert_eq!(
l.worktree_state_dir(),
state.canonicalize().unwrap().as_path()
);
assert_eq!(
l.common_dir(),
main.join(".mkit").canonicalize().unwrap().as_path()
);
assert_eq!(l.head_file(), state.canonicalize().unwrap().join("HEAD"));
assert!(l.heads_dir().starts_with(l.common_dir()));
}
#[test]
fn discover_honors_explicit_commondir_file() {
let tmp = tempfile::tempdir().unwrap();
let (main, tree, state) = scaffold_linked(tmp.path());
std::fs::write(state.join(COMMONDIR_FILE_NAME), "../..\n").unwrap();
let l = discover(&tree).unwrap();
assert_eq!(
l.common_dir(),
main.join(".mkit").canonicalize().unwrap().as_path()
);
std::fs::write(
state.join(COMMONDIR_FILE_NAME),
format!("{}\n", main.join(".mkit").display()),
)
.unwrap();
let l = discover(&tree).unwrap();
assert_eq!(
l.common_dir(),
main.join(".mkit").canonicalize().unwrap().as_path()
);
}
#[test]
fn discover_accepts_relative_pointer_target() {
let tmp = tempfile::tempdir().unwrap();
let (_main, tree, state) = scaffold_linked(tmp.path());
std::fs::write(
tree.join(MKIT_DIR),
"mkitdir: ../main/.mkit/worktrees/tree\n",
)
.unwrap();
let l = discover(&tree).unwrap();
assert_eq!(
l.worktree_state_dir(),
tree.join("../main/.mkit/worktrees/tree")
.canonicalize()
.unwrap()
);
assert!(l.worktree_state_dir().is_dir());
let _ = state;
}
#[test]
fn discover_fails_closed_on_broken_pointers() {
let tmp = tempfile::tempdir().unwrap();
let (_main, tree, state) = scaffold_linked(tmp.path());
let pointer = tree.join(MKIT_DIR);
std::fs::write(&pointer, "gitdir: /somewhere\n").unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::PointerMalformed(_))
));
std::fs::write(&pointer, "").unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::PointerMalformed(_))
));
std::fs::write(&pointer, "mkitdir: /a\nmkitdir: /b\n").unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::PointerMalformed(_))
));
std::fs::write(&pointer, [0x6d, 0x6b, 0xff, 0xfe]).unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::PointerMalformed(_))
));
std::fs::write(
&pointer,
format!(
"mkitdir: /{}\n",
"x".repeat(usize::try_from(MAX_POINTER_FILE_BYTES).unwrap())
),
)
.unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::PointerTooLarge(_))
));
#[cfg(unix)]
{
std::fs::remove_file(&pointer).unwrap();
let huge = tmp.path().join("huge");
std::fs::write(&huge, format!("mkitdir: /{}\n", "x".repeat(8192))).unwrap();
std::os::unix::fs::symlink(&huge, &pointer).unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::PointerSymlink(_))
));
}
write_pointer_file(&tree, &state).unwrap();
std::fs::remove_dir_all(&state).unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::StateDirMissing(_))
));
}
#[test]
fn discover_fails_closed_on_missing_common_dir() {
let tmp = tempfile::tempdir().unwrap();
let (main, tree, state) = scaffold_linked(tmp.path());
std::fs::write(state.join(COMMONDIR_FILE_NAME), "../../nope\n").unwrap();
assert!(matches!(
discover(&tree),
Err(DiscoverError::CommonDirMissing(_))
));
let _ = main;
}
#[test]
fn pointer_file_golden_bytes() {
let tmp = tempfile::tempdir().unwrap();
write_pointer_file(tmp.path(), Path::new("/main/.mkit/worktrees/w1")).unwrap();
let bytes = std::fs::read(tmp.path().join(MKIT_DIR)).unwrap();
assert_eq!(bytes, b"mkitdir: /main/.mkit/worktrees/w1\n");
}
}