diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-13 19:43:30 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-13 20:23:48 +0100 |
| commit | 8ecf357d9acc9ca2ec79e26a366cd1c07689a0cf (patch) | |
| tree | 30451ab0a67553e53643ee1cf0b0d337f6d9328a | |
| parent | 069a4c21c56291455fb9af09b693672889f98a03 (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
| -rw-r--r-- | src/bin/git_remote_nostr/fetch.rs | 182 |
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 @@ | |||
| 1 | use core::str; | 1 | use core::str; |
| 2 | use std::io::Stdin; | 2 | use std::{ |
| 3 | io::Stdin, | ||
| 4 | sync::{Arc, Mutex}, | ||
| 5 | time::Instant, | ||
| 6 | }; | ||
| 3 | 7 | ||
| 4 | use anyhow::{anyhow, bail, Result}; | 8 | use anyhow::{anyhow, bail, Result}; |
| 5 | use auth_git2::GitAuthenticator; | 9 | use auth_git2::GitAuthenticator; |
| 6 | use git2::Repository; | 10 | use git2::{Progress, Repository}; |
| 7 | use ngit::{ | 11 | use ngit::{ |
| 8 | git::{ | 12 | git::{ |
| 9 | nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, | 13 | nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, |
| @@ -18,7 +22,6 @@ use ngit::{ | |||
| 18 | use crate::utils::{ | 22 | use 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 | ||
| 24 | pub async fn run_fetch( | 27 | pub 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 | ||
| 175 | enum 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)] |
| 183 | fn report_on_transfer_progress( | 180 | fn 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 | |||
| 242 | struct 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 | } | ||
| 249 | impl<'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 | } |