Files
QuiCommit/src/config/mod.rs
SidneyZhang 14ebb6857a feat(commit): 添加提交消息模板支持
- 移除 config 命令中未使用的 List 子命令及相关显示字段
- 统一 ChangelogCommand 和 CommitCommand 的 ContentGenerator 初始化方式
2026-06-03 15:20:50 +08:00

423 lines
11 KiB
Rust

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 profile::{GitProfile, ProfileComparison, TokenConfig, TokenType};
/// Application configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
/// Configuration version for migration
#[serde(default = "default_version")]
pub version: String,
/// Default profile name
#[serde(skip_serializing_if = "Option::is_none")]
pub default_profile: Option<String>,
/// All configured profiles
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
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, skip_serializing_if = "HashMap::is_empty")]
pub repo_profiles: HashMap<String, String>,
/// Language settings
#[serde(default)]
pub language: LanguageConfig,
}
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(),
language: LanguageConfig::default(),
}
}
}
/// LLM configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
/// Current LLM provider (ollama, openai, anthropic, kimi, deepseek, openrouter)
#[serde(default = "default_llm_provider")]
pub provider: String,
/// Model to use
#[serde(default = "default_model")]
pub model: String,
/// API base URL (optional, will use provider default if not set)
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
/// 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,
/// API key storage method (keyring, config, environment)
#[serde(default = "default_api_key_storage")]
pub api_key_storage: String,
/// API key (stored in config for fallback)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
/// Enable thinking/reasoning mode (deepseek, kimi, anthropic)
#[serde(default)]
pub thinking_enabled: bool,
/// Budget tokens for thinking mode (Anthropic Claude 4)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget_tokens: Option<u32>,
}
fn default_api_key_storage() -> String {
"keyring".to_string()
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
provider: default_llm_provider(),
model: default_model(),
base_url: None,
max_tokens: default_max_tokens(),
temperature: default_temperature(),
timeout: default_timeout(),
api_key_storage: default_api_key_storage(),
api_key: None,
thinking_enabled: false,
thinking_budget_tokens: None,
}
}
}
/// 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,
}
impl Default for CommitConfig {
fn default() -> Self {
Self {
format: default_commit_format(),
auto_generate: true,
}
}
}
/// 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,
}
impl Default for TagConfig {
fn default() -> Self {
Self {
version_prefix: default_version_prefix(),
auto_generate: true,
}
}
}
/// 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,
}
impl Default for ChangelogConfig {
fn default() -> Self {
Self {
path: default_changelog_path(),
auto_generate: true,
}
}
}
/// Language configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageConfig {
/// Output language for messages (en, zh, etc.)
#[serde(default = "default_output_language")]
pub output_language: String,
/// Keep commit types in English
#[serde(default = "default_true")]
pub keep_types_english: bool,
/// Keep changelog types in English
#[serde(default = "default_true")]
pub keep_changelog_types_english: bool,
}
impl Default for LanguageConfig {
fn default() -> Self {
Self {
output_language: default_output_language(),
keep_types_english: true,
keep_changelog_types_english: true,
}
}
}
/// Supported languages
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Language {
English,
Chinese,
Japanese,
Korean,
Spanish,
French,
German,
}
impl Language {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"en" | "english" => Some(Language::English),
"zh" | "chinese" | "zh-cn" | "zh-tw" => Some(Language::Chinese),
"ja" | "japanese" => Some(Language::Japanese),
"ko" | "korean" => Some(Language::Korean),
"es" | "spanish" => Some(Language::Spanish),
"fr" | "french" => Some(Language::French),
"de" | "german" => Some(Language::German),
_ => None,
}
}
pub fn to_code(&self) -> &str {
match self {
Language::English => "en",
Language::Chinese => "zh",
Language::Japanese => "ja",
Language::Korean => "ko",
Language::Spanish => "es",
Language::French => "fr",
Language::German => "de",
}
}
pub fn display_name(&self) -> &str {
match self {
Language::English => "English",
Language::Chinese => "中文",
Language::Japanese => "日本語",
Language::Korean => "한국어",
Language::Spanish => "Español",
Language::French => "Français",
Language::German => "Deutsch",
}
}
}
// 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_model() -> String {
"llama2".to_string()
}
fn default_max_tokens() -> u32 {
500
}
fn default_temperature() -> f32 {
0.6
}
fn default_timeout() -> u64 {
30
}
fn default_commit_format() -> CommitFormat {
CommitFormat::Conventional
}
fn default_version_prefix() -> String {
"v".to_string()
}
fn default_changelog_path() -> String {
"CHANGELOG.md".to_string()
}
fn default_output_language() -> String {
"en".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"))
}
}
/// Encrypted PAT data for export
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedPat {
/// Profile name
pub profile_name: String,
/// Service name (e.g., github, gitlab)
pub service: String,
/// User email (for keyring lookup)
pub user_email: String,
/// Encrypted token value
pub encrypted_token: String,
}
/// Export data container with optional encrypted PATs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportData {
/// Configuration content (TOML string)
pub config: String,
/// Encrypted PATs (only present when exporting with encryption)
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub encrypted_pats: Vec<EncryptedPat>,
/// Export version for future compatibility
#[serde(default = "default_export_version")]
pub export_version: String,
}
fn default_export_version() -> String {
"1".to_string()
}
impl ExportData {
pub fn new(config: String) -> Self {
Self {
config,
encrypted_pats: Vec::new(),
export_version: default_export_version(),
}
}
pub fn with_encrypted_pats(config: String, pats: Vec<EncryptedPat>) -> Self {
Self {
config,
encrypted_pats: pats,
export_version: default_export_version(),
}
}
pub fn has_encrypted_pats(&self) -> bool {
!self.encrypted_pats.is_empty()
}
}