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::CiCommands;
use crate::cli::UI;
use crate::platform;
use anyhow::Result;
use std::path::PathBuf;

pub async fn execute(action: CiCommands, ui: &UI) -> Result<()> {
    match action {
        CiCommands::Status { branch } => status(branch, ui).await,
        CiCommands::Watch { branch, interval } => watch(branch, interval, ui).await,
    }
}

async fn status(branch: Option<String>, 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);

    let repo = crate::ops::open_repo(&path)?;

    let (branch_name, commit_sha) = resolve_ref(&repo, branch.as_deref())?;

    ui.header("SecureGit CI Status");
    ui.blank();
    ui.field("Branch", &branch_name);
    ui.field("Commit", &commit_sha[..std::cmp::min(7, commit_sha.len())]);
    ui.field(
        "Provider",
        format!("{} ({}/{})", remote.host, remote.owner, remote.repo),
    );
    ui.blank();

    let spinner = ui.spinner("Fetching CI status...");
    let status = client.get_check_runs(&commit_sha).await?;
    ui.finish_progress(&spinner, "");

    if status.check_runs.is_empty() {
        ui.info("No CI checks found for this commit");
        ui.blank();
        return Ok(());
    }

    ui.section("Checks");

    let mut passed = 0u32;
    let mut failed = 0u32;
    let mut pending = 0u32;

    for run in &status.check_runs {
        let (icon, conclusion_str) = match run.status.as_str() {
            "completed" => match run.conclusion.as_deref() {
                Some("success") => {
                    passed += 1;
                    (true, "passed")
                }
                Some("failure") => {
                    failed += 1;
                    (false, "failed")
                }
                Some("cancelled") => {
                    failed += 1;
                    (false, "cancelled")
                }
                Some("skipped") => {
                    passed += 1;
                    (true, "skipped")
                }
                Some(other) => {
                    pending += 1;
                    (false, other)
                }
                None => {
                    pending += 1;
                    (false, "unknown")
                }
            },
            "in_progress" | "running" => {
                pending += 1;
                (false, "running")
            }
            "queued" | "waiting" => {
                pending += 1;
                (false, "queued")
            }
            // GitLab statuses
            "success" => {
                passed += 1;
                (true, "passed")
            }
            "failed" => {
                failed += 1;
                (false, "failed")
            }
            _ => {
                pending += 1;
                (false, &*run.status)
            }
        };

        let duration = format_duration(run.started_at.as_deref(), run.completed_at.as_deref());
        let duration_str = if duration.is_empty() {
            String::new()
        } else {
            format!("  {}", duration)
        };

        ui.status_item(
            icon,
            format!("{:<20} {:<12}{}", run.name, conclusion_str, duration_str),
        );
    }

    let total = passed + failed + pending;
    let overall = if failed > 0 {
        "failing"
    } else if pending > 0 {
        "pending"
    } else {
        "passing"
    };

    ui.blank();
    ui.field(
        "Overall",
        format!("{} ({}/{} complete)", overall, passed + failed, total),
    );
    ui.blank();

    if ui.json {
        ui.json_out(&serde_json::json!({
            "branch": branch_name,
            "commit": commit_sha,
            "state": status.state,
            "total": total,
            "passed": passed,
            "failed": failed,
            "pending": pending,
            "checks": status.check_runs,
        }));
    }

    Ok(())
}

async fn watch(branch: Option<String>, interval: u64, ui: &UI) -> Result<()> {
    let path = PathBuf::from(".");
    let remote = platform::detect_remote(&path)?;

    let repo = crate::ops::open_repo(&path)?;
    let (branch_name, commit_sha) = resolve_ref(&repo, branch.as_deref())?;

    ui.header("SecureGit CI Watch");
    ui.blank();
    ui.field("Branch", &branch_name);
    ui.field("Commit", &commit_sha[..std::cmp::min(7, commit_sha.len())]);
    ui.info(format!("Polling every {}s (Ctrl+C to stop)", interval));
    ui.blank();

    loop {
        let token = platform::resolve_token(&remote.host)
            .ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
        let client = platform::create_client(&remote, token);

        let status = client.get_check_runs(&commit_sha).await?;

        // Clear and redraw
        let term = console::Term::stderr();
        let _ = term.clear_last_lines(status.check_runs.len() + 3);

        let mut all_complete = true;
        for run in &status.check_runs {
            let (icon, label) = match run.status.as_str() {
                "completed" => match run.conclusion.as_deref() {
                    Some("success") => (true, "passed"),
                    Some("failure") => (true, "failed"), // icon false but we'll handle
                    _ => (false, "done"),
                },
                _ => {
                    all_complete = false;
                    (false, "running")
                }
            };
            let is_pass = icon && label == "passed";
            ui.status_item(is_pass, format!("{:<20} {}", run.name, label));
        }

        ui.blank();
        ui.field("State", &status.state);

        if all_complete {
            ui.blank();
            if status.state == "success" {
                ui.success("All checks passed");
            } else {
                ui.error("Some checks failed");
            }
            break;
        }

        tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
    }

    Ok(())
}

fn resolve_ref(repo: &git2::Repository, branch: Option<&str>) -> Result<(String, String)> {
    if let Some(branch_name) = branch {
        // Try to resolve as branch first
        if let Ok(reference) = repo.find_branch(branch_name, git2::BranchType::Local) {
            if let Some(target) = reference.get().target() {
                return Ok((branch_name.to_string(), target.to_string()));
            }
        }
        // Try as a ref
        if let Ok(obj) = repo.revparse_single(branch_name) {
            return Ok((branch_name.to_string(), obj.id().to_string()));
        }
        anyhow::bail!("Could not resolve branch: {}", branch_name);
    }

    // Default: current HEAD
    let head = repo.head()?;
    let branch_name = head.shorthand().unwrap_or("HEAD").to_string();
    let sha = head.peel_to_commit()?.id().to_string();
    Ok((branch_name, sha))
}

fn format_duration(started: Option<&str>, completed: Option<&str>) -> String {
    let start = started.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok());
    let end = completed.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok());

    match (start, end) {
        (Some(s), Some(e)) => {
            let dur = e.signed_duration_since(s);
            let secs = dur.num_seconds();
            if secs < 60 {
                format!("{}s", secs)
            } else {
                format!("{}m {}s", secs / 60, secs % 60)
            }
        }
        (Some(_), None) => "...".to_string(),
        _ => String::new(),
    }
}