upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 08:00:18 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 13:01:00 +0000
commit10f5dd5f48659d6e99abc762d46be397d24b48d9 (patch)
tree1c344dfabb9b0416dfcc165f54b1b2ab94d21308 /src/bin
parent3d4918fea3280fd6653ad138160aa7327ab9b36e (diff)
poll grasp servers for readiness instead of fixed 5s wait
Replace arbitrary 5-second countdown with active polling of grasp servers during ngit init. Servers are checked every 1s with a 7s timeout (2s in tests). Shows progress like '1/2 grasp servers ready'. Grasp servers return 404 until the repo is provisioned, so we poll until they accept git connections. Proceeds anyway on timeout rather than failing, since some servers may still be initializing.
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/ngit/sub_commands/init.rs318
1 files changed, 294 insertions, 24 deletions
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
index 158ff29..ae6a202 100644
--- a/src/bin/ngit/sub_commands/init.rs
+++ b/src/bin/ngit/sub_commands/init.rs
@@ -3,14 +3,18 @@ use std::{
3 env, 3 env,
4 process::{Command, Stdio}, 4 process::{Command, Stdio},
5 str::FromStr, 5 str::FromStr,
6 sync::Arc, 6 sync::{
7 thread, 7 Arc, Mutex,
8 atomic::{AtomicBool, AtomicU64, Ordering},
9 },
8 time::Duration, 10 time::Duration,
9}; 11};
10 12
11use anyhow::{Context, Result, bail}; 13use anyhow::{Context, Result, bail};
12use console::{Style, Term}; 14use console::{Style, Term};
15use futures::future::join_all;
13use git2::Oid; 16use git2::Oid;
17use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
14use ngit::{ 18use ngit::{
15 cli_interactor::{ 19 cli_interactor::{
16 PromptChoiceParms, PromptConfirmParms, cli_error, multi_select_with_custom_value, 20 PromptChoiceParms, PromptConfirmParms, cli_error, multi_select_with_custom_value,
@@ -1391,17 +1395,13 @@ async fn publish_and_finalize(
1391 if fields.selected_grasp_servers.is_empty() { 1395 if fields.selected_grasp_servers.is_empty() {
1392 println!("running `ngit push` to publish your repository data"); 1396 println!("running `ngit push` to publish your repository data");
1393 } else { 1397 } else {
1394 let countdown_start = 5; 1398 wait_for_grasp_servers(
1395 println!( 1399 git_repo,
1396 "waiting {countdown_start}s for grasp servers to create your repo before we push your data" 1400 &fields.selected_grasp_servers,
1397 ); 1401 &user_ref.public_key,
1398 let term = Term::stdout(); 1402 &fields.identifier,
1399 for i in (1..=countdown_start).rev() { 1403 )
1400 term.write_line(format!("\rrunning `git push` in {i}s").as_str())?; 1404 .await?;
1401 thread::sleep(Duration::new(1, 0)); // Sleep for 1 second
1402 term.clear_last_lines(1)?;
1403 }
1404 term.flush().unwrap(); // Ensure the output is flushed to the terminal
1405 } 1405 }
1406 1406
1407 if let Err(err) = push_main_or_master_branch(git_repo) { 1407 if let Err(err) = push_main_or_master_branch(git_repo) {
@@ -1416,17 +1416,13 @@ async fn publish_and_finalize(
1416 "running `ngit sync` to ensure your repository data is available on repository git servers" 1416 "running `ngit sync` to ensure your repository data is available on repository git servers"
1417 ); 1417 );
1418 } else { 1418 } else {
1419 let countdown_start = 5; 1419 wait_for_grasp_servers(
1420 println!( 1420 git_repo,
1421 "waiting {countdown_start}s for any new grasp servers to create your repo before we sync your data" 1421 &fields.selected_grasp_servers,
1422 ); 1422 &user_ref.public_key,
1423 let term = Term::stdout(); 1423 &fields.identifier,
1424 for i in (1..=countdown_start).rev() { 1424 )
1425 term.write_line(format!("\rrunning `ngit sync` in {i}s").as_str())?; 1425 .await?;
1426 thread::sleep(Duration::new(1, 0)); // Sleep for 1 second
1427 term.clear_last_lines(1)?;
1428 }
1429 term.flush().unwrap(); // Ensure the output is flushed to the terminal
1430 } 1426 }
1431 1427
1432 if let Err(err) = run_ngit_sync() { 1428 if let Err(err) = run_ngit_sync() {
@@ -1690,3 +1686,277 @@ fn run_ngit_sync() -> Result<()> {
1690 bail!("ngit sync process exited with an error: {exit_status}"); 1686 bail!("ngit sync process exited with an error: {exit_status}");
1691 } 1687 }
1692} 1688}
1689
1690fn check_git_server_ready(git_repo_path: &std::path::Path, git_server_url: &str) -> bool {
1691 let Ok(git_repo) = git2::Repository::open(git_repo_path) else {
1692 return false;
1693 };
1694 let Ok(mut remote) = git_repo.remote_anonymous(git_server_url) else {
1695 return false;
1696 };
1697 match remote.connect(git2::Direction::Fetch) {
1698 Ok(()) => {
1699 let _ = remote.disconnect();
1700 true
1701 }
1702 Err(_) => false,
1703 }
1704}
1705
1706/// Holds the final style+message for a bar that completed before the detail
1707/// view was revealed.
1708struct DeferredServerFinish {
1709 bar: ProgressBar,
1710 style: ProgressStyle,
1711 message: String,
1712}
1713
1714/// Coordinates the delayed reveal of per-server detail bars.
1715/// Bars that finish before the expand timer fires store their final
1716/// style+message here. The timer applies them all at reveal time so
1717/// every bar — completed or still waiting — appears in the expanded view.
1718struct ServerRevealState {
1719 revealed: AtomicBool,
1720 deferred: Mutex<Vec<DeferredServerFinish>>,
1721}
1722
1723fn finish_server_bar(
1724 bar: &ProgressBar,
1725 style: ProgressStyle,
1726 message: String,
1727 reveal_state: &Arc<ServerRevealState>,
1728) {
1729 let mut deferred = reveal_state.deferred.lock().unwrap();
1730 if reveal_state.revealed.load(Ordering::Acquire) {
1731 drop(deferred);
1732 bar.set_style(style);
1733 bar.finish_with_message(message);
1734 } else {
1735 // Style is set now so the timer can drain it correctly; finish is
1736 // deferred until the detail view becomes visible.
1737 bar.set_style(style.clone());
1738 deferred.push(DeferredServerFinish {
1739 bar: bar.clone(),
1740 style,
1741 message,
1742 });
1743 }
1744}
1745
1746/// Poll grasp servers in parallel until all are ready or timeout is reached.
1747///
1748/// Shows a concise spinner with `x/y - complete` progress. After 5s without
1749/// all servers responding, expands to show per-server status bars (including
1750/// any that already finished). Times out after 15s (2s in tests) and proceeds
1751/// anyway rather than failing.
1752async fn wait_for_grasp_servers(
1753 git_repo: &Repo,
1754 grasp_servers: &[String],
1755 public_key: &PublicKey,
1756 identifier: &str,
1757) -> Result<()> {
1758 let clone_urls: Vec<String> = grasp_servers
1759 .iter()
1760 .filter_map(|gs| format_grasp_server_url_as_clone_url(gs, public_key, identifier).ok())
1761 .collect();
1762
1763 if clone_urls.is_empty() {
1764 return Ok(());
1765 }
1766
1767 let is_test = std::env::var("NGITTEST").is_ok();
1768 let timeout_secs: u64 = if is_test { 2 } else { 15 };
1769 let expand_delay_ms: u64 = if is_test { 500 } else { 5000 };
1770 let total = clone_urls.len() as u64;
1771
1772 // Spinner shown immediately with x/y count
1773 let spinner_multi = MultiProgress::new();
1774 let spinner_pb = spinner_multi.add(
1775 ProgressBar::new_spinner()
1776 .with_style(
1777 ProgressStyle::with_template("{spinner} {msg}")
1778 .unwrap()
1779 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"),
1780 )
1781 .with_message(format!(
1782 "waiting for servers to create bare git repo... (0/{total} - complete)"
1783 )),
1784 );
1785 spinner_pb.enable_steady_tick(Duration::from_millis(100));
1786
1787 // Detail MultiProgress starts hidden; revealed after expand_delay_ms.
1788 // A heading bar is pre-added at position 0 so it holds its slot before
1789 // any per-server bars are added.
1790 let detail_multi = MultiProgress::with_draw_target(ProgressDrawTarget::hidden());
1791 let heading_bar = detail_multi
1792 .add(ProgressBar::new(0).with_style(ProgressStyle::with_template("{msg}").unwrap()));
1793
1794 let ready_count = Arc::new(AtomicU64::new(0));
1795 let reveal_state = Arc::new(ServerRevealState {
1796 revealed: AtomicBool::new(false),
1797 deferred: Mutex::new(Vec::new()),
1798 });
1799
1800 // Per-server spinner bars (added to hidden detail_multi)
1801 let waiting_style = ProgressStyle::with_template(" {spinner} {msg}")
1802 .unwrap()
1803 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈");
1804 let server_bars: Vec<ProgressBar> = clone_urls
1805 .iter()
1806 .map(|url| {
1807 let name = url
1808 .trim_start_matches("https://")
1809 .trim_start_matches("http://")
1810 .to_string();
1811 detail_multi.add(
1812 ProgressBar::new_spinner()
1813 .with_style(waiting_style.clone())
1814 .with_message(
1815 console::style(format!("{name} - waiting"))
1816 .for_stderr()
1817 .dim()
1818 .to_string(),
1819 ),
1820 )
1821 })
1822 .collect();
1823
1824 // Background timer: after expand_delay_ms reveal the detail view and
1825 // flush any bars that already finished (the BarRevealState pattern).
1826 let detail_multi_for_timer = detail_multi.clone();
1827 let spinner_for_timer = spinner_pb.clone();
1828 let reveal_state_for_timer = reveal_state.clone();
1829 let server_bars_for_timer = server_bars.clone();
1830 let heading_bar_for_timer = heading_bar.clone();
1831 let timer_handle = tokio::spawn(async move {
1832 tokio::time::sleep(Duration::from_millis(expand_delay_ms)).await;
1833 spinner_for_timer.finish_and_clear();
1834 detail_multi_for_timer.set_draw_target(ProgressDrawTarget::stderr());
1835 // Show the heading in the expanded view.
1836 heading_bar_for_timer.finish_with_message("waiting for servers to create bare git repo...");
1837 // Lock deferred list, mark revealed, and flush bars that already
1838 // finished. Must hold the lock across the revealed.store so that
1839 // finish_server_bar cannot push after the drain.
1840 let mut deferred = reveal_state_for_timer.deferred.lock().unwrap();
1841 reveal_state_for_timer
1842 .revealed
1843 .store(true, Ordering::Release);
1844 for df in deferred.drain(..) {
1845 df.bar.set_style(df.style);
1846 df.bar.finish_with_message(df.message);
1847 }
1848 // Kick still-waiting bars into drawing by enabling their tick.
1849 for bar in &server_bars_for_timer {
1850 if !bar.is_finished() {
1851 bar.enable_steady_tick(Duration::from_millis(100));
1852 }
1853 }
1854 });
1855
1856 // Poll each server in parallel
1857 let git_repo_path = git_repo.get_path()?.to_path_buf();
1858 let futures: Vec<_> = clone_urls
1859 .iter()
1860 .enumerate()
1861 .map(|(i, url)| {
1862 let url = url.clone();
1863 let ready_count = ready_count.clone();
1864 let spinner_pb = spinner_pb.clone();
1865 let bar = server_bars[i].clone();
1866 let git_repo_path = git_repo_path.clone();
1867 let reveal_state = reveal_state.clone();
1868 async move {
1869 let poll_interval = Duration::from_millis(500);
1870 let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
1871 let mut ready = false;
1872 loop {
1873 let is_ready = tokio::task::spawn_blocking({
1874 let url = url.clone();
1875 let path = git_repo_path.clone();
1876 move || check_git_server_ready(&path, &url)
1877 })
1878 .await
1879 .unwrap_or(false);
1880
1881 if is_ready {
1882 ready = true;
1883 break;
1884 }
1885
1886 if tokio::time::Instant::now() >= deadline {
1887 break;
1888 }
1889
1890 tokio::time::sleep(poll_interval).await;
1891 }
1892
1893 let count = if ready {
1894 ready_count.fetch_add(1, Ordering::Relaxed) + 1
1895 } else {
1896 ready_count.load(Ordering::Relaxed)
1897 };
1898
1899 // Update spinner message
1900 spinner_pb.set_message(format!(
1901 "waiting for servers to create bare git repo... ({count}/{total} - complete)"
1902 ));
1903
1904 // Finish per-server bar (deferred if detail not yet visible)
1905 let name = url
1906 .trim_start_matches("https://")
1907 .trim_start_matches("http://")
1908 .to_string();
1909 if ready {
1910 let style = ProgressStyle::with_template(&format!(
1911 " {} {{msg}}",
1912 console::style("✔").for_stderr().green()
1913 ))
1914 .unwrap();
1915 let msg = console::style(format!("{name} - ready"))
1916 .for_stderr()
1917 .green()
1918 .to_string();
1919 finish_server_bar(&bar, style, msg, &reveal_state);
1920 } else {
1921 let style = ProgressStyle::with_template(&format!(
1922 " {} {{msg}}",
1923 console::style("✘").for_stderr().red()
1924 ))
1925 .unwrap();
1926 let msg = console::style(format!("{name} - timeout"))
1927 .for_stderr()
1928 .red()
1929 .to_string();
1930 finish_server_bar(&bar, style, msg, &reveal_state);
1931 }
1932
1933 ready
1934 }
1935 })
1936 .collect();
1937
1938 let results = join_all(futures).await;
1939 let final_ready = ready_count.load(Ordering::Relaxed);
1940
1941 // Cancel the expand timer if it hasn't fired yet.
1942 timer_handle.abort();
1943
1944 // If detail view was revealed, clear the detail bars.
1945 if reveal_state.revealed.load(Ordering::Acquire) {
1946 let _ = detail_multi.clear();
1947 }
1948
1949 let all_ready = results.iter().all(|&r| r);
1950 if all_ready {
1951 // Success — erase the spinner line entirely, leave nothing behind.
1952 spinner_pb.finish_and_clear();
1953 } else {
1954 // Partial timeout — leave a message so the user knows we proceeded.
1955 spinner_pb.set_style(ProgressStyle::with_template("{msg}").unwrap());
1956 spinner_pb.finish_with_message(format!(
1957 "timed out waiting for servers to create bare git repo ({final_ready}/{total} - complete), proceeding anyway"
1958 ));
1959 }
1960
1961 Ok(())
1962}