use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq)]
pub enum MsgProvider {
Tool, OpenAi, Anthropic, }
impl Default for MsgProvider {
fn default() -> Self {
Self::Tool
}
}
impl std::fmt::Display for MsgProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MsgProvider::Tool => write!(f, "tool"),
MsgProvider::OpenAi => write!(f, "openai"),
MsgProvider::Anthropic => write!(f, "anthropic"),
}
}
}
impl std::str::FromStr for MsgProvider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"tool" => Ok(MsgProvider::Tool),
"openai" => Ok(MsgProvider::OpenAi),
"anthropic" => Ok(MsgProvider::Anthropic),
_ => Err(format!(
"Unknown provider: {}. Use 'tool', 'openai', or 'anthropic'",
s
)),
}
}
}
#[derive(Debug, Clone)]
pub struct OpenAiConfig {
pub model: String,
pub api_key: String,
pub base_url: String,
}
impl Default for OpenAiConfig {
fn default() -> Self {
Self {
model: "gpt-4o-mini".to_string(),
api_key: String::new(),
base_url: "https://api.openai.com/v1".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct AnthropicConfig {
pub model: String,
pub api_key: String,
pub base_url: String,
}
impl Default for AnthropicConfig {
fn default() -> Self {
Self {
model: "claude-3-5-sonnet-20241022".to_string(),
api_key: String::new(),
base_url: "https://api.anthropic.com".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub aliases: HashMap<String, Vec<String>>,
pub main_branch: String,
pub verbose: bool,
pub msg_provider: MsgProvider,
pub openai: OpenAiConfig,
pub anthropic: AnthropicConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
aliases: HashMap::new(),
main_branch: "main".to_string(),
verbose: false,
msg_provider: MsgProvider::default(),
openai: OpenAiConfig::default(),
anthropic: AnthropicConfig::default(),
}
}
}
fn get_old_config_path() -> PathBuf {
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
path.push(".git-alias.toml");
path
}
pub fn get_config_path() -> PathBuf {
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
path.push(".config");
path.push("git-alias.toml");
path
}
fn migrate_old_config() -> bool {
let old_path = get_old_config_path();
let new_path = get_config_path();
if old_path.exists() && !new_path.exists() {
if let Ok(content) = fs::read_to_string(&old_path) {
if let Some(parent) = new_path.parent() {
let _ = fs::create_dir_all(parent);
}
if fs::write(&new_path, &content).is_ok() {
eprintln!(
"Hint: 已迁移旧配置 from {} to {}",
old_path.display(),
new_path.display()
);
return true;
}
}
}
false
}
pub fn load_config() -> Config {
migrate_old_config();
let config_path = get_config_path();
if !config_path.exists() {
return Config::default();
}
let content = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(_) => return Config::default(),
};
let config: toml::Value = match content.parse() {
Ok(c) => c,
Err(_) => return Config::default(),
};
let mut result = Config::default();
if let Some(aliases) = config.get("aliases") {
if let Some(table) = aliases.as_table() {
for (key, value) in table {
if let Some(arr) = value.as_array() {
let cmd: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
if !cmd.is_empty() {
result.aliases.insert(key.clone(), cmd);
}
}
}
}
}
if let Some(settings) = config.get("settings") {
if let Some(table) = settings.as_table() {
if let Some(branch) = table.get("main_branch") {
if let Some(s) = branch.as_str() {
result.main_branch = s.to_string();
}
}
if let Some(verbose) = table.get("verbose") {
if let Some(v) = verbose.as_bool() {
result.verbose = v;
}
}
}
}
if let Some(msg) = config.get("msg") {
if let Some(table) = msg.as_table() {
if let Some(provider) = table.get("provider") {
if let Some(s) = provider.as_str() {
if let Ok(p) = s.parse::<MsgProvider>() {
result.msg_provider = p;
}
}
}
if let Some(openai) = table.get("openai") {
if let Some(t) = openai.as_table() {
if let Some(model) = t.get("model") {
if let Some(s) = model.as_str() {
result.openai.model = s.to_string();
}
}
if let Some(api_key) = t.get("api_key") {
if let Some(s) = api_key.as_str() {
result.openai.api_key = s.to_string();
}
}
if let Some(base_url) = t.get("base_url") {
if let Some(s) = base_url.as_str() {
result.openai.base_url = s.to_string();
}
}
}
}
if let Some(anthropic) = table.get("anthropic") {
if let Some(t) = anthropic.as_table() {
if let Some(model) = t.get("model") {
if let Some(s) = model.as_str() {
result.anthropic.model = s.to_string();
}
}
if let Some(api_key) = t.get("api_key") {
if let Some(s) = api_key.as_str() {
result.anthropic.api_key = s.to_string();
}
}
if let Some(base_url) = t.get("base_url") {
if let Some(s) = base_url.as_str() {
result.anthropic.base_url = s.to_string();
}
}
}
}
}
}
result
}
pub fn create_default_config() -> Result<(), String> {
let config_path = get_config_path();
if config_path.exists() {
return Err("Config file already exists".to_string());
}
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
let default_config = r#"# Git-Alias Configuration File
#
# Usage:
# g config # 显示所有配置
# g config get <key> # 获取配置值
# g config set <key>=<value> # 设置配置值
# g config new # 重新生成默认配置(会提示确认覆盖)
#
# Config Keys:
# settings.main_branch # 默认分支名 (default: main)
# settings.verbose # 详细模式,显示执行的命令 (default: false)
# msg.provider # AI provider: tool / openai / anthropic (default: tool)
# msg.openai.model # OpenAI 模型 (default: gpt-4o-mini)
# msg.openai.api_key # OpenAI API Key
# msg.openai.base_url # OpenAI API 地址 (default: https://api.openai.com/v1)
# msg.anthropic.model # Anthropic 模型 (default: claude-3-5-sonnet-20241022)
# msg.anthropic.api_key # Anthropic API Key
# msg.anthropic.base_url # Anthropic API 地址 (default: https://api.anthropic.com)
#
# AI Provider 说明:
# tool - 使用本地 claude/opencode 等工具(需提前安装)
# openai - 使用 OpenAI API(需配置 api_key)
# anthropic - 使用 Anthropic API(需配置 api_key)
#
[aliases]
# 自定义别名,覆盖内置别名
# 格式: alias_name = ["git", "subcommand", "--flag"]
# Examples:
# myalias = ["status", "-s", "-b"]
# pushf = ["push", "--force-with-lease"]
[settings]
# 默认分支名(用于某些别名命令)
main_branch = "main"
# 详细模式:显示实际执行的 git 命令
verbose = false
[msg]
# AI provider: "tool" / "openai" / "anthropic"
# tool - 使用本地工具(claude/opencode),需提前安装
# openai - 使用 OpenAI API
# anthropic - 使用 Anthropic API
provider = "tool"
[msg.openai]
# OpenAI API 配置
model = "gpt-4o-mini" # 模型名称
api_key = "" # API Key (sk-...)
base_url = "https://api.openai.com/v1" # API 地址(支持代理)
[msg.anthropic]
# Anthropic API 配置
model = "claude-3-5-sonnet-20241022" # 模型名称
api_key = "" # API Key (sk-ant-...)
base_url = "https://api.anthropic.com" # API 地址(支持代理)
"#;
fs::write(&config_path, default_config)
.map_err(|e| format!("Failed to create config file: {}", e))?;
println!("Created config file at: {}", config_path.display());
Ok(())
}
pub fn create_default_config_with_overwrite() -> Result<(), String> {
let config_path = get_config_path();
if config_path.exists() {
print!("Config file exists at {}. Overwrite? (y/N): ", config_path.display());
std::io::stdout().flush().map_err(|e| e.to_string())?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).map_err(|e| e.to_string())?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
let default_config = r#"# Git-Alias Configuration File
#
# Usage:
# g config # 显示所有配置
# g config get <key> # 获取配置值
# g config set <key>=<value> # 设置配置值
# g config new # 重新生成默认配置(会提示确认覆盖)
#
# Config Keys:
# settings.main_branch # 默认分支名 (default: main)
# settings.verbose # 详细模式,显示执行的命令 (default: false)
# msg.provider # AI provider: tool / openai / anthropic (default: tool)
# msg.openai.model # OpenAI 模型 (default: gpt-4o-mini)
# msg.openai.api_key # OpenAI API Key
# msg.openai.base_url # OpenAI API 地址 (default: https://api.openai.com/v1)
# msg.anthropic.model # Anthropic 模型 (default: claude-3-5-sonnet-20241022)
# msg.anthropic.api_key # Anthropic API Key
# msg.anthropic.base_url # Anthropic API 地址 (default: https://api.anthropic.com)
#
# AI Provider 说明:
# tool - 使用本地 claude/opencode 等工具(需提前安装)
# openai - 使用 OpenAI API(需配置 api_key)
# anthropic - 使用 Anthropic API(需配置 api_key)
#
[aliases]
# 自定义别名,覆盖内置别名
# 格式: alias_name = ["git", "subcommand", "--flag"]
# Examples:
# myalias = ["status", "-s", "-b"]
# pushf = ["push", "--force-with-lease"]
[settings]
# 默认分支名(用于某些别名命令)
main_branch = "main"
# 详细模式:显示实际执行的 git 命令
verbose = false
[msg]
# AI provider: "tool" / "openai" / "anthropic"
# tool - 使用本地工具(claude/opencode),需提前安装
# openai - 使用 OpenAI API
# anthropic - 使用 Anthropic API
provider = "tool"
[msg.openai]
# OpenAI API 配置
model = "gpt-4o-mini" # 模型名称
api_key = "" # API Key (sk-...)
base_url = "https://api.openai.com/v1" # API 地址(支持代理)
[msg.anthropic]
# Anthropic API 配置
model = "claude-3-5-sonnet-20241022" # 模型名称
api_key = "" # API Key (sk-ant-...)
base_url = "https://api.anthropic.com" # API 地址(支持代理)
"#;
fs::write(&config_path, default_config)
.map_err(|e| format!("Failed to create config file: {}", e))?;
println!("Created config file at: {}", config_path.display());
Ok(())
}
pub fn save_config(config: &Config) -> Result<(), String> {
let config_path = get_config_path();
let mut content = String::new();
content.push_str("# Git-Alias Configuration File\n\n");
content.push_str("[aliases]\n");
content.push_str("# Add your custom aliases here\n");
content.push_str("# Example:\n");
content.push_str("# my = [\"status\", \"-s\", \"-b\"]\n");
content.push_str("# pushf = [\"push\", \"--force-with-lease\"]\n\n");
content.push_str("[settings]\n");
content.push_str(&format!("main_branch = \"{}\"\n", config.main_branch));
content.push_str(&format!("verbose = {}\n\n", config.verbose));
content.push_str("[msg]\n");
content.push_str(&format!("provider = \"{}\"\n\n", config.msg_provider));
content.push_str("[msg.openai]\n");
content.push_str(&format!("model = \"{}\"\n", config.openai.model));
content.push_str(&format!("api_key = \"{}\"\n", config.openai.api_key));
content.push_str(&format!("base_url = \"{}\"\n\n", config.openai.base_url));
content.push_str("[msg.anthropic]\n");
content.push_str(&format!("model = \"{}\"\n", config.anthropic.model));
content.push_str(&format!("api_key = \"{}\"\n", config.anthropic.api_key));
content.push_str(&format!("base_url = \"{}\"\n", config.anthropic.base_url));
fs::write(&config_path, content)
.map_err(|e| format!("Failed to save config file: {}", e))?;
Ok(())
}
pub fn get_config_value(config: &Config, path: &str) -> Option<String> {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return None;
}
match parts[0] {
"settings" => {
if parts.len() == 2 {
match parts[1] {
"main_branch" => Some(config.main_branch.clone()),
"verbose" => Some(config.verbose.to_string()),
_ => None,
}
} else {
None
}
}
"msg" => {
if parts.len() == 2 {
match parts[1] {
"provider" => Some(config.msg_provider.to_string()),
_ => None,
}
} else if parts.len() == 3 && parts[1] == "openai" {
match parts[2] {
"model" => Some(config.openai.model.clone()),
"api_key" => Some(config.openai.api_key.clone()),
"base_url" => Some(config.openai.base_url.clone()),
_ => None,
}
} else if parts.len() == 3 && parts[1] == "anthropic" {
match parts[2] {
"model" => Some(config.anthropic.model.clone()),
"api_key" => Some(config.anthropic.api_key.clone()),
"base_url" => Some(config.anthropic.base_url.clone()),
_ => None,
}
} else {
None
}
}
_ => None,
}
}
pub fn set_config_value<'a>(config: &'a mut Config, path: &str, value: &str) -> Result<&'a Config, String> {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return Err("Invalid path".to_string());
}
match parts[0] {
"settings" => {
if parts.len() == 2 {
match parts[1] {
"main_branch" => {
config.main_branch = value.to_string();
Ok(config)
}
"verbose" => {
config.verbose = value.parse().map_err(|_| "verbose must be true or false")?;
Ok(config)
}
_ => Err(format!("Unknown setting: {}", parts[1])),
}
} else {
Err("Invalid path".to_string())
}
}
"msg" => {
if parts.len() == 2 {
match parts[1] {
"provider" => {
config.msg_provider = value.parse()?;
Ok(config)
}
_ => Err(format!("Unknown msg setting: {}", parts[1])),
}
} else if parts.len() == 3 && parts[1] == "openai" {
match parts[2] {
"model" => {
config.openai.model = value.to_string();
Ok(config)
}
"api_key" => {
config.openai.api_key = value.to_string();
Ok(config)
}
"base_url" => {
config.openai.base_url = value.to_string();
Ok(config)
}
_ => Err(format!("Unknown openai setting: {}", parts[2])),
}
} else if parts.len() == 3 && parts[1] == "anthropic" {
match parts[2] {
"model" => {
config.anthropic.model = value.to_string();
Ok(config)
}
"api_key" => {
config.anthropic.api_key = value.to_string();
Ok(config)
}
"base_url" => {
config.anthropic.base_url = value.to_string();
Ok(config)
}
_ => Err(format!("Unknown anthropic setting: {}", parts[2])),
}
} else {
Err("Invalid path".to_string())
}
}
_ => Err(format!("Unknown section: {}", parts[0])),
}
}