upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 15:03:37 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 15:06:02 +0000
commit09c3ae91830bd9c7543b401b19f8c65a15205d32 (patch)
tree3c7505301c9868cd421fbf0097ed946b9f4b9088 /src
parent37244449d6d0d58bb639f181bd15092de1acaaee (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/bin/ngit/sub_commands/whoami.rs77
-rw-r--r--src/lib/git/mod.rs16
-rw-r--r--src/lib/login/existing.rs35
-rw-r--r--src/lib/login/mod.rs2
4 files changed, 100 insertions, 30 deletions
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 {
41 local: Option<UserJson>, 41 local: Option<UserJson>,
42 #[serde(skip_serializing_if = "Option::is_none")] 42 #[serde(skip_serializing_if = "Option::is_none")]
43 global: Option<UserJson>, 43 global: Option<UserJson>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 system: Option<UserJson>,
44 /// The account that would be used for operations in the current context 46 /// The account that would be used for operations in the current context
45 /// (local takes priority over global). 47 /// (local > global > system, matching git's priority order).
46 #[serde(skip_serializing_if = "Option::is_none")] 48 #[serde(skip_serializing_if = "Option::is_none")]
47 active: Option<UserJson>, 49 active: Option<UserJson>,
48} 50}
@@ -62,7 +64,7 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
62 64
63 let signer_info = extract_signer_cli_arguments(args).unwrap_or(None); 65 let signer_info = extract_signer_cli_arguments(args).unwrap_or(None);
64 66
65 // Try to load local login (silent, no prompts) 67 // Try to load login from each config level (silent, no prompts)
66 let local = load_user_for_scope( 68 let local = load_user_for_scope(
67 git_repo.as_ref(), 69 git_repo.as_ref(),
68 signer_info.as_ref(), 70 signer_info.as_ref(),
@@ -71,7 +73,6 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
71 ) 73 )
72 .await; 74 .await;
73 75
74 // Try to load global login (silent, no prompts)
75 let global = load_user_for_scope( 76 let global = load_user_for_scope(
76 git_repo.as_ref(), 77 git_repo.as_ref(),
77 signer_info.as_ref(), 78 signer_info.as_ref(),
@@ -80,45 +81,67 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
80 ) 81 )
81 .await; 82 .await;
82 83
84 let system = load_user_for_scope(
85 git_repo.as_ref(),
86 signer_info.as_ref(),
87 client.as_ref(),
88 SignerInfoSource::GitSystem,
89 )
90 .await;
91
83 if let Some(client) = client { 92 if let Some(client) = client {
84 client.disconnect().await?; 93 client.disconnect().await?;
85 } 94 }
86 95
96 // Active account follows git's priority order: local > global > system
97 let active_scope = if local.is_some() {
98 Some("local")
99 } else if global.is_some() {
100 Some("global")
101 } else if system.is_some() {
102 Some("system")
103 } else {
104 None
105 };
106
87 if command_args.json { 107 if command_args.json {
88 // active = local if present, else global 108 let active = active_scope.and_then(|scope| match scope {
89 let active = local 109 "local" => local.as_ref().map(|u| make_user_json(u, scope)),
90 .as_ref() 110 "global" => global.as_ref().map(|u| make_user_json(u, scope)),
91 .map(|u| make_user_json(u, "local")) 111 "system" => system.as_ref().map(|u| make_user_json(u, scope)),
92 .or_else(|| global.as_ref().map(|u| make_user_json(u, "global"))); 112 _ => None,
113 });
93 114
94 let output = WhoamiJson { 115 let output = WhoamiJson {
95 local: local.as_ref().map(|u| make_user_json(u, "local")), 116 local: local.as_ref().map(|u| make_user_json(u, "local")),
96 global: global.as_ref().map(|u| make_user_json(u, "global")), 117 global: global.as_ref().map(|u| make_user_json(u, "global")),
118 system: system.as_ref().map(|u| make_user_json(u, "system")),
97 active, 119 active,
98 }; 120 };
99 println!("{}", serde_json::to_string_pretty(&output)?); 121 println!("{}", serde_json::to_string_pretty(&output)?);
122 } else if local.is_none() && global.is_none() && system.is_none() {
123 println!("not logged in");
124 println!();
125 println!("use `ngit account login` to log in");
100 } else { 126 } else {
101 match (local.as_ref(), global.as_ref()) { 127 type UserEntry = Option<(String, String, Option<String>)>;
102 (None, None) => { 128 let entries: &[(&str, &UserEntry)] =
103 println!("not logged in"); 129 &[("local", &local), ("global", &global), ("system", &system)];
104 println!(); 130 let mut first = true;
105 println!("use `ngit account login` to log in"); 131 for (scope, user) in entries {
106 } 132 if let Some(u) = user {
107 (Some(u), None) => { 133 if !first {
108 println!("logged in to local repository as:"); 134 println!();
135 }
136 first = false;
137 let is_active = active_scope == Some(scope);
138 if is_active {
139 println!("{scope} (active):");
140 } else {
141 println!("{scope}:");
142 }
109 print_user_human(u); 143 print_user_human(u);
110 } 144 }
111 (None, Some(u)) => {
112 println!("logged in globally as:");
113 print_user_human(u);
114 }
115 (Some(local_u), Some(global_u)) => {
116 println!("local (active):");
117 print_user_human(local_u);
118 println!();
119 println!("global:");
120 print_user_human(global_u);
121 }
122 } 145 }
123 } 146 }
124 147
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<Optio
1089 } 1089 }
1090} 1090}
1091 1091
1092/// Read a config item from the system-level git config only (e.g.
1093/// /etc/gitconfig).
1094pub fn get_git_config_item_system(item: &str) -> Result<Option<String>> {
1095 let config = git2::Config::open_default().context("failed to open git config")?;
1096 // Try system level first, then ProgramData (Windows equivalent)
1097 for level in [git2::ConfigLevel::System, git2::ConfigLevel::ProgramData] {
1098 if let Ok(level_config) = config.open_level(level) {
1099 match level_config.get_entry(item) {
1100 Ok(entry) => return Ok(entry.value().map(|v| v.to_string())),
1101 Err(_) => continue,
1102 }
1103 }
1104 }
1105 Ok(None)
1106}
1107
1092pub fn save_git_config_item(git_repo: &Option<&Repo>, item: &str, value: &str) -> Result<()> { 1108pub fn save_git_config_item(git_repo: &Option<&Repo>, item: &str, value: &str) -> Result<()> {
1093 if let Some(git_repo) = git_repo { 1109 if let Some(git_repo) = git_repo {
1094 git_repo.save_git_config_item(item, value, false) 1110 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;
18use crate::{ 18use crate::{
19 cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, 19 cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms},
20 client::fetch_public_key, 20 client::fetch_public_key,
21 git::{Repo, RepoActions, get_git_config_item}, 21 git::{Repo, RepoActions, get_git_config_item, get_git_config_item_system},
22}; 22};
23 23
24/// load signer from git config and UserProfile from cache or relays 24/// load signer from git config and UserProfile from cache or relays
@@ -62,7 +62,8 @@ pub async fn load_existing_login(
62 Ok((signer, user_ref, source)) 62 Ok((signer, user_ref, source))
63} 63}
64 64
65/// priority order: cli arguments, local git config, global git config 65/// priority order: cli arguments, local git config, global git config, system
66/// git config
66pub fn get_signer_info( 67pub fn get_signer_info(
67 git_repo: &Option<&Repo>, 68 git_repo: &Option<&Repo>,
68 signer_info: &Option<SignerInfo>, 69 signer_info: &Option<SignerInfo>,
@@ -82,6 +83,7 @@ pub fn get_signer_info(
82 SignerInfoSource::CommandLineArguments, 83 SignerInfoSource::CommandLineArguments,
83 SignerInfoSource::GitLocal, 84 SignerInfoSource::GitLocal,
84 SignerInfoSource::GitGlobal, 85 SignerInfoSource::GitGlobal,
86 SignerInfoSource::GitSystem,
85 ] 87 ]
86 } { 88 } {
87 if let Ok(res) = 89 if let Ok(res) =
@@ -91,7 +93,7 @@ pub fn get_signer_info(
91 break; 93 break;
92 } 94 }
93 } 95 }
94 result.context("failed to get or find signer info in cli arguments, local git config or global git config")? 96 result.context("failed to get or find signer info in cli arguments, local git config, global git config or system git config")?
95 } 97 }
96 Some(SignerInfoSource::CommandLineArguments) => { 98 Some(SignerInfoSource::CommandLineArguments) => {
97 if let Some(signer_info) = signer_info { 99 if let Some(signer_info) = signer_info {
@@ -158,6 +160,33 @@ pub fn get_signer_info(
158 bail!("no signer info in global git config") 160 bail!("no signer info in global git config")
159 } 161 }
160 } 162 }
163 Some(SignerInfoSource::GitSystem) => {
164 if let Some(nsec) = get_git_config_item_system("nostr.nsec")
165 .context("failed to get system git config")?
166 {
167 (
168 SignerInfo::Nsec {
169 nsec: nsec.to_string(),
170 password: password.clone(),
171 npub: get_git_config_item_system("nostr.npub")
172 .context("failed to get system git config")?,
173 },
174 SignerInfoSource::GitSystem,
175 )
176 } else if let Some(bunker_uri) = get_git_config_item_system("nostr.bunker-uri")
177 .context("failed to get system git config")?
178 {
179 (SignerInfo::Bunker {
180 bunker_uri, bunker_app_key: get_git_config_item_system("nostr.bunker-app-key")
181 .context("failed to get system git config")?
182 .context("system git config item nostr.bunker-uri exists but nostr.bunker-app-key doesn't")?,
183 npub: get_git_config_item_system("nostr.npub")
184 .context("failed to get system git config")?,
185 }, SignerInfoSource::GitSystem)
186 } else {
187 bail!("no signer info in system git config")
188 }
189 }
161 }) 190 })
162} 191}
163 192
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 {
65pub enum SignerInfoSource { 65pub enum SignerInfoSource {
66 GitLocal, 66 GitLocal,
67 GitGlobal, 67 GitGlobal,
68 GitSystem,
68 CommandLineArguments, 69 CommandLineArguments,
69} 70}
70 71
@@ -91,6 +92,7 @@ fn print_logged_in_as(
91 SignerInfoSource::CommandLineArguments => " via cli arguments", 92 SignerInfoSource::CommandLineArguments => " via cli arguments",
92 SignerInfoSource::GitLocal => " to local repository", 93 SignerInfoSource::GitLocal => " to local repository",
93 SignerInfoSource::GitGlobal => "", 94 SignerInfoSource::GitGlobal => "",
95 SignerInfoSource::GitSystem => " via system git config",
94 } 96 }
95 ); 97 );
96 Ok(()) 98 Ok(())