feat:(first commit)created repository and complete 0.1.0
This commit is contained in:
302
src/config/manager.rs
Normal file
302
src/config/manager.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
use super::{AppConfig, GitProfile};
|
||||
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,
|
||||
}
|
||||
|
||||
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 = AppConfig::load(path)?;
|
||||
Ok(Self {
|
||||
config,
|
||||
config_path: path.to_path_buf(),
|
||||
modified: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
// Check if it's the default profile
|
||||
if self.config.default_profile.as_ref() == Some(&name.to_string()) {
|
||||
self.config.default_profile = None;
|
||||
}
|
||||
|
||||
// Remove from repo mappings
|
||||
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()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
self.config.llm.provider = provider;
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// Get OpenAI API key
|
||||
pub fn openai_api_key(&self) -> Option<&String> {
|
||||
self.config.llm.openai.api_key.as_ref()
|
||||
}
|
||||
|
||||
/// Set OpenAI API key
|
||||
pub fn set_openai_api_key(&mut self, key: String) {
|
||||
self.config.llm.openai.api_key = Some(key);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
/// Get Anthropic API key
|
||||
pub fn anthropic_api_key(&self) -> Option<&String> {
|
||||
self.config.llm.anthropic.api_key.as_ref()
|
||||
}
|
||||
|
||||
/// Set Anthropic API key
|
||||
pub fn set_anthropic_api_key(&mut self, key: String) {
|
||||
self.config.llm.anthropic.api_key = Some(key);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
532
src/config/mod.rs
Normal file
532
src/config/mod.rs
Normal file
@@ -0,0 +1,532 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub mod manager;
|
||||
pub mod profile;
|
||||
|
||||
pub use manager::ConfigManager;
|
||||
pub use profile::{GitProfile, ProfileSettings};
|
||||
|
||||
/// Application configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
/// Configuration version for migration
|
||||
#[serde(default = "default_version")]
|
||||
pub version: String,
|
||||
|
||||
/// Default profile name
|
||||
pub default_profile: Option<String>,
|
||||
|
||||
/// All configured profiles
|
||||
#[serde(default)]
|
||||
pub profiles: HashMap<String, GitProfile>,
|
||||
|
||||
/// LLM configuration
|
||||
#[serde(default)]
|
||||
pub llm: LlmConfig,
|
||||
|
||||
/// Commit configuration
|
||||
#[serde(default)]
|
||||
pub commit: CommitConfig,
|
||||
|
||||
/// Tag configuration
|
||||
#[serde(default)]
|
||||
pub tag: TagConfig,
|
||||
|
||||
/// Changelog configuration
|
||||
#[serde(default)]
|
||||
pub changelog: ChangelogConfig,
|
||||
|
||||
/// Repository-specific profile mappings
|
||||
#[serde(default)]
|
||||
pub repo_profiles: HashMap<String, String>,
|
||||
|
||||
/// Whether to encrypt sensitive data
|
||||
#[serde(default = "default_true")]
|
||||
pub encrypt_sensitive: bool,
|
||||
|
||||
/// Theme settings
|
||||
#[serde(default)]
|
||||
pub theme: ThemeConfig,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: default_version(),
|
||||
default_profile: None,
|
||||
profiles: HashMap::new(),
|
||||
llm: LlmConfig::default(),
|
||||
commit: CommitConfig::default(),
|
||||
tag: TagConfig::default(),
|
||||
changelog: ChangelogConfig::default(),
|
||||
repo_profiles: HashMap::new(),
|
||||
encrypt_sensitive: true,
|
||||
theme: ThemeConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// LLM configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LlmConfig {
|
||||
/// Default LLM provider
|
||||
#[serde(default = "default_llm_provider")]
|
||||
pub provider: String,
|
||||
|
||||
/// OpenAI configuration
|
||||
#[serde(default)]
|
||||
pub openai: OpenAiConfig,
|
||||
|
||||
/// Ollama configuration
|
||||
#[serde(default)]
|
||||
pub ollama: OllamaConfig,
|
||||
|
||||
/// Anthropic Claude configuration
|
||||
#[serde(default)]
|
||||
pub anthropic: AnthropicConfig,
|
||||
|
||||
/// Custom API configuration
|
||||
#[serde(default)]
|
||||
pub custom: Option<CustomLlmConfig>,
|
||||
|
||||
/// Maximum tokens for generation
|
||||
#[serde(default = "default_max_tokens")]
|
||||
pub max_tokens: u32,
|
||||
|
||||
/// Temperature for generation
|
||||
#[serde(default = "default_temperature")]
|
||||
pub temperature: f32,
|
||||
|
||||
/// Timeout in seconds
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
impl Default for LlmConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: default_llm_provider(),
|
||||
openai: OpenAiConfig::default(),
|
||||
ollama: OllamaConfig::default(),
|
||||
anthropic: AnthropicConfig::default(),
|
||||
custom: None,
|
||||
max_tokens: default_max_tokens(),
|
||||
temperature: default_temperature(),
|
||||
timeout: default_timeout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// OpenAI API configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenAiConfig {
|
||||
/// API key
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Model to use
|
||||
#[serde(default = "default_openai_model")]
|
||||
pub model: String,
|
||||
|
||||
/// API base URL (for custom endpoints)
|
||||
#[serde(default = "default_openai_base_url")]
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl Default for OpenAiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_key: None,
|
||||
model: default_openai_model(),
|
||||
base_url: default_openai_base_url(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ollama configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OllamaConfig {
|
||||
/// Ollama server URL
|
||||
#[serde(default = "default_ollama_url")]
|
||||
pub url: String,
|
||||
|
||||
/// Model to use
|
||||
#[serde(default = "default_ollama_model")]
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl Default for OllamaConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: default_ollama_url(),
|
||||
model: default_ollama_model(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Anthropic Claude configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnthropicConfig {
|
||||
/// API key
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Model to use
|
||||
#[serde(default = "default_anthropic_model")]
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl Default for AnthropicConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_key: None,
|
||||
model: default_anthropic_model(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom LLM API configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomLlmConfig {
|
||||
/// API endpoint URL
|
||||
pub url: String,
|
||||
|
||||
/// API key (optional)
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Model name
|
||||
pub model: String,
|
||||
|
||||
/// Request format template (JSON)
|
||||
pub request_template: String,
|
||||
|
||||
/// Response path to extract content (e.g., "choices.0.message.content")
|
||||
pub response_path: String,
|
||||
}
|
||||
|
||||
/// Commit configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommitConfig {
|
||||
/// Default commit format
|
||||
#[serde(default = "default_commit_format")]
|
||||
pub format: CommitFormat,
|
||||
|
||||
/// Enable AI generation by default
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_generate: bool,
|
||||
|
||||
/// Allow empty commits
|
||||
#[serde(default)]
|
||||
pub allow_empty: bool,
|
||||
|
||||
/// Sign commits with GPG
|
||||
#[serde(default)]
|
||||
pub gpg_sign: bool,
|
||||
|
||||
/// Default scope (optional)
|
||||
pub default_scope: Option<String>,
|
||||
|
||||
/// Maximum subject length
|
||||
#[serde(default = "default_max_subject_length")]
|
||||
pub max_subject_length: usize,
|
||||
|
||||
/// Require scope
|
||||
#[serde(default)]
|
||||
pub require_scope: bool,
|
||||
|
||||
/// Require body for certain types
|
||||
#[serde(default)]
|
||||
pub require_body: bool,
|
||||
|
||||
/// Types that require body
|
||||
#[serde(default = "default_body_required_types")]
|
||||
pub body_required_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for CommitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: default_commit_format(),
|
||||
auto_generate: true,
|
||||
allow_empty: false,
|
||||
gpg_sign: false,
|
||||
default_scope: None,
|
||||
max_subject_length: default_max_subject_length(),
|
||||
require_scope: false,
|
||||
require_body: false,
|
||||
body_required_types: default_body_required_types(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit format
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CommitFormat {
|
||||
Conventional,
|
||||
Commitlint,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CommitFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CommitFormat::Conventional => write!(f, "conventional"),
|
||||
CommitFormat::Commitlint => write!(f, "commitlint"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tag configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TagConfig {
|
||||
/// Default version prefix (e.g., "v")
|
||||
#[serde(default = "default_version_prefix")]
|
||||
pub version_prefix: String,
|
||||
|
||||
/// Enable AI generation for tag messages
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_generate: bool,
|
||||
|
||||
/// Sign tags with GPG
|
||||
#[serde(default)]
|
||||
pub gpg_sign: bool,
|
||||
|
||||
/// Include changelog in annotated tags
|
||||
#[serde(default = "default_true")]
|
||||
pub include_changelog: bool,
|
||||
|
||||
/// Default annotation template
|
||||
#[serde(default)]
|
||||
pub annotation_template: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for TagConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version_prefix: default_version_prefix(),
|
||||
auto_generate: true,
|
||||
gpg_sign: false,
|
||||
include_changelog: true,
|
||||
annotation_template: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changelog configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChangelogConfig {
|
||||
/// Changelog file path
|
||||
#[serde(default = "default_changelog_path")]
|
||||
pub path: String,
|
||||
|
||||
/// Enable AI generation for changelog entries
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_generate: bool,
|
||||
|
||||
/// Changelog format
|
||||
#[serde(default = "default_changelog_format")]
|
||||
pub format: ChangelogFormat,
|
||||
|
||||
/// Include commit hashes
|
||||
#[serde(default)]
|
||||
pub include_hashes: bool,
|
||||
|
||||
/// Include authors
|
||||
#[serde(default)]
|
||||
pub include_authors: bool,
|
||||
|
||||
/// Group by type
|
||||
#[serde(default = "default_true")]
|
||||
pub group_by_type: bool,
|
||||
|
||||
/// Custom categories
|
||||
#[serde(default)]
|
||||
pub custom_categories: Vec<ChangelogCategory>,
|
||||
}
|
||||
|
||||
impl Default for ChangelogConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: default_changelog_path(),
|
||||
auto_generate: true,
|
||||
format: default_changelog_format(),
|
||||
include_hashes: false,
|
||||
include_authors: false,
|
||||
group_by_type: true,
|
||||
custom_categories: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Changelog format
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ChangelogFormat {
|
||||
KeepAChangelog,
|
||||
GitHubReleases,
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Changelog category mapping
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChangelogCategory {
|
||||
/// Category title
|
||||
pub title: String,
|
||||
|
||||
/// Commit types included in this category
|
||||
pub types: Vec<String>,
|
||||
}
|
||||
|
||||
/// Theme configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThemeConfig {
|
||||
/// Enable colors
|
||||
#[serde(default = "default_true")]
|
||||
pub colors: bool,
|
||||
|
||||
/// Enable icons
|
||||
#[serde(default = "default_true")]
|
||||
pub icons: bool,
|
||||
|
||||
/// Preferred date format
|
||||
#[serde(default = "default_date_format")]
|
||||
pub date_format: String,
|
||||
}
|
||||
|
||||
impl Default for ThemeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
colors: true,
|
||||
icons: true,
|
||||
date_format: default_date_format(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default value functions
|
||||
fn default_version() -> String {
|
||||
"1".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_llm_provider() -> String {
|
||||
"ollama".to_string()
|
||||
}
|
||||
|
||||
fn default_max_tokens() -> u32 {
|
||||
500
|
||||
}
|
||||
|
||||
fn default_temperature() -> f32 {
|
||||
0.7
|
||||
}
|
||||
|
||||
fn default_timeout() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_openai_model() -> String {
|
||||
"gpt-4".to_string()
|
||||
}
|
||||
|
||||
fn default_openai_base_url() -> String {
|
||||
"https://api.openai.com/v1".to_string()
|
||||
}
|
||||
|
||||
fn default_ollama_url() -> String {
|
||||
"http://localhost:11434".to_string()
|
||||
}
|
||||
|
||||
fn default_ollama_model() -> String {
|
||||
"llama2".to_string()
|
||||
}
|
||||
|
||||
fn default_anthropic_model() -> String {
|
||||
"claude-3-sonnet-20240229".to_string()
|
||||
}
|
||||
|
||||
fn default_commit_format() -> CommitFormat {
|
||||
CommitFormat::Conventional
|
||||
}
|
||||
|
||||
fn default_max_subject_length() -> usize {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_body_required_types() -> Vec<String> {
|
||||
vec!["feat".to_string(), "fix".to_string()]
|
||||
}
|
||||
|
||||
fn default_version_prefix() -> String {
|
||||
"v".to_string()
|
||||
}
|
||||
|
||||
fn default_changelog_path() -> String {
|
||||
"CHANGELOG.md".to_string()
|
||||
}
|
||||
|
||||
fn default_changelog_format() -> ChangelogFormat {
|
||||
ChangelogFormat::KeepAChangelog
|
||||
}
|
||||
|
||||
fn default_date_format() -> String {
|
||||
"%Y-%m-%d".to_string()
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Load configuration from file
|
||||
pub fn load(path: &Path) -> Result<Self> {
|
||||
if path.exists() {
|
||||
let content = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read config file: {:?}", path))?;
|
||||
let config: AppConfig = toml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse config file: {:?}", path))?;
|
||||
Ok(config)
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Save configuration to file
|
||||
pub fn save(&self, path: &Path) -> Result<()> {
|
||||
let content = toml::to_string_pretty(self)
|
||||
.context("Failed to serialize config")?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create config directory: {:?}", parent))?;
|
||||
}
|
||||
|
||||
fs::write(path, content)
|
||||
.with_context(|| format!("Failed to write config file: {:?}", path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get default config path
|
||||
pub fn default_path() -> Result<PathBuf> {
|
||||
let config_dir = dirs::config_dir()
|
||||
.context("Could not find config directory")?;
|
||||
Ok(config_dir.join("quicommit").join("config.toml"))
|
||||
}
|
||||
|
||||
/// Get profile for a repository
|
||||
pub fn get_profile_for_repo(&self, repo_path: &str) -> Option<&GitProfile> {
|
||||
let profile_name = self.repo_profiles.get(repo_path)?;
|
||||
self.profiles.get(profile_name)
|
||||
}
|
||||
|
||||
/// Set profile for a repository
|
||||
pub fn set_profile_for_repo(&mut self, repo_path: String, profile_name: String) -> Result<()> {
|
||||
if !self.profiles.contains_key(&profile_name) {
|
||||
anyhow::bail!("Profile '{}' does not exist", profile_name);
|
||||
}
|
||||
self.repo_profiles.insert(repo_path, profile_name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
412
src/config/profile.rs
Normal file
412
src/config/profile.rs
Normal file
@@ -0,0 +1,412 @@
|
||||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Git profile containing user identity and authentication settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitProfile {
|
||||
/// Profile display name
|
||||
pub name: String,
|
||||
|
||||
/// Git user name
|
||||
pub user_name: String,
|
||||
|
||||
/// Git user email
|
||||
pub user_email: String,
|
||||
|
||||
/// Profile settings
|
||||
#[serde(default)]
|
||||
pub settings: ProfileSettings,
|
||||
|
||||
/// SSH configuration
|
||||
#[serde(default)]
|
||||
pub ssh: Option<SshConfig>,
|
||||
|
||||
/// GPG configuration
|
||||
#[serde(default)]
|
||||
pub gpg: Option<GpgConfig>,
|
||||
|
||||
/// Signing key (for commit/tag signing)
|
||||
#[serde(default)]
|
||||
pub signing_key: Option<String>,
|
||||
|
||||
/// Profile description
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Is this a work profile
|
||||
#[serde(default)]
|
||||
pub is_work: bool,
|
||||
|
||||
/// Company/Organization name (for work profiles)
|
||||
#[serde(default)]
|
||||
pub organization: Option<String>,
|
||||
}
|
||||
|
||||
impl GitProfile {
|
||||
/// Create a new basic profile
|
||||
pub fn new(name: String, user_name: String, user_email: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
user_name,
|
||||
user_email,
|
||||
settings: ProfileSettings::default(),
|
||||
ssh: None,
|
||||
gpg: None,
|
||||
signing_key: None,
|
||||
description: None,
|
||||
is_work: false,
|
||||
organization: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a builder for fluent API
|
||||
pub fn builder() -> GitProfileBuilder {
|
||||
GitProfileBuilder::default()
|
||||
}
|
||||
|
||||
/// Validate the profile
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.user_name.is_empty() {
|
||||
bail!("User name cannot be empty");
|
||||
}
|
||||
|
||||
if self.user_email.is_empty() {
|
||||
bail!("User email cannot be empty");
|
||||
}
|
||||
|
||||
crate::utils::validators::validate_email(&self.user_email)?;
|
||||
|
||||
if let Some(ref ssh) = self.ssh {
|
||||
ssh.validate()?;
|
||||
}
|
||||
|
||||
if let Some(ref gpg) = self.gpg {
|
||||
gpg.validate()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if profile has SSH configured
|
||||
pub fn has_ssh(&self) -> bool {
|
||||
self.ssh.is_some()
|
||||
}
|
||||
|
||||
/// Check if profile has GPG configured
|
||||
pub fn has_gpg(&self) -> bool {
|
||||
self.gpg.is_some() || self.signing_key.is_some()
|
||||
}
|
||||
|
||||
/// Get signing key (from GPG config or direct)
|
||||
pub fn signing_key(&self) -> Option<&str> {
|
||||
self.signing_key
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.or_else(|| self.gpg.as_ref().map(|g| g.key_id.as_str()))
|
||||
}
|
||||
|
||||
/// Apply this profile to a git repository
|
||||
pub fn apply_to_repo(&self, repo: &git2::Repository) -> Result<()> {
|
||||
let mut config = repo.config()?;
|
||||
|
||||
// Set user info
|
||||
config.set_str("user.name", &self.user_name)?;
|
||||
config.set_str("user.email", &self.user_email)?;
|
||||
|
||||
// Set signing key if available
|
||||
if let Some(key) = self.signing_key() {
|
||||
config.set_str("user.signingkey", key)?;
|
||||
|
||||
if self.settings.auto_sign_commits {
|
||||
config.set_bool("commit.gpgsign", true)?;
|
||||
}
|
||||
|
||||
if self.settings.auto_sign_tags {
|
||||
config.set_bool("tag.gpgsign", true)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Set SSH if configured
|
||||
if let Some(ref ssh) = self.ssh {
|
||||
if let Some(ref key_path) = ssh.private_key_path {
|
||||
config.set_str("core.sshCommand",
|
||||
&format!("ssh -i {}", key_path.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply this profile globally
|
||||
pub fn apply_global(&self) -> Result<()> {
|
||||
let mut config = git2::Config::open_default()?;
|
||||
|
||||
config.set_str("user.name", &self.user_name)?;
|
||||
config.set_str("user.email", &self.user_email)?;
|
||||
|
||||
if let Some(key) = self.signing_key() {
|
||||
config.set_str("user.signingkey", key)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Profile settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProfileSettings {
|
||||
/// Automatically sign commits
|
||||
#[serde(default)]
|
||||
pub auto_sign_commits: bool,
|
||||
|
||||
/// Automatically sign tags
|
||||
#[serde(default)]
|
||||
pub auto_sign_tags: bool,
|
||||
|
||||
/// Default commit format for this profile
|
||||
#[serde(default)]
|
||||
pub default_commit_format: Option<super::CommitFormat>,
|
||||
|
||||
/// Use this profile for specific repositories (path patterns)
|
||||
#[serde(default)]
|
||||
pub repo_patterns: Vec<String>,
|
||||
|
||||
/// Preferred LLM provider for this profile
|
||||
#[serde(default)]
|
||||
pub llm_provider: Option<String>,
|
||||
|
||||
/// Custom commit message template
|
||||
#[serde(default)]
|
||||
pub commit_template: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ProfileSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_sign_commits: false,
|
||||
auto_sign_tags: false,
|
||||
default_commit_format: None,
|
||||
repo_patterns: vec![],
|
||||
llm_provider: None,
|
||||
commit_template: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SSH configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SshConfig {
|
||||
/// SSH private key path
|
||||
pub private_key_path: Option<std::path::PathBuf>,
|
||||
|
||||
/// SSH public key path
|
||||
pub public_key_path: Option<std::path::PathBuf>,
|
||||
|
||||
/// SSH key passphrase (encrypted)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub passphrase: Option<String>,
|
||||
|
||||
/// SSH agent forwarding
|
||||
#[serde(default)]
|
||||
pub agent_forwarding: bool,
|
||||
|
||||
/// Custom SSH command
|
||||
#[serde(default)]
|
||||
pub ssh_command: Option<String>,
|
||||
|
||||
/// Known hosts file
|
||||
#[serde(default)]
|
||||
pub known_hosts_file: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl SshConfig {
|
||||
/// Validate SSH configuration
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if let Some(ref path) = self.private_key_path {
|
||||
if !path.exists() {
|
||||
bail!("SSH private key does not exist: {:?}", path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref path) = self.public_key_path {
|
||||
if !path.exists() {
|
||||
bail!("SSH public key does not exist: {:?}", path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get SSH command for git
|
||||
pub fn git_ssh_command(&self) -> Option<String> {
|
||||
if let Some(ref cmd) = self.ssh_command {
|
||||
Some(cmd.clone())
|
||||
} else if let Some(ref key_path) = self.private_key_path {
|
||||
Some(format!("ssh -i '{}'", key_path.display()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GPG configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GpgConfig {
|
||||
/// GPG key ID
|
||||
pub key_id: String,
|
||||
|
||||
/// GPG executable path
|
||||
#[serde(default = "default_gpg_program")]
|
||||
pub program: String,
|
||||
|
||||
/// GPG home directory
|
||||
#[serde(default)]
|
||||
pub home_dir: Option<std::path::PathBuf>,
|
||||
|
||||
/// Key passphrase (encrypted)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub passphrase: Option<String>,
|
||||
|
||||
/// Use GPG agent
|
||||
#[serde(default = "default_true")]
|
||||
pub use_agent: bool,
|
||||
}
|
||||
|
||||
impl GpgConfig {
|
||||
/// Validate GPG configuration
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
crate::utils::validators::validate_gpg_key_id(&self.key_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get GPG program path
|
||||
pub fn program(&self) -> &str {
|
||||
&self.program
|
||||
}
|
||||
}
|
||||
|
||||
fn default_gpg_program() -> String {
|
||||
"gpg".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Git profile builder
|
||||
#[derive(Default)]
|
||||
pub struct GitProfileBuilder {
|
||||
name: Option<String>,
|
||||
user_name: Option<String>,
|
||||
user_email: Option<String>,
|
||||
settings: ProfileSettings,
|
||||
ssh: Option<SshConfig>,
|
||||
gpg: Option<GpgConfig>,
|
||||
signing_key: Option<String>,
|
||||
description: Option<String>,
|
||||
is_work: bool,
|
||||
organization: Option<String>,
|
||||
}
|
||||
|
||||
impl GitProfileBuilder {
|
||||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user_name(mut self, user_name: impl Into<String>) -> Self {
|
||||
self.user_name = Some(user_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user_email(mut self, user_email: impl Into<String>) -> Self {
|
||||
self.user_email = Some(user_email.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn settings(mut self, settings: ProfileSettings) -> Self {
|
||||
self.settings = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ssh(mut self, ssh: SshConfig) -> Self {
|
||||
self.ssh = Some(ssh);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gpg(mut self, gpg: GpgConfig) -> Self {
|
||||
self.gpg = Some(gpg);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signing_key(mut self, key: impl Into<String>) -> Self {
|
||||
self.signing_key = Some(key.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, desc: impl Into<String>) -> Self {
|
||||
self.description = Some(desc.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn work(mut self, is_work: bool) -> Self {
|
||||
self.is_work = is_work;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn organization(mut self, org: impl Into<String>) -> Self {
|
||||
self.organization = Some(org.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<GitProfile> {
|
||||
let name = self.name.ok_or_else(|| anyhow::anyhow!("Name is required"))?;
|
||||
let user_name = self.user_name.ok_or_else(|| anyhow::anyhow!("User name is required"))?;
|
||||
let user_email = self.user_email.ok_or_else(|| anyhow::anyhow!("User email is required"))?;
|
||||
|
||||
Ok(GitProfile {
|
||||
name,
|
||||
user_name,
|
||||
user_email,
|
||||
settings: self.settings,
|
||||
ssh: self.ssh,
|
||||
gpg: self.gpg,
|
||||
signing_key: self.signing_key,
|
||||
description: self.description,
|
||||
is_work: self.is_work,
|
||||
organization: self.organization,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_profile_builder() {
|
||||
let profile = GitProfile::builder()
|
||||
.name("personal")
|
||||
.user_name("John Doe")
|
||||
.user_email("john@example.com")
|
||||
.description("Personal profile")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profile.name, "personal");
|
||||
assert_eq!(profile.user_name, "John Doe");
|
||||
assert_eq!(profile.user_email, "john@example.com");
|
||||
assert!(profile.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profile_validation() {
|
||||
let profile = GitProfile::new(
|
||||
"test".to_string(),
|
||||
"".to_string(),
|
||||
"invalid-email".to_string(),
|
||||
);
|
||||
|
||||
assert!(profile.validate().is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user