upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bin/ngit/sub_commands/apply.rs169
-rw-r--r--src/bin/ngit/sub_commands/init.rs321
2 files changed, 321 insertions, 169 deletions
diff --git a/src/bin/ngit/sub_commands/apply.rs b/src/bin/ngit/sub_commands/apply.rs
index 4b13975..3700c37 100644
--- a/src/bin/ngit/sub_commands/apply.rs
+++ b/src/bin/ngit/sub_commands/apply.rs
@@ -1,4 +1,5 @@
1use std::{ 1use std::{
2 collections::HashSet,
2 io::Write, 3 io::Write,
3 process::{Command, Stdio}, 4 process::{Command, Stdio},
4 time::Duration, 5 time::Duration,
@@ -8,7 +9,13 @@ use anyhow::{Context, Result, bail};
8use indicatif::{ProgressBar, ProgressStyle}; 9use indicatif::{ProgressBar, ProgressStyle};
9use ngit::{ 10use ngit::{
10 client::get_all_proposal_patch_pr_pr_update_events_from_cache, 11 client::get_all_proposal_patch_pr_pr_update_events_from_cache,
11 git_events::get_pr_tip_event_or_most_recent_patch_with_ancestors, 12 fetch::fetch_from_git_server,
13 git::str_to_sha1,
14 git_events::{
15 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
16 get_pr_tip_event_or_most_recent_patch_with_ancestors, tag_value,
17 },
18 repo_ref::{RepoRef, is_grasp_server_in_list},
12}; 19};
13use nostr::nips::nip19::Nip19; 20use nostr::nips::nip19::Nip19;
14use nostr_sdk::{EventId, FromBech32}; 21use nostr_sdk::{EventId, FromBech32};
@@ -123,17 +130,15 @@ pub async fn launch(id: &str, stdout: bool, offline: bool) -> Result<()> {
123 let patches = get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone()) 130 let patches = get_pr_tip_event_or_most_recent_patch_with_ancestors(commits_events.clone())
124 .context("failed to find any PR or patch events on this proposal")?; 131 .context("failed to find any PR or patch events on this proposal")?;
125 132
126 if patches.iter().any(|e| { 133 if patches
127 [ 134 .iter()
128 ngit::git_events::KIND_PULL_REQUEST, 135 .any(|e| [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&e.kind))
129 ngit::git_events::KIND_PULL_REQUEST_UPDATE, 136 {
130 ] 137 let pr_event = patches
131 .contains(&e.kind) 138 .first()
132 }) { 139 .context("patch chain should contain at least one event")?;
133 bail!( 140 apply_pr(&git_repo, &repo_ref, pr_event, stdout)?;
134 "this proposal uses PR format (not patches). Use `ngit checkout {}` instead.", 141 return Ok(());
135 event_id.to_hex()
136 );
137 } 142 }
138 143
139 if stdout { 144 if stdout {
@@ -159,16 +164,124 @@ fn parse_event_id(id: &str) -> Result<EventId> {
159 bail!("invalid event-id or nevent: {id}") 164 bail!("invalid event-id or nevent: {id}")
160} 165}
161 166
162fn output_patches_to_stdout(mut patches: Vec<nostr::Event>) { 167fn fetch_oid_for_pr(
163 patches.reverse(); 168 oid: &str,
164 for patch in patches { 169 git_repo: &Repo,
165 print!("{}\n\n", patch.content); 170 repo_ref: &RepoRef,
171 pr_event: &nostr::Event,
172) -> Result<()> {
173 let git_servers = {
174 let mut seen: HashSet<String> = HashSet::new();
175 let mut out: Vec<String> = vec![];
176 for tag in pr_event.tags.as_slice() {
177 if tag.kind().eq(&nostr::event::TagKind::Clone) {
178 for clone_url in tag.as_slice().iter().skip(1) {
179 seen.insert(clone_url.clone());
180 out.push(clone_url.clone());
181 }
182 }
183 }
184 for server in &repo_ref.git_server {
185 if seen.insert(server.clone()) {
186 out.push(server.clone());
187 }
188 }
189 out
190 };
191
192 let term = console::Term::stderr();
193 for git_server_url in &git_servers {
194 if fetch_from_git_server(
195 git_repo,
196 &[oid.to_string()],
197 git_server_url,
198 &repo_ref.to_nostr_git_url(&None),
199 &term,
200 is_grasp_server_in_list(git_server_url, &repo_ref.grasp_servers()),
201 )
202 .is_ok()
203 {
204 return Ok(());
205 }
206 }
207 if !git_repo.does_commit_exist(oid)? {
208 bail!(
209 "cannot find proposal git data from proposal git server hint or repository git servers"
210 );
166 } 211 }
212 Ok(())
167} 213}
168 214
169fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> { 215fn apply_pr(
216 git_repo: &Repo,
217 repo_ref: &RepoRef,
218 pr_event: &nostr::Event,
219 stdout: bool,
220) -> Result<()> {
221 let tip_oid = tag_value(pr_event, "c").context("PR event is missing 'c' (tip commit) tag")?;
222
223 // Ensure the tip commit is available locally
224 if !git_repo.does_commit_exist(&tip_oid)? {
225 fetch_oid_for_pr(&tip_oid, git_repo, repo_ref, pr_event)?;
226 }
227
228 let tip = str_to_sha1(&tip_oid).context("invalid tip commit OID in PR event")?;
229
230 // Determine the base commit: prefer the merge-base tag, fall back to
231 // computing the divergence point from main/master.
232 let base = if let Ok(merge_base_oid) = tag_value(pr_event, "merge-base") {
233 str_to_sha1(&merge_base_oid).context("invalid merge-base OID in PR event")?
234 } else {
235 let (_, main_tip) = git_repo
236 .get_main_or_master_branch()
237 .context("could not determine main branch to compute PR base commit")?;
238 let (ahead, _behind) = git_repo
239 .get_commits_ahead_behind(&main_tip, &tip)
240 .context("failed to compute commits between main and PR tip")?;
241 // ahead is youngest-first; the last element is the oldest PR commit,
242 // whose parent is the effective base.
243 let oldest_pr_commit = ahead
244 .last()
245 .context("no commits found between main and PR tip")?;
246 git_repo
247 .get_commit_parent(oldest_pr_commit)
248 .context("failed to get parent of the oldest PR commit")?
249 };
250
251 // Collect commits from base..tip (youngest-first from get_commits_ahead_behind)
252 let (commits_youngest_first, _) = git_repo
253 .get_commits_ahead_behind(&base, &tip)
254 .context("failed to enumerate commits in PR")?;
255
256 if commits_youngest_first.is_empty() {
257 bail!("no commits found between base and PR tip");
258 }
259
260 let total = commits_youngest_first.len() as u64;
261
262 // Generate patches oldest-first
263 let mut patch_texts: Vec<String> = Vec::with_capacity(commits_youngest_first.len());
264 for (i, commit) in commits_youngest_first.iter().rev().enumerate() {
265 let series_count = Some((i as u64 + 1, total));
266 let patch = git_repo
267 .make_patch_from_commit(commit, &series_count)
268 .with_context(|| format!("failed to generate patch for commit {commit}"))?;
269 patch_texts.push(patch);
270 }
271
272 if stdout {
273 for patch in &patch_texts {
274 print!("{patch}\n\n");
275 }
276 } else {
277 apply_patch_texts(patch_texts)?;
278 }
279
280 Ok(())
281}
282
283fn apply_patch_texts(patch_texts: Vec<String>) -> Result<()> {
170 println!("applying to current branch with `git am`"); 284 println!("applying to current branch with `git am`");
171 patches.reverse();
172 285
173 let mut am = std::process::Command::new("git") 286 let mut am = std::process::Command::new("git")
174 .arg("am") 287 .arg("am")
@@ -183,15 +296,25 @@ fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
183 .as_mut() 296 .as_mut()
184 .context("git am process failed to take stdin")?; 297 .context("git am process failed to take stdin")?;
185 298
186 for patch in patches { 299 for patch in patch_texts {
187 stdin 300 stdin
188 .write(format!("{}\n\n", patch.content).as_bytes()) 301 .write(format!("{patch}\n\n").as_bytes())
189 .context("failed to write patch content into git am stdin buffer")?; 302 .context("failed to write patch content into git am stdin buffer")?;
190 } 303 }
191 stdin.flush()?; 304 stdin.flush()?;
192 let output = am 305 am.wait_with_output()
193 .wait_with_output()
194 .context("failed to read git am stdout")?; 306 .context("failed to read git am stdout")?;
195 print!("{:?}", output.stdout);
196 Ok(()) 307 Ok(())
197} 308}
309
310fn output_patches_to_stdout(mut patches: Vec<nostr::Event>) {
311 patches.reverse();
312 for patch in patches {
313 print!("{}\n\n", patch.content);
314 }
315}
316
317fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> {
318 patches.reverse();
319 apply_patch_texts(patches.into_iter().map(|p| p.content).collect())
320}
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}