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::cli::args::ReleaseCommands;
use crate::cli::UI;
use crate::core::{Config, ScanEngine, Severity};
use crate::platform::{self, types::CreateRelease};
use anyhow::{bail, Result};
use std::path::PathBuf;

pub async fn execute(action: ReleaseCommands, ui: &UI) -> Result<()> {
    match action {
        ReleaseCommands::Create {
            version,
            name,
            body,
            draft,
            prerelease,
            no_scan,
            fail_on,
            attach,
        } => {
            create(
                CreateReleaseParams {
                    version,
                    name,
                    body,
                    draft,
                    prerelease,
                    no_scan,
                    fail_on,
                    attach,
                },
                ui,
            )
            .await
        }
        ReleaseCommands::List { count } => list(count, ui).await,
    }
}

struct CreateReleaseParams {
    version: String,
    name: Option<String>,
    body: Option<String>,
    draft: bool,
    prerelease: bool,
    no_scan: bool,
    fail_on: String,
    attach: Vec<PathBuf>,
}

async fn create(params: CreateReleaseParams, ui: &UI) -> Result<()> {
    let path = PathBuf::from(".");

    let remote = platform::detect_remote(&path)?;
    let token = platform::resolve_token(&remote.host).ok_or_else(|| {
        anyhow::anyhow!(
            "Not authenticated. Run: securegit auth login --provider {}",
            remote.host.to_string().to_lowercase()
        )
    })?;
    let client = platform::create_client(&remote, token);

    let release_name = params.name.unwrap_or_else(|| params.version.clone());

    ui.header("SecureGit Release");
    ui.blank();
    ui.field("Version", &params.version);
    ui.field("Name", &release_name);
    ui.field(
        "Provider",
        format!("{} ({}/{})", remote.host, remote.owner, remote.repo),
    );
    ui.blank();

    // Security scan
    let mut scan_report_json: Option<Vec<u8>> = None;
    let mut security_summary = String::new();

    if !params.no_scan {
        let threshold = Severity::parse_str(&params.fail_on).unwrap_or(Severity::Critical);
        let spinner = ui.spinner("Running security scan...");
        let config = Config::default();
        let engine = ScanEngine::new(config);
        let report = engine.scan_directory(&path).await?;
        ui.finish_progress(&spinner, "Scan complete");

        let critical = report.count_by_severity(Severity::Critical);
        let high = report.count_by_severity(Severity::High);
        let medium = report.count_by_severity(Severity::Medium);
        let low = report.count_by_severity(Severity::Low);
        let info_count = report.count_by_severity(Severity::Info);

        ui.blank();
        ui.section("Scan Summary");
        ui.severity_row(critical, high, medium, low, info_count);

        if report.has_findings_at_or_above(threshold) {
            let count = report
                .findings
                .iter()
                .filter(|f| f.severity >= threshold)
                .count();
            ui.blank();
            for finding in &report.findings {
                if finding.severity >= threshold {
                    ui.finding(finding);
                }
            }
            ui.blank();
            bail!(
                "Release blocked: {} finding(s) at or above {} severity. Use --no-scan to skip.",
                count,
                threshold
            );
        }

        ui.blank();
        ui.success(format!("Security gate passed (threshold: {})", threshold));

        // Build attestation report
        let commit_sha = crate::ops::open_repo(&path)
            .ok()
            .and_then(|r| {
                let head = r.head().ok()?;
                let commit = head.peel_to_commit().ok()?;
                Some(commit.id().to_string())
            })
            .unwrap_or_default();

        let attestation = serde_json::json!({
            "securegit_version": env!("CARGO_PKG_VERSION"),
            "scan_timestamp": chrono::Utc::now().to_rfc3339(),
            "scanned_files": report.scanned_files,
            "scanned_bytes": report.scanned_bytes,
            "duration_ms": report.duration_ms,
            "findings_summary": {
                "critical": critical,
                "high": high,
                "medium": medium,
                "low": low,
                "info": info_count,
            },
            "findings": report.findings,
            "commit_sha": commit_sha,
        });

        scan_report_json = Some(serde_json::to_vec_pretty(&attestation)?);

        security_summary = format!(
            "\n\n---\n**Security Scan** (by securegit {})\n- Files scanned: {}\n- Critical: {} | High: {} | Medium: {} | Low: {}\n- Scan report attached as release asset",
            env!("CARGO_PKG_VERSION"),
            report.scanned_files,
            critical,
            high,
            medium,
            low
        );
    }

    // Create tag if it doesn't exist
    let repo = crate::ops::open_repo(&path)?;
    let tag_exists = repo
        .revparse_single(&format!("refs/tags/{}", params.version))
        .is_ok();

    if !tag_exists {
        let spinner = ui.spinner(&format!("Creating tag {}...", params.version));
        let head = repo.head()?.peel_to_commit()?;
        repo.tag_lightweight(&params.version, head.as_object(), false)?;
        ui.finish_progress(&spinner, &format!("Tag {} created", params.version));
    }

    // Push tag
    let spinner = ui.spinner("Pushing tag...");
    let push_result = push_tag(&path, &params.version);
    if let Err(e) = push_result {
        ui.finish_progress(&spinner, "");
        ui.warning(format!("Could not auto-push tag: {}", e));
    } else {
        ui.finish_progress(&spinner, "Tag pushed");
    }

    // Create release
    let release_body = format!("{}{}", params.body.unwrap_or_default(), security_summary);

    let spinner = ui.spinner("Creating release...");
    let release = client
        .create_release(&CreateRelease {
            tag_name: params.version.clone(),
            name: release_name,
            body: release_body,
            draft: params.draft,
            prerelease: params.prerelease,
        })
        .await?;
    ui.finish_progress(&spinner, "Release created");

    // Upload scan report as asset
    if let Some(report_data) = scan_report_json {
        let asset_name = format!("securegit-scan-report-{}.json", params.version);
        let spinner = ui.spinner("Uploading scan report...");
        client
            .upload_release_asset(
                &release.upload_url,
                &asset_name,
                "application/json",
                report_data,
            )
            .await?;
        ui.finish_progress(&spinner, "Scan report uploaded");
    }

    // Upload additional assets
    for asset_path in &params.attach {
        if !asset_path.exists() {
            ui.warning(format!("Asset not found: {}", asset_path.display()));
            continue;
        }
        let asset_name = asset_path
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_else(|| "unknown".to_string());
        let data = std::fs::read(asset_path)?;
        let content_type = "application/octet-stream";

        let spinner = ui.spinner(&format!("Uploading {}...", asset_name));
        client
            .upload_release_asset(&release.upload_url, &asset_name, content_type, data)
            .await?;
        ui.finish_progress(&spinner, &format!("{} uploaded", asset_name));
    }

    ui.blank();
    ui.success("Release created");
    ui.blank();
    ui.field("Tag", &release.tag_name);
    ui.field("URL", &release.html_url);
    if params.draft {
        ui.field("Status", "Draft");
    }
    ui.blank();

    if ui.json {
        ui.json_out(&serde_json::to_value(&release).unwrap_or_default());
    }

    Ok(())
}

