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--src/bin/git_remote_nostr/list.rs6
-rw-r--r--src/bin/git_remote_nostr/main.rs13
-rw-r--r--src/bin/ngit/main.rs6
-rw-r--r--src/bin/ngit/sub_commands/apply.rs55
-rw-r--r--src/bin/ngit/sub_commands/checkout.rs55
-rw-r--r--src/bin/ngit/sub_commands/list.rs60
-rw-r--r--src/lib/client.rs126
-rw-r--r--src/lib/list.rs110
8 files changed, 354 insertions, 77 deletions
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs
index 7753ba1..4a7c1ec 100644
--- a/src/bin/git_remote_nostr/list.rs
+++ b/src/bin/git_remote_nostr/list.rs
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
4use client::get_state_from_cache; 4use client::get_state_from_cache;
5use git::RepoActions; 5use git::RepoActions;
6use ngit::{ 6use ngit::{
7 client, 7 client::{self, is_verbose},
8 fetch::fetch_from_git_server, 8 fetch::fetch_from_git_server,
9 git::{self}, 9 git::{self},
10 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value}, 10 git_events::{KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, tag_value},
@@ -27,7 +27,9 @@ pub async fn run_list(
27 27
28 let term = console::Term::stderr(); 28 let term = console::Term::stderr();
29 29
30 term.write_line("git servers: listing refs...")?; 30 if is_verbose() {
31 term.write_line("git servers: listing refs...")?;
32 }
31 let remote_states = list_from_remotes( 33 let remote_states = list_from_remotes(
32 &term, 34 &term,
33 git_repo, 35 git_repo,
diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs
index b26981d..3663d5b 100644
--- a/src/bin/git_remote_nostr/main.rs
+++ b/src/bin/git_remote_nostr/main.rs
@@ -12,7 +12,7 @@ use std::{
12}; 12};
13 13
14use anyhow::{Context, Result, bail}; 14use anyhow::{Context, Result, bail};
15use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache}; 15use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose};
16use git::{RepoActions, nostr_url::NostrUrlDecoded}; 16use git::{RepoActions, nostr_url::NostrUrlDecoded};
17use ngit::{ 17use ngit::{
18 client::{self, Params}, 18 client::{self, Params},
@@ -156,7 +156,10 @@ async fn fetching_with_report_for_helper(
156 trusted_maintainer_coordinate: &Nip19Coordinate, 156 trusted_maintainer_coordinate: &Nip19Coordinate,
157) -> Result<()> { 157) -> Result<()> {
158 let term = console::Term::stderr(); 158 let term = console::Term::stderr();
159 term.write_line("nostr: fetching...")?; 159 let verbose = is_verbose();
160 if verbose {
161 term.write_line("nostr: fetching...")?;
162 }
160 let (relay_reports, progress_reporter) = client 163 let (relay_reports, progress_reporter) = client
161 .fetch_all( 164 .fetch_all(
162 Some(git_repo_path), 165 Some(git_repo_path),
@@ -166,10 +169,12 @@ async fn fetching_with_report_for_helper(
166 .await?; 169 .await?;
167 let report = consolidate_fetch_reports(relay_reports); 170 let report = consolidate_fetch_reports(relay_reports);
168 if report.to_string().is_empty() { 171 if report.to_string().is_empty() {
169 term.write_line("nostr: no updates")?; 172 if verbose {
173 term.write_line("nostr: no updates")?;
174 }
170 } else { 175 } else {
171 term.write_line(&format!("nostr updates: {report}"))?; 176 term.write_line(&format!("nostr updates: {report}"))?;
172 } 177 }
173 progress_reporter.clear()?; 178 let _ = progress_reporter.clear();
174 Ok(()) 179 Ok(())
175} 180}
diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs
index 65f6bca..77ef955 100644
--- a/src/bin/ngit/main.rs
+++ b/src/bin/ngit/main.rs
@@ -15,10 +15,6 @@ use ngit::{
15 15
16mod sub_commands; 16mod sub_commands;
17 17
18fn is_verbose() -> bool {
19 std::env::var("NGIT_VERBOSE").is_ok() || std::env::var("NGIT_TEST").is_ok()
20}
21
22#[tokio::main] 18#[tokio::main]
23async fn main() { 19async fn main() {
24 let cli = Cli::parse(); 20 let cli = Cli::parse();
@@ -29,7 +25,7 @@ async fn main() {
29 std::env::set_var("NGIT_INTERACTIVE_MODE", "1"); 25 std::env::set_var("NGIT_INTERACTIVE_MODE", "1");
30 } 26 }
31 27
32 if cli.verbose || std::env::var("NGIT_TEST").is_ok() { 28 if cli.verbose || std::env::var("NGITTEST").is_ok() {
33 std::env::set_var("NGIT_VERBOSE", "1"); 29 std::env::set_var("NGIT_VERBOSE", "1");
34 } 30 }
35 31
diff --git a/src/bin/ngit/sub_commands/apply.rs b/src/bin/ngit/sub_commands/apply.rs
index 4ed6caa..2d5d8d5 100644
--- a/src/bin/ngit/sub_commands/apply.rs
+++ b/src/bin/ngit/sub_commands/apply.rs
@@ -1,9 +1,11 @@
1use std::{ 1use std::{
2 io::Write, 2 io::Write,
3 process::{Command, Stdio}, 3 process::{Command, Stdio},
4 time::Duration,
4}; 5};
5 6
6use anyhow::{Context, Result, bail}; 7use anyhow::{Context, Result, bail};
8use indicatif::{ProgressBar, ProgressStyle};
7use ngit::{ 9use ngit::{
8 client::get_all_proposal_patch_pr_pr_update_events_from_cache, 10 client::get_all_proposal_patch_pr_pr_update_events_from_cache,
9 git_events::get_pr_tip_event_or_most_recent_patch_with_ancestors, 11 git_events::get_pr_tip_event_or_most_recent_patch_with_ancestors,
@@ -18,16 +20,55 @@ use crate::{
18}; 20};
19 21
20fn run_git_fetch(remote_name: &str) -> Result<()> { 22fn run_git_fetch(remote_name: &str) -> Result<()> {
21 println!("fetching from {remote_name}..."); 23 let verbose = ngit::client::is_verbose();
22 let exit_status = Command::new("git") 24 if verbose {
25 println!("fetching from {remote_name}...");
26 }
27
28 let spinner = if verbose {
29 None
30 } else {
31 let pb = ProgressBar::new_spinner()
32 .with_style(
33 ProgressStyle::with_template("{spinner} {msg}")
34 .unwrap()
35 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
36 )
37 .with_message(format!("Fetching from {remote_name}..."));
38 pb.enable_steady_tick(Duration::from_millis(100));
39 Some(pb)
40 };
41
42 let output = Command::new("git")
23 .args(["fetch", remote_name]) 43 .args(["fetch", remote_name])
24 .stdout(Stdio::inherit()) 44 .stdout(if verbose {
25 .stderr(Stdio::inherit()) 45 Stdio::inherit()
26 .status() 46 } else {
47 Stdio::piped()
48 })
49 .stderr(if verbose {
50 Stdio::inherit()
51 } else {
52 Stdio::piped()
53 })
54 .output()
27 .context("failed to run git fetch")?; 55 .context("failed to run git fetch")?;
28 56
29 if !exit_status.success() { 57 if let Some(spinner) = spinner {
30 bail!("git fetch {remote_name} exited with error: {exit_status}"); 58 spinner.finish_and_clear();
59 }
60
61 if !output.status.success() {
62 if !verbose {
63 let stderr = String::from_utf8_lossy(&output.stderr);
64 if !stderr.is_empty() {
65 eprintln!("{stderr}");
66 }
67 }
68 bail!(
69 "git fetch {remote_name} exited with error: {}",
70 output.status
71 );
31 } 72 }
32 Ok(()) 73 Ok(())
33} 74}
diff --git a/src/bin/ngit/sub_commands/checkout.rs b/src/bin/ngit/sub_commands/checkout.rs
index 6ded778..2fc9a09 100644
--- a/src/bin/ngit/sub_commands/checkout.rs
+++ b/src/bin/ngit/sub_commands/checkout.rs
@@ -1,9 +1,11 @@
1use std::{ 1use std::{
2 collections::HashSet, 2 collections::HashSet,
3 process::{Command, Stdio}, 3 process::{Command, Stdio},
4 time::Duration,
4}; 5};
5 6
6use anyhow::{Context, Result, bail}; 7use anyhow::{Context, Result, bail};
8use indicatif::{ProgressBar, ProgressStyle};
7use ngit::{ 9use ngit::{
8 client::{ 10 client::{
9 Params, get_all_proposal_patch_pr_pr_update_events_from_cache, 11 Params, get_all_proposal_patch_pr_pr_update_events_from_cache,
@@ -97,16 +99,55 @@ pub async fn launch(id: &str) -> Result<()> {
97} 99}
98 100
99fn run_git_fetch(remote_name: &str) -> Result<()> { 101fn run_git_fetch(remote_name: &str) -> Result<()> {
100 println!("fetching from {remote_name}..."); 102 let verbose = ngit::client::is_verbose();
101 let exit_status = Command::new("git") 103 if verbose {
104 println!("fetching from {remote_name}...");
105 }
106
107 let spinner = if verbose {
108 None
109 } else {
110 let pb = ProgressBar::new_spinner()
111 .with_style(
112 ProgressStyle::with_template("{spinner} {msg}")
113 .unwrap()
114 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
115 )
116 .with_message(format!("Fetching from {remote_name}..."));
117 pb.enable_steady_tick(Duration::from_millis(100));
118 Some(pb)
119 };
120
121 let output = Command::new("git")
102 .args(["fetch", remote_name]) 122 .args(["fetch", remote_name])
103 .stdout(Stdio::inherit()) 123 .stdout(if verbose {
104 .stderr(Stdio::inherit()) 124 Stdio::inherit()
105 .status() 125 } else {
126 Stdio::piped()
127 })
128 .stderr(if verbose {
129 Stdio::inherit()
130 } else {
131 Stdio::piped()
132 })
133 .output()
106 .context("failed to run git fetch")?; 134 .context("failed to run git fetch")?;
107 135
108 if !exit_status.success() { 136 if let Some(spinner) = spinner {
109 bail!("git fetch {remote_name} exited with error: {exit_status}"); 137 spinner.finish_and_clear();
138 }
139
140 if !output.status.success() {
141 if !verbose {
142 let stderr = String::from_utf8_lossy(&output.stderr);
143 if !stderr.is_empty() {
144 eprintln!("{stderr}");
145 }
146 }
147 bail!(
148 "git fetch {remote_name} exited with error: {}",
149 output.status
150 );
110 } 151 }
111 Ok(()) 152 Ok(())
112} 153}
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs
index 68a10cc..ff43bd9 100644
--- a/src/bin/ngit/sub_commands/list.rs
+++ b/src/bin/ngit/sub_commands/list.rs
@@ -3,9 +3,11 @@ use std::{
3 io::Write, 3 io::Write,
4 ops::Add, 4 ops::Add,
5 process::{Command, Stdio}, 5 process::{Command, Stdio},
6 time::Duration,
6}; 7};
7 8
8use anyhow::{Context, Result, bail}; 9use anyhow::{Context, Result, bail};
10use indicatif::{ProgressBar, ProgressStyle};
9use ngit::{ 11use ngit::{
10 client::{ 12 client::{
11 Params, get_all_proposal_patch_pr_pr_update_events_from_cache, 13 Params, get_all_proposal_patch_pr_pr_update_events_from_cache,
@@ -39,16 +41,55 @@ use crate::{
39}; 41};
40 42
41fn run_git_fetch(remote_name: &str) -> Result<()> { 43fn run_git_fetch(remote_name: &str) -> Result<()> {
42 println!("fetching from {remote_name}..."); 44 let verbose = ngit::client::is_verbose();
43 let exit_status = Command::new("git") 45 if verbose {
46 println!("fetching from {remote_name}...");
47 }
48
49 let spinner = if verbose {
50 None
51 } else {
52 let pb = ProgressBar::new_spinner()
53 .with_style(
54 ProgressStyle::with_template("{spinner} {msg}")
55 .unwrap()
56 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
57 )
58 .with_message(format!("Fetching from {remote_name}..."));
59 pb.enable_steady_tick(Duration::from_millis(100));
60 Some(pb)
61 };
62
63 let output = Command::new("git")
44 .args(["fetch", remote_name]) 64 .args(["fetch", remote_name])
45 .stdout(Stdio::inherit()) 65 .stdout(if verbose {
46 .stderr(Stdio::inherit()) 66 Stdio::inherit()
47 .status() 67 } else {
68 Stdio::piped()
69 })
70 .stderr(if verbose {
71 Stdio::inherit()
72 } else {
73 Stdio::piped()
74 })
75 .output()
48 .context("failed to run git fetch")?; 76 .context("failed to run git fetch")?;
49 77
50 if !exit_status.success() { 78 if let Some(spinner) = spinner {
51 bail!("git fetch {remote_name} exited with error: {exit_status}"); 79 spinner.finish_and_clear();
80 }
81
82 if !output.status.success() {
83 if !verbose {
84 let stderr = String::from_utf8_lossy(&output.stderr);
85 if !stderr.is_empty() {
86 eprintln!("{stderr}");
87 }
88 }
89 bail!(
90 "git fetch {remote_name} exited with error: {}",
91 output.status
92 );
52 } 93 }
53 Ok(()) 94 Ok(())
54} 95}
@@ -297,10 +338,7 @@ fn show_proposal_details(
297 } 338 }
298 339
299 println!(); 340 println!();
300 println!( 341 println!("To checkout: ngit checkout {}", proposal.id);
301 "To checkout: ngit checkout {}",
302 proposal.id
303 );
304 println!("To apply: ngit apply {}", proposal.id); 342 println!("To apply: ngit apply {}", proposal.id);
305 343
306 Ok(()) 344 Ok(())
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 89fcaf7..583f01c 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -16,10 +16,10 @@ use std::{
16 fs::create_dir_all, 16 fs::create_dir_all,
17 path::Path, 17 path::Path,
18 sync::{ 18 sync::{
19 Arc, RwLock, 19 Arc, Mutex, RwLock,
20 atomic::{AtomicU64, Ordering}, 20 atomic::{AtomicU64, Ordering},
21 }, 21 },
22 time::Duration, 22 time::{Duration, Instant},
23}; 23};
24 24
25use anyhow::{Context, Result, anyhow, bail}; 25use anyhow::{Context, Result, anyhow, bail};
@@ -65,6 +65,65 @@ use crate::{
65 repo_state::RepoState, 65 repo_state::RepoState,
66}; 66};
67 67
68pub fn is_verbose() -> bool {
69 std::env::var("NGIT_VERBOSE").is_ok()
70}
71
72const SPINNER_EXPAND_DELAY_SECS: u64 = 5;
73
74struct SpinnerState {
75 spinner: ProgressBar,
76 start_time: Instant,
77 expanded_multi: Option<MultiProgress>,
78}
79
80impl SpinnerState {
81 fn new() -> Self {
82 let multi_progress = MultiProgress::new();
83 let spinner = multi_progress.add(
84 ProgressBar::new_spinner()
85 .with_style(
86 ProgressStyle::with_template("{spinner} {msg}")
87 .unwrap()
88 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
89 )
90 .with_message("Checking relays and git servers..."),
91 );
92 spinner.enable_steady_tick(Duration::from_millis(100));
93 Self {
94 spinner,
95 start_time: Instant::now(),
96 expanded_multi: None,
97 }
98 }
99
100 fn should_expand(&self) -> bool {
101 self.expanded_multi.is_none()
102 && self.start_time.elapsed().as_secs() >= SPINNER_EXPAND_DELAY_SECS
103 }
104
105 fn expand(&mut self) -> &MultiProgress {
106 if self.expanded_multi.is_none() {
107 self.spinner.finish_and_clear();
108 self.expanded_multi = Some(MultiProgress::new());
109 }
110 self.expanded_multi.as_ref().unwrap()
111 }
112
113 fn finish(&self, has_errors: bool) {
114 if has_errors {
115 if let Some(ref multi) = self.expanded_multi {
116 let _ = multi.clear();
117 }
118 } else {
119 self.spinner.finish_and_clear();
120 if let Some(ref multi) = self.expanded_multi {
121 let _ = multi.clear();
122 }
123 }
124 }
125}
126
68#[allow(clippy::struct_field_names)] 127#[allow(clippy::struct_field_names)]
69pub struct Client { 128pub struct Client {
70 client: nostr_sdk::Client, 129 client: nostr_sdk::Client,
@@ -370,14 +429,15 @@ impl Connect for Client {
370 ) 429 )
371 .await?; 430 .await?;
372 431
432 let verbose = is_verbose();
433 let spinner_state = if !verbose {
434 Some(Arc::new(Mutex::new(SpinnerState::new())))
435 } else {
436 None
437 };
373 let progress_reporter = MultiProgress::new(); 438 let progress_reporter = MultiProgress::new();
374 439
375 // Track successful relays for adaptive timeout (switch to SHORT when
376 // SUCCESS_THRESHOLD succeed)
377 let success_count = Arc::new(AtomicU64::new(0)); 440 let success_count = Arc::new(AtomicU64::new(0));
378
379 // Track current timeout value for progress bar display (starts at LONG,
380 // switches to SHORT)
381 let current_timeout = Arc::new(AtomicU64::new(long_timeout())); 441 let current_timeout = Arc::new(AtomicU64::new(long_timeout()));
382 442
383 let mut processed_relays = HashSet::new(); 443 let mut processed_relays = HashSet::new();
@@ -388,7 +448,6 @@ impl Connect for Client {
388 let relays = request 448 let relays = request
389 .repo_relays 449 .repo_relays
390 .union(&request.user_relays_for_profiles) 450 .union(&request.user_relays_for_profiles)
391 // don't look for events on blaster
392 .filter(|&r| !r.as_str().contains("nostr.mutinywallet.com")) 451 .filter(|&r| !r.as_str().contains("nostr.mutinywallet.com"))
393 .cloned() 452 .cloned()
394 .collect::<HashSet<RelayUrl>>() 453 .collect::<HashSet<RelayUrl>>()
@@ -412,12 +471,12 @@ impl Connect for Client {
412 let success_count_for_loop = success_count.clone(); 471 let success_count_for_loop = success_count.clone();
413 let current_timeout_for_loop = current_timeout.clone(); 472 let current_timeout_for_loop = current_timeout.clone();
414 let total_relays = relays.len() as u64; 473 let total_relays = relays.len() as u64;
474 let spinner_state_clone = spinner_state.clone();
415 475
416 let futures: Vec<_> = relays 476 let futures: Vec<_> = relays
417 .iter() 477 .iter()
418 .map(|r| { 478 .map(|r| {
419 if profile_relays_only.contains(r) { 479 if profile_relays_only.contains(r) {
420 // if relay isn't a repo relay, just filter for user profile
421 FetchRequest { 480 FetchRequest {
422 selected_relay: Some(r.to_owned()), 481 selected_relay: Some(r.to_owned()),
423 repo_coordinates_without_relays: vec![], 482 repo_coordinates_without_relays: vec![],
@@ -447,6 +506,8 @@ impl Connect for Client {
447 let current_timeout_clone = current_timeout_for_loop.clone(); 506 let current_timeout_clone = current_timeout_for_loop.clone();
448 let progress_reporter_clone = progress_reporter.clone(); 507 let progress_reporter_clone = progress_reporter.clone();
449 let total_relays_clone = total_relays; 508 let total_relays_clone = total_relays;
509 let spinner_state_for_task = spinner_state_clone.clone();
510 let verbose_for_task = verbose;
450 async move { 511 async move {
451 let relay_column_width = request.relay_column_width; 512 let relay_column_width = request.relay_column_width;
452 513
@@ -455,7 +516,7 @@ impl Connect for Client {
455 .clone() 516 .clone()
456 .context("fetch_all_from_relay called without a relay")?; 517 .context("fetch_all_from_relay called without a relay")?;
457 518
458 let pb = if std::env::var("NGITTEST").is_err() { 519 let pb = if verbose_for_task {
459 let pb = progress_reporter_clone.add( 520 let pb = progress_reporter_clone.add(
460 ProgressBar::new(1) 521 ProgressBar::new(1)
461 .with_prefix( 522 .with_prefix(
@@ -469,6 +530,26 @@ impl Connect for Client {
469 ); 530 );
470 pb.enable_steady_tick(Duration::from_millis(300)); 531 pb.enable_steady_tick(Duration::from_millis(300));
471 Some(pb) 532 Some(pb)
533 } else if let Some(ref state) = spinner_state_for_task {
534 let mut state = state.lock().unwrap();
535 if state.should_expand() {
536 let multi = state.expand().clone();
537 let pb = multi.add(
538 ProgressBar::new(1)
539 .with_prefix(
540 format!(
541 "{: <relay_column_width$} connecting",
542 &relay_url
543 )
544 .to_string(),
545 )
546 .with_style(pb_style(current_timeout_clone.clone())?),
547 );
548 pb.enable_steady_tick(Duration::from_millis(300));
549 Some(pb)
550 } else {
551 None
552 }
472 } else { 553 } else {
473 None 554 None
474 }; 555 };
@@ -508,36 +589,27 @@ impl Connect for Client {
508 bail!("{reason}"); 589 bail!("{reason}");
509 } 590 }
510 591
511 // Adaptive timeout using tokio::select!
512 // Start the fetch operation once and race it against an adaptive timeout
513 let pb_clone = pb.clone(); 592 let pb_clone = pb.clone();
514 let fetch_future = self.fetch_all_from_relay(git_repo_path, request, &pb_clone); 593 let fetch_future = self.fetch_all_from_relay(git_repo_path, request, &pb_clone);
515 tokio::pin!(fetch_future); 594 tokio::pin!(fetch_future);
516 595
517 // Create an adaptive timeout that switches from long to short
518 // when SUCCESS_THRESHOLD of relays succeed
519 let timeout_future = async { 596 let timeout_future = async {
520 // Poll for timeout or SUCCESS_THRESHOLD success threshold
521 let check_interval = Duration::from_millis(100); 597 let check_interval = Duration::from_millis(100);
522 let long_timeout_end = tokio::time::Instant::now() + Duration::from_secs(long_timeout()); 598 let long_timeout_end = tokio::time::Instant::now() + Duration::from_secs(long_timeout());
523 599
524 loop { 600 loop {
525 // Check if SUCCESS_THRESHOLD of relays have succeeded
526 let current_success_count = success_count_clone.load(Ordering::Relaxed); 601 let current_success_count = success_count_clone.load(Ordering::Relaxed);
527 let threshold = (total_relays_clone as f64 * SUCCESS_THRESHOLD).ceil() as u64; 602 let threshold = (total_relays_clone as f64 * SUCCESS_THRESHOLD).ceil() as u64;
528 603
529 if current_success_count >= threshold { 604 if current_success_count >= threshold {
530 // SUCCESS_THRESHOLD reached, switch to short timeout
531 tokio::time::sleep(Duration::from_secs(short_timeout())).await; 605 tokio::time::sleep(Duration::from_secs(short_timeout())).await;
532 return "short"; 606 return "short";
533 } 607 }
534 608
535 // Check if long timeout has expired
536 if tokio::time::Instant::now() >= long_timeout_end { 609 if tokio::time::Instant::now() >= long_timeout_end {
537 return "long"; 610 return "long";
538 } 611 }
539 612
540 // Sleep briefly before checking again
541 tokio::time::sleep(check_interval).await; 613 tokio::time::sleep(check_interval).await;
542 } 614 }
543 }; 615 };
@@ -546,11 +618,9 @@ impl Connect for Client {
546 let result = tokio::select! { 618 let result = tokio::select! {
547 result = &mut fetch_future => { 619 result = &mut fetch_future => {
548 if result.is_ok() { 620 if result.is_ok() {
549 // Increment success count
550 let new_count = success_count_clone.fetch_add(1, Ordering::Relaxed) + 1; 621 let new_count = success_count_clone.fetch_add(1, Ordering::Relaxed) + 1;
551 let threshold = (total_relays_clone as f64 * SUCCESS_THRESHOLD).ceil() as u64; 622 let threshold = (total_relays_clone as f64 * SUCCESS_THRESHOLD).ceil() as u64;
552 623
553 // If we've reached SUCCESS_THRESHOLD, update timeout display
554 if new_count >= threshold { 624 if new_count >= threshold {
555 current_timeout_clone.store(short_timeout(), Ordering::Relaxed); 625 current_timeout_clone.store(short_timeout(), Ordering::Relaxed);
556 } 626 }
@@ -565,7 +635,6 @@ impl Connect for Client {
565 635
566 match result { 636 match result {
567 Err(error) => { 637 Err(error) => {
568 // Check error for timeout/connection issues and add to skip list
569 if error.to_string().contains("connection timeout") || error.to_string().contains("timeout after") { 638 if error.to_string().contains("connection timeout") || error.to_string().contains("timeout after") {
570 self.skip_relay_for_session(relay_url.clone(), error.to_string()); 639 self.skip_relay_for_session(relay_url.clone(), error.to_string());
571 } 640 }
@@ -619,6 +688,12 @@ impl Connect for Client {
619 set 688 set
620 }; 689 };
621 } 690 }
691
692 if let Some(ref state) = spinner_state {
693 let has_errors = relay_reports.iter().any(Result::is_err);
694 state.lock().unwrap().finish(has_errors);
695 }
696
622 Ok((relay_reports, progress_reporter)) 697 Ok((relay_reports, progress_reporter))
623 } 698 }
624 699
@@ -2084,8 +2159,11 @@ pub async fn fetching_with_report(
2084 #[cfg(not(test))] client: &Client, 2159 #[cfg(not(test))] client: &Client,
2085 trusted_maintainer_coordinate: &Nip19Coordinate, 2160 trusted_maintainer_coordinate: &Nip19Coordinate,
2086) -> Result<FetchReport> { 2161) -> Result<FetchReport> {
2087 let term = console::Term::stderr(); 2162 let verbose = is_verbose();
2088 term.write_line("fetching updates...")?; 2163 if verbose {
2164 let term = console::Term::stderr();
2165 term.write_line("fetching updates...")?;
2166 }
2089 let (relay_reports, progress_reporter) = client 2167 let (relay_reports, progress_reporter) = client
2090 .fetch_all( 2168 .fetch_all(
2091 Some(git_repo_path), 2169 Some(git_repo_path),
diff --git a/src/lib/list.rs b/src/lib/list.rs
index ce8737c..3b37b37 100644
--- a/src/lib/list.rs
+++ b/src/lib/list.rs
@@ -3,10 +3,10 @@ use std::{
3 path::PathBuf, 3 path::PathBuf,
4 str::FromStr, 4 str::FromStr,
5 sync::{ 5 sync::{
6 Arc, 6 Arc, Mutex,
7 atomic::{AtomicU64, Ordering}, 7 atomic::{AtomicU64, Ordering},
8 }, 8 },
9 time::Duration, 9 time::{Duration, Instant},
10}; 10};
11 11
12use anyhow::{Result, anyhow}; 12use anyhow::{Result, anyhow};
@@ -16,6 +16,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle};
16use nostr::hashes::sha1::Hash as Sha1Hash; 16use nostr::hashes::sha1::Hash as Sha1Hash;
17 17
18use crate::{ 18use crate::{
19 client::is_verbose,
19 git::{ 20 git::{
20 Repo, RepoActions, 21 Repo, RepoActions,
21 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 22 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
@@ -28,6 +29,61 @@ use crate::{
28 }, 29 },
29}; 30};
30 31
32const SPINNER_EXPAND_DELAY_SECS: u64 = 5;
33
34struct GitSpinnerState {
35 spinner: ProgressBar,
36 start_time: Instant,
37 expanded_multi: Option<MultiProgress>,
38}
39
40impl GitSpinnerState {
41 fn new() -> Self {
42 let multi_progress = MultiProgress::new();
43 let spinner = multi_progress.add(
44 ProgressBar::new_spinner()
45 .with_style(
46 ProgressStyle::with_template("{spinner} {msg}")
47 .unwrap()
48 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
49 )
50 .with_message("Checking git servers..."),
51 );
52 spinner.enable_steady_tick(Duration::from_millis(100));
53 Self {
54 spinner,
55 start_time: Instant::now(),
56 expanded_multi: None,
57 }
58 }
59
60 fn should_expand(&self) -> bool {
61 self.expanded_multi.is_none()
62 && self.start_time.elapsed().as_secs() >= SPINNER_EXPAND_DELAY_SECS
63 }
64
65 fn expand(&mut self) -> &MultiProgress {
66 if self.expanded_multi.is_none() {
67 self.spinner.finish_and_clear();
68 self.expanded_multi = Some(MultiProgress::new());
69 }
70 self.expanded_multi.as_ref().unwrap()
71 }
72
73 fn finish(&self, has_errors: bool) {
74 if has_errors {
75 if let Some(ref multi) = self.expanded_multi {
76 let _ = multi.clear();
77 }
78 } else {
79 self.spinner.finish_and_clear();
80 if let Some(ref multi) = self.expanded_multi {
81 let _ = multi.clear();
82 }
83 }
84 }
85}
86
31/// Sync issues identified for a single remote 87/// Sync issues identified for a single remote
32#[derive(Default, Debug, Clone)] 88#[derive(Default, Debug, Clone)]
33pub struct RemoteIssues { 89pub struct RemoteIssues {
@@ -111,18 +167,18 @@ pub async fn list_from_remotes(
111 return HashMap::new(); 167 return HashMap::new();
112 } 168 }
113 169
114 let progress_reporter = if std::env::var("NGITTEST").is_err() { 170 let verbose = is_verbose();
115 MultiProgress::new() 171 let spinner_state = if !verbose {
172 Some(Arc::new(Mutex::new(GitSpinnerState::new())))
116 } else { 173 } else {
117 MultiProgress::with_draw_target(indicatif::ProgressDrawTarget::hidden()) 174 None
118 }; 175 };
176 let progress_reporter = MultiProgress::new();
119 177
120 // Track successful servers for adaptive timeout
121 let success_count = Arc::new(AtomicU64::new(0)); 178 let success_count = Arc::new(AtomicU64::new(0));
122 let current_timeout = Arc::new(AtomicU64::new(git_server_long_timeout())); 179 let current_timeout = Arc::new(AtomicU64::new(git_server_long_timeout()));
123 let total_servers = git_servers.len() as u64; 180 let total_servers = git_servers.len() as u64;
124 181
125 // Calculate column width for alignment
126 let server_column_width = git_servers 182 let server_column_width = git_servers
127 .iter() 183 .iter()
128 .map(|s| get_short_git_server_name(s).chars().count()) 184 .map(|s| get_short_git_server_name(s).chars().count())
@@ -139,11 +195,13 @@ pub async fn list_from_remotes(
139 let current_timeout_clone = current_timeout.clone(); 195 let current_timeout_clone = current_timeout.clone();
140 let progress_reporter_clone = progress_reporter.clone(); 196 let progress_reporter_clone = progress_reporter.clone();
141 let decoded_nostr_url = decoded_nostr_url.clone(); 197 let decoded_nostr_url = decoded_nostr_url.clone();
198 let spinner_state_clone = spinner_state.clone();
199 let verbose_for_task = verbose;
142 200
143 async move { 201 async move {
144 let server_name = get_short_git_server_name(&url); 202 let server_name = get_short_git_server_name(&url);
145 203
146 let pb = if std::env::var("NGITTEST").is_err() { 204 let pb = if verbose_for_task {
147 match git_server_pb_style(current_timeout_clone.clone()) { 205 match git_server_pb_style(current_timeout_clone.clone()) {
148 Ok(style) => { 206 Ok(style) => {
149 let pb = progress_reporter_clone.add( 207 let pb = progress_reporter_clone.add(
@@ -164,6 +222,28 @@ pub async fn list_from_remotes(
164 } 222 }
165 Err(_) => None, 223 Err(_) => None,
166 } 224 }
225 } else if let Some(ref spinner_state_arc) = spinner_state_clone {
226 let mut state = spinner_state_arc.lock().unwrap();
227 if state.should_expand() {
228 let multi = state.expand().clone();
229 let pb = multi.add(
230 ProgressBar::new(1)
231 .with_prefix(
232 console::style(format!(
233 "{: <server_column_width$} connecting",
234 &server_name
235 ))
236 .for_stderr()
237 .yellow()
238 .to_string(),
239 )
240 .with_style(git_server_pb_style(current_timeout_clone.clone()).unwrap()),
241 );
242 pb.enable_steady_tick(Duration::from_millis(300));
243 Some(pb)
244 } else {
245 None
246 }
167 } else { 247 } else {
168 None 248 None
169 }; 249 };
@@ -191,7 +271,6 @@ pub async fn list_from_remotes(
191 } 271 }
192 } 272 }
193 273
194 // Create the list operation future - spawn_blocking to avoid blocking async runtime
195 let git_repo_path = git_repo.get_path().ok().map(|p| p.to_path_buf()); 274 let git_repo_path = git_repo.get_path().ok().map(|p| p.to_path_buf());
196 let url_clone = url.clone(); 275 let url_clone = url.clone();
197 let decoded_nostr_url_clone = decoded_nostr_url.clone(); 276 let decoded_nostr_url_clone = decoded_nostr_url.clone();
@@ -199,7 +278,6 @@ pub async fn list_from_remotes(
199 278
200 let list_future = async move { 279 let list_future = async move {
201 match tokio::task::spawn_blocking(move || { 280 match tokio::task::spawn_blocking(move || {
202 // Re-open repo in blocking thread (git2::Repository is not Send)
203 let git_repo = match git_repo_path { 281 let git_repo = match git_repo_path {
204 Some(path) => Repo::from_path(&path).ok(), 282 Some(path) => Repo::from_path(&path).ok(),
205 None => None, 283 None => None,
@@ -274,11 +352,9 @@ pub async fn list_from_remotes(
274 Err((url, error)) 352 Err((url, error))
275 } 353 }
276 Ok(state) => { 354 Ok(state) => {
277 // Determine sync status message and styling using existing functions
278 let status_msg = if state.is_empty() { 355 let status_msg = if state.is_empty() {
279 "empty repository".to_string() 356 "empty repository".to_string()
280 } else if let Some(nostr_state) = nostr_state { 357 } else if let Some(nostr_state) = nostr_state {
281 // Use existing generate_remote_sync_warnings to get detailed status
282 let mut temp_states = HashMap::new(); 358 let mut temp_states = HashMap::new();
283 temp_states.insert(url.clone(), (state.clone(), is_grasp_server)); 359 temp_states.insert(url.clone(), (state.clone(), is_grasp_server));
284 let remote_issues = identify_remote_sync_issues(git_repo, nostr_state, &temp_states); 360 let remote_issues = identify_remote_sync_issues(git_repo, nostr_state, &temp_states);
@@ -287,7 +363,6 @@ pub async fn list_from_remotes(
287 if warnings.is_empty() { 363 if warnings.is_empty() {
288 "in sync".to_string() 364 "in sync".to_string()
289 } else { 365 } else {
290 // Extract the message after "WARNING: <server> "
291 let warning = &warnings[0]; 366 let warning = &warnings[0];
292 let server_name = get_short_git_server_name(&url); 367 let server_name = get_short_git_server_name(&url);
293 let prefix = format!("WARNING: {} ", server_name); 368 let prefix = format!("WARNING: {} ", server_name);
@@ -296,7 +371,6 @@ pub async fn list_from_remotes(
296 .to_string() 371 .to_string()
297 } 372 }
298 } else { 373 } else {
299 // No nostr state to compare against
300 "success".to_string() 374 "success".to_string()
301 }; 375 };
302 376
@@ -333,20 +407,22 @@ pub async fn list_from_remotes(
333 .await; 407 .await;
334 408
335 let mut remote_states = HashMap::new(); 409 let mut remote_states = HashMap::new();
336 let mut all_succeeded = true; 410 let mut has_errors = false;
337 for result in results { 411 for result in results {
338 match result { 412 match result {
339 Ok((url, state, is_grasp_server)) => { 413 Ok((url, state, is_grasp_server)) => {
340 remote_states.insert(url, (state, is_grasp_server)); 414 remote_states.insert(url, (state, is_grasp_server));
341 } 415 }
342 Err((url, error)) => { 416 Err((url, error)) => {
343 all_succeeded = false; 417 has_errors = true;
344 let _ = term.write_line(&format!("failed to list from {}: {}", url, error)); 418 let _ = term.write_line(&format!("failed to list from {}: {}", url, error));
345 } 419 }
346 } 420 }
347 } 421 }
348 422
349 if all_succeeded { 423 if let Some(ref spinner_state_arc) = spinner_state {
424 spinner_state_arc.lock().unwrap().finish(has_errors);
425 } else if !has_errors {
350 let _ = progress_reporter.clear(); 426 let _ = progress_reporter.clear();
351 } 427 }
352 428