diff options
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 2 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/push.rs | 25 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 412 |
3 files changed, 17 insertions, 422 deletions
diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs index f670b7b..e0821e9 100644 --- a/src/bin/git_remote_nostr/main.rs +++ b/src/bin/git_remote_nostr/main.rs | |||
| @@ -203,7 +203,7 @@ async fn main() -> Result<()> { | |||
| 203 | &repo_ref, | 203 | &repo_ref, |
| 204 | &stdin, | 204 | &stdin, |
| 205 | refspec, | 205 | refspec, |
| 206 | &client, | 206 | &mut client, |
| 207 | list_outputs.clone(), | 207 | list_outputs.clone(), |
| 208 | title_description, | 208 | title_description, |
| 209 | ) | 209 | ) |
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index e1c94f8..b64cdd9 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs | |||
| @@ -14,6 +14,7 @@ use git_events::{ | |||
| 14 | }; | 14 | }; |
| 15 | use git2::{Oid, Repository}; | 15 | use git2::{Oid, Repository}; |
| 16 | use ngit::{ | 16 | use ngit::{ |
| 17 | accept_maintainership::accept_maintainership_with_defaults, | ||
| 17 | client::{self, get_event_from_cache_by_id}, | 18 | client::{self, get_event_from_cache_by_id}, |
| 18 | git::{self, nostr_url::NostrUrlDecoded}, | 19 | git::{self, nostr_url::NostrUrlDecoded}, |
| 19 | git_events::{ | 20 | git_events::{ |
| @@ -50,7 +51,7 @@ pub async fn run_push( | |||
| 50 | repo_ref: &RepoRef, | 51 | repo_ref: &RepoRef, |
| 51 | stdin: &Stdin, | 52 | stdin: &Stdin, |
| 52 | initial_refspec: &str, | 53 | initial_refspec: &str, |
| 53 | client: &Client, | 54 | client: &mut Client, |
| 54 | list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>, | 55 | list_outputs: Option<HashMap<String, (HashMap<String, String>, bool)>>, |
| 55 | title_description: Option<(String, String)>, | 56 | title_description: Option<(String, String)>, |
| 56 | ) -> Result<()> { | 57 | ) -> Result<()> { |
| @@ -127,7 +128,7 @@ pub async fn run_push( | |||
| 127 | repo_ref, | 128 | repo_ref, |
| 128 | &git_state_refspecs, | 129 | &git_state_refspecs, |
| 129 | &proposal_refspecs, | 130 | &proposal_refspecs, |
| 130 | client, | 131 | client, // &mut Client |
| 131 | existing_state, | 132 | existing_state, |
| 132 | &term, | 133 | &term, |
| 133 | title_description.as_ref(), | 134 | title_description.as_ref(), |
| @@ -182,7 +183,7 @@ async fn create_and_publish_events_and_proposals( | |||
| 182 | repo_ref: &RepoRef, | 183 | repo_ref: &RepoRef, |
| 183 | git_server_refspecs: &Vec<String>, | 184 | git_server_refspecs: &Vec<String>, |
| 184 | proposal_refspecs: &Vec<String>, | 185 | proposal_refspecs: &Vec<String>, |
| 185 | client: &Client, | 186 | client: &mut Client, |
| 186 | existing_state: HashMap<String, String>, | 187 | existing_state: HashMap<String, String>, |
| 187 | term: &Term, | 188 | term: &Term, |
| 188 | title_description: Option<&(String, String)>, | 189 | title_description: Option<&(String, String)>, |
| @@ -216,16 +217,14 @@ async fn create_and_publish_events_and_proposals( | |||
| 216 | .clone() | 217 | .clone() |
| 217 | .is_some_and(|ms| ms.contains(&user_ref.public_key)) | 218 | .is_some_and(|ms| ms.contains(&user_ref.public_key)) |
| 218 | { | 219 | { |
| 219 | for refspec in git_server_refspecs { | 220 | // Auto-accept co-maintainership: publish the user's own announcement |
| 220 | let (_, to) = refspec_to_from_to(refspec).unwrap(); | 221 | // with defaults before proceeding with the push. The announcement is |
| 221 | eprintln!( | 222 | // required (not just for consent, but to prevent scammers from |
| 222 | "error {to} you have been offered co-maintainership of '{}'. to accept, run `ngit init` which will publish your own repository announcement. use `ngit init -d` to accept with defaults and no interactive prompts.", | 223 | // attributing a person's state events to a fake project with the same |
| 223 | repo_ref.name, | 224 | // identifier). See docs/design/co-maintainer-announcement-rationale.md. |
| 224 | ); | 225 | accept_maintainership_with_defaults(git_repo, repo_ref, &user_ref, client, &signer) |
| 225 | } | 226 | .await |
| 226 | if proposal_refspecs.is_empty() { | 227 | .context("failed to auto-accept co-maintainership")?; |
| 227 | return Ok((vec![], true)); | ||
| 228 | } | ||
| 229 | } | 228 | } |
| 230 | 229 | ||
| 231 | let mut events = vec![]; | 230 | let mut events = vec![]; |
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 | } | ||