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) {
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);
}
"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.8");
}
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);
}
}
"--help" | "-h" => {
print_help();
}
"--version" | "-V" => {
println!("git-alias 1.2.8");
}
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 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 range_title = if range_parts.len() == 2 {
format!("{} → {}", range_parts[0], range_parts[1])
} else {
range.to_string()
};
let log_output = utils::exec_git_output(&["log", "--no-merges", "--format=%h %s", range]);
let diff_output = utils::exec_git_output(&["log", "-p", "--no-merges", range]);
if log_output.trim().is_empty() {
eprintln!("{}", "错误: 未找到提交记录或范围无效".red().bold());
return 1;
}
let prompt = format!(
r#"请分析以下 Git 提交记录,生成一份版本变更摘要。
输出格式要求:
- 标题: ## 版本变更摘要 ({range})
- 按以下分类输出,每类用对应 emoji 标记:
- ### 🐛 Bug 修复
- ### ✨ 新功能
- ### 🔧 优化与重构
- ### 📝 其他
- 每条变更用 **粗体标题**:描述内容
- 简洁明了,适合作为 changelog
- 如果某类没有内容则写"无"
## 提交列表
{log}
## 详细变更
{diff}"#,
range = range_title,
log = log_output,
diff = diff_output
);
match call_ai_tool(&prompt) {
Ok(summary) => {
println!("{}", summary);
0
}
Err(e) => {
eprintln!("{}", e);
1
}
}
}
fn call_ai_tool(prompt: &str) -> 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()
};
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", &truncated_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", "opencode", "openspec"] {
if let Ok(output) = try_command(tool, &["-p", &truncated_prompt], minimal_path) {
if !output.trim().is_empty() {
return Ok(output);
}
}
}
Err("错误: 未找到可用的 AI 工具 (claude/opencode/openspec),请确保已安装并配置在 PATH 中".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())
}