upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-26 13:31:44 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-26 15:26:18 +0000
commit0d6ed93e4d143bb066205543af13f0ec6ddbdd58 (patch)
tree1b1940460ec149e7e7e224d620ff1f8b9e0c55f3 /src/bin/ngit/sub_commands
parentee68ccadce6a6c90747cbdaae557babb4683413e (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/ngit/sub_commands')
-rw-r--r--src/bin/ngit/sub_commands/sync.rs105
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;
6use ngit::{ 6use 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};
23use nostr_sdk::RelayUrl;
19 24
20#[derive(Debug, clap::Args)] 25#[derive(Debug, clap::Args)]
21pub struct SubCommandArgs { 26pub 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"))?;