mod alias;
mod config;
mod shell;
mod utils;
use colored::Colorize;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[cfg(windows)]
fn enable_ansi_support() {
unsafe {
use windows::Win32::System::Console::{
CONSOLE_MODE, GetConsoleMode, GetStdHandle, STD_OUTPUT_HANDLE,
};
use windows::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows::core::{s, w};
let kernel32 = GetModuleHandleW(w!("kernel32")).unwrap();
let set_console_mode_ptr = GetProcAddress(kernel32, s!("SetConsoleMode")).unwrap();
let set_console_mode: extern "system" fn(
windows::Win32::Foundation::HANDLE,
CONSOLE_MODE,
) -> i32 = std::mem::transmute(set_console_mode_ptr);
let console_handle = GetStdHandle(STD_OUTPUT_HANDLE).unwrap();
let mut mode = CONSOLE_MODE(0);
GetConsoleMode(console_handle, &mut mode).ok();
mode |= CONSOLE_MODE(0x0004); set_console_mode(console_handle, mode);
}
}
fn main() {
#[cfg(windows)]
enable_ansi_support();
colored::control::set_override(true);
let args: Vec<String> = std::env::args().collect();
let exe_name = get_exe_name();
let aliases = load_aliases();
if !exe_name.starts_with('g') && aliases.contains_key(&exe_name) {
eprintln!("{}: '{}' is not a valid command", "Error".red().bold(), exe_name);
eprintln!();
eprintln!("Use 'g {}' instead", exe_name.green());
std::process::exit(1);
}
if exe_name == "g" || exe_name == "git-alias" || exe_name == "git-alias.exe" {
handle_subcommand(&args, &aliases);
} else {
execute_alias(&exe_name, &args[1..], &aliases);
}
}
fn get_exe_name() -> String {
std::env::args()
.next()
.and_then(|p| std::path::Path::new(&p).file_stem().map(|s| s.to_string_lossy().to_string()))
.unwrap_or_else(|| "g".to_string())
}
fn load_aliases() -> HashMap<String, Vec<String>> {
let mut aliases = alias::get_builtin_aliases();
let config = config::load_config();
aliases.extend(config.aliases);
aliases
}
fn execute_alias(name: &str, args: &[String], aliases: &HashMap<String, Vec<String>>) {
let config = config::load_config();
if let Some(git_args) = aliases.get(name) {
if name == "cmai" || name == "gcmai" {
let cfg = config::load_config();
let use_english = args.iter().any(|arg| arg == "-en" || arg == "--en");
let filtered_args: Vec<&String> = args.iter().filter(|arg| *arg != "-en" && *arg != "--en").collect();
let diff_args = ["diff"];
let staged_args = ["diff", "--cached"];
let diff_output = utils::exec_git_output(&diff_args);
let staged_output = utils::exec_git_output(&staged_args);
if diff_output.trim().is_empty() && staged_output.trim().is_empty() {
eprintln!("{}", "错误: 没有可提交的变更".red().bold());
std::process::exit(1);
}
let prompt = if use_english {
format!(
r#"Analyze the following Git changes and generate a concise commit message.
Requirements:
- Maximum 80 characters
- Use English
- Format: <type>(<scope>): <subject> (Conventional Commits)
- Type must be one of: feat, fix, docs, style, refactor, perf, test, chore, ci, revert
- Optional scope: module/component name (e.g., api, ui, auth, db)
- Be concise and clear, suitable as a commit message
- IMPORTANT: Only output the commit message itself, no explanation, no markdown formatting, no code fences
## Staged Changes
{staged}
## Unstaged Changes
{unstaged}"#,
staged = staged_output,
unstaged = diff_output
)
} else {
format!(
r#"请分析以下 Git 变更,生成一行简洁的 commit message。
要求:
- 最多 80 字符
- 用中文描述
- 格式:<类型>(<范围>): <简短描述> (符合 Conventional Commits 规范)
- 类型必须为以下之一:feat, fix, docs, style, refactor, perf, test, chore, ci, revert
- 可选范围:模块/组件名(如 api, ui, auth, db)
- 简洁明了,适合作为 commit message
- 重要:只输出 commit message 本身,不要解释,不要 markdown 格式,不要代码块
## Staged 变更
{staged}
## Unstaged 变更
{unstaged}"#,
staged = staged_output,
unstaged = diff_output
)
};
match call_ai_summarize(&prompt, &cfg) {
Ok(summary) => {
let summary = strip_code_fences(&summary);
eprintln!("{}: {}", "Commit message".cyan().bold(), summary);
let commit_args =
vec!["commit".to_string(), "-m".to_string(), summary.to_string()];
if cfg.verbose {
eprintln!("{} git {}", "Executing:".cyan(), commit_args.join(" "));
}
let exit_code = utils::execute_git_command(&commit_args);
std::process::exit(exit_code);
},
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
},
}
}
let mut full_args = git_args.clone();
full_args.extend_from_slice(args);
if config.verbose {
eprintln!("{} git {}", "Executing:".cyan(), full_args.join(" "));
}
let exit_code = utils::execute_git_command(&full_args);
std::process::exit(exit_code);
} else {
let mut full_args = vec![name.to_string()];
full_args.extend_from_slice(args);
if config.verbose {
eprintln!("{} git {}", "Executing:".cyan(), full_args.join(" "));
}
let exit_code = utils::execute_git_command(&full_args);
std::process::exit(exit_code);
}
}
fn handle_subcommand(args: &[String], aliases: &HashMap<String, Vec<String>>) {
if args.len() < 2 {
print_help();
return;
}
match args[1].as_str() {
"msg" => {
let exit_code = handle_msg(args.get(2).map(|s| s.as_str()));
std::process::exit(exit_code);
},
"cmai" => {
execute_alias("cmai", &args[2..], aliases);
},
"g" => {
if args.len() < 3 {
print_help();
return;
}
match args[2].as_str() {
"list" => {
let filter = args.get(3);
alias::list_aliases(aliases, filter.map(|s| s.as_str()));
},
"completions" => {
if let Some(shell) = args.get(3) {
shell::print_completions(shell, aliases);
} else {
print_completions_help();
}
},
"init" => {
if let Err(e) = config::create_default_config() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
},
"install" => {
if let Err(e) = install_all() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
},
"msg" => {
let exit_code = handle_msg(args.get(3).map(|s| s.as_str()));
std::process::exit(exit_code);
},
"--help" | "-h" => {
print_help();
},
"--version" | "-V" => {
println!("git-alias 1.2.25");
},
sub => {
let mut full_args = vec![sub.to_string()];
full_args.extend_from_slice(&args[3..]);
let exit_code = utils::execute_git_command(&full_args);
std::process::exit(exit_code);
},
}
},
"list" => {
let filter = args.get(2);
alias::list_aliases(aliases, filter.map(|s| s.as_str()));
},
"completions" => {
if let Some(shell) = args.get(2) {
shell::print_completions(shell, aliases);
} else {
print_completions_help();
}
},
"init" => {
if let Err(e) = config::create_default_config() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
},
"install" => {
if let Err(e) = install_all() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
},
"config" => {
handle_config(&args[2..]);
},
"--help" | "-h" => {
print_help();
},
"--version" | "-V" => {
println!("git-alias 1.2.22");
},
alias_name => {
if alias_name.starts_with('g') && aliases.contains_key(alias_name) {
eprintln!("{}: 'g {}' is not needed", "Hint".yellow().bold(), alias_name);
eprintln!();
eprintln!("Use '{}' directly instead", alias_name.green());
std::process::exit(1);
}
if aliases.contains_key(alias_name) {
let remaining_args = &args[2..];
let git_args = aliases.get(alias_name).unwrap();
let mut full_args = git_args.clone();
full_args.extend_from_slice(remaining_args);
let exit_code = utils::execute_git_command(&full_args);
std::process::exit(exit_code);
} else {
let mut full_args = vec![alias_name.to_string()];
full_args.extend_from_slice(&args[2..]);
let exit_code = utils::execute_git_command(&full_args);
std::process::exit(exit_code);
}
},
}
}
fn install_all() -> Result<(), String> {
let install_dir = get_install_dir()?;
fs::create_dir_all(&install_dir)
.map_err(|e| format!("Failed to create directory {}: {}", install_dir.display(), e))?;
let current_exe = std::env::current_exe()
.map_err(|e| format!("Failed to get current executable path: {}", e))?;
let aliases = load_aliases();
let mut installed = vec![];
for alias_name in aliases.keys() {
if !alias_name.starts_with('g') {
continue;
}
let target_path = install_dir.join(alias_name);
#[cfg(windows)]
{
fs::copy(¤t_exe, &target_path).map_err(|e| {
format!("Failed to copy binary to {}: {}", target_path.display(), e)
})?;
}
#[cfg(unix)]
{
if target_path.exists() || target_path.symlink_metadata().is_ok() {
fs::remove_file(&target_path)
.map_err(|e| format!("Failed to remove existing file: {}", e))?;
}
std::os::unix::fs::symlink(¤t_exe, &target_path)
.map_err(|e| format!("Failed to create symlink: {}", e))?;
}
installed.push(alias_name.clone());
}
let g_path = install_dir.join("g");
fs::copy(¤t_exe, &g_path)
.map_err(|e| format!("Failed to copy binary to {}: {}", g_path.display(), e))?;
installed.push("g".to_string());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = fs::Permissions::from_mode(0o755);
fs::set_permissions(&g_path, permissions)
.map_err(|e| format!("Failed to set permissions: {}", e))?;
}
println!("{}", "Installation successful!".green().bold());
println!();
println!("Installed to: {}", install_dir.display());
println!("Total: {} commands installed", installed.len());
println!();
println!("Usage:");
println!(" g s # git status");
println!(" gs # git status");
println!(" g ls # git log --no-merges");
println!(" gls # git log --no-merges");
println!();
println!("Run: {} list", "g".green());
Ok(())
}
fn get_install_dir() -> Result<PathBuf, String> {
let home = dirs::home_dir().ok_or_else(|| "Failed to get home directory".to_string())?;
let install_dir = home.join(".local").join("bin");
Ok(install_dir)
}
fn print_help() {
println!("{}", "Git-Alias - Fast Git Alias Tool".cyan().bold());
println!();
println!("{}", "USAGE:".yellow().bold());
println!(" {} [args...] Run git command directly", "gs".green());
println!(" g {} [args...] Run git command via g prefix", "s".green());
println!();
println!("{}", "SUBCOMMANDS:".yellow().bold());
println!(" g list [filter] List all aliases");
println!(" g completions <sh> Generate shell completions (bash/zsh/fish)");
println!(" g init Create config file at ~/.git-alias.toml");
println!(" g install Install all alias commands to ~/.local/bin");
println!(" g msg <range> AI summarize commits (claude/opencode)");
println!();
println!("{}", "NOTES:".yellow().bold());
println!(" - Direct call must start with 'g' (e.g., gs, gls)");
println!(" - g prefix works with any alias (e.g., g s, g ls)");
println!(" - g gs is not supported, use gs instead");
println!(" - Run 'g list' to see all available aliases");
}
fn print_completions_help() {
println!("{}", "Shell Completions".cyan().bold());
println!();
println!("{}", "Usage:".yellow().bold());
println!(" {} <shell>", "g completions".green());
println!();
println!("{}", "Supported shells:".yellow().bold());
println!(" {} Bash shell", "bash".cyan());
println!(" {} Zsh shell", "zsh".cyan());
println!(" {} Fish shell", "fish".cyan());
println!();
println!("{}", "Examples:".yellow().bold());
println!(" {} {}", "#".dimmed(), "Bash - 临时启用".white());
println!(" {}", "source <(g completions bash)".green());
println!();
println!(" {} {}", "#".dimmed(), "Bash - 持久化 (添加到 ~/.bashrc)".white());
println!(" {}", "echo 'source <(g completions bash)' >> ~/.bashrc".green());
println!();
println!(" {} {}", "#".dimmed(), "Zsh - 临时启用".white());
println!(" {}", "source <(g completions zsh)".green());
println!();
println!(" {} {}", "#".dimmed(), "Zsh - 持久化 (添加到 ~/.zshrc)".white());
println!(" {}", "echo 'source <(g completions zsh)' >> ~/.zshrc".green());
println!();
println!(" {} {}", "#".dimmed(), "Fish - 持久化".white());
println!(" {}", "g completions fish > ~/.config/fish/completions/g.fish".green());
}
fn print_msg_help() {
println!("{}", "Git Msg - AI Commit Summarizer".cyan().bold());
println!();
println!("{}", "USAGE:".yellow().bold());
println!(" {} <commit_range> Generate AI-powered changelog summary", "g msg".green());
println!();
println!("{}", "EXAMPLES:".yellow().bold());
println!(" {} release/v0.3.4..release/v0.3.5 # Compare branches", "g msg".green());
println!(" {} db8201ed..8a61f32a # Compare commits", "g msg".green());
println!(" {} HEAD~10..HEAD # Last 10 commits", "g msg".green());
println!();
println!("{}", "REQUIREMENTS:".yellow().bold());
println!(" - Requires AI tool installed: claude or opencode");
println!(" - AI tool must be in PATH or WinGet Links");
}
fn handle_msg(range: Option<&str>) -> i32 {
let cfg = config::load_config();
let range = match range {
Some("-h") | Some("--help") | None => {
print_msg_help();
return 0;
},
Some(r) => r,
};
let range_parts: Vec<&str> = range.split("..").collect();
let actual_range = range.to_string();
let log_output =
utils::exec_git_output(&["log", "--no-merges", "--format=%h %s", &actual_range]);
let diff_output = utils::exec_git_output(&["log", "-p", "--no-merges", &actual_range]);
if log_output.trim().is_empty() && range_parts.len() == 2 {
let base = utils::exec_git_output(&["merge-base", range_parts[0], range_parts[1]]);
let base = base.trim();
if !base.is_empty() {
let fallback_range = format!("{}..{}", base, range_parts[1]);
let fallback_log =
utils::exec_git_output(&["log", "--no-merges", "--format=%h %s", &fallback_range]);
if !fallback_log.trim().is_empty() {
eprintln!(
"{}: 使用 {} 作为分支点,显示 {} 的新提交",
"Hint".yellow().bold(),
base,
range_parts[1]
);
let fallback_diff =
utils::exec_git_output(&["log", "-p", "--no-merges", &fallback_range]);
return generate_summary(&fallback_range, &fallback_log, &fallback_diff, &cfg);
}
let fallback_range2 = format!("{}..{}", base, range_parts[0]);
let fallback_log2 =
utils::exec_git_output(&["log", "--no-merges", "--format=%h %s", &fallback_range2]);
if !fallback_log2.trim().is_empty() {
eprintln!(
"{}: 使用 {} 作为分支点,显示 {} 的新提交",
"Hint".yellow().bold(),
base,
range_parts[0]
);
let fallback_diff2 =
utils::exec_git_output(&["log", "-p", "--no-merges", &fallback_range2]);
return generate_summary(&fallback_range2, &fallback_log2, &fallback_diff2, &cfg);
}
}
eprintln!("{}", "错误: 未找到提交记录或范围无效".red().bold());
eprintln!();
eprintln!("提示: 请确保分支名正确,本地分支不存在时可使用远程分支名,如:");
eprintln!(" {} {}", "origin/feature/xxx".cyan(), "替代".dimmed());
eprintln!(" {} {}", "git branch -a".cyan(), "查看所有分支");
return 1;
}
if log_output.trim().is_empty() {
eprintln!("{}", "错误: 未找到提交记录或范围无效".red().bold());
eprintln!();
eprintln!("提示: 请确保分支名正确,本地分支不存在时可使用远程分支名,如:");
eprintln!(" {} {}", "origin/feature/xxx".cyan(), "替代".dimmed());
eprintln!(" {} {}", "git branch -a".cyan(), "查看所有分支");
return 1;
}
let cfg = config::load_config();
generate_summary(&actual_range, &log_output, &diff_output, &cfg)
}
fn generate_summary(range: &str, log_output: &str, diff_output: &str, cfg: &config::Config) -> i32 {
let prompt = format!(
r#"请分析以下 Git 提交记录,生成一份版本变更摘要。
输出格式要求:
- 标题: ## 版本变更摘要 ({range})
- 按以下分类输出,每类用对应 emoji 标记:
- ### 🐛 Bug 修复
- ### ✨ 新功能
- ### 🔧 优化与重构
- ### 📝 其他
- 每条变更用 **粗体标题**:描述内容
- 简洁明了,适合作为 changelog
- 如果某类没有内容则写"无"
## 提交列表
{log}
## 详细变更
{diff}"#,
range = range,
log = log_output,
diff = diff_output
);
match call_ai_summarize(&prompt, cfg) {
Ok(summary) => {
println!("{}", summary);
0
},
Err(e) => {
eprintln!("{}", e);
1
},
}
}
fn call_ai_summarize(prompt: &str, cfg: &config::Config) -> Result<String, String> {
const MAX_PROMPT_LEN: usize = 30000;
let truncated_prompt: String = if prompt.len() > MAX_PROMPT_LEN {
prompt.chars().take(MAX_PROMPT_LEN).collect()
} else {
prompt.to_string()
};
match cfg.msg_provider {
config::MsgProvider::OpenAi => call_openai_api(&truncated_prompt, &cfg.openai),
config::MsgProvider::Anthropic => call_anthropic_api(&truncated_prompt, &cfg.anthropic),
config::MsgProvider::Tool => call_tool(&truncated_prompt),
}
}
fn strip_code_fences(input: &str) -> String {
let trimmed = input.trim();
if trimmed.starts_with("```") {
let content_start = match trimmed[3..].find('\n') {
Some(rel) => rel + 4, None => 3, };
if trimmed.ends_with("```") {
let end_offset = trimmed.len() - 3;
if content_start < end_offset {
return trimmed[content_start..end_offset].trim().to_string();
}
}
}
trimmed.to_string()
}
fn call_tool(prompt: &str) -> Result<String, String> {
let claude_exe = r"C:\Users\liuzhifeng\AppData\Local\Microsoft\WinGet\Packages\Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe\claude.exe";
if let Ok(output) = try_command_direct(claude_exe, &["-p", prompt]) {
if !output.trim().is_empty() {
return Ok(output);
}
}
let minimal_path = r"C:\Users\liuzhifeng\AppData\Local\Microsoft\WinGet\Links;C:\Users\liuzhifeng\AppData\Roaming\npm";
for tool in ["claude"] {
if let Ok(output) = try_command(tool, &["-p", prompt], minimal_path) {
if !output.trim().is_empty() {
return Ok(output);
}
}
}
for tool in ["opencode"] {
if let Ok(output) = try_command(tool, &["run", prompt], minimal_path) {
if !output.trim().is_empty() {
return Ok(output);
}
}
}
Err("错误: 未找到可用的 AI 工具 (claude/opencode),请确保已安装并配置在 PATH 中".to_string())
}
fn call_openai_api(prompt: &str, cfg: &config::OpenAiConfig) -> Result<String, String> {
let client = reqwest::blocking::Client::new();
let request_body = serde_json::json!({
"model": cfg.model,
"messages": [
{"role": "user", "content": prompt}
]
});
let response = client
.post(format!("{}/chat/completions", cfg.base_url))
.header("Authorization", format!("Bearer {}", cfg.api_key))
.header("Content-Type", "application/json")
.json(&request_body)
.send()
.map_err(|e| format!("OpenAI API 请求失败: {}", e))?;
let response_body: serde_json::Value =
response.json().map_err(|e| format!("解析 OpenAI 响应失败: {}", e))?;
if let Some(error) = response_body.get("error") {
return Err(format!("OpenAI API 错误: {}", error));
}
let content = response_body["choices"][0]["message"]["content"]
.as_str()
.ok_or("无法从 OpenAI 响应中提取内容")?;
Ok(content.to_string())
}
fn call_anthropic_api(prompt: &str, cfg: &config::AnthropicConfig) -> Result<String, String> {
let client = reqwest::blocking::Client::new();
let request_body = serde_json::json!({
"model": cfg.model,
"max_tokens": 4096,
"messages": [
{"role": "user", "content": prompt}
]
});
let response = client
.post(format!("{}/v1/messages", cfg.base_url))
.header("x-api-key", &cfg.api_key)
.header("anthropic-version", "2023-06-01")
.header("Content-Type", "application/json")
.json(&request_body)
.send()
.map_err(|e| format!("Anthropic API 请求失败: {}", e))?;
let response_body: serde_json::Value =
response.json().map_err(|e| format!("解析 Anthropic 响应失败: {}", e))?;
if let Some(error) = response_body.get("error") {
return Err(format!("Anthropic API 错误: {}", error));
}
let content = response_body["content"]
.as_array()
.and_then(|arr| arr.iter().find(|item| item["type"] == "text"))
.and_then(|item| item["text"].as_str())
.ok_or("无法从 Anthropic 响应中提取内容")?;
Ok(content.to_string())
}
fn try_command_direct(cmd_path: &str, args: &[&str]) -> Result<String, String> {
std::process::Command::new(cmd_path)
.args(args)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.map_err(|e| e.to_string())
}
fn try_command(cmd: &str, args: &[&str], path: &str) -> Result<String, String> {
std::process::Command::new(cmd)
.args(args)
.env("PATH", path)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.map_err(|e| e.to_string())
}
fn handle_config(args: &[String]) {
let cfg = config::load_config();
if args.is_empty() {
print_config(&cfg);
return;
}
match args[0].as_str() {
"get" => {
if args.len() < 2 {
eprintln!("{}: 使用 'g config get <key>' 获取配置值", "Error".red().bold());
std::process::exit(1);
}
match config::get_config_value(&cfg, &args[1]) {
Some(value) => println!("{}", value),
None => {
eprintln!("{}: 未找到配置项 '{}'", "Error".red().bold(), args[1]);
std::process::exit(1);
},
}
},
"set" => {
if args.len() < 2 {
eprintln!("{}: 使用 'g config set <key>=<value>' 设置配置", "Error".red().bold());
std::process::exit(1);
}
let (key, value) = if args[1].contains('=') {
let parts: Vec<&str> = args[1].splitn(2, '=').collect();
(parts[0], parts[1])
} else if args.len() >= 3 {
(args[1].as_str(), args[2].as_str())
} else {
eprintln!("{}: 使用 'g config set <key>=<value>' 设置配置", "Error".red().bold());
std::process::exit(1);
};
let mut new_cfg = cfg;
match config::set_config_value(&mut new_cfg, key, value) {
Ok(_) => {
if let Err(e) = config::save_config(&new_cfg) {
eprintln!("{}: 保存配置失败: {}", "Error".red().bold(), e);
std::process::exit(1);
}
println!("{}: {} = {}", "Set".green().bold(), key, value);
},
Err(e) => {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
},
}
},
"-h" | "--help" => {
print_config_help();
},
"new" => {
if let Err(e) = config::create_default_config_with_overwrite() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
},
_ => {
eprintln!("{}: unknown subcommand '{}'", "Error".red().bold(), args[0]);
print_config_help();
std::process::exit(1);
},
}
}
fn print_config(cfg: &config::Config) {
println!("{}", "Git-Alias Configuration".cyan().bold());
println!();
println!("{}", "[settings]".yellow());
println!(" main_branch = {}", cfg.main_branch);
println!(" verbose = {}", cfg.verbose);
println!();
println!("{}", "[msg]".yellow());
println!(" provider = {}", cfg.msg_provider);
println!();
println!("{}", "[msg.openai]".yellow());
println!(" model = {}", cfg.openai.model);
println!(" api_key = {}", if cfg.openai.api_key.is_empty() { "<not set>" } else { "******" });
println!(" base_url = {}", cfg.openai.base_url);
println!();
println!("{}", "[msg.anthropic]".yellow());
println!(" model = {}", cfg.anthropic.model);
println!(
" api_key = {}",
if cfg.anthropic.api_key.is_empty() { "<not set>" } else { "******" }
);
println!(" base_url = {}", cfg.anthropic.base_url);
}
fn print_config_help() {
println!("{}", "Git-Alias Config".cyan().bold());
println!();
println!("{}", "USAGE:".yellow().bold());
println!(" g config # 显示所有配置");
println!(" g config get <key> # 获取配置值");
println!(" g config set <key>=<value> # 设置配置值");
println!(" g config new # 生成默认配置文件(可覆盖)");
println!();
println!("{}", "EXAMPLES:".yellow().bold());
println!(" g config get msg.provider");
println!(" g config set msg.provider=openai");
println!(" g config set msg.openai.model=gpt-4o");
println!(" g config set msg.openai.api_key=sk-...");
println!(" g config new # 生成默认配置文件");
println!();
println!("{}", "CONFIG KEYS:".yellow().bold());
println!(" settings.main_branch");
println!(" settings.verbose");
println!(" msg.provider (tool/openai/anthropic)");
println!(" msg.openai.model");
println!(" msg.openai.api_key");
println!(" msg.openai.base_url");
println!(" msg.anthropic.model");
println!(" msg.anthropic.api_key");
println!(" msg.anthropic.base_url");
}