From 8ad0040d66434d016de38e80051f338ef2c5aeff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=86=A0=E8=BE=B0?= Date: Wed, 24 Jun 2026 17:27:25 +0800 Subject: [PATCH] fix(gemini): prefix windows hook with git bash --- src/hooks/init.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/hooks/init.rs b/src/hooks/init.rs index 28363f4ce..af9940b26 100644 --- a/src/hooks/init.rs +++ b/src/hooks/init.rs @@ -3603,6 +3603,12 @@ const GEMINI_HOOK_SCRIPT: &str = r#"#!/bin/bash exec rtk hook gemini "#; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GeminiHookPlatform { + Windows, + Other, +} + fn resolve_gemini_dir() -> Result { resolve_home_subdir(GEMINI_DIR) } @@ -3684,7 +3690,7 @@ fn patch_gemini_settings( ) -> Result<()> { let InitContext { verbose, dry_run } = ctx; let settings_path = gemini_dir.join(SETTINGS_JSON); - let hook_cmd = hook_path.to_string_lossy().to_string(); + let hook_cmd = gemini_hook_command(hook_path); // Read or create settings.json let mut settings: serde_json::Value = if settings_path.exists() { @@ -3792,6 +3798,85 @@ fn patch_gemini_settings( Ok(()) } +fn gemini_hook_command(hook_path: &Path) -> String { + let bash_path = find_git_for_windows_bash(); + let platform = if cfg!(windows) { + GeminiHookPlatform::Windows + } else { + GeminiHookPlatform::Other + }; + + gemini_hook_command_for_platform(hook_path, platform, bash_path.as_deref()) +} + +fn gemini_hook_command_for_platform( + hook_path: &Path, + platform: GeminiHookPlatform, + bash_path: Option<&Path>, +) -> String { + match (platform, bash_path) { + (GeminiHookPlatform::Windows, Some(bash_path)) => { + format!( + "{} {}", + quote_command_arg(bash_path), + quote_command_arg(hook_path) + ) + } + _ => hook_path.to_string_lossy().to_string(), + } +} + +fn quote_command_arg(path: &Path) -> String { + let raw = path.to_string_lossy(); + if raw.contains([' ', '\t', '"']) { + format!("\"{}\"", raw.replace('"', "\\\"")) + } else { + raw.to_string() + } +} + +#[cfg(windows)] +fn find_git_for_windows_bash() -> Option { + for candidate in [ + r"C:\Program Files\Git\usr\bin\bash.exe", + r"C:\Program Files (x86)\Git\usr\bin\bash.exe", + ] { + let path = PathBuf::from(candidate); + if path.exists() { + return Some(path); + } + } + + let output = std::process::Command::new("where.exe") + .arg("bash.exe") + .output() + .ok()?; + if !output.status.success() { + return None; + } + + String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(PathBuf::from) + .find(|path| is_git_for_windows_bash(path)) +} + +#[cfg(windows)] +fn is_git_for_windows_bash(path: &Path) -> bool { + let normalized = path + .to_string_lossy() + .replace('\\', "/") + .to_ascii_lowercase(); + normalized.ends_with("/usr/bin/bash.exe") && normalized.contains("/git/") +} + +#[cfg(not(windows))] +fn find_git_for_windows_bash() -> Option { + None +} + /// Remove Gemini artifacts during uninstall fn uninstall_gemini(ctx: InitContext) -> Result> { let InitContext { verbose, dry_run } = ctx; @@ -4183,6 +4268,36 @@ mod tests { use super::*; use tempfile::TempDir; + #[test] + fn test_gemini_hook_command_windows_prefixes_git_bash() { + let hook_path = Path::new(r"C:\Users\me\.gemini\hooks\rtk-hook-gemini.sh"); + let bash_path = Path::new(r"D:\installed_softs\Git\usr\bin\bash.exe"); + + assert_eq!( + gemini_hook_command_for_platform( + hook_path, + GeminiHookPlatform::Windows, + Some(bash_path) + ), + r"D:\installed_softs\Git\usr\bin\bash.exe C:\Users\me\.gemini\hooks\rtk-hook-gemini.sh" + ); + } + + #[test] + fn test_gemini_hook_command_windows_quotes_paths_with_spaces() { + let hook_path = Path::new(r"C:\Users\Jane Doe\.gemini\hooks\rtk-hook-gemini.sh"); + let bash_path = Path::new(r"C:\Program Files\Git\usr\bin\bash.exe"); + + assert_eq!( + gemini_hook_command_for_platform( + hook_path, + GeminiHookPlatform::Windows, + Some(bash_path) + ), + r#""C:\Program Files\Git\usr\bin\bash.exe" "C:\Users\Jane Doe\.gemini\hooks\rtk-hook-gemini.sh""# + ); + } + #[test] fn test_init_mentions_all_top_level_commands() { for cmd in [