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
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 31 additions & 3 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,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)]) {
Expand Down Expand Up @@ -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!(
Expand Down
3 changes: 1 addition & 2 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,15 +827,14 @@ 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");
command.env(
"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",
Expand Down