diff --git a/crates/git-credential-nostr/src/lib.rs b/crates/git-credential-nostr/src/lib.rs index b51443600d..b791649e93 100644 --- a/crates/git-credential-nostr/src/lib.rs +++ b/crates/git-credential-nostr/src/lib.rs @@ -158,6 +158,13 @@ pub fn run() -> i32 { let req = parse_stdin(); if !req.has_authtype_capability { + if std::env::var("BUZZ_NOSTR_GIT_AUTH_REQUIRED").as_deref() == Ok("1") { + eprintln!( + "error: Nostr git authentication requires Git 2.46 or newer \ + (credential authtype support)" + ); + return 1; + } println!(); let _ = io::stdout().flush(); return 0; diff --git a/crates/git-credential-nostr/tests/integration.rs b/crates/git-credential-nostr/tests/integration.rs index 6697b45f39..99745bccb8 100644 --- a/crates/git-credential-nostr/tests/integration.rs +++ b/crates/git-credential-nostr/tests/integration.rs @@ -21,6 +21,7 @@ fn run_helper(input: &str, env_vars: &[(&str, &str)]) -> std::process::Output { .current_dir(std::env::temp_dir()) .env_remove("NOSTR_PRIVATE_KEY") .env_remove("BUZZ_AUTH_TAG") + .env_remove("BUZZ_NOSTR_GIT_AUTH_REQUIRED") .env_remove("GIT_CONFIG_COUNT") // Prevent git config on the test machine from supplying credentials. .env("GIT_CONFIG_GLOBAL", "/dev/null") @@ -183,9 +184,10 @@ fn malformed_nip_oa_auth_tag_fails_closed() { assert!(!String::from_utf8_lossy(&out.stdout).contains("credential=")); } -/// Old git (no `capability[]=authtype` in input) → empty line on stdout, exit 0. +/// A globally configured helper must still decline silently for old Git when +/// Buzz has not marked the operation as requiring Nostr authentication. #[test] -fn old_git_no_authtype_capability() { +fn old_git_without_buzz_requirement_declines_silently() { let input = "protocol=https\n\ host=relay.example.com\n\ path=git/owner/repo.git/info/refs\n\ @@ -213,6 +215,34 @@ fn old_git_no_authtype_capability() { ); } +/// Buzz explicitly supplies a Nostr key for relay git operations. If Git is +/// too old to support `authtype`, fail with the real requirement instead of +/// silently falling through to Git's misleading username prompt. +#[test] +fn old_git_with_buzz_requirement_reports_minimum_version() { + let input = "protocol=https\n\ + host=relay.example.com\n\ + path=git/owner/repo.git/info/refs\n\ + \n"; + + let nsec = fresh_nsec(); + let out = run_helper( + input, + &[ + ("NOSTR_PRIVATE_KEY", &nsec), + ("BUZZ_NOSTR_GIT_AUTH_REQUIRED", "1"), + ], + ); + + assert_eq!(out.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("Git 2.46 or newer"), + "expected minimum Git version diagnostic, got:\n{stderr}" + ); + assert!(!String::from_utf8_lossy(&out.stdout).contains("credential=")); +} + /// No key configured at all → exit 1, stderr mentions "no nostr key configured". #[test] fn missing_key() { diff --git a/desktop/src-tauri/src/commands/project_git_exec.rs b/desktop/src-tauri/src/commands/project_git_exec.rs index 3f16c0fba9..bca00b0313 100644 --- a/desktop/src-tauri/src/commands/project_git_exec.rs +++ b/desktop/src-tauri/src/commands/project_git_exec.rs @@ -172,6 +172,9 @@ fn configure_git_auth(command: &mut Command, auth: &GitAuthConfig, needs_credent return apply_git_config(command, &entries); }; command.env("NOSTR_PRIVATE_KEY", &auth.nsec); + // Git before 2.46 never advertises credential authtype support, so let + // the helper surface Buzz's actual minimum-version requirement. + command.env("BUZZ_NOSTR_GIT_AUTH_REQUIRED", "1"); entries.push(("credential.helper", cred_helper.display().to_string())); entries.push(("credential.useHttpPath", "true".to_string())); } @@ -315,8 +318,8 @@ 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, configure_git_auth, git_needs_credentials, git_subcommand, + validate_clone_url, validate_clone_url_against_relay, GitAuthConfig, }; #[test] @@ -350,6 +353,25 @@ mod tests { assert!(!git_needs_credentials(&["rev-parse", "HEAD"])); } + #[test] + fn remote_git_marks_nostr_auth_as_required() { + let auth = GitAuthConfig { + git_path: "git".into(), + credential_helper: Some("git-credential-nostr".into()), + nsec: "nsec1test".into(), + allow_file_transport: false, + }; + let mut command = std::process::Command::new("git"); + + configure_git_auth(&mut command, &auth, true); + + let required = command + .get_envs() + .find_map(|(key, value)| (key == "BUZZ_NOSTR_GIT_AUTH_REQUIRED").then_some(value)) + .flatten(); + assert_eq!(required, Some(std::ffi::OsStr::new("1"))); + } + #[test] fn clean_branch_accepts_plain_and_prefixed_names() { assert_eq!(