async fn list(count: usize, ui: &UI) -> Result<()> {
    let path = PathBuf::from(".");
    let remote = platform::detect_remote(&path)?;
    let token = platform::resolve_token(&remote.host)
        .ok_or_else(|| anyhow::anyhow!("Not authenticated. Run: securegit auth login"))?;
    let client = platform::create_client(&remote, token);

    ui.header("SecureGit Releases");
    ui.blank();
    ui.field("Repository", format!("{}/{}", remote.owner, remote.repo));
    ui.blank();

    let spinner = ui.spinner("Fetching releases...");
    let releases = client.list_releases(count).await?;
    ui.finish_progress(&spinner, "");

    if releases.is_empty() {
        ui.info("No releases found");
    } else {
        for release in &releases {
            let pre = if release.prerelease {
                " [pre-release]"
            } else {
                ""
            };
            let d = if release.draft { " [draft]" } else { "" };
            ui.list_item(format!(
                "{:<12} {}{}{}",
                release.tag_name, release.name, pre, d
            ));
        }
    }

    if ui.json {
        ui.json_out(&serde_json::to_value(&releases).unwrap_or_default());
    }

    ui.blank();
    Ok(())
}

fn push_tag(path: &PathBuf, tag: &str) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let mut remote = repo.find_remote("origin").or_else(|_| {
        let remotes = repo.remotes()?;
        let name = remotes
            .get(0)
            .ok_or_else(|| git2::Error::from_str("no remotes"))?;
        repo.find_remote(name)
    })?;

    let callbacks = crate::auth::build_git2_callbacks(None, None, None);
    let mut push_opts = git2::PushOptions::new();
    push_opts.remote_callbacks(callbacks);

    let refspec = format!("refs/tags/{}:refs/tags/{}", tag, tag);
    remote.push(&[&refspec], Some(&mut push_opts))?;
    Ok(())
}