fix(desktop): use forward slashes for git credential.helper on Windows (#3023)

## Problem

On Windows, Projects **Remote** view shows an empty file tree and
Sync/Clone fails with a mangled credential-helper path, for example:

```
C:\Users\<user>\AppData\Local\Buzz\git-credential-nostr.exe get: line 1:
C:Users<user>AppDataLocalBuzzgit-credential-nostr.exe: command not found
fatal: could not read Username for 'https://<relay>/git/...': terminal prompts disabled
```

Buzz injects an absolute path into `credential.helper` via
`Path::display()`. On Windows that yields backslashes. Git for Windows
runs credential helpers through MinGW bash, which treats `\` as escapes
and destroys the path, so NIP-98 auth never runs and the blobless temp
clone behind Remote view fails.

macOS/Linux are unaffected (paths already use `/`).

This is unrelated to shipping a stub helper - the bundled
`git-credential-nostr.exe` is a real binary. User `~/.gitconfig`
workarounds also cannot help here because Projects git sets
`GIT_CONFIG_GLOBAL=/dev/null` and injects its own helper.

Closes #3025

## Fix

Normalize the helper path to forward slashes before writing
`GIT_CONFIG_VALUE_*`:

- `desktop/src-tauri/src/commands/project_git_exec.rs` (Projects Remote
/ Sync)
- `desktop/src-tauri/src/managed_agents/runtime.rs` (agent spawn git
auth)

Forward slashes are accepted by Git on every platform; on macOS/Linux
the replace is a no-op. No `cfg(windows)`, packaging, or libgit2
changes.

## How to reproduce (before)

1. Install Buzz on Windows with Git for Windows
2. Connect to a relay that has a repository with at least one pushed
branch
3. Open **Projects** -> select the repo -> **Remote**
4. Observe empty tree; Sync/Clone shows the mangled-path / `command not
found` error above

## Test plan

- [x] Unit: `cargo test --manifest-path desktop/src-tauri/Cargo.toml
credential_helper_config_value` (formatter covered on all platforms;
no-op for Unix-style paths)
- [x] Local Windows NSIS build + install of this branch
- [x] Projects -> Remote / Sync against a Buzz relay repo succeeds on
Windows after the fix

---------

Signed-off-by: Bjorn de Jong <bcrdejong@users.noreply.github.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Bjorn de Jong <bcrdejong@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Bjorn de Jong
2026-07-27 13:56:34 -04:00
committed by GitHub
co-authored by Bjorn de Jong Cursor Will Pfleger
parent 32ead93101
commit 8995316844
3 changed files with 30 additions and 6 deletions
+4 -2
View File
@@ -191,9 +191,11 @@ const overrides = new Map([
// runtime.rs re-entered the list after the #1968 merge: main's
// definition-authoritative resolver comments grew it to 982, and the BYOH
// typed harness-descriptor resolution in spawn_agent_child landed on top at
// 1020. This PR's session-title env write in spawn_agent_child adds 12.
// 1020. The session-title env write in spawn_agent_child adds 12.
// Queued to shrink with the next runtime split pass (#2974 follow-up).
["src-tauri/src/managed_agents/runtime.rs", 1032],
// +1: #3023 credential-helper slash normalization (MinGW bash treats
// backslashes as escapes).
["src-tauri/src/managed_agents/runtime.rs", 1033],
// applyWorkspace reposDir parameter plus the validateReposDir binding,
// threaded through Tauri invokes for configurable repos_dir, plus the
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
@@ -173,12 +173,23 @@ fn configure_git_auth(command: &mut Command, auth: &GitAuthConfig, needs_credent
return apply_git_config(command, &entries);
};
command.env("NOSTR_PRIVATE_KEY", &auth.nsec);
entries.push(("credential.helper", cred_helper.display().to_string()));
entries.push((
"credential.helper",
credential_helper_config_value(cred_helper),
));
entries.push(("credential.useHttpPath", "true".to_string()));
}
apply_git_config(command, &entries);
}
/// 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('\\', "/")
}
fn apply_git_config(command: &mut Command, entries: &[(&str, String)]) {
command.env("GIT_CONFIG_COUNT", entries.len().to_string());
for (index, (key, value)) in entries.iter().enumerate() {
@@ -316,10 +327,20 @@ fn validate_clone_url_against_relay(clone_url: &str, relay_base: &str) -> Result
#[cfg(test)]
mod tests {
use super::{
clean_branch, clean_target_ref, git_needs_credentials, git_subcommand, validate_clone_url,
validate_clone_url_against_relay,
clean_branch, clean_target_ref, credential_helper_config_value, git_needs_credentials,
git_subcommand, validate_clone_url, validate_clone_url_against_relay,
};
#[test]
fn credential_helper_config_value_uses_forward_slashes() {
let path =
std::path::PathBuf::from(r"C:\Users\x\AppData\Local\Buzz\git-credential-nostr.exe");
assert_eq!(
credential_helper_config_value(&path),
"C:/Users/x/AppData/Local/Buzz/git-credential-nostr.exe",
);
}
#[test]
fn git_subcommand_skips_global_config_options() {
assert_eq!(
@@ -835,7 +835,8 @@ pub fn spawn_agent_child(
"GIT_CONFIG_KEY_0",
format!("credential.{relay_http_url}/git.helper"),
);
command.env("GIT_CONFIG_VALUE_0", cred_helper.display().to_string());
let helper = cred_helper.to_string_lossy().replace('\\', "/");
command.env("GIT_CONFIG_VALUE_0", helper);
command.env(
"GIT_CONFIG_KEY_1",
format!("credential.{relay_http_url}/git.useHttpPath"),