use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct AuthConfig {
providers: HashMap<String, ProviderConfig>,
active_provider: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ProviderConfig {
pub api_key: String,
pub model: Option<String>,
pub endpoint: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CommitConfig {
pub prompt: PromptTemplates,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PromptTemplates {
pub system: String,
pub user: String,
}
impl Default for CommitConfig {
fn default() -> Self {
Self {
prompt: PromptTemplates {
system:
"You are a helpful assistant that generates clear and concise git commit messages.Follow conventional commits format."
.to_string(),
user:
"Generate a concise git commit message for the following changes:\n\n{{diff}}"
.to_string(),
},
exclude: vec![],
}
}
}
impl CommitConfig {
pub fn new(exclude_patterns: Vec<String>) -> Self {
Self {
exclude: exclude_patterns,
..Default::default()
}
}
}
impl AuthConfig {
pub fn load() -> Result<Self> {
let config_path = Self::get_path()?;
if !config_path.exists() {
println!("Auth config file not found, creating default config {}", config_path.display());
let default_config = AuthConfig::default();
default_config.save()?;
return Ok(default_config);
}
let config_str =
std::fs::read_to_string(&config_path).context("Failed to read config file")?;
let config: AuthConfig =
serde_yaml::from_str(&config_str).context("Failed to parse config file")?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = Self::get_path()?;
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)?;
}
let config_str = serde_yaml::to_string(self).context("Failed to serialize config")?;
std::fs::write(&config_path, config_str).context("Failed to write config file")?;
Ok(())
}
pub fn get_path() -> Result<PathBuf> {
if let Ok(config_dir) = std::env::var("FUCKMIT_CONFIG_DIR") {
let mut path = PathBuf::from(config_dir);
path.push("auth.yml");
return Ok(path);
}
let mut path = dirs::config_dir()
.ok_or_else(|| anyhow!("Could not determine config directory"))?;
path.push("fuckmit");
path.push("auth.yml");
Ok(path)
}
pub fn add_provider(&mut self, provider: &str, api_key: &str) -> Result<()> {
let provider_config = ProviderConfig {
api_key: api_key.to_string(),
model: None,
endpoint: None,
};
self.providers.insert(provider.to_string(), provider_config);
if self.active_provider.is_none() {
self.active_provider = Some(provider.to_string());
}
Ok(())
}
pub fn set_active_provider(&mut self, provider: &str) -> Result<bool> {
if self.providers.contains_key(provider) {
self.active_provider = Some(provider.to_string());
Ok(true)
} else {
Ok(false)
}
}
pub fn get_active_provider(&self) -> Result<String> {
self.active_provider.clone()
.ok_or_else(|| anyhow!("No active provider set. Use 'fuckmit auth add <provider> <apiKey>' to add a provider."))
}
pub fn set_provider_property(
&mut self,
provider: &str,
property: &str,
value: &str,
) -> Result<()> {
let provider_config = self
.providers
.get_mut(provider)
.ok_or_else(|| anyhow!("Provider not found"))?;
match property {
"api_key" => provider_config.api_key = value.to_string(),
"model" => provider_config.model = Some(value.to_string()),
"endpoint" => provider_config.endpoint = Some(value.to_string()),
_ => return Err(anyhow!("Invalid property: {}", property)),
}
Ok(())
}
pub fn get_provider_config(&self, provider: &str) -> Result<&ProviderConfig> {
self.providers
.get(provider)
.ok_or_else(|| anyhow!("Provider not found: {}", provider))
}
pub fn get_providers(&self) -> &HashMap<String, ProviderConfig> {
&self.providers
}
pub fn has_providers(&self) -> bool {
!self.providers.is_empty()
}
pub fn get_active_provider_name(&self) -> Option<&str> {
self.active_provider.as_deref()
}
}
pub fn get_config_dir() -> Result<PathBuf> {
if let Ok(config_dir) = std::env::var("FUCKMIT_CONFIG_DIR") {
return Ok(PathBuf::from(config_dir));
}
let mut path = dirs::config_dir()
.ok_or_else(|| anyhow!("Could not determine config directory"))?;
path.push("fuckmit");
Ok(path)
}
pub fn get_commit_config() -> Result<CommitConfig> {
if let Ok(local_config) = load_local_commit_config() {
return Ok(local_config);
}
Ok(CommitConfig::default())
}
fn load_local_commit_config() -> Result<CommitConfig> {
let local_paths = [".fuckmit.yml", ".fuckmit.yaml"];
for path in local_paths.iter() {
let path = Path::new(path);
if path.exists() {
let config_str =
std::fs::read_to_string(path).context("Failed to read local commit config")?;
let config: CommitConfig =
serde_yaml::from_str(&config_str).context("Failed to parse local commit config")?;
return Ok(config);
}
}
if let Ok(config_dir) = get_config_dir() {
let symlink_path = config_dir.join(".fuckmit.yml");
if symlink_path.exists() {
let config_str = std::fs::read_to_string(&symlink_path)
.context("Failed to read active commit config")?;
let config: CommitConfig = serde_yaml::from_str(&config_str)
.context("Failed to parse active commit config")?;
return Ok(config);
}
let default_path = config_dir.join("default.fuckmit.yml");
if default_path.exists() {
let config_str = std::fs::read_to_string(&default_path)
.context("Failed to read default commit config")?;
let config: CommitConfig = serde_yaml::from_str(&config_str)
.context("Failed to parse default commit config")?;
return Ok(config);
}
}
Err(anyhow!("Commit config not found"))
}