From 09c3ae91830bd9c7543b401b19f8c65a15205d32 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 5 Mar 2026 15:03:37 +0000 Subject: fix(whoami): detect and fall back to system git config for nostr login Add GitSystem to SignerInfoSource so credentials stored in the system git config (/etc/gitconfig) are included in the priority fallback chain (local > global > system) and shown as a separate level in whoami output. --- CHANGELOG.md | 4 ++ src/bin/ngit/sub_commands/whoami.rs | 77 ++++++++++++++++++++++++------------- src/lib/git/mod.rs | 16 ++++++++ src/lib/login/existing.rs | 35 +++++++++++++++-- src/lib/login/mod.rs | 2 + 5 files changed, 104 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d14989..46f52c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `ngit account whoami` now detects and displays login credentials set at the system git config level (`/etc/gitconfig`), and falls back to system config during normal operations when no local or global credentials are found + ### Added - Cover notes (kind 1624, experimental): a new event kind that lets the author or a maintainer attach context or a summary to a pr/issue. designed to pinned to the top of long threads. diff --git a/src/bin/ngit/sub_commands/whoami.rs b/src/bin/ngit/sub_commands/whoami.rs index 19ce573..5c0a461 100644 --- a/src/bin/ngit/sub_commands/whoami.rs +++ b/src/bin/ngit/sub_commands/whoami.rs @@ -41,8 +41,10 @@ struct WhoamiJson { local: Option, #[serde(skip_serializing_if = "Option::is_none")] global: Option, + #[serde(skip_serializing_if = "Option::is_none")] + system: Option, /// The account that would be used for operations in the current context - /// (local takes priority over global). + /// (local > global > system, matching git's priority order). #[serde(skip_serializing_if = "Option::is_none")] active: Option, } @@ -62,7 +64,7 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { let signer_info = extract_signer_cli_arguments(args).unwrap_or(None); - // Try to load local login (silent, no prompts) + // Try to load login from each config level (silent, no prompts) let local = load_user_for_scope( git_repo.as_ref(), signer_info.as_ref(), @@ -71,7 +73,6 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { ) .await; - // Try to load global login (silent, no prompts) let global = load_user_for_scope( git_repo.as_ref(), signer_info.as_ref(), @@ -80,45 +81,67 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { ) .await; + let system = load_user_for_scope( + git_repo.as_ref(), + signer_info.as_ref(), + client.as_ref(), + SignerInfoSource::GitSystem, + ) + .await; + if let Some(client) = client { client.disconnect().await?; } + // Active account follows git's priority order: local > global > system + let active_scope = if local.is_some() { + Some("local") + } else if global.is_some() { + Some("global") + } else if system.is_some() { + Some("system") + } else { + None + }; + if command_args.json { - // active = local if present, else global - let active = local - .as_ref() - .map(|u| make_user_json(u, "local")) - .or_else(|| global.as_ref().map(|u| make_user_json(u, "global"))); + let active = active_scope.and_then(|scope| match scope { + "local" => local.as_ref().map(|u| make_user_json(u, scope)), + "global" => global.as_ref().map(|u| make_user_json(u, scope)), + "system" => system.as_ref().map(|u| make_user_json(u, scope)), + _ => None, + }); let output = WhoamiJson { local: local.as_ref().map(|u| make_user_json(u, "local")), global: global.as_ref().map(|u| make_user_json(u, "global")), + system: system.as_ref().map(|u| make_user_json(u, "system")), active, }; println!("{}", serde_json::to_string_pretty(&output)?); + } else if local.is_none() && global.is_none() && system.is_none() { + println!("not logged in"); + println!(); + println!("use `ngit account login` to log in"); } else { - match (local.as_ref(), global.as_ref()) { - (None, None) => { - println!("not logged in"); - println!(); - println!("use `ngit account login` to log in"); - } - (Some(u), None) => { - println!("logged in to local repository as:"); + type UserEntry = Option<(String, String, Option)>; + let entries: &[(&str, &UserEntry)] = + &[("local", &local), ("global", &global), ("system", &system)]; + let mut first = true; + for (scope, user) in entries { + if let Some(u) = user { + if !first { + println!(); + } + first = false; + let is_active = active_scope == Some(scope); + if is_active { + println!("{scope} (active):"); + } else { + println!("{scope}:"); + } print_user_human(u); } - (None, Some(u)) => { - println!("logged in globally as:"); - print_user_human(u); - } - (Some(local_u), Some(global_u)) => { - println!("local (active):"); - print_user_human(local_u); - println!(); - println!("global:"); - print_user_human(global_u); - } } } diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index 23fa5f0..ca7aa3f 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs @@ -1089,6 +1089,22 @@ pub fn get_git_config_item(git_repo: &Option<&Repo>, item: &str) -> Result Result> { + let config = git2::Config::open_default().context("failed to open git config")?; + // Try system level first, then ProgramData (Windows equivalent) + for level in [git2::ConfigLevel::System, git2::ConfigLevel::ProgramData] { + if let Ok(level_config) = config.open_level(level) { + match level_config.get_entry(item) { + Ok(entry) => return Ok(entry.value().map(|v| v.to_string())), + Err(_) => continue, + } + } + } + Ok(None) +} + pub fn save_git_config_item(git_repo: &Option<&Repo>, item: &str, value: &str) -> Result<()> { if let Some(git_repo) = git_repo { git_repo.save_git_config_item(item, value, false) diff --git a/src/lib/login/existing.rs b/src/lib/login/existing.rs index e60621d..2e45ca4 100644 --- a/src/lib/login/existing.rs +++ b/src/lib/login/existing.rs @@ -18,7 +18,7 @@ use crate::client::MockConnect; use crate::{ cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, client::fetch_public_key, - git::{Repo, RepoActions, get_git_config_item}, + git::{Repo, RepoActions, get_git_config_item, get_git_config_item_system}, }; /// load signer from git config and UserProfile from cache or relays @@ -62,7 +62,8 @@ pub async fn load_existing_login( Ok((signer, user_ref, source)) } -/// priority order: cli arguments, local git config, global git config +/// priority order: cli arguments, local git config, global git config, system +/// git config pub fn get_signer_info( git_repo: &Option<&Repo>, signer_info: &Option, @@ -82,6 +83,7 @@ pub fn get_signer_info( SignerInfoSource::CommandLineArguments, SignerInfoSource::GitLocal, SignerInfoSource::GitGlobal, + SignerInfoSource::GitSystem, ] } { if let Ok(res) = @@ -91,7 +93,7 @@ pub fn get_signer_info( break; } } - result.context("failed to get or find signer info in cli arguments, local git config or global git config")? + result.context("failed to get or find signer info in cli arguments, local git config, global git config or system git config")? } Some(SignerInfoSource::CommandLineArguments) => { if let Some(signer_info) = signer_info { @@ -158,6 +160,33 @@ pub fn get_signer_info( bail!("no signer info in global git config") } } + Some(SignerInfoSource::GitSystem) => { + if let Some(nsec) = get_git_config_item_system("nostr.nsec") + .context("failed to get system git config")? + { + ( + SignerInfo::Nsec { + nsec: nsec.to_string(), + password: password.clone(), + npub: get_git_config_item_system("nostr.npub") + .context("failed to get system git config")?, + }, + SignerInfoSource::GitSystem, + ) + } else if let Some(bunker_uri) = get_git_config_item_system("nostr.bunker-uri") + .context("failed to get system git config")? + { + (SignerInfo::Bunker { + bunker_uri, bunker_app_key: get_git_config_item_system("nostr.bunker-app-key") + .context("failed to get system git config")? + .context("system git config item nostr.bunker-uri exists but nostr.bunker-app-key doesn't")?, + npub: get_git_config_item_system("nostr.npub") + .context("failed to get system git config")?, + }, SignerInfoSource::GitSystem) + } else { + bail!("no signer info in system git config") + } + } }) } diff --git a/src/lib/login/mod.rs b/src/lib/login/mod.rs index 47847c3..938cec4 100644 --- a/src/lib/login/mod.rs +++ b/src/lib/login/mod.rs @@ -65,6 +65,7 @@ pub enum SignerInfo { pub enum SignerInfoSource { GitLocal, GitGlobal, + GitSystem, CommandLineArguments, } @@ -91,6 +92,7 @@ fn print_logged_in_as( SignerInfoSource::CommandLineArguments => " via cli arguments", SignerInfoSource::GitLocal => " to local repository", SignerInfoSource::GitGlobal => "", + SignerInfoSource::GitSystem => " via system git config", } ); Ok(()) -- cgit v1.2.3