diff options
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/ngit/sub_commands/sync.rs | 105 |
1 files changed, 99 insertions, 6 deletions
diff --git a/src/bin/ngit/sub_commands/sync.rs b/src/bin/ngit/sub_commands/sync.rs index 99cd2d8..4d7e799 100644 --- a/src/bin/ngit/sub_commands/sync.rs +++ b/src/bin/ngit/sub_commands/sync.rs | |||
| @@ -6,16 +6,21 @@ use git2::Oid; | |||
| 6 | use ngit::{ | 6 | use ngit::{ |
| 7 | client::{ | 7 | client::{ |
| 8 | Client, Connect, Params, fetching_with_report, get_repo_ref_from_cache, | 8 | Client, Connect, Params, fetching_with_report, get_repo_ref_from_cache, |
| 9 | get_state_from_cache, | 9 | get_state_from_cache, send_events, |
| 10 | }, | 10 | }, |
| 11 | fetch::fetch_from_git_server, | 11 | fetch::fetch_from_git_server, |
| 12 | git::{Repo, RepoActions, nostr_url::NostrUrlDecoded}, | 12 | git::{Repo, RepoActions, nostr_url::NostrUrlDecoded}, |
| 13 | list::{get_ahead_behind, list_from_remotes}, | 13 | list::{get_ahead_behind, list_from_remotes}, |
| 14 | login::existing::load_existing_login, | ||
| 14 | push::push_to_remote, | 15 | push::push_to_remote, |
| 15 | repo_ref::{get_repo_coordinates_when_remote_unknown, is_grasp_server_clone_url}, | 16 | repo_ref::{ |
| 17 | format_grasp_server_url_as_relay_url, get_repo_coordinates_when_remote_unknown, | ||
| 18 | is_grasp_server_clone_url, | ||
| 19 | }, | ||
| 16 | repo_state::RepoState, | 20 | repo_state::RepoState, |
| 17 | utils::{get_short_git_server_name, join_with_and}, | 21 | utils::{get_short_git_server_name, join_with_and}, |
| 18 | }; | 22 | }; |
| 23 | use nostr_sdk::RelayUrl; | ||
| 19 | 24 | ||
| 20 | #[derive(Debug, clap::Args)] | 25 | #[derive(Debug, clap::Args)] |
| 21 | pub struct SubCommandArgs { | 26 | pub struct SubCommandArgs { |
| @@ -58,7 +63,7 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { | |||
| 58 | None | 63 | None |
| 59 | }; | 64 | }; |
| 60 | 65 | ||
| 61 | let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); | 66 | let mut client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo))); |
| 62 | 67 | ||
| 63 | let (nostr_remote_name, decoded_nostr_url) = git_repo | 68 | let (nostr_remote_name, decoded_nostr_url) = git_repo |
| 64 | .get_first_nostr_remote_when_in_ngit_binary() | 69 | .get_first_nostr_remote_when_in_ngit_binary() |
| @@ -67,14 +72,84 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { | |||
| 67 | 72 | ||
| 68 | let repo_coordinate = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?; | 73 | let repo_coordinate = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?; |
| 69 | 74 | ||
| 70 | let _ = fetching_with_report(git_repo_path, &client, &repo_coordinate).await?; | 75 | let fetch_report = fetching_with_report(git_repo_path, &client, &repo_coordinate).await?; |
| 71 | |||
| 72 | // TODO push announcement event, then state event to grasps | ||
| 73 | 76 | ||
| 74 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await?; | 77 | let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinate).await?; |
| 75 | 78 | ||
| 76 | let nostr_state = get_state_from_cache(Some(git_repo_path), &repo_ref).await?; | 79 | let nostr_state = get_state_from_cache(Some(git_repo_path), &repo_ref).await?; |
| 77 | 80 | ||
| 81 | // Publish the current state event to any grasp server relays that are | ||
| 82 | // missing it or have a stale version. Grasp servers reject git pushes | ||
| 83 | // unless the state event is already present on their relay, so we must | ||
| 84 | // do this before attempting any git push. | ||
| 85 | // | ||
| 86 | // We use the per-relay state events captured during the fetch rather than | ||
| 87 | // the local database, because the database only stores the canonical latest | ||
| 88 | // event and cannot tell us what each individual relay holds. | ||
| 89 | let grasp_relays_needing_state: Vec<RelayUrl> = repo_ref | ||
| 90 | .git_server | ||
| 91 | .iter() | ||
| 92 | .filter(|url| is_grasp_server_clone_url(url)) | ||
| 93 | .filter_map(|url| { | ||
| 94 | format_grasp_server_url_as_relay_url(url) | ||
| 95 | .ok() | ||
| 96 | .and_then(|relay_str| RelayUrl::parse(&relay_str).ok()) | ||
| 97 | }) | ||
| 98 | .filter(|relay_url| { | ||
| 99 | // Include this relay if it was absent from the fetch results, had | ||
| 100 | // no state event, or had a state event older than the canonical one. | ||
| 101 | match fetch_report.state_per_relay.get(relay_url) { | ||
| 102 | // relay wasn't queried, or returned no state event | ||
| 103 | None | Some(None) => true, | ||
| 104 | Some(Some(relay_event)) => relay_event.id != nostr_state.event.id, | ||
| 105 | } | ||
| 106 | }) | ||
| 107 | .collect(); | ||
| 108 | |||
| 109 | // relay URL -> whether the state event was successfully published to it. | ||
| 110 | // Only populated for grasp relays that needed the state event; grasp | ||
| 111 | // relays that already had the current state event are considered succeeded. | ||
| 112 | let mut grasp_relay_publish_results: HashMap<String, bool> = HashMap::new(); | ||
| 113 | |||
| 114 | if !grasp_relays_needing_state.is_empty() { | ||
| 115 | // Attempt to load an existing login silently so the signer is | ||
| 116 | // available for NIP-42 auth if a relay requests it. We do not | ||
| 117 | // prompt the user, do not fetch profile updates, and ignore any | ||
| 118 | // failure — the events are already signed so publishing works | ||
| 119 | // without a signer. | ||
| 120 | if let Ok((signer, _, _)) = load_existing_login( | ||
| 121 | &Some(&git_repo), | ||
| 122 | &None, | ||
| 123 | &None, | ||
| 124 | &None, | ||
| 125 | Some(&client), | ||
| 126 | true, // silent | ||
| 127 | false, // prompt_for_password | ||
| 128 | false, // fetch_profile_updates | ||
| 129 | ) | ||
| 130 | .await | ||
| 131 | { | ||
| 132 | client.set_signer(signer).await; | ||
| 133 | } | ||
| 134 | // Send only to the specific grasp relays that are missing or have a | ||
| 135 | // stale state event — no user write relays. | ||
| 136 | if let Ok(results) = send_events( | ||
| 137 | &client, | ||
| 138 | Some(git_repo_path), | ||
| 139 | vec![nostr_state.event.clone()], | ||
| 140 | vec![], // no user write relays | ||
| 141 | grasp_relays_needing_state, | ||
| 142 | true, | ||
| 143 | false, | ||
| 144 | ) | ||
| 145 | .await | ||
| 146 | { | ||
| 147 | for (relay_url, succeeded) in results { | ||
| 148 | grasp_relay_publish_results.insert(relay_url, succeeded); | ||
| 149 | } | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 78 | let term = console::Term::stderr(); | 153 | let term = console::Term::stderr(); |
| 79 | 154 | ||
| 80 | let remote_states = list_from_remotes( | 155 | let remote_states = list_from_remotes( |
| @@ -178,6 +253,24 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { | |||
| 178 | } | 253 | } |
| 179 | } | 254 | } |
| 180 | 255 | ||
| 256 | // Skip grasp servers whose relay did not receive the state event — | ||
| 257 | // they would reject the git push anyway. | ||
| 258 | if (*is_grasp_server || is_grasp_server_clone_url(url)) | ||
| 259 | && !grasp_relay_publish_results.is_empty() | ||
| 260 | { | ||
| 261 | if let Ok(relay_url) = format_grasp_server_url_as_relay_url(url) { | ||
| 262 | if grasp_relay_publish_results | ||
| 263 | .get(&relay_url) | ||
| 264 | .is_some_and(|succeeded| !succeeded) | ||
| 265 | { | ||
| 266 | term.write_line(&format!( | ||
| 267 | "WARNING: skipping {remote_name} - state event failed to reach its relay" | ||
| 268 | ))?; | ||
| 269 | continue; | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 181 | if refspecs.is_empty() { | 274 | if refspecs.is_empty() { |
| 182 | if !not_updated.is_empty() || !not_deleted.is_empty() { | 275 | if !not_updated.is_empty() || !not_deleted.is_empty() { |
| 183 | term.write_line(&format!("{remote_name} in sync excluding"))?; | 276 | term.write_line(&format!("{remote_name} in sync excluding"))?; |