use std::path::Path;
use tracing::debug;
use crate::error::{Error, Result};
use crate::git::cli::GitCli;
pub(crate) fn attach(
git: &dyn GitCli,
worktree_dir: &Path,
path: &str,
name: &str,
mirror: &Path,
private_modules: &Path,
sha: &str,
) -> Result<()> {
let work = worktree_dir.join(path);
if !work.is_dir() {
return Err(Error::operation(format!(
"submodule {path} has no directory to attach"
)));
}
let gitdir = private_modules.join(name);
if let Some(parent) = gitdir.parent() {
std::fs::create_dir_all(parent)?;
}
let scratch = private_modules.join(format!(".wt-attach-{}", sanitize(name)));
let _ = std::fs::remove_dir_all(&scratch);
let result = git.run(
worktree_dir,
&[
"clone",
"--local",
"--no-checkout",
"--separate-git-dir",
&gitdir.to_string_lossy(),
&mirror.to_string_lossy(),
&scratch.to_string_lossy(),
],
);
let _ = std::fs::remove_dir_all(&scratch);
result?;
std::fs::write(work.join(".git"), format!("gitdir: {}\n", gitdir.display()))?;
git.run(&work, &["config", "core.worktree", &work.to_string_lossy()])?;
git.run(&work, &["update-ref", "--no-deref", "HEAD", sha])?;
git.run(&work, &["read-tree", "HEAD"])?;
if let Err(e) = git.run(&work, &["update-index", "--refresh"]) {
debug!(submodule = %path, error = %e, "index refresh found differences");
}
Ok(())
}
fn sanitize(name: &str) -> String {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::cli::RealGit;
use crate::testutil::TestRepo;
#[test]
fn sanitize_flattens_path_separators() {
assert_eq!(sanitize("libs/sub"), "libs-sub");
assert_eq!(sanitize("a.b"), "a-b");
}
#[test]
fn attach_refuses_a_missing_directory() {
let repo = TestRepo::init();
let err = attach(
&RealGit,
repo.root(),
"nope",
"nope",
&repo.root().join(".git"),
&repo.root().join(".git/modules"),
"HEAD",
)
.unwrap_err();
assert!(matches!(err, Error::Operation(_)));
}
#[test]
fn attaches_a_gitdir_to_files_that_are_already_present() {
let repo = TestRepo::init();
repo.add_submodule("libs/sub");
repo.add_worktree("topic", "../wt-attach");
let wt = repo.root().parent().unwrap().join("wt-attach");
let sha = repo
.git(&["-C", &wt.to_string_lossy(), "rev-parse", ":libs/sub"])
.trim()
.to_string();
let work = wt.join("libs/sub");
std::fs::create_dir_all(&work).unwrap();
std::fs::copy(repo.root().join("libs/sub/sub.txt"), work.join("sub.txt")).unwrap();
let private = wt.join(".git");
let private_modules = std::fs::read_to_string(&private)
.ok()
.and_then(|s| s.strip_prefix("gitdir: ").map(|p| p.trim().to_string()))
.map(|p| Path::new(&p).join("modules"))
.expect("linked worktree gitfile");
attach(
&RealGit,
&wt,
"libs/sub",
"libs/sub",
&repo.root().join(".git/modules/libs/sub"),
&private_modules,
&sha,
)
.unwrap();
let status = repo.git(&["-C", &wt.to_string_lossy(), "submodule", "status"]);
assert!(
status.starts_with(' '),
"submodule not in sync after attach: {status:?}"
);
assert!(status.contains(&sha), "wrong commit: {status:?}");
assert_eq!(
std::fs::read_to_string(work.join("sub.txt")).unwrap(),
"submodule\n"
);
let inner = repo.git(&["-C", &work.to_string_lossy(), "status", "--short"]);
assert!(inner.trim().is_empty(), "submodule is dirty: {inner:?}");
}
}