upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/git_remote_nostr/fetch.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-09-13 19:43:30 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-13 20:23:48 +0100
commit8ecf357d9acc9ca2ec79e26a366cd1c07689a0cf (patch)
tree30451ab0a67553e53643ee1cf0b0d337f6d9328a /src/bin/git_remote_nostr/fetch.rs
parent069a4c21c56291455fb9af09b693672889f98a03 (diff)
fix(remote): add rebustness to fetch reporting
by avoiding bugs where lines are removed accidentally by storing report in a mutex and rewriting the entire report at each update
Diffstat (limited to 'src/bin/git_remote_nostr/fetch.rs')
-rw-r--r--src/bin/git_remote_nostr/fetch.rs182
1 files changed, 139 insertions, 43 deletions
diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs
index 59067fa..c2c0cb1 100644
--- a/src/bin/git_remote_nostr/fetch.rs
+++ b/src/bin/git_remote_nostr/fetch.rs
@@ -1,9 +1,13 @@
1use core::str; 1use core::str;
2use std::io::Stdin; 2use std::{
3 io::Stdin,
4 sync::{Arc, Mutex},
5 time::Instant,
6};
3 7
4use anyhow::{anyhow, bail, Result}; 8use anyhow::{anyhow, bail, Result};
5use auth_git2::GitAuthenticator; 9use auth_git2::GitAuthenticator;
6use git2::Repository; 10use git2::{Progress, Repository};
7use ngit::{ 11use ngit::{
8 git::{ 12 git::{
9 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, 13 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
@@ -18,7 +22,6 @@ use ngit::{
18use crate::utils::{ 22use crate::utils::{
19 fetch_or_list_error_is_not_authentication_failure, find_proposal_and_patches_by_branch_name, 23 fetch_or_list_error_is_not_authentication_failure, find_proposal_and_patches_by_branch_name,
20 get_oids_from_fetch_batch, get_open_proposals, get_read_protocols_to_try, join_with_and, 24 get_oids_from_fetch_batch, get_open_proposals, get_read_protocols_to_try, join_with_and,
21 report_on_sideband_progress,
22}; 25};
23 26
24pub async fn run_fetch( 27pub async fn run_fetch(
@@ -134,7 +137,6 @@ fn fetch_from_git_server(
134 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol), 137 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol),
135 term, 138 term,
136 ); 139 );
137 term.clear_last_lines(1)?;
138 if let Err(error) = res { 140 if let Err(error) = res {
139 term.write_line( 141 term.write_line(
140 format!("fetch: {formatted_url} failed over {protocol}: {error}").as_str(), 142 format!("fetch: {formatted_url} failed over {protocol}: {error}").as_str(),
@@ -172,55 +174,144 @@ fn fetch_from_git_server(
172 } 174 }
173} 175}
174 176
175enum ProgressStatus {
176 InProgress,
177 Complete,
178}
179
180#[allow(clippy::cast_precision_loss)] 177#[allow(clippy::cast_precision_loss)]
181#[allow(clippy::float_cmp)] 178#[allow(clippy::float_cmp)]
182#[allow(clippy::needless_pass_by_value)] 179#[allow(clippy::needless_pass_by_value)]
183fn report_on_transfer_progress( 180fn report_on_transfer_progress(
184 progress_stats: &git2::Progress<'_>, 181 progress_stats: &Progress<'_>,
185 term: &console::Term, 182 start_time: &Instant,
186 status: ProgressStatus, 183 end_time: &Option<Instant>,
187) { 184) -> Vec<String> {
185 let mut report = vec![];
188 let total = progress_stats.total_objects() as f64; 186 let total = progress_stats.total_objects() as f64;
189 if total == 0.0 { 187 if total == 0.0 {
190 return; 188 return report;
191 } 189 }
192 let received = progress_stats.received_objects() as f64; 190 let received = progress_stats.received_objects() as f64;
193 let percentage = (received / total) * 100.0; 191 let percentage = ((received / total) * 100.0)
192 // always round down because 100% complete is misleading when its not complete
193 .floor();
194 194
195 // Get the total received bytes
196 let received_bytes = progress_stats.received_bytes() as f64; 195 let received_bytes = progress_stats.received_bytes() as f64;
197 196
198 // Determine whether to use KiB or MiB
199 let (size, unit) = if received_bytes >= (1024.0 * 1024.0) { 197 let (size, unit) = if received_bytes >= (1024.0 * 1024.0) {
200 // Convert to MiB
201 (received_bytes / (1024.0 * 1024.0), "MiB") 198 (received_bytes / (1024.0 * 1024.0), "MiB")
202 } else { 199 } else {
203 // Convert to KiB
204 (received_bytes / 1024.0, "KiB") 200 (received_bytes / 1024.0, "KiB")
205 }; 201 };
206 202
203 let speed = {
204 let duration = if let Some(end_time) = end_time {
205 (*end_time - *start_time).as_millis() as f64
206 } else {
207 start_time.elapsed().as_millis() as f64
208 };
209
210 if duration > 0.0 {
211 (received_bytes / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds
212 } else {
213 0.0
214 }
215 };
216
207 // Format the output for receiving objects 217 // Format the output for receiving objects
208 if received < total || matches!(status, ProgressStatus::Complete) { 218 report.push(format!(
209 let _ = term.write_line( 219 "Receiving objects: {percentage}% ({received}/{total}) {size:.2} {unit} | {speed:.2} MiB/s{}",
210 format!( 220 if received == total {
211 "Receiving objects: {percentage:.0}% ({received}/{total}) {size:.2} {unit}, done.", 221 ", done."
212 ) 222 } else { ""},
213 .as_str(), 223 ));
214 ); 224 if received == total {
215 }
216 if received == total || matches!(status, ProgressStatus::Complete) {
217 let indexed_deltas = progress_stats.indexed_deltas() as f64; 225 let indexed_deltas = progress_stats.indexed_deltas() as f64;
218 let total_deltas = progress_stats.total_deltas() as f64; 226 let total_deltas = progress_stats.total_deltas() as f64;
219 let percentage = (indexed_deltas / total_deltas) * 100.0; 227 let percentage = ((indexed_deltas / total_deltas) * 100.0)
220 let _ = term.write_line( 228 // always round down because 100% complete is misleading when its not complete
221 format!("Resolving deltas: {percentage:.0}% ({indexed_deltas}/{total_deltas}) done.") 229 .floor();
222 .as_str(), 230 report.push(format!(
223 ); 231 "Resolving deltas: {percentage}% ({indexed_deltas}/{total_deltas}){}",
232 if indexed_deltas == total_deltas {
233 ", done."
234 } else {
235 ""
236 },
237 ));
238 }
239 report
240}
241
242struct FetchReporter<'a> {
243 remote_msgs: Vec<String>,
244 transfer_progress_msgs: Vec<String>,
245 term: &'a console::Term,
246 start_time: Option<Instant>,
247 end_time: Option<Instant>,
248}
249impl<'a> FetchReporter<'a> {
250 fn new(term: &'a console::Term) -> Self {
251 Self {
252 remote_msgs: vec![],
253 transfer_progress_msgs: vec![],
254 term,
255 start_time: None,
256 end_time: None,
257 }
258 }
259 fn write_all(&self, lines_to_clear: usize) {
260 let _ = self.term.clear_last_lines(lines_to_clear);
261 for msg in &self.remote_msgs {
262 let _ = self.term.write_line(msg);
263 }
264 for msg in &self.transfer_progress_msgs {
265 let _ = self.term.write_line(msg);
266 }
267 }
268 fn write_transfer_progress(&self, lines_to_clear: usize) {
269 let _ = self.term.clear_last_lines(lines_to_clear);
270 for msg in &self.transfer_progress_msgs {
271 let _ = self.term.write_line(msg);
272 }
273 }
274 fn count_all_existing_lines(&self) -> usize {
275 self.remote_msgs.len() + self.transfer_progress_msgs.len()
276 }
277 fn process_remote_msg(&mut self, data: &[u8]) {
278 let existing_lines = self.count_all_existing_lines();
279 if let Ok(data) = str::from_utf8(data) {
280 let data = data
281 .split(['\n', '\r'])
282 .find(|line| !line.is_empty())
283 .unwrap_or("")
284 .trim();
285 if !data.is_empty() {
286 let msg = format!("remote: {data}");
287 if let Some(last) = self.remote_msgs.last() {
288 if (last.contains('%') && !last.contains("100%"))
289 || last == &msg.replace(", done.", "")
290 {
291 self.remote_msgs.pop();
292 }
293 }
294 self.remote_msgs.push(msg);
295 self.write_all(existing_lines);
296 }
297 }
298 }
299 fn process_transfer_progress_update(&mut self, progress_stats: &git2::Progress<'_>) {
300 if self.start_time.is_none() {
301 self.start_time = Some(Instant::now());
302 }
303 let existing_lines = self.count_all_existing_lines();
304 let updated =
305 report_on_transfer_progress(progress_stats, &self.start_time.unwrap(), &self.end_time);
306 if self.transfer_progress_msgs.len() <= updated.len() {
307 if self.end_time.is_none() && updated.first().is_some_and(|f| f.contains("100%")) {
308 self.end_time = Some(Instant::now());
309 }
310 // once "Resolving Deltas" is complete, deltas get reset to 0 and it stops
311 // reporting on it so we want to keep the old report
312 self.transfer_progress_msgs = updated;
313 }
314 self.write_all(existing_lines);
224 } 315 }
225} 316}
226 317
@@ -239,25 +330,30 @@ fn fetch_from_git_server_url(
239 let auth = GitAuthenticator::default(); 330 let auth = GitAuthenticator::default();
240 let mut fetch_options = git2::FetchOptions::new(); 331 let mut fetch_options = git2::FetchOptions::new();
241 let mut remote_callbacks = git2::RemoteCallbacks::new(); 332 let mut remote_callbacks = git2::RemoteCallbacks::new();
242 remote_callbacks.sideband_progress(|data| { 333 let fetch_reporter = Arc::new(Mutex::new(FetchReporter::new(term)));
243 report_on_sideband_progress(data, term); 334 remote_callbacks.sideband_progress({
244 true 335 let fetch_reporter = Arc::clone(&fetch_reporter);
336 move |data| {
337 let mut reporter = fetch_reporter.lock().unwrap();
338 reporter.process_remote_msg(data);
339 true
340 }
245 }); 341 });
246 remote_callbacks.transfer_progress(|stats| { 342 remote_callbacks.transfer_progress({
247 let _ = term.clear_last_lines(1); 343 let fetch_reporter = Arc::clone(&fetch_reporter);
248 report_on_transfer_progress(&stats, term, ProgressStatus::InProgress); 344 move |stats| {
249 true 345 let mut reporter = fetch_reporter.lock().unwrap();
346 reporter.process_transfer_progress_update(&stats);
347 true
348 }
250 }); 349 });
251 350
252 if !dont_authenticate { 351 if !dont_authenticate {
253 remote_callbacks.credentials(auth.credentials(&git_config)); 352 remote_callbacks.credentials(auth.credentials(&git_config));
254 } 353 }
255 fetch_options.remote_callbacks(remote_callbacks); 354 fetch_options.remote_callbacks(remote_callbacks);
256 term.write_line("")?;
257 git_server_remote.download(oids, Some(&mut fetch_options))?; 355 git_server_remote.download(oids, Some(&mut fetch_options))?;
258 356
259 report_on_transfer_progress(&git_server_remote.stats(), term, ProgressStatus::Complete);
260
261 git_server_remote.disconnect()?; 357 git_server_remote.disconnect()?;
262 Ok(()) 358 Ok(())
263} 359}