use std::path::{Path, PathBuf};
fn gitdir_pointer(p: &Path) -> Option<PathBuf> {
if p.is_dir() {
return None;
}
let text = std::fs::read_to_string(p).ok()?;
let rest = text.trim().strip_prefix("gitdir:")?;
let target = PathBuf::from(rest.trim());
if target.as_os_str().is_empty() {
return None;
}
if target.is_absolute() {
Some(target)
} else {
p.parent().map(|d| d.join(target))
}
}
pub fn main_checkout_of(start: &Path) -> Option<PathBuf> {
let wt_root = worktree_root_of(start)?;
let gitdir = gitdir_pointer(&wt_root.join(".git"))?;
let commondir_file = gitdir.join("commondir");
let common = match std::fs::read_to_string(&commondir_file) {
Ok(text) => {
let rel = PathBuf::from(text.trim());
if rel.is_absolute() {
rel
} else {
normalize(&gitdir.join(rel))
}
}
Err(_) => gitdir.parent()?.parent()?.to_path_buf(),
};
let root = common.parent()?.to_path_buf();
if root.as_os_str().is_empty() {
None
} else {
Some(root)
}
}
fn worktree_root_of(start: &Path) -> Option<PathBuf> {
let mut dir = Some(start);
while let Some(d) = dir {
if d.join(".git").exists() {
return Some(d.to_path_buf());
}
dir = d.parent();
}
None
}
pub fn worktrees_of(main_path: &Path) -> Vec<PathBuf> {
let Some(main_root) = worktree_root_of(main_path) else {
return Vec::new();
};
if !main_root.join(".git").is_dir() {
return Vec::new();
}
let Ok(relative) = main_path.strip_prefix(&main_root) else {
return Vec::new();
};
let dir = main_root.join(".git").join("worktrees");
let Ok(entries) = std::fs::read_dir(&dir) else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.flatten() {
let gitdir_file = entry.path().join("gitdir");
let Ok(text) = std::fs::read_to_string(&gitdir_file) else {
continue;
};
let dot_git = PathBuf::from(text.trim());
if dot_git.as_os_str().is_empty() {
continue;
}
let Some(root) = dot_git.parent() else {
continue;
};
let mapped = root.join(relative);
if std::fs::metadata(&mapped).is_ok() {
out.push(mapped);
}
}
out.sort();
out.dedup();
out
}
fn normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for c in p.components() {
match c {
std::path::Component::ParentDir => {
out.pop();
}
std::path::Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn repo_with_worktree() -> Option<(tempfile::TempDir, PathBuf, PathBuf)> {
let tmp = tempfile::tempdir().ok()?;
let main = tmp.path().join("main");
std::fs::create_dir_all(&main).ok()?;
let git = |args: &[&str], cwd: &Path| -> bool {
Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init", "-q"], &main) {
return None; }
let _ = git(&["config", "user.email", "t@example.com"], &main);
let _ = git(&["config", "user.name", "t"], &main);
std::fs::write(main.join("f.txt"), "x").ok()?;
let _ = git(&["add", "f.txt"], &main);
let _ = git(&["commit", "-qm", "init"], &main);
let wt = tmp.path().join("wt");
if !git(
&["worktree", "add", "-q", wt.to_str()?, "-b", "feat"],
&main,
) {
return None;
}
Some((tmp, main, wt))
}
#[test]
fn worktree_path_resolves_to_its_main_checkout() {
let Some((_tmp, main, wt)) = repo_with_worktree() else {
eprintln!("skipping: git unavailable or worktree creation failed");
return;
};
let main_c = std::fs::canonicalize(&main).unwrap();
let got = main_checkout_of(&wt).expect("worktree resolves to a main checkout");
assert_eq!(std::fs::canonicalize(&got).unwrap(), main_c);
let deep = wt.join("a").join("b");
std::fs::create_dir_all(&deep).unwrap();
let got = main_checkout_of(&deep.join("c.rs")).expect("nested path resolves");
assert_eq!(std::fs::canonicalize(&got).unwrap(), main_c);
}
#[test]
fn main_checkout_enumerates_its_worktrees() {
let Some((_tmp, main, wt)) = repo_with_worktree() else {
eprintln!("skipping: git unavailable or worktree creation failed");
return;
};
let found = worktrees_of(&main);
assert_eq!(found.len(), 1, "expected exactly one worktree: {found:?}");
assert_eq!(
std::fs::canonicalize(&found[0]).unwrap(),
std::fs::canonicalize(&wt).unwrap()
);
}
#[test]
fn pruned_worktree_is_not_enumerated() {
let Some((_tmp, main, wt)) = repo_with_worktree() else {
eprintln!("skipping: git unavailable or worktree creation failed");
return;
};
std::fs::remove_dir_all(&wt).unwrap();
assert!(
worktrees_of(&main).is_empty(),
"a worktree deleted from disk must not be derived as a grant"
);
}
#[test]
fn plain_checkout_and_non_repo_resolve_to_none() {
let Some((_tmp, main, _wt)) = repo_with_worktree() else {
eprintln!("skipping: git unavailable or worktree creation failed");
return;
};
assert!(main_checkout_of(&main).is_none());
let tmp2 = tempfile::tempdir().unwrap();
assert!(main_checkout_of(tmp2.path()).is_none());
assert!(worktrees_of(tmp2.path()).is_empty());
}
#[test]
fn normalize_resolves_dotdot_without_the_filesystem() {
assert_eq!(
normalize(Path::new("/a/b/.git/worktrees/w/../..")),
PathBuf::from("/a/b/.git")
);
}
}