526 lines
16 KiB
Rust
526 lines
16 KiB
Rust
use super::{AppConfig, GitProfile, TokenConfig};
|
|
use crate::utils::keyring::{KeyringManager, get_default_base_url, get_default_model, provider_needs_api_key};
|
|
use anyhow::{bail, Context, Result};
|
|
// use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Configuration manager
|
|
pub struct ConfigManager {
|
|
config: AppConfig,
|
|
config_path: PathBuf,
|
|
modified: bool,
|
|
keyring: KeyringManager,
|
|
}
|
|
|
|
impl ConfigManager {
|
|
/// Create new config manager with default path
|
|
pub fn new() -> Result<Self> {
|
|
let config_path = AppConfig::default_path()?;
|
|
Self::with_path(&config_path)
|
|
}
|
|
|
|
/// Create config manager with specific path
|
|
pub fn with_path(path: &Path) -> Result<Self> {
|
|
let config = if path.exists() {
|
|
AppConfig::load(path)?
|
|
} else {
|
|
AppConfig::default()
|
|
};
|
|
Ok(Self {
|
|
config,
|
|
config_path: path.to_path_buf(),
|
|
modified: false,
|
|
keyring: KeyringManager::new(),
|
|
})
|
|
}
|
|
|
|
/// Create config manager with fresh config (ignoring existing)
|
|
pub fn with_path_fresh(path: &Path) -> Result<Self> {
|
|
Ok(Self {
|
|
config: AppConfig::default(),
|
|
config_path: path.to_path_buf(),
|
|
modified: true,
|
|
keyring: KeyringManager::new(),
|
|
})
|
|
}
|
|
|
|
/// Get configuration reference
|
|
pub fn config(&self) -> &AppConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Get mutable configuration reference
|
|
pub fn config_mut(&mut self) -> &mut AppConfig {
|
|
self.modified = true;
|
|
&mut self.config
|
|
}
|
|
|
|
/// Save configuration if modified
|
|
pub fn save(&mut self) -> Result<()> {
|
|
if self.modified {
|
|
self.config.save(&self.config_path)?;
|
|
self.modified = false;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// /// Force save configuration
|
|
// pub fn force_save(&self) -> Result<()> {
|
|
// self.config.save(&self.config_path)
|
|
// }
|
|
|
|
/// Get configuration file path
|
|
pub fn path(&self) -> &Path {
|
|
&self.config_path
|
|
}
|
|
|
|
// Profile management
|
|
|
|
/// Add a new profile
|
|
pub fn add_profile(&mut self, name: String, profile: GitProfile) -> Result<()> {
|
|
if self.config.profiles.contains_key(&name) {
|
|
bail!("Profile '{}' already exists", name);
|
|
}
|
|
self.config.profiles.insert(name, profile);
|
|
self.modified = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a profile
|
|
pub fn remove_profile(&mut self, name: &str) -> Result<()> {
|
|
if !self.config.profiles.contains_key(name) {
|
|
bail!("Profile '{}' does not exist", name);
|
|
}
|
|
|
|
if self.config.default_profile.as_ref() == Some(&name.to_string()) {
|
|
self.config.default_profile = None;
|
|
}
|
|
|
|
self.config.repo_profiles.retain(|_, v| v != name);
|
|
|
|
self.config.profiles.remove(name);
|
|
self.modified = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// Update a profile
|
|
pub fn update_profile(&mut self, name: &str, profile: GitProfile) -> Result<()> {
|
|
if !self.config.profiles.contains_key(name) {
|
|
bail!("Profile '{}' does not exist", name);
|
|
}
|
|
self.config.profiles.insert(name.to_string(), profile);
|
|
self.modified = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get a profile
|
|
pub fn get_profile(&self, name: &str) -> Option<&GitProfile> {
|
|
self.config.profiles.get(name)
|
|
}
|
|
|
|
// /// Get mutable profile
|
|
// pub fn get_profile_mut(&mut self, name: &str) -> Option<&mut GitProfile> {
|
|
// self.modified = true;
|
|
// self.config.profiles.get_mut(name)
|
|
// }
|
|
|
|
/// List all profile names
|
|
pub fn list_profiles(&self) -> Vec<&String> {
|
|
self.config.profiles.keys().collect()
|
|
}
|
|
|
|
/// Check if profile exists
|
|
pub fn has_profile(&self, name: &str) -> bool {
|
|
self.config.profiles.contains_key(name)
|
|
}
|
|
|
|
/// Set default profile
|
|
pub fn set_default_profile(&mut self, name: Option<String>) -> Result<()> {
|
|
if let Some(ref n) = name {
|
|
if !self.config.profiles.contains_key(n) {
|
|
bail!("Profile '{}' does not exist", n);
|
|
}
|
|
}
|
|
self.config.default_profile = name;
|
|
self.modified = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get default profile
|
|
pub fn default_profile(&self) -> Option<&GitProfile> {
|
|
self.config
|
|
.default_profile
|
|
.as_ref()
|
|
.and_then(|name| self.config.profiles.get(name))
|
|
}
|
|
|
|
/// Get default profile name
|
|
pub fn default_profile_name(&self) -> Option<&String> {
|
|
self.config.default_profile.as_ref()
|
|
}
|
|
|
|
/// Record profile usage
|
|
pub fn record_profile_usage(&mut self, name: &str, repo_path: Option<String>) -> Result<()> {
|
|
if let Some(profile) = self.config.profiles.get_mut(name) {
|
|
profile.record_usage(repo_path);
|
|
self.modified = true;
|
|
Ok(())
|
|
} else {
|
|
bail!("Profile '{}' does not exist", name);
|
|
}
|
|
}
|
|
|
|
// /// Get profile usage statistics
|
|
// pub fn get_profile_usage(&self, name: &str) -> Option<&super::UsageStats> {
|
|
// self.config.profiles.get(name).map(|p| &p.usage)
|
|
// }
|
|
|
|
// Token management
|
|
|
|
/// Add a token to a profile
|
|
pub fn add_token_to_profile(&mut self, profile_name: &str, service: String, token: TokenConfig) -> Result<()> {
|
|
if let Some(profile) = self.config.profiles.get_mut(profile_name) {
|
|
profile.add_token(service, token);
|
|
self.modified = true;
|
|
Ok(())
|
|
} else {
|
|
bail!("Profile '{}' does not exist", profile_name);
|
|
}
|
|
}
|
|
|
|
// /// Get a token from a profile
|
|
// pub fn get_token_from_profile(&self, profile_name: &str, service: &str) -> Option<&TokenConfig> {
|
|
// self.config.profiles.get(profile_name)?.get_token(service)
|
|
// }
|
|
|
|
/// Remove a token from a profile
|
|
pub fn remove_token_from_profile(&mut self, profile_name: &str, service: &str) -> Result<()> {
|
|
if let Some(profile) = self.config.profiles.get_mut(profile_name) {
|
|
profile.remove_token(service);
|
|
self.modified = true;
|
|
Ok(())
|
|
} else {
|
|
bail!("Profile '{}' does not exist", profile_name);
|
|
}
|
|
}
|
|
|
|
// /// List all tokens in a profile
|
|
// pub fn list_profile_tokens(&self, profile_name: &str) -> Option<Vec<&String>> {
|
|
// self.config.profiles.get(profile_name).map(|p| p.tokens.keys().collect())
|
|
// }
|
|
|
|
// Repository profile management
|
|
|
|
// /// Get profile for repository
|
|
// pub fn get_repo_profile(&self, repo_path: &str) -> Option<&GitProfile> {
|
|
// self.config
|
|
// .repo_profiles
|
|
// .get(repo_path)
|
|
// .and_then(|name| self.config.profiles.get(name))
|
|
// }
|
|
|
|
/// Set profile for repository
|
|
pub fn set_repo_profile(&mut self, repo_path: String, profile_name: String) -> Result<()> {
|
|
if !self.config.profiles.contains_key(&profile_name) {
|
|
bail!("Profile '{}' does not exist", profile_name);
|
|
}
|
|
self.config.repo_profiles.insert(repo_path, profile_name);
|
|
self.modified = true;
|
|
Ok(())
|
|
}
|
|
|
|
// /// Remove repository profile mapping
|
|
// pub fn remove_repo_profile(&mut self, repo_path: &str) {
|
|
// self.config.repo_profiles.remove(repo_path);
|
|
// self.modified = true;
|
|
// }
|
|
|
|
// /// List repository profile mappings
|
|
// pub fn list_repo_profiles(&self) -> &HashMap<String, String> {
|
|
// &self.config.repo_profiles
|
|
// }
|
|
|
|
// /// Get effective profile for a repository (repo-specific -> default)
|
|
// pub fn get_effective_profile(&self, repo_path: Option<&str>) -> Option<&GitProfile> {
|
|
// if let Some(path) = repo_path {
|
|
// if let Some(profile) = self.get_repo_profile(path) {
|
|
// return Some(profile);
|
|
// }
|
|
// }
|
|
// self.default_profile()
|
|
// }
|
|
|
|
/// Check and compare profile with git configuration
|
|
pub fn check_profile_config(&self, profile_name: &str, repo: &git2::Repository) -> Result<super::ProfileComparison> {
|
|
let profile = self.get_profile(profile_name)
|
|
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
|
|
profile.compare_with_git_config(repo)
|
|
}
|
|
|
|
// LLM configuration
|
|
|
|
/// Get LLM provider
|
|
pub fn llm_provider(&self) -> &str {
|
|
&self.config.llm.provider
|
|
}
|
|
|
|
/// Set LLM provider
|
|
pub fn set_llm_provider(&mut self, provider: String) {
|
|
let default_model = get_default_model(&provider);
|
|
self.config.llm.provider = provider.clone();
|
|
if self.config.llm.model.is_empty() || self.config.llm.model == "llama2" {
|
|
self.config.llm.model = default_model.to_string();
|
|
}
|
|
self.modified = true;
|
|
}
|
|
|
|
/// Get model
|
|
pub fn llm_model(&self) -> &str {
|
|
&self.config.llm.model
|
|
}
|
|
|
|
/// Set model
|
|
pub fn set_llm_model(&mut self, model: String) {
|
|
self.config.llm.model = model;
|
|
self.modified = true;
|
|
}
|
|
|
|
/// Get base URL (returns provider default if not set)
|
|
pub fn llm_base_url(&self) -> String {
|
|
match &self.config.llm.base_url {
|
|
Some(url) => url.clone(),
|
|
None => get_default_base_url(&self.config.llm.provider).to_string(),
|
|
}
|
|
}
|
|
|
|
/// Set base URL
|
|
pub fn set_llm_base_url(&mut self, url: Option<String>) {
|
|
self.config.llm.base_url = url;
|
|
self.modified = true;
|
|
}
|
|
|
|
/// Get API key from configured storage method
|
|
pub fn get_api_key(&self) -> Option<String> {
|
|
// First try environment variables (always checked)
|
|
if let Some(key) = self.keyring.get_api_key(&self.config.llm.provider).unwrap_or(None) {
|
|
return Some(key);
|
|
}
|
|
|
|
// Then try config file if configured
|
|
if self.config.llm.api_key_storage == "config" {
|
|
return self.config.llm.api_key.clone();
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Store API key in configured storage method
|
|
pub fn set_api_key(&self, api_key: &str) -> Result<()> {
|
|
match self.config.llm.api_key_storage.as_str() {
|
|
"keyring" => {
|
|
if !self.keyring.is_available() {
|
|
bail!("Keyring is not available. Set QUICOMMIT_API_KEY environment variable instead or change api_key_storage to 'config'.");
|
|
}
|
|
self.keyring.store_api_key(&self.config.llm.provider, api_key)
|
|
},
|
|
"config" => {
|
|
// We can't modify self.config here since self is immutable
|
|
// This will be handled by the caller updating the config
|
|
Ok(())
|
|
},
|
|
"environment" => {
|
|
bail!("API key storage set to 'environment'. Please set QUICOMMIT_{}_API_KEY environment variable.", self.config.llm.provider.to_uppercase());
|
|
},
|
|
_ => {
|
|
bail!("Invalid API key storage method: {}", self.config.llm.api_key_storage);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Delete API key from configured storage method
|
|
pub fn delete_api_key(&self) -> Result<()> {
|
|
match self.config.llm.api_key_storage.as_str() {
|
|
"keyring" => {
|
|
if self.keyring.is_available() {
|
|
self.keyring.delete_api_key(&self.config.llm.provider)?;
|
|
}
|
|
},
|
|
"config" => {
|
|
// We can't modify self.config here since self is immutable
|
|
// This will be handled by the caller updating the config
|
|
},
|
|
"environment" => {
|
|
// Environment variables are not managed by the app
|
|
},
|
|
_ => {
|
|
bail!("Invalid API key storage method: {}", self.config.llm.api_key_storage);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if API key is configured
|
|
pub fn has_api_key(&self) -> bool {
|
|
if !provider_needs_api_key(&self.config.llm.provider) {
|
|
return true;
|
|
}
|
|
|
|
// Check environment variables
|
|
if self.keyring.get_api_key(&self.config.llm.provider).unwrap_or(None).is_some() {
|
|
return true;
|
|
}
|
|
|
|
// Check config file if configured
|
|
if self.config.llm.api_key_storage == "config" {
|
|
return self.config.llm.api_key.is_some();
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Get keyring manager reference
|
|
pub fn keyring(&self) -> &KeyringManager {
|
|
&self.keyring
|
|
}
|
|
|
|
// /// Configure LLM provider with all settings
|
|
// pub fn configure_llm(&mut self, provider: String, model: Option<String>, base_url: Option<String>, api_key: Option<&str>) -> Result<()> {
|
|
// self.set_llm_provider(provider.clone());
|
|
|
|
// if let Some(m) = model {
|
|
// self.set_llm_model(m);
|
|
// }
|
|
|
|
// self.set_llm_base_url(base_url);
|
|
|
|
// if let Some(key) = api_key {
|
|
// if provider_needs_api_key(&provider) {
|
|
// self.set_api_key(key)?;
|
|
// }
|
|
// }
|
|
|
|
// Ok(())
|
|
// }
|
|
|
|
// Commit configuration
|
|
|
|
// /// Get commit format
|
|
// pub fn commit_format(&self) -> super::CommitFormat {
|
|
// self.config.commit.format
|
|
// }
|
|
|
|
/// Set commit format
|
|
pub fn set_commit_format(&mut self, format: super::CommitFormat) {
|
|
self.config.commit.format = format;
|
|
self.modified = true;
|
|
}
|
|
|
|
// /// Check if auto-generate is enabled
|
|
// pub fn auto_generate_commits(&self) -> bool {
|
|
// self.config.commit.auto_generate
|
|
// }
|
|
|
|
/// Set auto-generate commits
|
|
pub fn set_auto_generate_commits(&mut self, enabled: bool) {
|
|
self.config.commit.auto_generate = enabled;
|
|
self.modified = true;
|
|
}
|
|
|
|
// Tag configuration
|
|
|
|
// /// Get version prefix
|
|
// pub fn version_prefix(&self) -> &str {
|
|
// &self.config.tag.version_prefix
|
|
// }
|
|
|
|
/// Set version prefix
|
|
pub fn set_version_prefix(&mut self, prefix: String) {
|
|
self.config.tag.version_prefix = prefix;
|
|
self.modified = true;
|
|
}
|
|
|
|
// Changelog configuration
|
|
|
|
// /// Get changelog path
|
|
// pub fn changelog_path(&self) -> &str {
|
|
// &self.config.changelog.path
|
|
// }
|
|
|
|
/// Set changelog path
|
|
pub fn set_changelog_path(&mut self, path: String) {
|
|
self.config.changelog.path = path;
|
|
self.modified = true;
|
|
}
|
|
|
|
// Language configuration
|
|
|
|
// /// Get output language
|
|
// pub fn output_language(&self) -> &str {
|
|
// &self.config.language.output_language
|
|
// }
|
|
|
|
/// Set output language
|
|
pub fn set_output_language(&mut self, language: String) {
|
|
self.config.language.output_language = language;
|
|
self.modified = true;
|
|
}
|
|
|
|
/// Get language enum from config
|
|
pub fn get_language(&self) -> Option<super::Language> {
|
|
super::Language::from_str(&self.config.language.output_language)
|
|
}
|
|
|
|
/// Check if commit types should be kept in English
|
|
pub fn keep_types_english(&self) -> bool {
|
|
self.config.language.keep_types_english
|
|
}
|
|
|
|
/// Set keep types English flag
|
|
pub fn set_keep_types_english(&mut self, keep: bool) {
|
|
self.config.language.keep_types_english = keep;
|
|
self.modified = true;
|
|
}
|
|
|
|
/// Check if changelog types should be kept in English
|
|
pub fn keep_changelog_types_english(&self) -> bool {
|
|
self.config.language.keep_changelog_types_english
|
|
}
|
|
|
|
/// Set keep changelog types English flag
|
|
pub fn set_keep_changelog_types_english(&mut self, keep: bool) {
|
|
self.config.language.keep_changelog_types_english = keep;
|
|
self.modified = true;
|
|
}
|
|
|
|
/// Export configuration to TOML string
|
|
pub fn export(&self) -> Result<String> {
|
|
toml::to_string_pretty(&self.config)
|
|
.context("Failed to serialize config")
|
|
}
|
|
|
|
/// Import configuration from TOML string
|
|
pub fn import(&mut self, toml_str: &str) -> Result<()> {
|
|
self.config = toml::from_str(toml_str)
|
|
.context("Failed to parse config")?;
|
|
self.modified = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reset to default configuration
|
|
pub fn reset(&mut self) {
|
|
self.config = AppConfig::default();
|
|
self.modified = true;
|
|
}
|
|
}
|
|
|
|
impl Default for ConfigManager {
|
|
fn default() -> Self {
|
|
Self {
|
|
config: AppConfig::default(),
|
|
config_path: PathBuf::new(),
|
|
modified: false,
|
|
keyring: KeyringManager::new(),
|
|
}
|
|
}
|
|
}
|