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 anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ServerPlatform {
    GitHub,
    GitLab,
}

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

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    pub name: String,
    pub platform: ServerPlatform,
    pub api_url: String,
    pub web_url: Option<String>,
    pub push_enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServerRegistry {
    pub servers: Vec<ServerConfig>,
}

impl ServerRegistry {
    /// Load the server registry from `~/.config/securegit/servers.json`.
    pub fn load() -> Result<Self> {
        let path = registry_path()?;
        if !path.exists() {
            return Ok(Self::default());
        }
        let content =
            std::fs::read_to_string(&path).context("Failed to read server registry file")?;
        serde_json::from_str(&content).context("Failed to parse server registry")
    }

    /// Save the registry to disk.
    pub fn save(&self) -> Result<()> {
        let path = registry_path()?;
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).context("Failed to create config directory")?;
        }
        let json = serde_json::to_string_pretty(self)?;
        std::fs::write(&path, json).context("Failed to write server registry")?;
        Ok(())
    }

    /// Add a server. Fails if a server with the same name already exists.
    pub fn add(&mut self, server: ServerConfig) -> Result<()> {
        if self.servers.iter().any(|s| s.name == server.name) {
            bail!("Server '{}' already exists", server.name);
        }
        self.servers.push(server);
        Ok(())
    }

    /// Remove a server by name. Fails if the server doesn't exist.
    pub fn remove(&mut self, name: &str) -> Result<()> {
        let len_before = self.servers.len();
        self.servers.retain(|s| s.name != name);
        if self.servers.len() == len_before {
            bail!("Server '{}' not found", name);
        }
        Ok(())
    }

    /// Get a server config by name.
    pub fn get(&self, name: &str) -> Option<&ServerConfig> {
        self.servers.iter().find(|s| s.name == name)
    }

    /// Return all servers with push_enabled = true.
    pub fn push_targets(&self) -> Vec<&ServerConfig> {
        self.servers.iter().filter(|s| s.push_enabled).collect()
    }
}

fn registry_path() -> Result<PathBuf> {
    let home = dirs::home_dir().context("Could not determine home directory")?;
    Ok(home.join(".config/securegit/servers.json"))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_server(name: &str, platform: ServerPlatform, push: bool) -> ServerConfig {
        ServerConfig {
            name: name.to_string(),
            platform,
            api_url: format!("https://{}.example.com/api", name),
            web_url: None,
            push_enabled: push,
        }
    }

    #[test]
    fn test_add_and_get_server() {
        let mut reg = ServerRegistry::default();
        let server = make_server("test-gh", ServerPlatform::GitHub, true);
        reg.add(server).unwrap();

        let found = reg.get("test-gh");
        assert!(found.is_some());
        assert_eq!(found.unwrap().platform, ServerPlatform::GitHub);
    }

    #[test]
    fn test_add_duplicate_name_fails() {
        let mut reg = ServerRegistry::default();
        reg.add(make_server("dup", ServerPlatform::GitHub, true))
            .unwrap();
        let result = reg.add(make_server("dup", ServerPlatform::GitLab, false));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("already exists"));
    }

    #[test]
    fn test_remove_server() {
        let mut reg = ServerRegistry::default();
        reg.add(make_server("to-remove", ServerPlatform::GitLab, false))
            .unwrap();
        assert!(reg.get("to-remove").is_some());

        reg.remove("to-remove").unwrap();
        assert!(reg.get("to-remove").is_none());
    }

    #[test]
    fn test_remove_nonexistent_fails() {
        let mut reg = ServerRegistry::default();
        let result = reg.remove("ghost");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not found"));
    }

    #[test]
    fn test_push_targets_filters() {
        let mut reg = ServerRegistry::default();
        reg.add(make_server("push-yes", ServerPlatform::GitHub, true))
            .unwrap();
        reg.add(make_server("push-no", ServerPlatform::GitLab, false))
            .unwrap();
        reg.add(make_server("push-also", ServerPlatform::GitLab, true))
            .unwrap();

        let targets = reg.push_targets();
        assert_eq!(targets.len(), 2);
        assert!(targets.iter().any(|s| s.name == "push-yes"));
        assert!(targets.iter().any(|s| s.name == "push-also"));
    }

    #[test]
    fn test_save_and_load_roundtrip() {
        let tmpdir = tempfile::tempdir().unwrap();
        let path = tmpdir.path().join("servers.json");

        let mut reg = ServerRegistry::default();
        reg.add(make_server("roundtrip", ServerPlatform::GitHub, true))
            .unwrap();

        // Save to temp path
        let json = serde_json::to_string_pretty(&reg).unwrap();
        std::fs::write(&path, &json).unwrap();

        // Load from temp path
        let content = std::fs::read_to_string(&path).unwrap();
        let loaded: ServerRegistry = serde_json::from_str(&content).unwrap();
        assert_eq!(loaded.servers.len(), 1);
        assert_eq!(loaded.servers[0].name, "roundtrip");
        assert_eq!(loaded.servers[0].platform, ServerPlatform::GitHub);
    }

    #[test]
    fn test_empty_registry() {
        let reg = ServerRegistry::default();
        assert!(reg.servers.is_empty());
        assert!(reg.push_targets().is_empty());
        assert!(reg.get("anything").is_none());
    }

    #[test]
    fn test_server_platform_display() {
        assert_eq!(ServerPlatform::GitHub.to_string(), "github");
        assert_eq!(ServerPlatform::GitLab.to_string(), "gitlab");
    }
}