securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::auth::SecureString;
use crate::git::GitSanitizer;
use anyhow::Result;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};

/// Maximum recursion depth for submodules.
const MAX_SUBMODULE_DEPTH: usize = 10;

/// Info extracted from git2 submodules (all owned, Send-safe).
struct SubmoduleInfo {
    name: String,
    url: String,
    path: PathBuf,
}

/// Acquire all submodules for a repository.
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);
        }

        // Extract submodule info synchronously, then drop the repo
        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
        }; // repo dropped here

        if infos.is_empty() {
            return Ok(0);
        }

        let mut count = 0;

        for info in &infos {
            // Security: validate submodule URL
            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
            );

            // Clone the submodule in a blocking task (git2 types are not Send)
            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()
                    );

                    // Sanitize the submodule's .git directory
                    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;

                    // Recursively acquire nested submodules
                    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)
    })
}