use crate::auth::SecureString;
use crate::git::GitSanitizer;
use anyhow::Result;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
const MAX_SUBMODULE_DEPTH: usize = 10;
struct SubmoduleInfo {
name: String,
url: String,
path: PathBuf,
}
pub async fn acquire_submodules(
repo_path: &Path,
token: Option<&SecureString>,
ssh_key: Option<&Path>,
max_depth: usize,
) -> Result<usize> {
acquire_recursive(
repo_path.to_path_buf(),
token.cloned(),
ssh_key.map(|p| p.to_path_buf()),
0,
max_depth.min(MAX_SUBMODULE_DEPTH),
)
.await
}
fn acquire_recursive(
repo_path: PathBuf,
token: Option<SecureString>,
ssh_key: Option<PathBuf>,
current_depth: usize,
max_depth: usize,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<usize>> + Send>> {
Box::pin(async move {
if current_depth >= max_depth {
warn!(
"Maximum submodule depth ({}) reached at {}",
max_depth,
repo_path.display()
);
return Ok(0);
}
let infos = {
let repo = git2::Repository::open(&repo_path)?;
let submodules = repo.submodules()?;
let mut infos = Vec::new();
for submodule in &submodules {
let name = submodule.name().unwrap_or("<unnamed>").to_string();
let url = match submodule.url() {
Some(u) => u.to_string(),
None => {
warn!("Submodule '{}' has no URL, skipping", name);
continue;
}
};
let path = repo_path.join(submodule.path());
infos.push(SubmoduleInfo { name, url, path });
}
infos
};
if infos.is_empty() {
return Ok(0);
}
let mut count = 0;
for info in &infos {
if info.url.starts_with("file://") {
warn!(
"Submodule '{}' uses file:// protocol, skipping for security",
info.name
);
continue;
}
info!(
"Acquiring submodule '{}' from {} (depth {})",
info.name,
info.url,
current_depth + 1
);
let sub_url = info.url.clone();
let sub_path = info.path.clone();
let token_clone = token.clone();
let ssh_key_clone = ssh_key.clone();
let clone_result = tokio::task::spawn_blocking(move || -> Result<(), git2::Error> {
let mut builder = git2::build::RepoBuilder::new();
let callbacks = crate::auth::build_git2_callbacks(
token_clone.as_ref(),
ssh_key_clone.as_deref(),
None,
);
let mut fetch_opts = git2::FetchOptions::new();
fetch_opts.remote_callbacks(callbacks);
builder.fetch_options(fetch_opts);
builder.clone(&sub_url, &sub_path)?;
Ok(())
})
.await?;
match clone_result {
Ok(()) => {
debug!(
"Cloned submodule '{}' to {}",
info.name,
info.path.display()
);
let git_dir = info.path.join(".git");
if git_dir.exists() && git_dir.is_dir() {
let sanitizer = GitSanitizer::new(crate::core::SanitizeConfig::default());
if let Err(e) = sanitizer.sanitize(&git_dir) {
warn!("Failed to sanitize submodule '{}': {}", info.name, e);
}
}
count += 1;
let nested = acquire_recursive(
info.path.clone(),
token.clone(),
ssh_key.clone(),
current_depth + 1,
max_depth,
)
.await?;
count += nested;
}
Err(e) => {
warn!("Failed to clone submodule '{}': {}", info.name, e);
}
}
}
Ok(count)
})
}