diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-26 13:31:44 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-26 15:26:18 +0000 |
| commit | 0d6ed93e4d143bb066205543af13f0ec6ddbdd58 (patch) | |
| tree | 1b1940460ec149e7e7e224d620ff1f8b9e0c55f3 /src/bin | |
| parent | ee68ccadce6a6c90747cbdaae557babb4683413e (diff) | |
feat: publish state event to stale grasp relays before sync push
FetchReport now captures the full state event seen on each relay during
the nostr fetch (state_per_relay: HashMap<RelayUrl, Option<Event>>).
ngit sync uses this to identify grasp server relays with a missing or
outdated state event and publishes the current state event to them
before attempting git pushes, preventing rejections. An existing login
is loaded silently (no prompt, no profile fetch) to provide a signer
for NIP-42 auth if requested.
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"))?; |