upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/init.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 12:38:37 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 13:10:24 +0000
commite6bb9effa194fe63b5e969c090dbe6e93f13d312 (patch)
treeb0d3e93b8d1113af81a8c835417a601e68ca28b6 /src/bin/ngit/sub_commands/init.rs
parenteb3860359cfc2f2bab95a546bd6003091584b918 (diff)
feat: support PR-format proposals in ngit apply
Instead of erroring when a proposal uses PR format (kind 1618/1619), ngit apply now fetches the tip commit from git servers, determines the base commit via the merge-base tag or by walking ahead of main, generates patch text for each commit using git2, and applies via git am. Also fixes a bug where clone-tag server hints were silently skipped in the fetch fallback, and refactors the git-am invocation into a shared helper to avoid duplication between the patch and PR code paths.
Diffstat (limited to 'src/bin/ngit/sub_commands/init.rs')
-rw-r--r--src/bin/ngit/sub_commands/init.rs321
1 files changed, 175 insertions, 146 deletions
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
index 1b577ed..75306d1 100644
--- a/src/bin/ngit/sub_commands/init.rs
+++ b/src/bin/ngit/sub_commands/init.rs
@@ -274,8 +274,7 @@ fn apply_grasp_infrastructure(
274 public_key: &PublicKey, 274 public_key: &PublicKey,
275 identifier: &str, 275 identifier: &str,
276) -> Result<()> { 276) -> Result<()> {
277 let mut grasp_relay_insert_idx = 0; 277 for (grasp_relay_insert_idx, grasp_server) in grasp_servers.iter().enumerate() {
278 for grasp_server in grasp_servers {
279 // Always add grasp-derived clone URL 278 // Always add grasp-derived clone URL
280 let clone_url = format_grasp_server_url_as_clone_url(grasp_server, public_key, identifier)?; 279 let clone_url = format_grasp_server_url_as_clone_url(grasp_server, public_key, identifier)?;
281 280
@@ -312,7 +311,6 @@ fn apply_grasp_infrastructure(
312 if !relays.contains(&relay_url) { 311 if !relays.contains(&relay_url) {
313 relays.insert(grasp_relay_insert_idx, relay_url); 312 relays.insert(grasp_relay_insert_idx, relay_url);
314 } 313 }
315 grasp_relay_insert_idx += 1;
316 } 314 }
317 Ok(()) 315 Ok(())
318} 316}
@@ -1713,15 +1711,90 @@ struct DeferredServerFinish {
1713 message: String, 1711 message: String,
1714} 1712}
1715 1713
1716/// Coordinates the delayed reveal of per-server detail bars.
1717/// Bars that finish before the expand timer fires store their final
1718/// style+message here. The timer applies them all at reveal time so
1719/// every bar — completed or still waiting — appears in the expanded view.
1720struct ServerRevealState { 1714struct ServerRevealState {
1721 revealed: AtomicBool, 1715 revealed: AtomicBool,
1722 deferred: Mutex<Vec<DeferredServerFinish>>, 1716 deferred: Mutex<Vec<DeferredServerFinish>>,
1723} 1717}
1724 1718
1719struct PollContext {
1720 timeout_secs: u64,
1721 total: u64,
1722 ready_count: Arc<AtomicU64>,
1723 spinner_pb: ProgressBar,
1724 reveal_state: Arc<ServerRevealState>,
1725}
1726
1727fn create_server_bars(
1728 clone_urls: &[String],
1729 detail_multi: &MultiProgress,
1730) -> Vec<ProgressBar> {
1731 let waiting_style = ProgressStyle::with_template(" {spinner} {msg}")
1732 .unwrap()
1733 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈");
1734 clone_urls
1735 .iter()
1736 .map(|url| {
1737 let name = url
1738 .trim_start_matches("https://")
1739 .trim_start_matches("http://")
1740 .to_string();
1741 detail_multi.add(
1742 ProgressBar::new_spinner()
1743 .with_style(waiting_style.clone())
1744 .with_message(
1745 console::style(format!("{name} - waiting"))
1746 .for_stderr()
1747 .dim()
1748 .to_string(),
1749 ),
1750 )
1751 })
1752 .collect()
1753}
1754
1755fn spawn_expand_timer(
1756 expand_delay_ms: u64,
1757 spinner_pb: ProgressBar,
1758 detail_multi: MultiProgress,
1759 heading_bar: ProgressBar,
1760 reveal_state: Arc<ServerRevealState>,
1761 server_bars: Vec<ProgressBar>,
1762) -> tokio::task::JoinHandle<()> {
1763 tokio::spawn(async move {
1764 tokio::time::sleep(Duration::from_millis(expand_delay_ms)).await;
1765 spinner_pb.finish_and_clear();
1766 detail_multi.set_draw_target(ProgressDrawTarget::stderr());
1767 heading_bar.finish_with_message("waiting for servers to create bare git repo...");
1768 let mut deferred = reveal_state.deferred.lock().unwrap();
1769 reveal_state.revealed.store(true, Ordering::Release);
1770 for df in deferred.drain(..) {
1771 df.bar.set_style(df.style);
1772 df.bar.finish_with_message(df.message);
1773 }
1774 for bar in &server_bars {
1775 if !bar.is_finished() {
1776 bar.enable_steady_tick(Duration::from_millis(100));
1777 }
1778 }
1779 })
1780}
1781
1782fn finalize_spinner(
1783 all_ready: bool,
1784 spinner_pb: &ProgressBar,
1785 final_ready: u64,
1786 total: u64,
1787) {
1788 if all_ready {
1789 spinner_pb.finish_and_clear();
1790 } else {
1791 spinner_pb.set_style(ProgressStyle::with_template("{msg}").unwrap());
1792 spinner_pb.finish_with_message(format!(
1793 "timed out waiting for servers to create bare git repo ({final_ready}/{total} - complete), proceeding anyway"
1794 ));
1795 }
1796}
1797
1725fn finish_server_bar( 1798fn finish_server_bar(
1726 bar: &ProgressBar, 1799 bar: &ProgressBar,
1727 style: ProgressStyle, 1800 style: ProgressStyle,
@@ -1745,6 +1818,78 @@ fn finish_server_bar(
1745 } 1818 }
1746} 1819}
1747 1820
1821async fn poll_single_server(
1822 url: String,
1823 git_repo_path: std::path::PathBuf,
1824 bar: ProgressBar,
1825 ctx: Arc<PollContext>,
1826) -> bool {
1827 let poll_interval = Duration::from_millis(500);
1828 let deadline = tokio::time::Instant::now() + Duration::from_secs(ctx.timeout_secs);
1829 let mut ready = false;
1830 loop {
1831 let is_ready = tokio::task::spawn_blocking({
1832 let url = url.clone();
1833 let path = git_repo_path.clone();
1834 move || check_git_server_ready(&path, &url)
1835 })
1836 .await
1837 .unwrap_or(false);
1838
1839 if is_ready {
1840 ready = true;
1841 break;
1842 }
1843
1844 if tokio::time::Instant::now() >= deadline {
1845 break;
1846 }
1847
1848 tokio::time::sleep(poll_interval).await;
1849 }
1850
1851 let count = if ready {
1852 ctx.ready_count.fetch_add(1, Ordering::Relaxed) + 1
1853 } else {
1854 ctx.ready_count.load(Ordering::Relaxed)
1855 };
1856
1857 ctx.spinner_pb.set_message(format!(
1858 "waiting for servers to create bare git repo... ({count}/{total} - complete)",
1859 total = ctx.total
1860 ));
1861
1862 let name = url
1863 .trim_start_matches("https://")
1864 .trim_start_matches("http://")
1865 .to_string();
1866 if ready {
1867 let style = ProgressStyle::with_template(&format!(
1868 " {} {{msg}}",
1869 console::style("✔").for_stderr().green()
1870 ))
1871 .unwrap();
1872 let msg = console::style(format!("{name} - ready"))
1873 .for_stderr()
1874 .green()
1875 .to_string();
1876 finish_server_bar(&bar, style, msg, &ctx.reveal_state);
1877 } else {
1878 let style = ProgressStyle::with_template(&format!(
1879 " {} {{msg}}",
1880 console::style("✘").for_stderr().red()
1881 ))
1882 .unwrap();
1883 let msg = console::style(format!("{name} - timeout"))
1884 .for_stderr()
1885 .red()
1886 .to_string();
1887 finish_server_bar(&bar, style, msg, &ctx.reveal_state);
1888 }
1889
1890 ready
1891}
1892
1748/// Poll grasp servers in parallel until all are ready or timeout is reached. 1893/// Poll grasp servers in parallel until all are ready or timeout is reached.
1749/// 1894///
1750/// Shows a concise spinner with `x/y - complete` progress. After 5s without 1895/// Shows a concise spinner with `x/y - complete` progress. After 5s without
@@ -1799,166 +1944,50 @@ async fn wait_for_grasp_servers(
1799 deferred: Mutex::new(Vec::new()), 1944 deferred: Mutex::new(Vec::new()),
1800 }); 1945 });
1801 1946
1802 // Per-server spinner bars (added to hidden detail_multi) 1947 let server_bars = create_server_bars(&clone_urls, &detail_multi);
1803 let waiting_style = ProgressStyle::with_template(" {spinner} {msg}")
1804 .unwrap()
1805 .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈");
1806 let server_bars: Vec<ProgressBar> = clone_urls
1807 .iter()
1808 .map(|url| {
1809 let name = url
1810 .trim_start_matches("https://")
1811 .trim_start_matches("http://")
1812 .to_string();
1813 detail_multi.add(
1814 ProgressBar::new_spinner()
1815 .with_style(waiting_style.clone())
1816 .with_message(
1817 console::style(format!("{name} - waiting"))
1818 .for_stderr()
1819 .dim()
1820 .to_string(),
1821 ),
1822 )
1823 })
1824 .collect();
1825 1948
1826 // Background timer: after expand_delay_ms reveal the detail view and 1949 let timer_handle = spawn_expand_timer(
1827 // flush any bars that already finished (the BarRevealState pattern). 1950 expand_delay_ms,
1828 let detail_multi_for_timer = detail_multi.clone(); 1951 spinner_pb.clone(),
1829 let spinner_for_timer = spinner_pb.clone(); 1952 detail_multi.clone(),
1830 let reveal_state_for_timer = reveal_state.clone(); 1953 heading_bar,
1831 let server_bars_for_timer = server_bars.clone(); 1954 reveal_state.clone(),
1832 let heading_bar_for_timer = heading_bar.clone(); 1955 server_bars.clone(),
1833 let timer_handle = tokio::spawn(async move { 1956 );
1834 tokio::time::sleep(Duration::from_millis(expand_delay_ms)).await;
1835 spinner_for_timer.finish_and_clear();
1836 detail_multi_for_timer.set_draw_target(ProgressDrawTarget::stderr());
1837 // Show the heading in the expanded view.
1838 heading_bar_for_timer.finish_with_message("waiting for servers to create bare git repo...");
1839 // Lock deferred list, mark revealed, and flush bars that already
1840 // finished. Must hold the lock across the revealed.store so that
1841 // finish_server_bar cannot push after the drain.
1842 let mut deferred = reveal_state_for_timer.deferred.lock().unwrap();
1843 reveal_state_for_timer
1844 .revealed
1845 .store(true, Ordering::Release);
1846 for df in deferred.drain(..) {
1847 df.bar.set_style(df.style);
1848 df.bar.finish_with_message(df.message);
1849 }
1850 // Kick still-waiting bars into drawing by enabling their tick.
1851 for bar in &server_bars_for_timer {
1852 if !bar.is_finished() {
1853 bar.enable_steady_tick(Duration::from_millis(100));
1854 }
1855 }
1856 });
1857 1957
1858 // Poll each server in parallel 1958 // Poll each server in parallel
1859 let git_repo_path = git_repo.get_path()?.to_path_buf(); 1959 let git_repo_path = git_repo.get_path()?.to_path_buf();
1960 let poll_ctx = Arc::new(PollContext {
1961 timeout_secs,
1962 total,
1963 ready_count: ready_count.clone(),
1964 spinner_pb: spinner_pb.clone(),
1965 reveal_state: reveal_state.clone(),
1966 });
1860 let futures: Vec<_> = clone_urls 1967 let futures: Vec<_> = clone_urls
1861 .iter() 1968 .iter()
1862 .enumerate() 1969 .enumerate()
1863 .map(|(i, url)| { 1970 .map(|(i, url)| {
1864 let url = url.clone(); 1971 poll_single_server(
1865 let ready_count = ready_count.clone(); 1972 url.clone(),
1866 let spinner_pb = spinner_pb.clone(); 1973 git_repo_path.clone(),
1867 let bar = server_bars[i].clone(); 1974 server_bars[i].clone(),
1868 let git_repo_path = git_repo_path.clone(); 1975 poll_ctx.clone(),
1869 let reveal_state = reveal_state.clone(); 1976 )
1870 async move {
1871 let poll_interval = Duration::from_millis(500);
1872 let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
1873 let mut ready = false;
1874 loop {
1875 let is_ready = tokio::task::spawn_blocking({
1876 let url = url.clone();
1877 let path = git_repo_path.clone();
1878 move || check_git_server_ready(&path, &url)
1879 })
1880 .await
1881 .unwrap_or(false);
1882
1883 if is_ready {
1884 ready = true;
1885 break;
1886 }
1887
1888 if tokio::time::Instant::now() >= deadline {
1889 break;
1890 }
1891
1892 tokio::time::sleep(poll_interval).await;
1893 }
1894
1895 let count = if ready {
1896 ready_count.fetch_add(1, Ordering::Relaxed) + 1
1897 } else {
1898 ready_count.load(Ordering::Relaxed)
1899 };
1900
1901 // Update spinner message
1902 spinner_pb.set_message(format!(
1903 "waiting for servers to create bare git repo... ({count}/{total} - complete)"
1904 ));
1905
1906 // Finish per-server bar (deferred if detail not yet visible)
1907 let name = url
1908 .trim_start_matches("https://")
1909 .trim_start_matches("http://")
1910 .to_string();
1911 if ready {
1912 let style = ProgressStyle::with_template(&format!(
1913 " {} {{msg}}",
1914 console::style("✔").for_stderr().green()
1915 ))
1916 .unwrap();
1917 let msg = console::style(format!("{name} - ready"))
1918 .for_stderr()
1919 .green()
1920 .to_string();
1921 finish_server_bar(&bar, style, msg, &reveal_state);
1922 } else {
1923 let style = ProgressStyle::with_template(&format!(
1924 " {} {{msg}}",
1925 console::style("✘").for_stderr().red()
1926 ))
1927 .unwrap();
1928 let msg = console::style(format!("{name} - timeout"))
1929 .for_stderr()
1930 .red()
1931 .to_string();
1932 finish_server_bar(&bar, style, msg, &reveal_state);
1933 }
1934
1935 ready
1936 }
1937 }) 1977 })
1938 .collect(); 1978 .collect();
1939 1979
1940 let results = join_all(futures).await; 1980 let results = join_all(futures).await;
1941 let final_ready = ready_count.load(Ordering::Relaxed); 1981 let final_ready = ready_count.load(Ordering::Relaxed);
1942 1982
1943 // Cancel the expand timer if it hasn't fired yet.
1944 timer_handle.abort(); 1983 timer_handle.abort();
1945 1984
1946 // If detail view was revealed, clear the detail bars.
1947 if reveal_state.revealed.load(Ordering::Acquire) { 1985 if reveal_state.revealed.load(Ordering::Acquire) {
1948 let _ = detail_multi.clear(); 1986 let _ = detail_multi.clear();
1949 } 1987 }
1950 1988
1951 let all_ready = results.iter().all(|&r| r); 1989 let all_ready = results.iter().all(|&r| r);
1952 if all_ready { 1990 finalize_spinner(all_ready, &spinner_pb, final_ready, total);
1953 // Success — erase the spinner line entirely, leave nothing behind.
1954 spinner_pb.finish_and_clear();
1955 } else {
1956 // Partial timeout — leave a message so the user knows we proceeded.
1957 spinner_pb.set_style(ProgressStyle::with_template("{msg}").unwrap());
1958 spinner_pb.finish_with_message(format!(
1959 "timed out waiting for servers to create bare git repo ({final_ready}/{total} - complete), proceeding anyway"
1960 ));
1961 }
1962 1991
1963 Ok(()) 1992 Ok(())
1964} 1993}