diff options
Diffstat (limited to 'src/bin/ngit')
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 318 |
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 | ||
| 11 | use anyhow::{Context, Result, bail}; | 13 | use anyhow::{Context, Result, bail}; |
| 12 | use console::{Style, Term}; | 14 | use console::{Style, Term}; |
| 15 | use futures::future::join_all; | ||
| 13 | use git2::Oid; | 16 | use git2::Oid; |
| 17 | use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; | ||
| 14 | use ngit::{ | 18 | use 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 | |||
| 1690 | fn 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. | ||
| 1708 | struct 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. | ||
| 1718 | struct ServerRevealState { | ||
| 1719 | revealed: AtomicBool, | ||
| 1720 | deferred: Mutex<Vec<DeferredServerFinish>>, | ||
| 1721 | } | ||
| 1722 | |||
| 1723 | fn 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. | ||
| 1752 | async 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 | } | ||