gitsem 0.5.3

Semantic search and spatial navigation for Git repositories — map, get, and grep for AI coding agents
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::process::Command;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EmbeddingProviderType {
    OpenAI,
    Gemma,
}

impl std::str::FromStr for EmbeddingProviderType {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "openai" => Ok(EmbeddingProviderType::OpenAI),
            "gemma" => Ok(EmbeddingProviderType::Gemma),
            _ => Err(anyhow::anyhow!(
                "Unknown provider: {}. Valid options: openai, gemma",
                s
            )),
        }
    }
}

impl std::fmt::Display for EmbeddingProviderType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EmbeddingProviderType::OpenAI => write!(f, "openai"),
            EmbeddingProviderType::Gemma => write!(f, "gemma"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingConfig {
    pub provider: EmbeddingProviderType,
    pub openai: OpenAIConfig,
    pub gemma: GemmaConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GemmaConfig {
    pub embedding_dim: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIConfig {
    pub api_key: Option<String>,
    pub model: String,
    pub max_tokens: usize,
}

impl Default for EmbeddingConfig {
    fn default() -> Self {
        Self {
            provider: EmbeddingProviderType::Gemma,
            openai: OpenAIConfig::default(),
            gemma: GemmaConfig::default(),
        }
    }
}

impl Default for GemmaConfig {
    fn default() -> Self {
        Self { embedding_dim: 768 }
    }
}

impl Default for OpenAIConfig {
    fn default() -> Self {
        Self {
            api_key: std::env::var("OPENAI_API_KEY").ok(),
            model: "text-embedding-3-small".to_string(),
            max_tokens: 8000,
        }
    }
}

impl EmbeddingConfig {
    pub fn get_git_config(key: &str) -> Option<String> {
        Command::new("git")
            .args(["config", "--get", key])
            .output()
            .ok()
            .and_then(|output| {
                if output.status.success() {
                    String::from_utf8(output.stdout)
                        .ok()
                        .map(|s| s.trim().to_string())
                } else {
                    None
                }
            })
    }

    pub fn set_git_config(key: &str, value: &str) -> Result<()> {
        let status = Command::new("git")
            .args(["config", key, value])
            .status()
            .context("Failed to execute git config")?;

        if !status.success() {
            anyhow::bail!("Failed to set git config {}", key);
        }

        Ok(())
    }

    pub fn unset_git_config(key: &str) -> Result<()> {
        let status = Command::new("git")
            .args(["config", "--unset", key])
            .status()
            .context("Failed to execute git config")?;

        if !status.success() {
            anyhow::bail!("Failed to unset git config {}", key);
        }

        Ok(())
    }

    pub fn load_or_default() -> Result<Self> {
        let provider_str = Self::get_git_config("topology.provider")
            .or_else(|| std::env::var("TOPOLOGY_PROVIDER").ok())
            .unwrap_or_else(|| "gemma".to_string());

        let provider = provider_str.parse()?;

        Ok(Self {
            provider,
            openai: OpenAIConfig::load(),
            gemma: GemmaConfig::load(),
        })
    }

    pub fn show() -> Result<()> {
        let config = Self::load_or_default()?;

        println!("[topology]");
        println!("  provider = {}", config.provider);
        println!();

        println!("[topology.openai]");
        println!("  model = {}", config.openai.model);
        println!("  maxTokens = {}", config.openai.max_tokens);
        if config.openai.api_key.is_some() {
            println!("  apiKey = ***set via OPENAI_API_KEY***");
        }
        println!();

        println!("[topology.gemma]");
        println!("  embeddingDim = {}", config.gemma.embedding_dim);

        Ok(())
    }
}

impl OpenAIConfig {
    fn load() -> Self {
        Self {
            api_key: std::env::var("OPENAI_API_KEY").ok(),
            model: EmbeddingConfig::get_git_config("topology.openai.model")
                .unwrap_or_else(|| "text-embedding-3-small".to_string()),
            max_tokens: EmbeddingConfig::get_git_config("topology.openai.maxTokens")
                .and_then(|s| s.parse().ok())
                .unwrap_or(8000),
        }
    }
}

impl GemmaConfig {
    fn load() -> Self {
        Self {
            embedding_dim: EmbeddingConfig::get_git_config("topology.gemma.embeddingDim")
                .and_then(|s| s.parse().ok())
                .unwrap_or(768),
        }
    }
}