Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions desktop/src-tauri/src/commands/project_git_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ 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.
/// escapes, so paths are normalized to forward slashes first. The result is
/// then shell-escaped — see `git_credential_helper_config_value` for why a
/// naive whole-value quote doesn't work.
fn credential_helper_config_value(path: &std::path::Path) -> String {
path.to_string_lossy().replace('\\', "/")
crate::managed_agents::git_credential_helper_config_value(path)
}

fn apply_git_config(command: &mut Command, entries: &[(&str, String)]) {
Expand Down Expand Up @@ -341,6 +343,24 @@ mod tests {
);
}

#[test]
fn credential_helper_config_value_escapes_spaces() {
let path = std::path::PathBuf::from("/Users/x/My Apps/git-credential-nostr");
assert_eq!(
credential_helper_config_value(&path),
r"/Users/x/My\ Apps/git-credential-nostr",
);
}

#[test]
fn credential_helper_config_value_escapes_apostrophes() {
let path = std::path::PathBuf::from("/Users/o'brien/git-credential-nostr");
assert_eq!(
credential_helper_config_value(&path),
r"/Users/o\'brien/git-credential-nostr",
);
}

#[test]
fn git_subcommand_skips_global_config_options() {
assert_eq!(
Expand Down
23 changes: 23 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,29 @@ pub fn resolve_command(command: &str) -> Option<PathBuf> {
result
}

/// Escape a path for use as a git `credential.helper` config value.
///
/// Git builds the helper command by concatenating the raw `credential.helper`
/// string with an operation (`get`/`store`/`erase`) and running the result
/// through `sh -c`, so any shell metacharacter in the path (most commonly a
/// space) gets word-split and breaks the invocation. Git also detects an
/// absolute-path helper by checking whether the *unescaped* string starts
/// with a path separator (or drive letter on Windows) — quoting the value as
/// a whole hides that leading character and git falls back to treating it as
/// a bare helper name instead. So each shell-special character is escaped
/// individually with a backslash, leaving the leading separator intact.
pub fn git_credential_helper_config_value(path: &std::path::Path) -> String {
let normalized = path.to_string_lossy().replace('\\', "/");
let mut escaped = String::with_capacity(normalized.len());
for ch in normalized.chars() {
if !(ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '/' | ':')) {
escaped.push('\\');
}
escaped.push(ch);
}
escaped
}

/// Clear the resolve_command cache so that newly-installed binaries are detected.
pub fn clear_resolve_cache() {
let mut guard = resolve_cache().lock().unwrap_or_else(|e| e.into_inner());
Expand Down
14 changes: 8 additions & 6 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use super::agent_env::build_buzz_agent_provider_defaults;

use crate::{
managed_agents::{
append_log_marker, known_acp_runtime, login_shell_path, managed_agent_log_path,
missing_command_message, normalize_agent_args, open_log_file, resolve_command,
spawn_key_refusal, KnownAcpRuntime, ManagedAgentPairRuntime, ManagedAgentRecord,
ManagedAgentRuntimeKey, ManagedAgentSummary,
append_log_marker, git_credential_helper_config_value, known_acp_runtime, login_shell_path,
managed_agent_log_path, missing_command_message, normalize_agent_args, open_log_file,
resolve_command, spawn_key_refusal, KnownAcpRuntime, ManagedAgentPairRuntime,
ManagedAgentRecord, ManagedAgentRuntimeKey, ManagedAgentSummary,
},
util::now_iso,
};
Expand Down Expand Up @@ -835,8 +835,10 @@ pub fn spawn_agent_child(
"GIT_CONFIG_KEY_0",
format!("credential.{relay_http_url}/git.helper"),
);
let helper = cred_helper.to_string_lossy().replace('\\', "/");
command.env("GIT_CONFIG_VALUE_0", helper);
command.env(
"GIT_CONFIG_VALUE_0",
git_credential_helper_config_value(&cred_helper),
);
command.env(
"GIT_CONFIG_KEY_1",
format!("credential.{relay_http_url}/git.useHttpPath"),
Expand Down