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
pub mod github;
pub mod gitlab;
pub mod server_registry;
pub mod types;

use crate::acquire::sources::RepoSource;
use crate::auth::SecureString;
use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use std::path::Path;
use types::*;

/// Information about the remote repository.
#[derive(Debug, Clone)]
pub struct RemoteRepo {
    pub host: PlatformHost,
    pub owner: String,
    pub repo: String,
}

#[derive(Debug, Clone, PartialEq)]
pub enum PlatformHost {
    GitHub,
    GitLab,
}

impl std::fmt::Display for PlatformHost {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PlatformHost::GitHub => write!(f, "GitHub"),
            PlatformHost::GitLab => write!(f, "GitLab"),
        }
    }
}

/// Shared platform operations for GitHub / GitLab.
#[async_trait]
pub trait Platform: Send + Sync {
    fn host(&self) -> PlatformHost;
    async fn create_pull_request(&self, pr: &CreatePR) -> Result<PullRequest>;
    async fn list_pull_requests(&self, state: &str) -> Result<Vec<PullRequest>>;
    async fn get_pull_request(&self, number: u64) -> Result<PullRequest>;
    async fn create_issue(&self, issue: &CreateIssue) -> Result<Issue>;
    async fn search_issues(&self, query: &str) -> Result<Vec<Issue>>;
    async fn add_labels(&self, number: u64, labels: &[String]) -> Result<()>;
    async fn create_release(&self, release: &CreateRelease) -> Result<Release>;
    async fn list_releases(&self, count: usize) -> Result<Vec<Release>>;
    async fn upload_release_asset(
        &self,
        upload_url: &str,
        name: &str,
        content_type: &str,
        data: Vec<u8>,
    ) -> Result<()>;
    async fn get_check_runs(&self, ref_name: &str) -> Result<CombinedStatus>;
    async fn get_authenticated_user(&self) -> Result<String>;
    async fn create_repo(&self, repo: &CreateRepo) -> Result<Repository>;
}

/// Detect platform from the current git repository's remote URL.
pub fn detect_remote(path: &Path) -> Result<RemoteRepo> {
    let repo = git2::Repository::open(path).context("Not in a git repository")?;

    // Try common remote names
    for remote_name in &["origin", "upstream", "gitlab"] {
        if let Ok(remote) = repo.find_remote(remote_name) {
            if let Some(url) = remote.url() {
                if let Ok(info) = parse_remote_url(url) {
                    return Ok(info);
                }
            }
        }
    }

    // Try all remotes
    if let Ok(remotes) = repo.remotes() {
        for name in remotes.iter().flatten() {
            if let Ok(remote) = repo.find_remote(name) {
                if let Some(url) = remote.url() {
                    if let Ok(info) = parse_remote_url(url) {
                        return Ok(info);
                    }
                }
            }
        }
    }

    bail!("Could not detect a GitHub or GitLab remote. Add one with: securegit remote add origin <url>")
}

/// Parse a remote URL into platform info.
fn parse_remote_url(url: &str) -> Result<RemoteRepo> {
    // Try RepoSource parser first (handles https URLs and shorthands)
    if let Ok(source) = RepoSource::parse(url) {
        match source {
            RepoSource::GitHub { owner, repo, .. } => {
                return Ok(RemoteRepo {
                    host: PlatformHost::GitHub,
                    owner,
                    repo,
                });
            }
            RepoSource::GitLab { owner, repo, .. } => {
                return Ok(RemoteRepo {
                    host: PlatformHost::GitLab,
                    owner,
                    repo,
                });
            }
            _ => {}
        }
    }

    // Handle SSH URLs: git@github.com:owner/repo.git
    if url.starts_with("git@github.com:") {
        let path = url
            .trim_start_matches("git@github.com:")
            .trim_end_matches(".git");
        let parts: Vec<&str> = path.split('/').collect();
        if parts.len() == 2 {
            return Ok(RemoteRepo {
                host: PlatformHost::GitHub,
                owner: parts[0].to_string(),
                repo: parts[1].to_string(),
            });
        }
    }

    if url.starts_with("git@gitlab.com:") {
        let path = url
            .trim_start_matches("git@gitlab.com:")
            .trim_end_matches(".git");
        let parts: Vec<&str> = path.split('/').collect();
        if parts.len() == 2 {
            return Ok(RemoteRepo {
                host: PlatformHost::GitLab,
                owner: parts[0].to_string(),
                repo: parts[1].to_string(),
            });
        }
    }

    // Handle generic git URLs with github/gitlab in host
    let cleaned = url.trim_end_matches(".git").trim_end_matches('/');
    if cleaned.contains("github") {
        let parts: Vec<&str> = cleaned.split('/').collect();
        if parts.len() >= 2 {
            let repo = parts[parts.len() - 1].to_string();
            let owner = parts[parts.len() - 2].to_string();
            return Ok(RemoteRepo {
                host: PlatformHost::GitHub,
                owner,
                repo,
            });
        }
    }
    if cleaned.contains("gitlab") {
        let parts: Vec<&str> = cleaned.split('/').collect();
        if parts.len() >= 2 {
            let repo = parts[parts.len() - 1].to_string();
            let owner = parts[parts.len() - 2].to_string();
            return Ok(RemoteRepo {
                host: PlatformHost::GitLab,
                owner,
                repo,
            });
        }
    }

    bail!("Could not parse remote URL as GitHub or GitLab: {}", url)
}

/// Create a platform client for the detected remote.
pub fn create_client(remote: &RemoteRepo, token: SecureString) -> Box<dyn Platform> {
    match remote.host {
        PlatformHost::GitHub => Box::new(github::GitHubClient::new(
            token,
            remote.owner.clone(),
            remote.repo.clone(),
        )),
        PlatformHost::GitLab => Box::new(gitlab::GitLabClient::new(
            token,
            remote.owner.clone(),
            remote.repo.clone(),
        )),
    }
}

/// Create a platform client for a named server config.
pub fn create_client_for_server(
    server: &server_registry::ServerConfig,
    token: SecureString,
    owner: &str,
    repo: &str,
) -> Box<dyn Platform> {
    match server.platform {
        server_registry::ServerPlatform::GitHub => Box::new(github::GitHubClient::new_with_base(
            token,
            owner.to_string(),
            repo.to_string(),
            server.api_url.clone(),
        )),
        server_registry::ServerPlatform::GitLab => Box::new(gitlab::GitLabClient::new_with_base(
            token,
            owner.to_string(),
            repo.to_string(),
            server.api_url.clone(),
        )),
    }
}

/// Resolve authentication for a given platform host.
pub fn resolve_token(host: &PlatformHost) -> Option<SecureString> {
    let hostname = match host {
        PlatformHost::GitHub => "github.com",
        PlatformHost::GitLab => "gitlab.com",
    };
    crate::auth::token_for_host(hostname)
}