diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 1c89ee4f77..1c97ea9469 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -44,6 +44,7 @@ mod project_git; mod project_git_branches; mod project_git_diff; mod project_git_exec; +pub(crate) use project_git_exec::credential_helper_config_value; mod project_git_push; mod project_git_workflow; mod project_repo_paths; diff --git a/desktop/src-tauri/src/commands/project_git_exec.rs b/desktop/src-tauri/src/commands/project_git_exec.rs index e4a8ad7b41..dfca8a3a72 100644 --- a/desktop/src-tauri/src/commands/project_git_exec.rs +++ b/desktop/src-tauri/src/commands/project_git_exec.rs @@ -185,9 +185,15 @@ fn configure_git_auth(command: &mut Command, auth: &GitAuthConfig, needs_credent /// Format a path for git `credential.helper`. /// /// Git for Windows invokes helpers via MinGW bash, which treats `\` as -/// escapes. Forward slashes work on every platform git supports. -fn credential_helper_config_value(path: &std::path::Path) -> String { - path.to_string_lossy().replace('\\', "/") +/// escapes — use forward slashes. If the path contains whitespace or `'`, +/// emit a `!` shell command with POSIX single quotes so `sh -c` does not +/// word-split (or break on an apostrophe). +pub(crate) fn credential_helper_config_value(path: &std::path::Path) -> String { + let path = path.to_string_lossy().replace('\\', "/"); + if path.chars().all(|c| !c.is_whitespace() && c != '\'') { + return path; + } + format!("!'{}'", path.replace('\'', "'\\''")) } fn apply_git_config(command: &mut Command, entries: &[(&str, String)]) { @@ -341,6 +347,28 @@ mod tests { ); } + #[test] + fn credential_helper_config_value_quotes_spaces() { + let path = std::path::PathBuf::from( + r"C:\Users\Buzz User\AppData\Local\Buzz\git-credential-nostr.exe", + ); + assert_eq!( + credential_helper_config_value(&path), + "!'C:/Users/Buzz User/AppData/Local/Buzz/git-credential-nostr.exe'", + ); + } + + #[test] + fn credential_helper_config_value_escapes_apostrophes() { + let path = std::path::PathBuf::from( + r"C:\Users\O'Buzz User\AppData\Local\Buzz\git-credential-nostr.exe", + ); + assert_eq!( + credential_helper_config_value(&path), + "!'C:/Users/O'\\''Buzz User/AppData/Local/Buzz/git-credential-nostr.exe'", + ); + } + #[test] fn git_subcommand_skips_global_config_options() { assert_eq!( diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index f3b4cb67fd..74ca02398e 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -827,7 +827,6 @@ pub fn spawn_agent_child( // NOSTR_PRIVATE_KEY mirrors BUZZ_PRIVATE_KEY — keep in sync. if let Some(cred_helper) = resolve_command("git-credential-nostr") { let relay_http_url = crate::relay::relay_http_base_url(&effective_relay_url); - command.env("NOSTR_PRIVATE_KEY", &record.private_key_nsec); command.env("GIT_TERMINAL_PROMPT", "0"); command.env("GIT_CONFIG_COUNT", "2"); @@ -835,7 +834,7 @@ pub fn spawn_agent_child( "GIT_CONFIG_KEY_0", format!("credential.{relay_http_url}/git.helper"), ); - let helper = cred_helper.to_string_lossy().replace('\\', "/"); + let helper = crate::commands::credential_helper_config_value(&cred_helper); command.env("GIT_CONFIG_VALUE_0", helper); command.env( "GIT_CONFIG_KEY_1",