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:
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"))?;