diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-13 20:55:12 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-13 20:55:12 +0100 |
| commit | fb48125595af09c557d03013e4e096d1d072e791 (patch) | |
| tree | 7011c39d48fb44b8bc39b6b7ee5a1e825290d971 /src | |
| parent | b685eefd3ca2821cba3d740d6f114db2d9649a5a (diff) | |
fix(remote): add robustness to push 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')
| -rw-r--r-- | src/bin/git_remote_nostr/push.rs | 212 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/utils.rs | 18 |
2 files changed, 171 insertions, 59 deletions
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs index 0a7f1a6..71afc98 100644 --- a/src/bin/git_remote_nostr/push.rs +++ b/src/bin/git_remote_nostr/push.rs | |||
| @@ -2,6 +2,8 @@ use core::str; | |||
| 2 | use std::{ | 2 | use std::{ |
| 3 | collections::{HashMap, HashSet}, | 3 | collections::{HashMap, HashSet}, |
| 4 | io::Stdin, | 4 | io::Stdin, |
| 5 | sync::{Arc, Mutex}, | ||
| 6 | time::Instant, | ||
| 5 | }; | 7 | }; |
| 6 | 8 | ||
| 7 | use anyhow::{anyhow, bail, Context, Result}; | 9 | use anyhow::{anyhow, bail, Context, Result}; |
| @@ -39,7 +41,7 @@ use crate::{ | |||
| 39 | utils::{ | 41 | utils::{ |
| 40 | find_proposal_and_patches_by_branch_name, get_all_proposals, get_remote_name_by_url, | 42 | find_proposal_and_patches_by_branch_name, get_all_proposals, get_remote_name_by_url, |
| 41 | get_short_git_server_name, get_write_protocols_to_try, join_with_and, | 43 | get_short_git_server_name, get_write_protocols_to_try, join_with_and, |
| 42 | push_error_is_not_authentication_failure, read_line, report_on_sideband_progress, | 44 | push_error_is_not_authentication_failure, read_line, |
| 43 | }, | 45 | }, |
| 44 | }; | 46 | }; |
| 45 | 47 | ||
| @@ -404,25 +406,35 @@ fn push_to_remote_url( | |||
| 404 | let auth = GitAuthenticator::default(); | 406 | let auth = GitAuthenticator::default(); |
| 405 | let mut push_options = git2::PushOptions::new(); | 407 | let mut push_options = git2::PushOptions::new(); |
| 406 | let mut remote_callbacks = git2::RemoteCallbacks::new(); | 408 | let mut remote_callbacks = git2::RemoteCallbacks::new(); |
| 409 | let push_reporter = Arc::new(Mutex::new(PushReporter::new(term))); | ||
| 410 | |||
| 407 | remote_callbacks.credentials(auth.credentials(&git_config)); | 411 | remote_callbacks.credentials(auth.credentials(&git_config)); |
| 408 | remote_callbacks.push_update_reference(|name, error| { | 412 | |
| 409 | if let Some(error) = error { | 413 | remote_callbacks.push_update_reference({ |
| 410 | term.write_line( | 414 | let push_reporter = Arc::clone(&push_reporter); |
| 411 | format!( | 415 | move |name, error| { |
| 416 | let mut reporter = push_reporter.lock().unwrap(); | ||
| 417 | if let Some(error) = error { | ||
| 418 | let existing_lines = reporter.count_all_existing_lines(); | ||
| 419 | reporter.update_reference_errors.push(format!( | ||
| 412 | "WARNING: {} failed to push {name} error: {error}", | 420 | "WARNING: {} failed to push {name} error: {error}", |
| 413 | get_short_git_server_name(git_repo, git_server_url), | 421 | get_short_git_server_name(git_repo, git_server_url), |
| 414 | ) | 422 | )); |
| 415 | .as_str(), | 423 | reporter.write_all(existing_lines); |
| 416 | ) | 424 | } |
| 417 | .unwrap(); | 425 | Ok(()) |
| 418 | } | 426 | } |
| 419 | Ok(()) | ||
| 420 | }); | 427 | }); |
| 421 | remote_callbacks.push_negotiation(|updates| { | 428 | |
| 422 | for update in updates { | 429 | remote_callbacks.push_negotiation({ |
| 423 | let _ = term.write_line( | 430 | let push_reporter = Arc::clone(&push_reporter); |
| 424 | format!( | 431 | move |updates| { |
| 425 | " {}..{} {} -> {}", | 432 | let mut reporter = push_reporter.lock().unwrap(); |
| 433 | let existing_lines = reporter.count_all_existing_lines(); | ||
| 434 | |||
| 435 | for update in updates { | ||
| 436 | let msg = format!( | ||
| 437 | "push: {}..{} {} -> {}", | ||
| 426 | oid_to_shorthand_string(update.dst()).unwrap(), | 438 | oid_to_shorthand_string(update.dst()).unwrap(), |
| 427 | oid_to_shorthand_string(update.src()).unwrap(), | 439 | oid_to_shorthand_string(update.src()).unwrap(), |
| 428 | update | 440 | update |
| @@ -435,36 +447,30 @@ fn push_to_remote_url( | |||
| 435 | .unwrap_or("") | 447 | .unwrap_or("") |
| 436 | .replace("refs/heads/", "") | 448 | .replace("refs/heads/", "") |
| 437 | .replace("refs/tags/", "tags/"), | 449 | .replace("refs/tags/", "tags/"), |
| 438 | ) | 450 | ); |
| 439 | .as_str(), | 451 | reporter.negotiation.push(msg); |
| 440 | ); | 452 | } |
| 441 | // now a blank line for transfer progress to delete | 453 | reporter.write_all(existing_lines); |
| 442 | let _ = term.write_line(""); | 454 | Ok(()) |
| 443 | } | 455 | } |
| 444 | Ok(()) | ||
| 445 | }); | 456 | }); |
| 446 | remote_callbacks.push_transfer_progress( | 457 | |
| 458 | remote_callbacks.push_transfer_progress({ | ||
| 459 | let push_reporter = Arc::clone(&push_reporter); | ||
| 447 | #[allow(clippy::cast_precision_loss)] | 460 | #[allow(clippy::cast_precision_loss)] |
| 448 | |current, total, bytes| { | 461 | move |current, total, bytes| { |
| 449 | let _ = term.clear_last_lines(1); | 462 | let mut reporter = push_reporter.lock().unwrap(); |
| 450 | let percentage = (current as f64 / total as f64) * 100.0; | 463 | reporter.process_transfer_progress_update(current, total, bytes); |
| 451 | let (size, unit) = if bytes >= (1024 * 1024) { | 464 | } |
| 452 | (bytes as f64 / (1024.0 * 1024.0), "MiB") | 465 | }); |
| 453 | } else { | ||
| 454 | (bytes as f64 / 1024.0, "KiB") | ||
| 455 | }; | ||
| 456 | let _ = term.write_line( | ||
| 457 | format!( | ||
| 458 | "Writing objects: {percentage:.0}% ({current}/{total}) {size:.2} {unit}, done." | ||
| 459 | ) | ||
| 460 | .as_str(), | ||
| 461 | ); | ||
| 462 | }, | ||
| 463 | ); | ||
| 464 | 466 | ||
| 465 | remote_callbacks.sideband_progress(|data| { | 467 | remote_callbacks.sideband_progress({ |
| 466 | report_on_sideband_progress(data, term); | 468 | let push_reporter = Arc::clone(&push_reporter); |
| 467 | true | 469 | move |data| { |
| 470 | let mut reporter = push_reporter.lock().unwrap(); | ||
| 471 | reporter.process_remote_msg(data); | ||
| 472 | true | ||
| 473 | } | ||
| 468 | }); | 474 | }); |
| 469 | push_options.remote_callbacks(remote_callbacks); | 475 | push_options.remote_callbacks(remote_callbacks); |
| 470 | git_server_remote.push(remote_refspecs, Some(&mut push_options))?; | 476 | git_server_remote.push(remote_refspecs, Some(&mut push_options))?; |
| @@ -472,6 +478,130 @@ fn push_to_remote_url( | |||
| 472 | Ok(()) | 478 | Ok(()) |
| 473 | } | 479 | } |
| 474 | 480 | ||
| 481 | #[allow(clippy::cast_precision_loss)] | ||
| 482 | #[allow(clippy::float_cmp)] | ||
| 483 | #[allow(clippy::needless_pass_by_value)] | ||
| 484 | fn report_on_transfer_progress( | ||
| 485 | current: usize, | ||
| 486 | total: usize, | ||
| 487 | bytes: usize, | ||
| 488 | start_time: &Instant, | ||
| 489 | end_time: &Option<Instant>, | ||
| 490 | ) -> String { | ||
| 491 | let percentage = ((current as f64 / total as f64) * 100.0) | ||
| 492 | // always round down because 100% complete is misleading when its not complete | ||
| 493 | .floor(); | ||
| 494 | let (size, unit) = if bytes as f64 >= (1024.0 * 1024.0) { | ||
| 495 | (bytes as f64 / (1024.0 * 1024.0), "MiB") | ||
| 496 | } else { | ||
| 497 | (bytes as f64 / 1024.0, "KiB") | ||
| 498 | }; | ||
| 499 | let speed = { | ||
| 500 | let duration = if let Some(end_time) = end_time { | ||
| 501 | (*end_time - *start_time).as_millis() as f64 | ||
| 502 | } else { | ||
| 503 | start_time.elapsed().as_millis() as f64 | ||
| 504 | }; | ||
| 505 | |||
| 506 | if duration > 0.0 { | ||
| 507 | (bytes as f64 / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds | ||
| 508 | } else { | ||
| 509 | 0.0 | ||
| 510 | } | ||
| 511 | }; | ||
| 512 | |||
| 513 | format!( | ||
| 514 | "push: Writing objects: {percentage}% ({current}/{total}) {size:.2} {unit} | {speed:.2} MiB/s{}", | ||
| 515 | if current == total { ", done." } else { "" }, | ||
| 516 | ) | ||
| 517 | } | ||
| 518 | |||
| 519 | struct PushReporter<'a> { | ||
| 520 | remote_msgs: Vec<String>, | ||
| 521 | negotiation: Vec<String>, | ||
| 522 | transfer_progress_msgs: Vec<String>, | ||
| 523 | update_reference_errors: Vec<String>, | ||
| 524 | term: &'a console::Term, | ||
| 525 | start_time: Option<Instant>, | ||
| 526 | end_time: Option<Instant>, | ||
| 527 | } | ||
| 528 | impl<'a> PushReporter<'a> { | ||
| 529 | fn new(term: &'a console::Term) -> Self { | ||
| 530 | Self { | ||
| 531 | remote_msgs: vec![], | ||
| 532 | negotiation: vec![], | ||
| 533 | transfer_progress_msgs: vec![], | ||
| 534 | update_reference_errors: vec![], | ||
| 535 | term, | ||
| 536 | start_time: None, | ||
| 537 | end_time: None, | ||
| 538 | } | ||
| 539 | } | ||
| 540 | fn write_all(&self, lines_to_clear: usize) { | ||
| 541 | let _ = self.term.clear_last_lines(lines_to_clear); | ||
| 542 | for msg in &self.remote_msgs { | ||
| 543 | let _ = self.term.write_line(msg); | ||
| 544 | } | ||
| 545 | for msg in &self.negotiation { | ||
| 546 | let _ = self.term.write_line(msg); | ||
| 547 | } | ||
| 548 | for msg in &self.transfer_progress_msgs { | ||
| 549 | let _ = self.term.write_line(msg); | ||
| 550 | } | ||
| 551 | for msg in &self.update_reference_errors { | ||
| 552 | let _ = self.term.write_line(msg); | ||
| 553 | } | ||
| 554 | } | ||
| 555 | |||
| 556 | fn count_all_existing_lines(&self) -> usize { | ||
| 557 | self.remote_msgs.len() | ||
| 558 | + self.negotiation.len() | ||
| 559 | + self.transfer_progress_msgs.len() | ||
| 560 | + self.update_reference_errors.len() | ||
| 561 | } | ||
| 562 | fn process_remote_msg(&mut self, data: &[u8]) { | ||
| 563 | let existing_lines = self.count_all_existing_lines(); | ||
| 564 | if let Ok(data) = str::from_utf8(data) { | ||
| 565 | let data = data | ||
| 566 | .split(['\n', '\r']) | ||
| 567 | .find(|line| !line.is_empty()) | ||
| 568 | .unwrap_or("") | ||
| 569 | .trim(); | ||
| 570 | if !data.is_empty() { | ||
| 571 | let msg = format!("remote: {data}"); | ||
| 572 | if let Some(last) = self.remote_msgs.last() { | ||
| 573 | if (last.contains('%') && !last.contains("100%")) | ||
| 574 | || last == &msg.replace(", done.", "") | ||
| 575 | { | ||
| 576 | self.remote_msgs.pop(); | ||
| 577 | } | ||
| 578 | } | ||
| 579 | self.remote_msgs.push(msg); | ||
| 580 | self.write_all(existing_lines); | ||
| 581 | } | ||
| 582 | } | ||
| 583 | } | ||
| 584 | fn process_transfer_progress_update(&mut self, current: usize, total: usize, bytes: usize) { | ||
| 585 | if self.start_time.is_none() { | ||
| 586 | self.start_time = Some(Instant::now()); | ||
| 587 | } | ||
| 588 | let existing_lines = self.count_all_existing_lines(); | ||
| 589 | |||
| 590 | let report = report_on_transfer_progress( | ||
| 591 | current, | ||
| 592 | total, | ||
| 593 | bytes, | ||
| 594 | &self.start_time.unwrap(), | ||
| 595 | &self.end_time, | ||
| 596 | ); | ||
| 597 | if report.contains("100%") { | ||
| 598 | self.end_time = Some(Instant::now()); | ||
| 599 | } | ||
| 600 | self.transfer_progress_msgs = vec![report]; | ||
| 601 | self.write_all(existing_lines); | ||
| 602 | } | ||
| 603 | } | ||
| 604 | |||
| 475 | type HashMapUrlRefspecs = HashMap<String, Vec<String>>; | 605 | type HashMapUrlRefspecs = HashMap<String, Vec<String>>; |
| 476 | 606 | ||
| 477 | #[allow(clippy::too_many_lines)] | 607 | #[allow(clippy::too_many_lines)] |
diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs index bbe7cfa..a13d398 100644 --- a/src/bin/git_remote_nostr/utils.rs +++ b/src/bin/git_remote_nostr/utils.rs | |||
| @@ -293,24 +293,6 @@ pub fn error_might_be_authentication_related(error: &anyhow::Error) -> bool { | |||
| 293 | false | 293 | false |
| 294 | } | 294 | } |
| 295 | 295 | ||
| 296 | pub fn report_on_sideband_progress(data: &[u8], term: &console::Term) { | ||
| 297 | if let Ok(data) = str::from_utf8(data) { | ||
| 298 | let data = data | ||
| 299 | .split(['\n', '\r']) | ||
| 300 | .find(|line| !line.is_empty()) | ||
| 301 | .unwrap_or(""); | ||
| 302 | if !data.is_empty() { | ||
| 303 | let s = format!("remote: {data}"); | ||
| 304 | let _ = term.clear_last_lines(1); | ||
| 305 | let _ = term.write_line(s.as_str()); | ||
| 306 | if !s.contains('%') || s.contains("100%") { | ||
| 307 | // print it twice so the next sideband_progress doesn't delete it | ||
| 308 | let _ = term.write_line(s.as_str()); | ||
| 309 | } | ||
| 310 | } | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | #[cfg(test)] | 296 | #[cfg(test)] |
| 315 | mod tests { | 297 | mod tests { |
| 316 | use super::*; | 298 | use super::*; |