upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/git_remote_nostr/push.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/git_remote_nostr/push.rs')
-rw-r--r--src/bin/git_remote_nostr/push.rs319
1 files changed, 10 insertions, 309 deletions
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index 909a0ab..4e0d760 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -2,12 +2,10 @@ use core::str;
2use std::{ 2use std::{
3 collections::{HashMap, HashSet}, 3 collections::{HashMap, HashSet},
4 io::Stdin, 4 io::Stdin,
5 sync::{Arc, Mutex}, 5 sync::Arc,
6 time::Instant,
7}; 6};
8 7
9use anyhow::{Context, Result, anyhow, bail}; 8use anyhow::{Context, Result, bail};
10use auth_git2::GitAuthenticator;
11use client::{get_events_from_local_cache, get_state_from_cache, send_events, sign_event}; 9use client::{get_events_from_local_cache, get_state_from_cache, send_events, sign_event};
12use console::Term; 10use console::Term;
13use git::{RepoActions, sha1_to_oid}; 11use git::{RepoActions, sha1_to_oid};
@@ -17,17 +15,18 @@ use git_events::{
17}; 15};
18use git2::{Oid, Repository}; 16use git2::{Oid, Repository};
19use ngit::{ 17use ngit::{
20 cli_interactor::count_lines_per_msg_vec,
21 client::{self, get_event_from_cache_by_id, sign_draft_event}, 18 client::{self, get_event_from_cache_by_id, sign_draft_event},
22 git::{ 19 git::{self, nostr_url::NostrUrlDecoded},
23 self,
24 nostr_url::{CloneUrl, NostrUrlDecoded},
25 oid_to_shorthand_string,
26 },
27 git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root}, 20 git_events::{self, KIND_PULL_REQUEST, event_to_cover_letter, get_event_root},
21 list::list_from_remotes,
28 login::{self, user::UserRef}, 22 login::{self, user::UserRef},
23 push::{push_to_remote, push_to_remote_url},
29 repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url}, 24 repo_ref::{self, get_repo_config_from_yaml, is_grasp_server, normalize_grasp_server_url},
30 repo_state, 25 repo_state,
26 utils::{
27 find_proposal_and_patches_by_branch_name, get_all_proposals, get_remote_name_by_url,
28 get_short_git_server_name, read_line,
29 },
31}; 30};
32use nostr::{event::UnsignedEvent, nips::nip10::Marker}; 31use nostr::{event::UnsignedEvent, nips::nip10::Marker};
33use nostr_sdk::{ 32use nostr_sdk::{
@@ -37,16 +36,7 @@ use nostr_sdk::{
37use repo_ref::RepoRef; 36use repo_ref::RepoRef;
38use repo_state::RepoState; 37use repo_state::RepoState;
39 38
40use crate::{ 39use crate::{client::Client, git::Repo};
41 client::Client,
42 git::Repo,
43 list::list_from_remotes,
44 utils::{
45 Direction, find_proposal_and_patches_by_branch_name, get_all_proposals,
46 get_remote_name_by_url, get_short_git_server_name, get_write_protocols_to_try,
47 join_with_and, read_line, set_protocol_preference,
48 },
49};
50 40
51#[allow(clippy::too_many_lines)] 41#[allow(clippy::too_many_lines)]
52#[allow(clippy::type_complexity)] 42#[allow(clippy::type_complexity)]
@@ -590,295 +580,6 @@ async fn generate_patches_or_pr_event_or_pr_updates(
590 Ok(events) 580 Ok(events)
591} 581}
592 582
593fn push_to_remote(
594 git_repo: &Repo,
595 git_server_url: &str,
596 decoded_nostr_url: &NostrUrlDecoded,
597 remote_refspecs: &[String],
598 term: &Term,
599 is_grasp_server: bool,
600) -> Result<()> {
601 let server_url = git_server_url.parse::<CloneUrl>()?;
602 let protocols_to_attempt =
603 get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_grasp_server);
604
605 let mut failed_protocols = vec![];
606 let mut success = false;
607
608 for protocol in &protocols_to_attempt {
609 term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?;
610
611 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?;
612
613 if let Err(error) = push_to_remote_url(git_repo, &formatted_url, remote_refspecs, term) {
614 term.write_line(
615 format!("push: {formatted_url} failed over {protocol}: {error}").as_str(),
616 )?;
617 failed_protocols.push(protocol);
618 } else {
619 success = true;
620 if !failed_protocols.is_empty() {
621 term.write_line(format!("push: succeeded over {protocol}").as_str())?;
622 let _ = set_protocol_preference(git_repo, protocol, &server_url, &Direction::Push);
623 }
624 break;
625 }
626 }
627 if success {
628 Ok(())
629 } else {
630 let error = anyhow!(
631 "{} failed over {}{}",
632 server_url.short_name(),
633 join_with_and(&failed_protocols),
634 if decoded_nostr_url.protocol.is_some() {
635 " and nostr url contains protocol override so no other protocols were attempted"
636 } else {
637 ""
638 },
639 );
640 term.write_line(format!("push: {error}").as_str())?;
641 Err(error)
642 }
643}
644
645fn push_to_remote_url(
646 git_repo: &Repo,
647 git_server_url: &str,
648 remote_refspecs: &[String],
649 term: &Term,
650) -> Result<()> {
651 let git_config = git_repo.git_repo.config()?;
652 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?;
653 let auth = GitAuthenticator::default();
654 let mut push_options = git2::PushOptions::new();
655 let mut remote_callbacks = git2::RemoteCallbacks::new();
656 let push_reporter = Arc::new(Mutex::new(PushReporter::new(term)));
657
658 remote_callbacks.credentials(auth.credentials(&git_config));
659
660 remote_callbacks.push_update_reference({
661 let push_reporter = Arc::clone(&push_reporter);
662 move |name, error| {
663 let mut reporter = push_reporter.lock().unwrap();
664 if let Some(error) = error {
665 let existing_lines = reporter.count_all_existing_lines();
666 reporter.update_reference_errors.push(format!(
667 "WARNING: {} failed to push {name} error: {error}",
668 get_short_git_server_name(git_repo, git_server_url),
669 ));
670 reporter.write_all(existing_lines);
671 }
672 Ok(())
673 }
674 });
675
676 remote_callbacks.push_negotiation({
677 let push_reporter = Arc::clone(&push_reporter);
678 move |updates| {
679 let mut reporter = push_reporter.lock().unwrap();
680 let existing_lines = reporter.count_all_existing_lines();
681
682 for update in updates {
683 let dst_refname = update
684 .dst_refname()
685 .unwrap_or("")
686 .replace("refs/heads/", "")
687 .replace("refs/tags/", "tags/");
688 let msg = if update.dst().is_zero() {
689 format!("push: - [delete] {dst_refname}")
690 } else if update.src().is_zero() {
691 if update.dst_refname().unwrap_or("").contains("refs/tags") {
692 format!("push: * [new tag] {dst_refname}")
693 } else {
694 format!("push: * [new branch] {dst_refname}")
695 }
696 } else {
697 let force = remote_refspecs
698 .iter()
699 .any(|r| r.contains(&dst_refname) && r.contains('+'));
700 format!(
701 "push: {} {}..{} {} -> {dst_refname}",
702 if force { "+" } else { " " },
703 oid_to_shorthand_string(update.src()).unwrap(),
704 oid_to_shorthand_string(update.dst()).unwrap(),
705 update
706 .src_refname()
707 .unwrap_or("")
708 .replace("refs/heads/", "")
709 .replace("refs/tags/", "tags/"),
710 )
711 };
712 // other possibilities will result in push to fail but better reporting is
713 // needed:
714 // deleting a non-existant branch:
715 // ! [remote rejected] <old-branch-name> -> <old-branch-name> (not found)
716 // adding a branch that already exists:
717 // ! [remote rejected] <new-branch-name> -> <new-branch-name> (already exists)
718 // pushing without non-fast-forward without force:
719 // ! [rejected] <branch-name> -> <branch-name> (non-fast-forward)
720 reporter.negotiation.push(msg);
721 }
722 reporter.write_all(existing_lines);
723 Ok(())
724 }
725 });
726
727 remote_callbacks.push_transfer_progress({
728 let push_reporter = Arc::clone(&push_reporter);
729 #[allow(clippy::cast_precision_loss)]
730 move |current, total, bytes| {
731 let mut reporter = push_reporter.lock().unwrap();
732 reporter.process_transfer_progress_update(current, total, bytes);
733 }
734 });
735
736 remote_callbacks.sideband_progress({
737 let push_reporter = Arc::clone(&push_reporter);
738 move |data| {
739 let mut reporter = push_reporter.lock().unwrap();
740 reporter.process_remote_msg(data);
741 true
742 }
743 });
744 push_options.remote_callbacks(remote_callbacks);
745 git_server_remote.push(remote_refspecs, Some(&mut push_options))?;
746 let _ = git_server_remote.disconnect();
747 Ok(())
748}
749
750#[allow(clippy::cast_precision_loss)]
751#[allow(clippy::float_cmp)]
752#[allow(clippy::needless_pass_by_value)]
753fn report_on_transfer_progress(
754 current: usize,
755 total: usize,
756 bytes: usize,
757 start_time: &Instant,
758 end_time: Option<&Instant>,
759) -> Option<String> {
760 if total == 0 {
761 return None;
762 }
763 let percentage = ((current as f64 / total as f64) * 100.0)
764 // always round down because 100% complete is misleading when its not complete
765 .floor();
766 let (size, unit) = if bytes as f64 >= (1024.0 * 1024.0) {
767 (bytes as f64 / (1024.0 * 1024.0), "MiB")
768 } else {
769 (bytes as f64 / 1024.0, "KiB")
770 };
771 let speed = {
772 let duration = if let Some(end_time) = end_time {
773 (*end_time - *start_time).as_millis() as f64
774 } else {
775 start_time.elapsed().as_millis() as f64
776 };
777
778 if duration > 0.0 {
779 (bytes as f64 / (1024.0 * 1024.0)) / (duration / 1000.0) // Convert bytes to MiB and milliseconds to seconds
780 } else {
781 0.0
782 }
783 };
784
785 Some(format!(
786 "push: Writing objects: {percentage}% ({current}/{total}) {size:.2} {unit} | {speed:.2} MiB/s{}",
787 if current == total { ", done." } else { "" },
788 ))
789}
790
791struct PushReporter<'a> {
792 remote_msgs: Vec<String>,
793 negotiation: Vec<String>,
794 transfer_progress_msgs: Vec<String>,
795 update_reference_errors: Vec<String>,
796 term: &'a console::Term,
797 start_time: Option<Instant>,
798 end_time: Option<Instant>,
799}
800impl<'a> PushReporter<'a> {
801 fn new(term: &'a console::Term) -> Self {
802 Self {
803 remote_msgs: vec![],
804 negotiation: vec![],
805 transfer_progress_msgs: vec![],
806 update_reference_errors: vec![],
807 term,
808 start_time: None,
809 end_time: None,
810 }
811 }
812 fn write_all(&self, lines_to_clear: usize) {
813 let _ = self.term.clear_last_lines(lines_to_clear);
814 for msg in &self.remote_msgs {
815 let _ = self.term.write_line(format!("remote: {msg}").as_str());
816 }
817 for msg in &self.negotiation {
818 let _ = self.term.write_line(msg);
819 }
820 for msg in &self.transfer_progress_msgs {
821 let _ = self.term.write_line(msg);
822 }
823 for msg in &self.update_reference_errors {
824 let _ = self.term.write_line(msg);
825 }
826 }
827
828 fn count_all_existing_lines(&self) -> usize {
829 let width = self.term.size().1;
830 count_lines_per_msg_vec(width, &self.remote_msgs, "remote: ".len())
831 + count_lines_per_msg_vec(width, &self.negotiation, 0)
832 + count_lines_per_msg_vec(width, &self.transfer_progress_msgs, 0)
833 + count_lines_per_msg_vec(width, &self.update_reference_errors, 0)
834 }
835 fn process_remote_msg(&mut self, data: &[u8]) {
836 if let Ok(data) = str::from_utf8(data) {
837 let data = data
838 .split(['\n', '\r'])
839 .map(str::trim)
840 .filter(|line| !line.trim().is_empty())
841 .collect::<Vec<&str>>();
842 for data in data {
843 let existing_lines = self.count_all_existing_lines();
844 let msg = data.to_string();
845 if let Some(last) = self.remote_msgs.last() {
846 if (last.contains('%') && !last.contains("100%"))
847 || last == &msg.replace(", done.", "")
848 {
849 self.remote_msgs.pop();
850 self.remote_msgs.push(msg);
851 } else {
852 self.remote_msgs.push(msg);
853 }
854 } else {
855 self.remote_msgs.push(msg);
856 }
857 self.write_all(existing_lines);
858 }
859 }
860 }
861 fn process_transfer_progress_update(&mut self, current: usize, total: usize, bytes: usize) {
862 if self.start_time.is_none() {
863 self.start_time = Some(Instant::now());
864 }
865 if let Some(report) = report_on_transfer_progress(
866 current,
867 total,
868 bytes,
869 &self.start_time.unwrap(),
870 self.end_time.as_ref(),
871 ) {
872 let existing_lines = self.count_all_existing_lines();
873 if report.contains("100%") {
874 self.end_time = Some(Instant::now());
875 }
876 self.transfer_progress_msgs = vec![report];
877 self.write_all(existing_lines);
878 }
879 }
880}
881
882type HashMapUrlRefspecs = HashMap<String, Vec<String>>; 583type HashMapUrlRefspecs = HashMap<String, Vec<String>>;
883 584
884#[allow(clippy::too_many_lines)] 585#[allow(clippy::too_many_lines)]