diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-20 20:09:09 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-20 21:21:48 +0000 |
| commit | 64747526c9f6ab43f9dac461d056bb42992573b4 (patch) | |
| tree | c2506828ae7b188e3e4b569cd73202ec37779278 /src/bin/ngit/sub_commands/init.rs | |
| parent | 365dfb9a1e986b68bc2389e2a3cd3da30b0d4636 (diff) | |
extract grasp/maintainership helpers to lib and auto-accept on push
move apply_grasp_infrastructure, latest_event_repo_ref to lib/repo_ref.rs
and wait_for_grasp_servers + grasp_servers_from_user_or_fallback to a
new lib/accept_maintainership.rs so both binaries can share them.
add accept_maintainership_with_defaults which publishes the co-maintainer's
own Kind:30617 announcement with defaults (user grasp servers, shared
metadata from existing events) then waits for grasp server provisioning
and updates nostr.repo config and origin remote.
replace the push error block with a call to accept_maintainership_with_defaults
so pushing now silently accepts co-maintainership instead of failing.
Diffstat (limited to 'src/bin/ngit/sub_commands/init.rs')
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 412 |
1 files changed, 4 insertions, 408 deletions
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 39c8e8e..d24a41e 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs | |||
| @@ -3,19 +3,14 @@ use std::{ | |||
| 3 | env, | 3 | env, |
| 4 | process::{Command, Stdio}, | 4 | process::{Command, Stdio}, |
| 5 | str::FromStr, | 5 | str::FromStr, |
| 6 | sync::{ | 6 | sync::Arc, |
| 7 | Arc, Mutex, | ||
| 8 | atomic::{AtomicBool, AtomicU64, Ordering}, | ||
| 9 | }, | ||
| 10 | time::Duration, | ||
| 11 | }; | 7 | }; |
| 12 | 8 | ||
| 13 | use anyhow::{Context, Result, bail}; | 9 | use anyhow::{Context, Result, bail}; |
| 14 | use console::{Style, Term}; | 10 | use console::{Style, Term}; |
| 15 | use futures::future::join_all; | ||
| 16 | use git2::Oid; | 11 | use git2::Oid; |
| 17 | use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; | ||
| 18 | use ngit::{ | 12 | use ngit::{ |
| 13 | accept_maintainership::{grasp_servers_from_user_or_fallback, wait_for_grasp_servers}, | ||
| 19 | cli_interactor::{ | 14 | cli_interactor::{ |
| 20 | PromptChoiceParms, PromptConfirmParms, cli_error, multi_select_with_custom_value, | 15 | PromptChoiceParms, PromptConfirmParms, cli_error, multi_select_with_custom_value, |
| 21 | show_multi_input_prompt_success, | 16 | show_multi_input_prompt_success, |
| @@ -25,8 +20,8 @@ use ngit::{ | |||
| 25 | git::nostr_url::{CloneUrl, NostrUrlDecoded}, | 20 | git::nostr_url::{CloneUrl, NostrUrlDecoded}, |
| 26 | list::list_from_remote, | 21 | list::list_from_remote, |
| 27 | repo_ref::{ | 22 | repo_ref::{ |
| 28 | detect_existing_grasp_servers, extract_npub, extract_pks, | 23 | apply_grasp_infrastructure, detect_existing_grasp_servers, extract_npub, extract_pks, |
| 29 | format_grasp_server_url_as_relay_url, is_grasp_server_clone_url, | 24 | format_grasp_server_url_as_relay_url, is_grasp_server_clone_url, latest_event_repo_ref, |
| 30 | normalize_grasp_server_url, save_repo_config_to_yaml, | 25 | normalize_grasp_server_url, save_repo_config_to_yaml, |
| 31 | }, | 26 | }, |
| 32 | repo_state::RepoState, | 27 | repo_state::RepoState, |
| @@ -129,16 +124,6 @@ fn my_event_repo_ref(repo_ref: &RepoRef, my_pubkey: &PublicKey) -> Option<RepoRe | |||
| 129 | .and_then(|e| RepoRef::try_from((e.clone(), None)).ok()) | 124 | .and_then(|e| RepoRef::try_from((e.clone(), None)).ok()) |
| 130 | } | 125 | } |
| 131 | 126 | ||
| 132 | /// Find the latest event (by `created_at`) across all maintainer events and | ||
| 133 | /// parse it into a `RepoRef` for shared metadata (name, description, web). | ||
| 134 | fn latest_event_repo_ref(repo_ref: &RepoRef) -> Option<RepoRef> { | ||
| 135 | repo_ref | ||
| 136 | .events | ||
| 137 | .values() | ||
| 138 | .max_by_key(|e| e.created_at) | ||
| 139 | .and_then(|e| RepoRef::try_from((e.clone(), None)).ok()) | ||
| 140 | } | ||
| 141 | |||
| 142 | /// Check if a grasp-format clone URL belongs to the given public key. | 127 | /// Check if a grasp-format clone URL belongs to the given public key. |
| 143 | fn is_my_grasp_clone_url(url: &str, my_pubkey: &PublicKey) -> bool { | 128 | fn is_my_grasp_clone_url(url: &str, my_pubkey: &PublicKey) -> bool { |
| 144 | if !is_grasp_server_clone_url(url) { | 129 | if !is_grasp_server_clone_url(url) { |
| @@ -261,60 +246,6 @@ fn resolve_hashtags(args_hashtag: &[String], state: &InitState) -> Result<Vec<St | |||
| 261 | Ok(vec![]) | 246 | Ok(vec![]) |
| 262 | } | 247 | } |
| 263 | 248 | ||
| 264 | /// Derive clone-urls and relays from selected grasp servers. | ||
| 265 | /// | ||
| 266 | /// For each grasp server, adds/replaces the corresponding clone URL in | ||
| 267 | /// `git_servers` and adds a relay URL to `relays`. Grasp-derived infrastructure | ||
| 268 | /// is always added — the other lists (`git_servers`, `relays`) | ||
| 269 | /// contain *additional* infrastructure beyond what grasp servers provide. | ||
| 270 | fn apply_grasp_infrastructure( | ||
| 271 | grasp_servers: &[String], | ||
| 272 | git_servers: &mut Vec<String>, | ||
| 273 | relays: &mut Vec<String>, | ||
| 274 | public_key: &PublicKey, | ||
| 275 | identifier: &str, | ||
| 276 | ) -> Result<()> { | ||
| 277 | for (grasp_relay_insert_idx, grasp_server) in grasp_servers.iter().enumerate() { | ||
| 278 | // Always add grasp-derived clone URL | ||
| 279 | let clone_url = format_grasp_server_url_as_clone_url(grasp_server, public_key, identifier)?; | ||
| 280 | |||
| 281 | let grasp_server_clone_root = if clone_url.contains("https://") { | ||
| 282 | format!("https://{grasp_server}") | ||
| 283 | } else { | ||
| 284 | grasp_server.to_string() | ||
| 285 | }; | ||
| 286 | |||
| 287 | let matching_positions: Vec<usize> = git_servers | ||
| 288 | .iter() | ||
| 289 | .enumerate() | ||
| 290 | .filter_map(|(idx, url)| { | ||
| 291 | if url.contains(&grasp_server_clone_root) { | ||
| 292 | Some(idx) | ||
| 293 | } else { | ||
| 294 | None | ||
| 295 | } | ||
| 296 | }) | ||
| 297 | .collect(); | ||
| 298 | |||
| 299 | if matching_positions.is_empty() { | ||
| 300 | git_servers.push(clone_url); | ||
| 301 | } else { | ||
| 302 | git_servers[matching_positions[0]] = clone_url; | ||
| 303 | for &position in matching_positions.iter().skip(1).rev() { | ||
| 304 | git_servers.remove(position); | ||
| 305 | } | ||
| 306 | } | ||
| 307 | |||
| 308 | // Prepend grasp-derived relay in order (for relay hint) so that the | ||
| 309 | // first grasp server in the list ends up at relays[0]. | ||
| 310 | let relay_url = format_grasp_server_url_as_relay_url(grasp_server)?; | ||
| 311 | if !relays.contains(&relay_url) { | ||
| 312 | relays.insert(grasp_relay_insert_idx, relay_url); | ||
| 313 | } | ||
| 314 | } | ||
| 315 | Ok(()) | ||
| 316 | } | ||
| 317 | |||
| 318 | /// Resolve which grasp servers to use. Handles flag overrides, detection from | 249 | /// Resolve which grasp servers to use. Handles flag overrides, detection from |
| 319 | /// existing URLs, user grasp list / system fallbacks, and interactive | 250 | /// existing URLs, user grasp list / system fallbacks, and interactive |
| 320 | /// prompting. | 251 | /// prompting. |
| @@ -392,26 +323,6 @@ fn resolve_grasp_servers( | |||
| 392 | Ok(selected) | 323 | Ok(selected) |
| 393 | } | 324 | } |
| 394 | 325 | ||
| 395 | fn grasp_servers_from_user_or_fallback( | ||
| 396 | user_ref: &ngit::login::user::UserRef, | ||
| 397 | client: &Client, | ||
| 398 | ) -> Vec<String> { | ||
| 399 | if user_ref.grasp_list.urls.is_empty() { | ||
| 400 | client | ||
| 401 | .get_grasp_default_set() | ||
| 402 | .iter() | ||
| 403 | .map(std::string::ToString::to_string) | ||
| 404 | .collect() | ||
| 405 | } else { | ||
| 406 | user_ref | ||
| 407 | .grasp_list | ||
| 408 | .urls | ||
| 409 | .iter() | ||
| 410 | .map(std::string::ToString::to_string) | ||
| 411 | .collect() | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 415 | // --------------------------------------------------------------------------- | 326 | // --------------------------------------------------------------------------- |
| 416 | // Validation | 327 | // Validation |
| 417 | // --------------------------------------------------------------------------- | 328 | // --------------------------------------------------------------------------- |
| @@ -1626,24 +1537,6 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 1626 | .await | 1537 | .await |
| 1627 | } | 1538 | } |
| 1628 | 1539 | ||
| 1629 | fn format_grasp_server_url_as_clone_url( | ||
| 1630 | url: &str, | ||
| 1631 | public_key: &PublicKey, | ||
| 1632 | identifier: &str, | ||
| 1633 | ) -> Result<String> { | ||
| 1634 | let grasp_server_url = normalize_grasp_server_url(url)?; | ||
| 1635 | if grasp_server_url.contains("http://") { | ||
| 1636 | return Ok(format!( | ||
| 1637 | "{grasp_server_url}/{}/{identifier}.git", | ||
| 1638 | public_key.to_bech32()? | ||
| 1639 | )); | ||
| 1640 | } | ||
| 1641 | Ok(format!( | ||
| 1642 | "https://{grasp_server_url}/{}/{identifier}.git", | ||
| 1643 | public_key.to_bech32()? | ||
| 1644 | )) | ||
| 1645 | } | ||
| 1646 | |||
| 1647 | fn parse_relay_url(s: &str) -> Result<RelayUrl> { | 1540 | fn parse_relay_url(s: &str) -> Result<RelayUrl> { |
| 1648 | // Attempt to parse the original string | 1541 | // Attempt to parse the original string |
| 1649 | match RelayUrl::parse(s) { | 1542 | match RelayUrl::parse(s) { |
| @@ -1733,300 +1626,3 @@ fn run_ngit_sync() -> Result<()> { | |||
| 1733 | bail!("ngit sync process exited with an error: {exit_status}"); | 1626 | bail!("ngit sync process exited with an error: {exit_status}"); |
| 1734 | } | 1627 | } |
| 1735 | } | 1628 | } |
| 1736 | |||
| 1737 | fn check_git_server_ready(git_repo_path: &std::path::Path, git_server_url: &str) -> bool { | ||
| 1738 | let Ok(git_repo) = git2::Repository::open(git_repo_path) else { | ||
| 1739 | return false; | ||
| 1740 | }; | ||
| 1741 | let Ok(mut remote) = git_repo.remote_anonymous(git_server_url) else { | ||
| 1742 | return false; | ||
| 1743 | }; | ||
| 1744 | match remote.connect(git2::Direction::Fetch) { | ||
| 1745 | Ok(()) => { | ||
| 1746 | let _ = remote.disconnect(); | ||
| 1747 | true | ||
| 1748 | } | ||
| 1749 | Err(_) => false, | ||
| 1750 | } | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | /// Holds the final style+message for a bar that completed before the detail | ||
| 1754 | /// view was revealed. | ||
| 1755 | struct DeferredServerFinish { | ||
| 1756 | bar: ProgressBar, | ||
| 1757 | style: ProgressStyle, | ||
| 1758 | message: String, | ||
| 1759 | } | ||
| 1760 | |||
| 1761 | struct ServerRevealState { | ||
| 1762 | revealed: AtomicBool, | ||
| 1763 | deferred: Mutex<Vec<DeferredServerFinish>>, | ||
| 1764 | } | ||
| 1765 | |||
| 1766 | struct PollContext { | ||
| 1767 | timeout_secs: u64, | ||
| 1768 | total: u64, | ||
| 1769 | ready_count: Arc<AtomicU64>, | ||
| 1770 | spinner_pb: ProgressBar, | ||
| 1771 | reveal_state: Arc<ServerRevealState>, | ||
| 1772 | } | ||
| 1773 | |||
| 1774 | fn create_server_bars(clone_urls: &[String], detail_multi: &MultiProgress) -> Vec<ProgressBar> { | ||
| 1775 | let waiting_style = ProgressStyle::with_template(" {spinner} {msg}") | ||
| 1776 | .unwrap() | ||
| 1777 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"); | ||
| 1778 | clone_urls | ||
| 1779 | .iter() | ||
| 1780 | .map(|url| { | ||
| 1781 | let name = url | ||
| 1782 | .trim_start_matches("https://") | ||
| 1783 | .trim_start_matches("http://") | ||
| 1784 | .to_string(); | ||
| 1785 | detail_multi.add( | ||
| 1786 | ProgressBar::new_spinner() | ||
| 1787 | .with_style(waiting_style.clone()) | ||
| 1788 | .with_message( | ||
| 1789 | console::style(format!("{name} - waiting")) | ||
| 1790 | .for_stderr() | ||
| 1791 | .dim() | ||
| 1792 | .to_string(), | ||
| 1793 | ), | ||
| 1794 | ) | ||
| 1795 | }) | ||
| 1796 | .collect() | ||
| 1797 | } | ||
| 1798 | |||
| 1799 | fn spawn_expand_timer( | ||
| 1800 | expand_delay_ms: u64, | ||
| 1801 | spinner_pb: ProgressBar, | ||
| 1802 | detail_multi: MultiProgress, | ||
| 1803 | heading_bar: ProgressBar, | ||
| 1804 | reveal_state: Arc<ServerRevealState>, | ||
| 1805 | server_bars: Vec<ProgressBar>, | ||
| 1806 | ) -> tokio::task::JoinHandle<()> { | ||
| 1807 | tokio::spawn(async move { | ||
| 1808 | tokio::time::sleep(Duration::from_millis(expand_delay_ms)).await; | ||
| 1809 | spinner_pb.finish_and_clear(); | ||
| 1810 | detail_multi.set_draw_target(ProgressDrawTarget::stderr()); | ||
| 1811 | heading_bar.finish_with_message("waiting for servers to create bare git repo..."); | ||
| 1812 | let mut deferred = reveal_state.deferred.lock().unwrap(); | ||
| 1813 | reveal_state.revealed.store(true, Ordering::Release); | ||
| 1814 | for df in deferred.drain(..) { | ||
| 1815 | df.bar.set_style(df.style); | ||
| 1816 | df.bar.finish_with_message(df.message); | ||
| 1817 | } | ||
| 1818 | for bar in &server_bars { | ||
| 1819 | if !bar.is_finished() { | ||
| 1820 | bar.enable_steady_tick(Duration::from_millis(100)); | ||
| 1821 | } | ||
| 1822 | } | ||
| 1823 | }) | ||
| 1824 | } | ||
| 1825 | |||
| 1826 | fn finalize_spinner(all_ready: bool, spinner_pb: &ProgressBar, final_ready: u64, total: u64) { | ||
| 1827 | if all_ready { | ||
| 1828 | spinner_pb.finish_and_clear(); | ||
| 1829 | } else { | ||
| 1830 | spinner_pb.set_style(ProgressStyle::with_template("{msg}").unwrap()); | ||
| 1831 | spinner_pb.finish_with_message(format!( | ||
| 1832 | "timed out waiting for servers to create bare git repo ({final_ready}/{total} - complete), proceeding anyway" | ||
| 1833 | )); | ||
| 1834 | } | ||
| 1835 | } | ||
| 1836 | |||
| 1837 | fn finish_server_bar( | ||
| 1838 | bar: &ProgressBar, | ||
| 1839 | style: ProgressStyle, | ||
| 1840 | message: String, | ||
| 1841 | reveal_state: &Arc<ServerRevealState>, | ||
| 1842 | ) { | ||
| 1843 | let mut deferred = reveal_state.deferred.lock().unwrap(); | ||
| 1844 | if reveal_state.revealed.load(Ordering::Acquire) { | ||
| 1845 | drop(deferred); | ||
| 1846 | bar.set_style(style); | ||
| 1847 | bar.finish_with_message(message); | ||
| 1848 | } else { | ||
| 1849 | // Style is set now so the timer can drain it correctly; finish is | ||
| 1850 | // deferred until the detail view becomes visible. | ||
| 1851 | bar.set_style(style.clone()); | ||
| 1852 | deferred.push(DeferredServerFinish { | ||
| 1853 | bar: bar.clone(), | ||
| 1854 | style, | ||
| 1855 | message, | ||
| 1856 | }); | ||
| 1857 | } | ||
| 1858 | } | ||
| 1859 | |||
| 1860 | async fn poll_single_server( | ||
| 1861 | url: String, | ||
| 1862 | git_repo_path: std::path::PathBuf, | ||
| 1863 | bar: ProgressBar, | ||
| 1864 | ctx: Arc<PollContext>, | ||
| 1865 | ) -> bool { | ||
| 1866 | let poll_interval = Duration::from_millis(500); | ||
| 1867 | let deadline = tokio::time::Instant::now() + Duration::from_secs(ctx.timeout_secs); | ||
| 1868 | let mut ready = false; | ||
| 1869 | loop { | ||
| 1870 | let is_ready = tokio::task::spawn_blocking({ | ||
| 1871 | let url = url.clone(); | ||
| 1872 | let path = git_repo_path.clone(); | ||
| 1873 | move || check_git_server_ready(&path, &url) | ||
| 1874 | }) | ||
| 1875 | .await | ||
| 1876 | .unwrap_or(false); | ||
| 1877 | |||
| 1878 | if is_ready { | ||
| 1879 | ready = true; | ||
| 1880 | break; | ||
| 1881 | } | ||
| 1882 | |||
| 1883 | if tokio::time::Instant::now() >= deadline { | ||
| 1884 | break; | ||
| 1885 | } | ||
| 1886 | |||
| 1887 | tokio::time::sleep(poll_interval).await; | ||
| 1888 | } | ||
| 1889 | |||
| 1890 | let count = if ready { | ||
| 1891 | ctx.ready_count.fetch_add(1, Ordering::Relaxed) + 1 | ||
| 1892 | } else { | ||
| 1893 | ctx.ready_count.load(Ordering::Relaxed) | ||
| 1894 | }; | ||
| 1895 | |||
| 1896 | ctx.spinner_pb.set_message(format!( | ||
| 1897 | "waiting for servers to create bare git repo... ({count}/{total} - complete)", | ||
| 1898 | total = ctx.total | ||
| 1899 | )); | ||
| 1900 | |||
| 1901 | let name = url | ||
| 1902 | .trim_start_matches("https://") | ||
| 1903 | .trim_start_matches("http://") | ||
| 1904 | .to_string(); | ||
| 1905 | if ready { | ||
| 1906 | let style = ProgressStyle::with_template(&format!( | ||
| 1907 | " {} {{msg}}", | ||
| 1908 | console::style("✔").for_stderr().green() | ||
| 1909 | )) | ||
| 1910 | .unwrap(); | ||
| 1911 | let msg = console::style(format!("{name} - ready")) | ||
| 1912 | .for_stderr() | ||
| 1913 | .green() | ||
| 1914 | .to_string(); | ||
| 1915 | finish_server_bar(&bar, style, msg, &ctx.reveal_state); | ||
| 1916 | } else { | ||
| 1917 | let style = ProgressStyle::with_template(&format!( | ||
| 1918 | " {} {{msg}}", | ||
| 1919 | console::style("✘").for_stderr().red() | ||
| 1920 | )) | ||
| 1921 | .unwrap(); | ||
| 1922 | let msg = console::style(format!("{name} - timeout")) | ||
| 1923 | .for_stderr() | ||
| 1924 | .red() | ||
| 1925 | .to_string(); | ||
| 1926 | finish_server_bar(&bar, style, msg, &ctx.reveal_state); | ||
| 1927 | } | ||
| 1928 | |||
| 1929 | ready | ||
| 1930 | } | ||
| 1931 | |||
| 1932 | /// Poll grasp servers in parallel until all are ready or timeout is reached. | ||
| 1933 | /// | ||
| 1934 | /// Shows a concise spinner with `x/y - complete` progress. After 5s without | ||
| 1935 | /// all servers responding, expands to show per-server status bars (including | ||
| 1936 | /// any that already finished). Times out after 15s (2s in tests) and proceeds | ||
| 1937 | /// anyway rather than failing. | ||
| 1938 | async fn wait_for_grasp_servers( | ||
| 1939 | git_repo: &Repo, | ||
| 1940 | grasp_servers: &[String], | ||
| 1941 | public_key: &PublicKey, | ||
| 1942 | identifier: &str, | ||
| 1943 | ) -> Result<()> { | ||
| 1944 | let clone_urls: Vec<String> = grasp_servers | ||
| 1945 | .iter() | ||
| 1946 | .filter_map(|gs| format_grasp_server_url_as_clone_url(gs, public_key, identifier).ok()) | ||
| 1947 | .collect(); | ||
| 1948 | |||
| 1949 | if clone_urls.is_empty() { | ||
| 1950 | return Ok(()); | ||
| 1951 | } | ||
| 1952 | |||
| 1953 | let is_test = std::env::var("NGITTEST").is_ok(); | ||
| 1954 | let timeout_secs: u64 = if is_test { 2 } else { 15 }; | ||
| 1955 | let expand_delay_ms: u64 = if is_test { 500 } else { 5000 }; | ||
| 1956 | let total = clone_urls.len() as u64; | ||
| 1957 | |||
| 1958 | // Spinner shown immediately with x/y count | ||
| 1959 | let spinner_multi = MultiProgress::new(); | ||
| 1960 | let spinner_pb = spinner_multi.add( | ||
| 1961 | ProgressBar::new_spinner() | ||
| 1962 | .with_style( | ||
| 1963 | ProgressStyle::with_template("{spinner} {msg}") | ||
| 1964 | .unwrap() | ||
| 1965 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"), | ||
| 1966 | ) | ||
| 1967 | .with_message(format!( | ||
| 1968 | "waiting for servers to create bare git repo... (0/{total} - complete)" | ||
| 1969 | )), | ||
| 1970 | ); | ||
| 1971 | spinner_pb.enable_steady_tick(Duration::from_millis(100)); | ||
| 1972 | |||
| 1973 | // Detail MultiProgress starts hidden; revealed after expand_delay_ms. | ||
| 1974 | // A heading bar is pre-added at position 0 so it holds its slot before | ||
| 1975 | // any per-server bars are added. | ||
| 1976 | let detail_multi = MultiProgress::with_draw_target(ProgressDrawTarget::hidden()); | ||
| 1977 | let heading_bar = detail_multi | ||
| 1978 | .add(ProgressBar::new(0).with_style(ProgressStyle::with_template("{msg}").unwrap())); | ||
| 1979 | |||
| 1980 | let ready_count = Arc::new(AtomicU64::new(0)); | ||
| 1981 | let reveal_state = Arc::new(ServerRevealState { | ||
| 1982 | revealed: AtomicBool::new(false), | ||
| 1983 | deferred: Mutex::new(Vec::new()), | ||
| 1984 | }); | ||
| 1985 | |||
| 1986 | let server_bars = create_server_bars(&clone_urls, &detail_multi); | ||
| 1987 | |||
| 1988 | let timer_handle = spawn_expand_timer( | ||
| 1989 | expand_delay_ms, | ||
| 1990 | spinner_pb.clone(), | ||
| 1991 | detail_multi.clone(), | ||
| 1992 | heading_bar, | ||
| 1993 | reveal_state.clone(), | ||
| 1994 | server_bars.clone(), | ||
| 1995 | ); | ||
| 1996 | |||
| 1997 | // Poll each server in parallel | ||
| 1998 | let git_repo_path = git_repo.get_path()?.to_path_buf(); | ||
| 1999 | let poll_ctx = Arc::new(PollContext { | ||
| 2000 | timeout_secs, | ||
| 2001 | total, | ||
| 2002 | ready_count: ready_count.clone(), | ||
| 2003 | spinner_pb: spinner_pb.clone(), | ||
| 2004 | reveal_state: reveal_state.clone(), | ||
| 2005 | }); | ||
| 2006 | let futures: Vec<_> = clone_urls | ||
| 2007 | .iter() | ||
| 2008 | .enumerate() | ||
| 2009 | .map(|(i, url)| { | ||
| 2010 | poll_single_server( | ||
| 2011 | url.clone(), | ||
| 2012 | git_repo_path.clone(), | ||
| 2013 | server_bars[i].clone(), | ||
| 2014 | poll_ctx.clone(), | ||
| 2015 | ) | ||
| 2016 | }) | ||
| 2017 | .collect(); | ||
| 2018 | |||
| 2019 | let results = join_all(futures).await; | ||
| 2020 | let final_ready = ready_count.load(Ordering::Relaxed); | ||
| 2021 | |||
| 2022 | timer_handle.abort(); | ||
| 2023 | |||
| 2024 | if reveal_state.revealed.load(Ordering::Acquire) { | ||
| 2025 | let _ = detail_multi.clear(); | ||
| 2026 | } | ||
| 2027 | |||
| 2028 | let all_ready = results.iter().all(|&r| r); | ||
| 2029 | finalize_spinner(all_ready, &spinner_pb, final_ready, total); | ||
| 2030 | |||
| 2031 | Ok(()) | ||
| 2032 | } | ||