diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-13 10:51:25 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-13 10:51:25 +0000 |
| commit | 40b439ae4d69b858274be51dd5af513c3b4f46f0 (patch) | |
| tree | 64beb8589b8a2da5aee7aecf8dc9564e21d676d0 /src/lib/list.rs | |
| parent | cfd8cc19b6a81ad78bc30d5b21cefe21d574d09e (diff) | |
feat: add spinner for git fetch in non-verbose mode
Shows a progress spinner when fetching from git remotes in non-verbose mode.
Suppresses git fetch output and listing messages when not in verbose mode.
Uses NGITTEST environment variable for test timeouts.
Diffstat (limited to 'src/lib/list.rs')
| -rw-r--r-- | src/lib/list.rs | 110 |
1 files changed, 93 insertions, 17 deletions
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 | ||