upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit')
-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}