use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result};
use super::paths::get_index_dir_for_project;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeedCandidate {
pub worktree_root: PathBuf,
pub index_dir: PathBuf,
}
pub fn list_worktree_roots(repo_path: &Path) -> Vec<PathBuf> {
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(repo_path)
.args(["worktree", "list", "--porcelain"]);
for var in [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_COMMON_DIR",
"GIT_PREFIX",
] {
cmd.env_remove(var);
}
let output = match cmd.output() {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let stdout = String::from_utf8_lossy(&output.stdout);
let mut roots = Vec::new();
for line in stdout.lines() {
if let Some(path) = line.strip_prefix("worktree ") {
let raw = PathBuf::from(path.trim());
if let Ok(canon) = std::fs::canonicalize(&raw) {
roots.push(canon);
}
}
}
roots
}
pub fn seed_candidates(project_root: &Path, model: &str) -> Result<Vec<SeedCandidate>> {
let self_canon =
std::fs::canonicalize(project_root).unwrap_or_else(|_| project_root.to_path_buf());
let mut candidates = Vec::new();
for root in list_worktree_roots(project_root) {
if root == self_canon {
continue;
}
let index_dir = get_index_dir_for_project(&root, model)?;
candidates.push(SeedCandidate {
worktree_root: root,
index_dir,
});
}
Ok(candidates)
}
pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst).with_context(|| format!("Failed to create {}", dst.display()))?;
for entry in
std::fs::read_dir(src).with_context(|| format!("Failed to read {}", src.display()))?
{
let entry = entry?;
let file_type = entry.file_type()?;
let from = entry.path();
let to = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir_all(&from, &to)?;
} else {
std::fs::copy(&from, &to).with_context(|| {
format!("Failed to copy {} -> {}", from.display(), to.display())
})?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use tempfile::TempDir;
fn git(args: &[&str], cwd: &Path) {
let mut cmd = Command::new("git");
cmd.args(args)
.current_dir(cwd)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null");
for var in [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_COMMON_DIR",
"GIT_PREFIX",
] {
cmd.env_remove(var);
}
let status = cmd.output().expect("git available");
assert!(
status.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&status.stderr)
);
}
#[test]
fn test_non_git_dir_has_no_candidates() {
let dir = TempDir::new().unwrap();
assert!(list_worktree_roots(dir.path()).is_empty());
assert!(seed_candidates(dir.path(), "m").unwrap().is_empty());
}
#[test]
fn test_single_worktree_lists_self_only() {
let dir = TempDir::new().unwrap();
git(&["init", "-q"], dir.path());
std::fs::write(dir.path().join("a.txt"), "hello").unwrap();
git(&["add", "."], dir.path());
git(&["commit", "-qm", "init"], dir.path());
let roots = list_worktree_roots(dir.path());
assert_eq!(roots.len(), 1);
assert!(seed_candidates(dir.path(), "m").unwrap().is_empty());
}
#[test]
fn test_linked_worktree_sees_main_as_candidate() {
let root = TempDir::new().unwrap();
let main = root.path().join("main");
std::fs::create_dir(&main).unwrap();
git(&["init", "-q", "-b", "main"], &main);
std::fs::write(main.join("a.txt"), "hello").unwrap();
git(&["add", "."], &main);
git(&["commit", "-qm", "init"], &main);
let wt_path = root.path().join("wt");
git(
&[
"worktree",
"add",
"-q",
"-b",
"feature",
wt_path.to_str().unwrap(),
],
&main,
);
let candidates = seed_candidates(&wt_path, "lightonai/model").unwrap();
assert_eq!(
candidates.len(),
1,
"feature worktree should see exactly the main worktree, got {candidates:?}"
);
let main_canon = std::fs::canonicalize(&main).unwrap();
assert_eq!(candidates[0].worktree_root, main_canon);
let expected = get_index_dir_for_project(&main_canon, "lightonai/model").unwrap();
assert_eq!(candidates[0].index_dir, expected);
}
#[test]
fn test_copy_dir_all_roundtrips_nested_files() {
let root = TempDir::new().unwrap();
let src = root.path().join("src");
let nested = src.join("sub");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(src.join("top.bin"), b"top").unwrap();
std::fs::write(nested.join("deep.bin"), b"deep").unwrap();
let dst = root.path().join("dst");
copy_dir_all(&src, &dst).unwrap();
assert_eq!(std::fs::read(dst.join("top.bin")).unwrap(), b"top");
assert_eq!(std::fs::read(dst.join("sub/deep.bin")).unwrap(), b"deep");
}
}