upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/git_remote_nostr/fetch.rs7
-rw-r--r--src/bin/git_remote_nostr/push.rs8
-rw-r--r--src/bin/git_remote_nostr/utils.rs14
-rw-r--r--src/lib/cli_interactor.rs48
-rw-r--r--src/lib/client.rs16
-rw-r--r--src/lib/login/mod.rs233
-rw-r--r--tests/ngit/login.rs32
9 files changed, 294 insertions, 72 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4aab076..19b3b6f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1723,6 +1723,7 @@ dependencies = [
1723 "nostr-sqlite", 1723 "nostr-sqlite",
1724 "once_cell", 1724 "once_cell",
1725 "passwords", 1725 "passwords",
1726 "qrcode",
1726 "scrypt", 1727 "scrypt",
1727 "serde", 1728 "serde",
1728 "serde_json", 1729 "serde_json",
@@ -2295,6 +2296,12 @@ dependencies = [
2295] 2296]
2296 2297
2297[[package]] 2298[[package]]
2299name = "qrcode"
2300version = "0.14.1"
2301source = "registry+https://github.com/rust-lang/crates.io-index"
2302checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
2303
2304[[package]]
2298name = "quinn" 2305name = "quinn"
2299version = "0.11.5" 2306version = "0.11.5"
2300source = "registry+https://github.com/rust-lang/crates.io-index" 2307source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index d770c14..16ab010 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ nostr-sdk = "0.34.0"
30nostr-signer = "0.34.0" 30nostr-signer = "0.34.0"
31nostr-sqlite = "0.34.0" 31nostr-sqlite = "0.34.0"
32passwords = "3.1.13" 32passwords = "3.1.13"
33qrcode = { version = "0.14.1", default-features = false }
33scrypt = "0.11.0" 34scrypt = "0.11.0"
34serde = { version = "1.0.181", features = ["derive"] } 35serde = { version = "1.0.181", features = ["derive"] }
35serde_json = "1.0.105" 36serde_json = "1.0.105"
diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs
index 46e7ad3..ff55d6f 100644
--- a/src/bin/git_remote_nostr/fetch.rs
+++ b/src/bin/git_remote_nostr/fetch.rs
@@ -10,6 +10,7 @@ use anyhow::{anyhow, bail, Context, Result};
10use auth_git2::GitAuthenticator; 10use auth_git2::GitAuthenticator;
11use git2::{Progress, Repository}; 11use git2::{Progress, Repository};
12use ngit::{ 12use ngit::{
13 cli_interactor::count_lines_per_msg_vec,
13 git::{ 14 git::{
14 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 15 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
15 utils::check_ssh_keys, 16 utils::check_ssh_keys,
@@ -23,9 +24,9 @@ use nostr::nips::nip19;
23use nostr_sdk::{Event, ToBech32}; 24use nostr_sdk::{Event, ToBech32};
24 25
25use crate::utils::{ 26use crate::utils::{
26 count_lines_per_msg_vec, fetch_or_list_error_is_not_authentication_failure, 27 fetch_or_list_error_is_not_authentication_failure, find_proposal_and_patches_by_branch_name,
27 find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, get_open_proposals, 28 get_oids_from_fetch_batch, get_open_proposals, get_read_protocols_to_try, join_with_and,
28 get_read_protocols_to_try, join_with_and, set_protocol_preference, Direction, 29 set_protocol_preference, Direction,
29}; 30};
30 31
31pub async fn run_fetch( 32pub async fn run_fetch(
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index 3bda6ba..0f4e792 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -16,6 +16,7 @@ use git_events::{
16 generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch, 16 generate_cover_letter_and_patch_events, generate_patch_event, get_commit_id_from_patch,
17}; 17};
18use ngit::{ 18use ngit::{
19 cli_interactor::count_lines_per_msg_vec,
19 client::{self, get_event_from_cache_by_id}, 20 client::{self, get_event_from_cache_by_id},
20 git::{ 21 git::{
21 self, 22 self,
@@ -39,10 +40,9 @@ use crate::{
39 git::Repo, 40 git::Repo,
40 list::list_from_remotes, 41 list::list_from_remotes,
41 utils::{ 42 utils::{
42 count_lines_per_msg_vec, find_proposal_and_patches_by_branch_name, get_all_proposals, 43 find_proposal_and_patches_by_branch_name, get_all_proposals, get_remote_name_by_url,
43 get_remote_name_by_url, get_short_git_server_name, get_write_protocols_to_try, 44 get_short_git_server_name, get_write_protocols_to_try, join_with_and,
44 join_with_and, push_error_is_not_authentication_failure, read_line, 45 push_error_is_not_authentication_failure, read_line, set_protocol_preference, Direction,
45 set_protocol_preference, Direction,
46 }, 46 },
47}; 47};
48 48
diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs
index 7b5c2d2..3ae1bab 100644
--- a/src/bin/git_remote_nostr/utils.rs
+++ b/src/bin/git_remote_nostr/utils.rs
@@ -384,20 +384,6 @@ pub fn error_might_be_authentication_related(error: &anyhow::Error) -> bool {
384 false 384 false
385} 385}
386 386
387fn count_lines_per_msg(width: u16, msg: &str, prefix_len: usize) -> usize {
388 if width == 0 {
389 return 1;
390 }
391 // ((msg_len+prefix) / width).ceil() implemented using Integer Arithmetic
392 ((msg.chars().count() + prefix_len) + (width - 1) as usize) / width as usize
393}
394
395pub fn count_lines_per_msg_vec(width: u16, msgs: &[String], prefix_len: usize) -> usize {
396 msgs.iter()
397 .map(|msg| count_lines_per_msg(width, msg, prefix_len))
398 .sum()
399}
400
401#[cfg(test)] 387#[cfg(test)]
402mod tests { 388mod tests {
403 use super::*; 389 use super::*;
diff --git a/src/lib/cli_interactor.rs b/src/lib/cli_interactor.rs
index 4cf6357..dcaccf1 100644
--- a/src/lib/cli_interactor.rs
+++ b/src/lib/cli_interactor.rs
@@ -1,5 +1,6 @@
1use anyhow::{Context, Result}; 1use anyhow::{Context, Result};
2use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password}; 2use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password};
3use indicatif::TermLike;
3#[cfg(test)] 4#[cfg(test)]
4use mockall::*; 5use mockall::*;
5 6
@@ -184,3 +185,50 @@ impl PromptMultiChoiceParms {
184 self 185 self
185 } 186 }
186} 187}
188
189#[derive(Debug, Default)]
190pub struct Printer {
191 printed_lines: Vec<String>,
192}
193impl Printer {
194 pub fn println(&mut self, line: String) {
195 eprintln!("{line}");
196 self.printed_lines.push(line);
197 }
198 pub fn println_with_custom_formatting(
199 &mut self,
200 line: String,
201 line_without_formatting: String,
202 ) {
203 eprintln!("{line}");
204 self.printed_lines.push(line_without_formatting);
205 }
206 pub fn printlns(&mut self, lines: Vec<String>) {
207 for line in lines {
208 self.println(line);
209 }
210 }
211 pub fn clear_all(&mut self) {
212 let term = console::Term::stderr();
213 let _ = term.clear_last_lines(count_lines_per_msg_vec(
214 term.width(),
215 &self.printed_lines,
216 0,
217 ));
218 self.printed_lines.drain(..);
219 }
220}
221
222pub fn count_lines_per_msg(width: u16, msg: &str, prefix_len: usize) -> usize {
223 if width == 0 {
224 return 1;
225 }
226 // ((msg_len+prefix) / width).ceil() implemented using Integer Arithmetic
227 ((msg.chars().count() + prefix_len) + (width - 1) as usize) / width as usize
228}
229
230pub fn count_lines_per_msg_vec(width: u16, msgs: &[String], prefix_len: usize) -> usize {
231 msgs.iter()
232 .map(|msg| count_lines_per_msg(width, msg, prefix_len))
233 .sum()
234}
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 455725c..59e17f2 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -53,6 +53,7 @@ pub struct Client {
53 fallback_relays: Vec<String>, 53 fallback_relays: Vec<String>,
54 more_fallback_relays: Vec<String>, 54 more_fallback_relays: Vec<String>,
55 blaster_relays: Vec<String>, 55 blaster_relays: Vec<String>,
56 fallback_signer_relays: Vec<String>,
56} 57}
57 58
58#[cfg_attr(test, automock)] 59#[cfg_attr(test, automock)]
@@ -66,6 +67,7 @@ pub trait Connect {
66 fn get_fallback_relays(&self) -> &Vec<String>; 67 fn get_fallback_relays(&self) -> &Vec<String>;
67 fn get_more_fallback_relays(&self) -> &Vec<String>; 68 fn get_more_fallback_relays(&self) -> &Vec<String>;
68 fn get_blaster_relays(&self) -> &Vec<String>; 69 fn get_blaster_relays(&self) -> &Vec<String>;
70 fn get_fallback_signer_relays(&self) -> &Vec<String>;
69 async fn send_event_to( 71 async fn send_event_to(
70 &self, 72 &self,
71 git_repo_path: &Path, 73 git_repo_path: &Path,
@@ -132,6 +134,13 @@ impl Connect for Client {
132 } else { 134 } else {
133 vec!["wss://nostr.mutinywallet.com".to_string()] 135 vec!["wss://nostr.mutinywallet.com".to_string()]
134 }; 136 };
137
138 let fallback_signer_relays: Vec<String> = if std::env::var("NGITTEST").is_ok() {
139 vec!["ws://localhost:8051".to_string()]
140 } else {
141 vec!["wss://relay.nsec.app".to_string()]
142 };
143
135 Client { 144 Client {
136 client: nostr_sdk::ClientBuilder::new() 145 client: nostr_sdk::ClientBuilder::new()
137 .opts(Options::new().relay_limits(RelayLimits::disable())) 146 .opts(Options::new().relay_limits(RelayLimits::disable()))
@@ -139,6 +148,7 @@ impl Connect for Client {
139 fallback_relays, 148 fallback_relays,
140 more_fallback_relays, 149 more_fallback_relays,
141 blaster_relays, 150 blaster_relays,
151 fallback_signer_relays,
142 } 152 }
143 } 153 }
144 fn new(opts: Params) -> Self { 154 fn new(opts: Params) -> Self {
@@ -153,6 +163,7 @@ impl Connect for Client {
153 fallback_relays: opts.fallback_relays, 163 fallback_relays: opts.fallback_relays,
154 more_fallback_relays: opts.more_fallback_relays, 164 more_fallback_relays: opts.more_fallback_relays,
155 blaster_relays: opts.blaster_relays, 165 blaster_relays: opts.blaster_relays,
166 fallback_signer_relays: opts.fallback_signer_relays,
156 } 167 }
157 } 168 }
158 169
@@ -198,6 +209,10 @@ impl Connect for Client {
198 &self.blaster_relays 209 &self.blaster_relays
199 } 210 }
200 211
212 fn get_fallback_signer_relays(&self) -> &Vec<String> {
213 &self.fallback_signer_relays
214 }
215
201 async fn send_event_to( 216 async fn send_event_to(
202 &self, 217 &self,
203 git_repo_path: &Path, 218 git_repo_path: &Path,
@@ -629,6 +644,7 @@ pub struct Params {
629 pub fallback_relays: Vec<String>, 644 pub fallback_relays: Vec<String>,
630 pub more_fallback_relays: Vec<String>, 645 pub more_fallback_relays: Vec<String>,
631 pub blaster_relays: Vec<String>, 646 pub blaster_relays: Vec<String>,
647 pub fallback_signer_relays: Vec<String>,
632} 648}
633 649
634fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> { 650fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> {
diff --git a/src/lib/login/mod.rs b/src/lib/login/mod.rs
index b6e7623..825ec30 100644
--- a/src/lib/login/mod.rs
+++ b/src/lib/login/mod.rs
@@ -1,14 +1,19 @@
1use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; 1use std::{collections::HashSet, path::Path, str::FromStr, sync::Arc, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use console::Style;
5use dialoguer::theme::{ColorfulTheme, Theme};
4use nostr::{ 6use nostr::{
5 nips::{nip05, nip46::NostrConnectURI}, 7 nips::{nip05, nip46::NostrConnectURI},
6 PublicKey, 8 PublicKey,
7}; 9};
8use nostr_sdk::{ 10use nostr_sdk::{
9 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32, 11 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32,
12 Url,
10}; 13};
11use nostr_signer::Nip46Signer; 14use nostr_signer::Nip46Signer;
15use qrcode::QrCode;
16use tokio::sync::{oneshot, Mutex};
12 17
13#[cfg(not(test))] 18#[cfg(not(test))]
14use crate::client::Client; 19use crate::client::Client;
@@ -16,7 +21,8 @@ use crate::client::Client;
16use crate::client::MockConnect; 21use crate::client::MockConnect;
17use crate::{ 22use crate::{
18 cli_interactor::{ 23 cli_interactor::{
19 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, 24 Interactor, InteractorPrompt, Printer, PromptConfirmParms, PromptInputParms,
25 PromptPasswordParms,
20 }, 26 },
21 client::{fetch_public_key, get_event_from_global_cache, Connect}, 27 client::{fetch_public_key, get_event_from_global_cache, Connect},
22 git::{Repo, RepoActions}, 28 git::{Repo, RepoActions},
@@ -356,60 +362,198 @@ async fn fresh_login(
356 #[cfg(not(test))] client: Option<&Client>, 362 #[cfg(not(test))] client: Option<&Client>,
357 always_save: bool, 363 always_save: bool,
358) -> Result<(NostrSigner, UserRef)> { 364) -> Result<(NostrSigner, UserRef)> {
359 let mut public_key: Option<PublicKey> = None; 365 let app_key = Keys::generate();
360 // prompt for nsec 366 let app_key_secret = app_key.secret_key()?.to_secret_hex();
361 let mut prompt = "login with nostr address / nsec"; 367 let relays = if let Some(client) = client {
362 let signer = loop { 368 client
363 let input = Interactor::default() 369 .get_fallback_signer_relays()
364 .input(PromptInputParms::default().with_prompt(prompt)) 370 .iter()
365 .context("failed to get nsec input from interactor")?; 371 .flat_map(|s| Url::parse(s))
366 if let Ok(keys) = nostr::Keys::from_str(&input) { 372 .collect::<Vec<Url>>()
367 if let Err(error) = save_keys(git_repo, &keys, always_save) { 373 } else {
368 eprintln!("{error}"); 374 vec![]
375 };
376 let offline = client.is_none();
377 let nostr_connect_url = NostrConnectURI::client(app_key.public_key(), relays.clone(), "ngit");
378 let qr = generate_qr(&nostr_connect_url.to_string())?;
379
380 let printer = Arc::new(Mutex::new(Printer::default()));
381 if !offline {
382 let printer_clone = Arc::clone(&printer);
383 let mut printer_locked = printer_clone.lock().await;
384 printer_locked.printlns(qr);
385 printer_locked.println(format!(
386 "scan QR or paste into remote signer: {nostr_connect_url}"
387 ));
388 printer_locked.println_with_custom_formatting(
389 {
390 let mut s = String::new();
391 let _ = ColorfulTheme::default().format_confirm_prompt(
392 &mut s,
393 "login with nsec / bunker url / nostr address instead",
394 Some(true),
395 );
396 s
397 },
398 "? login with nsec / bunker url / nostr address instead? (y/n) › yes".to_string(),
399 );
400 }
401
402 let (tx, rx) = oneshot::channel();
403 let printer_clone = Arc::clone(&printer);
404
405 let qr_listener = tokio::spawn(async move {
406 if offline {
407 return;
408 }
409 if let Ok(nip46_signer) = Nip46Signer::new(
410 nostr_connect_url.clone(),
411 app_key.clone(),
412 Duration::from_secs(10 * 60),
413 None,
414 )
415 .await
416 {
417 let signer = NostrSigner::nip46(nip46_signer);
418 if let Ok(pub_key) = fetch_public_key(&signer).await {
419 let mut printer_locked = printer_clone.lock().await;
420 printer_locked.clear_all();
421
422 printer_locked.println_with_custom_formatting(
423 format!(
424 "{}",
425 Style::new().bold().apply_to("connected to remote signer"),
426 ),
427 "connected to remote signer".to_string(),
428 );
429 printer_locked.println("press any key to continue...".to_string());
430 let _ = tx.send(Some((signer, pub_key)));
369 } 431 }
370 break NostrSigner::Keys(keys);
371 } 432 }
372 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) { 433 });
373 uri 434 if !offline {
374 } else if input.contains('@') { 435 let _ = console::Term::stderr().read_char();
375 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await { 436 }
376 uri 437 qr_listener.abort();
377 } else { 438 let printer_clone = Arc::clone(&printer);
378 prompt = "failed. try again with nostr address / bunker uri / nsec"; 439 let mut printer = printer_clone.lock().await;
379 continue; 440 printer.clear_all();
441
442 let (signer, public_key) = {
443 if let Ok(Some((signer, public_key))) = rx.await {
444 let bunker_url = NostrConnectURI::Bunker {
445 signer_public_key: public_key,
446 relays: relays.clone(),
447 secret: None,
448 };
449 if let Err(error) = save_bunker(
450 git_repo,
451 &public_key,
452 &bunker_url.to_string(),
453 &app_key_secret,
454 always_save,
455 ) {
456 eprintln!("{error}");
380 } 457 }
458 (signer, public_key)
381 } else { 459 } else {
382 prompt = "invalid. try again with nostr address / bunker uri / nsec"; 460 let mut public_key: Option<PublicKey> = None;
383 continue; 461 // prompt for nsec
384 }; 462 let mut prompt = "login with nsec / bunker url / nostr address";
385 let app_key = Keys::generate().secret_key()?.to_secret_hex(); 463 let signer = loop {
386 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key).await { 464 let input = Interactor::default()
387 Ok(signer) => { 465 .input(PromptInputParms::default().with_prompt(prompt))
388 let pub_key = fetch_public_key(&signer).await?; 466 .context("failed to get nsec input from interactor")?;
389 if let Err(error) = 467 if let Ok(keys) = nostr::Keys::from_str(&input) {
390 save_bunker(git_repo, &pub_key, &uri.to_string(), &app_key, always_save) 468 if let Err(error) = save_keys(git_repo, &keys, always_save) {
391 { 469 eprintln!("{error}");
392 eprintln!("{error}"); 470 }
471 break NostrSigner::Keys(keys);
393 } 472 }
394 public_key = Some(pub_key); 473 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) {
395 break signer; 474 uri
396 } 475 } else if input.contains('@') {
397 Err(_) => { 476 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await {
398 prompt = "failed. try again with nostr address / bunker uri / nsec"; 477 uri
399 } 478 } else {
479 prompt = "failed. try again with nostr address / bunker uri / nsec";
480 continue;
481 }
482 } else {
483 prompt = "invalid. try again with nostr address / bunker uri / nsec";
484 continue;
485 };
486 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key_secret).await {
487 Ok(signer) => {
488 let pub_key = fetch_public_key(&signer).await?;
489 if let Err(error) = save_bunker(
490 git_repo,
491 &pub_key,
492 &uri.to_string(),
493 &app_key_secret,
494 always_save,
495 ) {
496 eprintln!("{error}");
497 }
498 public_key = Some(pub_key);
499 break signer;
500 }
501 Err(_) => {
502 prompt = "failed. try again with nostr address / bunker uri / nsec";
503 }
504 }
505 };
506 let public_key = if let Some(public_key) = public_key {
507 public_key
508 } else {
509 signer.public_key().await?
510 };
511 (signer, public_key)
400 } 512 }
401 }; 513 };
402 let public_key = if let Some(public_key) = public_key {
403 public_key
404 } else {
405 signer.public_key().await?
406 };
407 // lookup profile 514 // lookup profile
408 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?; 515 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?;
409 print_logged_in_as(&user_ref, client.is_none())?; 516 print_logged_in_as(&user_ref, client.is_none())?;
410 Ok((signer, user_ref)) 517 Ok((signer, user_ref))
411} 518}
412 519
520fn generate_qr(data: &str) -> Result<Vec<String>> {
521 let mut lines = vec![];
522 let qr =
523 QrCode::new(data.as_bytes()).context("failed to create QR of nostrconnect login url")?;
524 let colors = qr.to_colors();
525 let rows: Vec<&[qrcode::Color]> = colors.chunks(qr.width()).collect();
526 for (row, data) in rows.iter().enumerate() {
527 let odd = row % 2 != 0;
528 if odd {
529 continue;
530 }
531 let mut line = String::new();
532 for (col, color) in data.iter().enumerate() {
533 let top = color;
534 let mut bottom = qrcode::Color::Light;
535 if let Some(next_row_data) = rows.get(row + 1) {
536 if let Some(color) = next_row_data.get(col) {
537 bottom = *color;
538 }
539 }
540 line.push(if *top == qrcode::Color::Dark {
541 if bottom == qrcode::Color::Dark {
542 '█'
543 } else {
544 '▀'
545 }
546 } else if bottom == qrcode::Color::Dark {
547 '▄'
548 } else {
549 ' '
550 });
551 }
552 lines.push(line);
553 }
554 Ok(lines)
555}
556
413pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> { 557pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> {
414 let term = console::Term::stderr(); 558 let term = console::Term::stderr();
415 term.write_line("contacting login service provider...")?; 559 term.write_line("contacting login service provider...")?;
@@ -447,7 +591,7 @@ fn save_bunker(
447 { 591 {
448 let global = !Interactor::default().confirm( 592 let global = !Interactor::default().confirm(
449 PromptConfirmParms::default() 593 PromptConfirmParms::default()
450 .with_prompt("just for this repository?") 594 .with_prompt("save login just for this repository?")
451 .with_default(false), 595 .with_default(false),
452 )?; 596 )?;
453 let npub = public_key.to_bech32()?; 597 let npub = public_key.to_bech32()?;
@@ -523,6 +667,7 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
523 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?; 667 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?;
524 } 668 }
525 } else { 669 } else {
670 eprintln!("{error}");
526 Err(error)?; 671 Err(error)?;
527 } 672 }
528 }; 673 };
diff --git a/tests/ngit/login.rs b/tests/ngit/login.rs
index 3bcfbf9..477b25b 100644
--- a/tests/ngit/login.rs
+++ b/tests/ngit/login.rs
@@ -3,7 +3,7 @@ use git::GitTestRepo;
3use serial_test::serial; 3use serial_test::serial;
4use test_utils::*; 4use test_utils::*;
5 5
6static EXPECTED_NSEC_PROMPT: &str = "login with nostr address / nsec"; 6static EXPECTED_NSEC_PROMPT: &str = "login with nsec / bunker url / nostr address";
7static EXPECTED_LOCAL_REPOSITORY_PROMPT: &str = "just for this repository?"; 7static EXPECTED_LOCAL_REPOSITORY_PROMPT: &str = "just for this repository?";
8static EXPECTED_REQUIRE_PASSWORD_PROMPT: &str = "require password?"; 8static EXPECTED_REQUIRE_PASSWORD_PROMPT: &str = "require password?";
9static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password"; 9static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password";
@@ -29,6 +29,18 @@ fn standard_first_time_login_encrypting_nsec() -> Result<CliTester> {
29 p.expect_end_eventually()?; 29 p.expect_end_eventually()?;
30 Ok(p) 30 Ok(p)
31} 31}
32
33fn expect_qr_prompt_opt_for_other_methods(p: &mut CliTester) -> Result<()> {
34 p.expect_eventually("scan QR or paste into remote signer")?;
35 p.expect_eventually("\r\n")?;
36 p.expect_eventually("login with nsec / bunker url / nostr address instead")?;
37 p.expect_eventually("\r\n")?;
38 p.send_line("")?;
39 // p.expect_eventually("\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r")?;
40 // p.expect_eventually("\r\r\r\r\r\r\r\r\r\r\r\r\r")?;
41
42 Ok(())
43}
32mod with_relays { 44mod with_relays {
33 use anyhow::Ok; 45 use anyhow::Ok;
34 use futures::join; 46 use futures::join;
@@ -61,8 +73,8 @@ mod with_relays {
61 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 73 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
62 let test_repo = GitTestRepo::default(); 74 let test_repo = GitTestRepo::default();
63 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); 75 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]);
64 76 expect_qr_prompt_opt_for_other_methods(&mut p)?;
65 p.expect_input(EXPECTED_NSEC_PROMPT)? 77 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
66 .succeeds_with(TEST_KEY_1_NSEC)?; 78 .succeeds_with(TEST_KEY_1_NSEC)?;
67 79
68 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))? 80 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))?
@@ -102,7 +114,8 @@ mod with_relays {
102 let test_repo = GitTestRepo::default(); 114 let test_repo = GitTestRepo::default();
103 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); 115 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]);
104 116
105 p.expect_input(EXPECTED_NSEC_PROMPT)? 117 expect_qr_prompt_opt_for_other_methods(&mut p)?;
118 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
106 .succeeds_with(TEST_KEY_1_NSEC)?; 119 .succeeds_with(TEST_KEY_1_NSEC)?;
107 120
108 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))? 121 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))?
@@ -591,7 +604,8 @@ mod with_relays {
591 let test_repo = GitTestRepo::default(); 604 let test_repo = GitTestRepo::default();
592 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); 605 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]);
593 606
594 p.expect_input(EXPECTED_NSEC_PROMPT)? 607 expect_qr_prompt_opt_for_other_methods(&mut p)?;
608 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
595 .succeeds_with(TEST_KEY_1_NSEC)?; 609 .succeeds_with(TEST_KEY_1_NSEC)?;
596 610
597 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))? 611 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))?
@@ -654,7 +668,8 @@ mod with_relays {
654 let test_repo = GitTestRepo::default(); 668 let test_repo = GitTestRepo::default();
655 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); 669 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]);
656 670
657 p.expect_input(EXPECTED_NSEC_PROMPT)? 671 expect_qr_prompt_opt_for_other_methods(&mut p)?;
672 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
658 .succeeds_with(TEST_KEY_1_NSEC)?; 673 .succeeds_with(TEST_KEY_1_NSEC)?;
659 674
660 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))? 675 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))?
@@ -777,7 +792,8 @@ mod with_relays {
777 let test_repo = GitTestRepo::default(); 792 let test_repo = GitTestRepo::default();
778 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); 793 let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]);
779 794
780 p.expect_input(EXPECTED_NSEC_PROMPT)? 795 expect_qr_prompt_opt_for_other_methods(&mut p)?;
796 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
781 .succeeds_with(TEST_KEY_1_NSEC)?; 797 .succeeds_with(TEST_KEY_1_NSEC)?;
782 798
783 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))? 799 p.expect_confirm(EXPECTED_LOCAL_REPOSITORY_PROMPT, Some(false))?
@@ -1124,12 +1140,14 @@ mod with_offline_flag {
1124 use super::*; 1140 use super::*;
1125 1141
1126 #[test] 1142 #[test]
1143 #[serial]
1127 // combined into a single test as it is computationally expensive to run 1144 // combined into a single test as it is computationally expensive to run
1128 fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds() 1145 fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds()
1129 -> Result<()> { 1146 -> Result<()> {
1130 let test_repo = GitTestRepo::default(); 1147 let test_repo = GitTestRepo::default();
1131 let mut p = 1148 let mut p =
1132 CliTester::new_with_timeout_from_dir(15000, &test_repo.dir, ["login", "--offline"]); 1149 CliTester::new_with_timeout_from_dir(15000, &test_repo.dir, ["login", "--offline"]);
1150
1133 p.expect_input(EXPECTED_NSEC_PROMPT)? 1151 p.expect_input(EXPECTED_NSEC_PROMPT)?
1134 .succeeds_with(TEST_KEY_1_NSEC)?; 1152 .succeeds_with(TEST_KEY_1_NSEC)?;
1135 1153