diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/git_remote_nostr/list.rs | 6 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 13 | ||||
| -rw-r--r-- | src/bin/ngit/main.rs | 6 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/apply.rs | 55 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/checkout.rs | 55 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/list.rs | 60 | ||||
| -rw-r--r-- | src/lib/client.rs | 126 | ||||
| -rw-r--r-- | src/lib/list.rs | 110 |
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}; | |||
| 4 | use client::get_state_from_cache; | 4 | use client::get_state_from_cache; |
| 5 | use git::RepoActions; | 5 | use git::RepoActions; |
| 6 | use ngit::{ | 6 | use 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 | ||
| 14 | use anyhow::{Context, Result, bail}; | 14 | use anyhow::{Context, Result, bail}; |
| 15 | use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache}; | 15 | use client::{Connect, consolidate_fetch_reports, get_repo_ref_from_cache, is_verbose}; |
| 16 | use git::{RepoActions, nostr_url::NostrUrlDecoded}; | 16 | use git::{RepoActions, nostr_url::NostrUrlDecoded}; |
| 17 | use ngit::{ | 17 | use 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 | ||
| 16 | mod sub_commands; | 16 | mod sub_commands; |
| 17 | 17 | ||
| 18 | fn 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] |
| 23 | async fn main() { | 19 | async 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 @@ | |||
| 1 | use std::{ | 1 | use std::{ |
| 2 | io::Write, | 2 | io::Write, |
| 3 | process::{Command, Stdio}, | 3 | process::{Command, Stdio}, |
| 4 | time::Duration, | ||
| 4 | }; | 5 | }; |
| 5 | 6 | ||
| 6 | use anyhow::{Context, Result, bail}; | 7 | use anyhow::{Context, Result, bail}; |
| 8 | use indicatif::{ProgressBar, ProgressStyle}; | ||
| 7 | use ngit::{ | 9 | use 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 | ||
| 20 | fn run_git_fetch(remote_name: &str) -> Result<()> { | 22 | fn 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 @@ | |||
| 1 | use std::{ | 1 | use std::{ |
| 2 | collections::HashSet, | 2 | collections::HashSet, |
| 3 | process::{Command, Stdio}, | 3 | process::{Command, Stdio}, |
| 4 | time::Duration, | ||
| 4 | }; | 5 | }; |
| 5 | 6 | ||
| 6 | use anyhow::{Context, Result, bail}; | 7 | use anyhow::{Context, Result, bail}; |
| 8 | use indicatif::{ProgressBar, ProgressStyle}; | ||
| 7 | use ngit::{ | 9 | use 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 | ||
| 99 | fn run_git_fetch(remote_name: &str) -> Result<()> { | 101 | fn 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 | ||
| 8 | use anyhow::{Context, Result, bail}; | 9 | use anyhow::{Context, Result, bail}; |
| 10 | use indicatif::{ProgressBar, ProgressStyle}; | ||
| 9 | use ngit::{ | 11 | use 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 | ||
| 41 | fn run_git_fetch(remote_name: &str) -> Result<()> { | 43 | fn 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 | ||
| 25 | use anyhow::{Context, Result, anyhow, bail}; | 25 | use anyhow::{Context, Result, anyhow, bail}; |
| @@ -65,6 +65,65 @@ use crate::{ | |||
| 65 | repo_state::RepoState, | 65 | repo_state::RepoState, |
| 66 | }; | 66 | }; |
| 67 | 67 | ||
| 68 | pub fn is_verbose() -> bool { | ||
| 69 | std::env::var("NGIT_VERBOSE").is_ok() | ||
| 70 | } | ||
| 71 | |||
| 72 | const SPINNER_EXPAND_DELAY_SECS: u64 = 5; | ||
| 73 | |||
| 74 | struct SpinnerState { | ||
| 75 | spinner: ProgressBar, | ||
| 76 | start_time: Instant, | ||
| 77 | expanded_multi: Option<MultiProgress>, | ||
| 78 | } | ||
| 79 | |||
| 80 | impl 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)] |
| 69 | pub struct Client { | 128 | pub 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 | ||
| 12 | use anyhow::{Result, anyhow}; | 12 | use anyhow::{Result, anyhow}; |
| @@ -16,6 +16,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle}; | |||
| 16 | use nostr::hashes::sha1::Hash as Sha1Hash; | 16 | use nostr::hashes::sha1::Hash as Sha1Hash; |
| 17 | 17 | ||
| 18 | use crate::{ | 18 | use 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 | ||
| 32 | const SPINNER_EXPAND_DELAY_SECS: u64 = 5; | ||
| 33 | |||
| 34 | struct GitSpinnerState { | ||
| 35 | spinner: ProgressBar, | ||
| 36 | start_time: Instant, | ||
| 37 | expanded_multi: Option<MultiProgress>, | ||
| 38 | } | ||
| 39 | |||
| 40 | impl 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)] |
| 33 | pub struct RemoteIssues { | 89 | pub 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 | ||