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::acquire::download::Downloader;
use crate::acquire::sources::RepoSource;
use crate::acquire::strategy::{AcquireOptions, AcquisitionStrategy};
use crate::archive::{ArchiveValidator, SafeExtractor};
use crate::auth;
use crate::core::{AcquisitionReport, Config, ScanReport};
use crate::git::GitSanitizer;
use anyhow::Result;
use async_trait::async_trait;
use std::path::Path;
use tracing::info;

pub struct ZipWithHistoryStrategy {
    config: Config,
    downloader: Downloader,
}

impl ZipWithHistoryStrategy {
    pub fn new(config: Config) -> Self {
        Self {
            config,
            downloader: Downloader::new(),
        }
    }
}

#[async_trait]
impl AcquisitionStrategy for ZipWithHistoryStrategy {
    async fn acquire(
        &self,
        source: &RepoSource,
        target: &Path,
        opts: &AcquireOptions,
    ) -> Result<AcquisitionReport> {
        // Phase 1: Download and extract ZIP (with auth headers)
        let zip_url = source.zip_url()?;
        let temp_zip = tempfile::Builder::new()
            .prefix("securegit-")
            .suffix(".zip")
            .tempfile()?;

        let headers = if let Some(ref tok) = opts.token {
            let host = source.host().unwrap_or_default();
            auth::build_http_headers(Some(tok), &host)
        } else {
            let host = source.host().unwrap_or_default();
            auth::build_http_headers(None, &host)
        };

        self.downloader
            .download_with_headers(&zip_url, temp_zip.path(), &headers)
            .await?;

        let validator = ArchiveValidator::new(self.config.archive.clone());
        let extractor = SafeExtractor::new(validator);
        let temp_dir = tempfile::tempdir()?;
        extractor
            .extract_safe(temp_zip.path(), temp_dir.path())
            .await?;

        // Phase 2: Clone bare repository (with auth + depth)
        let clone_url = source.clone_url()?;
        let bare_dir = tempfile::tempdir()?;

        // git2 types are not Send, so run the clone in a blocking task
        let bare_path = bare_dir.path().to_path_buf();
        let clone_url_owned = clone_url.clone();
        let token_clone = opts.token.clone();
        let ssh_key_clone = opts.ssh_key.clone();
        let depth = opts.depth;
        tokio::task::spawn_blocking(move || -> Result<()> {
            let mut builder = git2::build::RepoBuilder::new();
            builder.bare(true);

            let callbacks =
                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);

            if let Some(d) = depth {
                fetch_opts.depth(d as i32);
            }

            builder.fetch_options(fetch_opts);
            builder.clone(&clone_url_owned, &bare_path)?;
            Ok(())
        })
        .await??;

        // Phase 3: Sanitize git directory
        let sanitizer = GitSanitizer::new(self.config.sanitization.clone());
        let sanitize_report = sanitizer.sanitize(bare_dir.path())?;

        // Phase 4: Merge - copy ZIP contents, then move bare repo to .git
        std::fs::create_dir_all(target)?;
        copy_dir_all(temp_dir.path(), target)?;

        // Convert bare to regular .git directory (moves/copies bare dir)
        crate::git::convert::convert_bare_to_git_dir(bare_dir.path(), &target.join(".git"))?;

        // Phase 5: Post-acquisition — submodules
        if opts.recurse_submodules {
            info!("Recursively acquiring submodules...");
            let max_depth = self.config.submodules.max_depth;
            match crate::acquire::submodules::acquire_submodules(
                target,
                opts.token.as_ref(),
                opts.ssh_key.as_deref(),
                max_depth,
            )
            .await
            {
                Ok(count) => {
                    if count > 0 {
                        info!("Acquired {} submodule(s)", count);
                    }
                }
                Err(e) => {
                    tracing::warn!("Submodule acquisition failed: {}", e);
                }
            }
        }

        // Phase 6: Post-acquisition — LFS
        if opts.lfs {
            info!("Resolving LFS pointers...");
            let clone_url = source.clone_url()?;
            match crate::lfs::resolve_lfs_pointers(target, &clone_url, opts.token.as_ref(), true)
                .await
            {
                Ok(count) => {
                    if count > 0 {
                        info!("Resolved {} LFS object(s)", count);
                    }
                }
                Err(e) => {
                    tracing::warn!("LFS resolution failed: {}", e);
                }
            }
        }

        Ok(AcquisitionReport {
            target: target.to_path_buf(),
            scan_report: ScanReport::new(),
            sanitize_report,
            has_history: true,
            head_commit: None,
        })
    }
}

fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
    std::fs::create_dir_all(dst)?;
    for entry in std::fs::read_dir(src)? {
        let entry = entry?;
        let ty = entry.file_type()?;
        if ty.is_dir() {
            copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
        } else {
            std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
        }
    }
    Ok(())
}