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:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-26 12:47:12 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-26 15:26:18 +0000
commitee68ccadce6a6c90747cbdaae557babb4683413e (patch)
treec3be43110d6f9d0f82fb56c9b839644e51b4788f /src/bin/git_remote_nostr/push.rs
parentf252dd0f1fb7374b5b6d44e77facdc902ee52c43 (diff)
fix: rollback local state event cache on total push failure
When all git servers reject or skip a push, delete the newly-published state event from the local nostr cache and restore the previous state event (if any), so that a subsequent retry starts from a clean baseline rather than a state that no server ever accepted.
Diffstat (limited to 'src/bin/git_remote_nostr/push.rs')
-rw-r--r--src/bin/git_remote_nostr/push.rs136
1 files changed, 104 insertions, 32 deletions
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index 2ab01cb..e5c33b6 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -6,7 +6,10 @@ use std::{
6}; 6};
7 7
8use anyhow::{Context, Result, bail}; 8use anyhow::{Context, Result, bail};
9use client::{get_events_from_local_cache, get_state_from_cache, send_events, sign_event}; 9use client::{
10 delete_event_from_local_cache, get_events_from_local_cache, get_state_from_cache, send_events,
11 sign_event,
12};
10use console::Term; 13use console::Term;
11use git::{RepoActions, sha1_to_oid}; 14use git::{RepoActions, sha1_to_oid};
12use git_events::{ 15use git_events::{
@@ -15,7 +18,9 @@ use git_events::{
15use git2::{Oid, Repository}; 18use git2::{Oid, Repository};
16use ngit::{ 19use ngit::{
17 accept_maintainership::accept_maintainership_with_defaults, 20 accept_maintainership::accept_maintainership_with_defaults,
18 client::{self, get_event_from_cache_by_id}, 21 client::{
22 self, get_event_from_cache_by_id, get_filter_state_events, save_event_in_local_cache,
23 },
19 git::{self, nostr_url::NostrUrlDecoded}, 24 git::{self, nostr_url::NostrUrlDecoded},
20 git_events::{ 25 git_events::{
21 self, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, get_event_root, 26 self, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_to_cover_letter, get_event_root,
@@ -128,19 +133,24 @@ pub async fn run_push(
128 133
129 // all refspecs aren't rejected 134 // all refspecs aren't rejected
130 if !(git_state_refspecs.is_empty() && proposal_refspecs.is_empty()) { 135 if !(git_state_refspecs.is_empty() && proposal_refspecs.is_empty()) {
131 let (rejected_proposal_refspecs, rejected, relay_results) = 136 let (
132 create_and_publish_events_and_proposals( 137 rejected_proposal_refspecs,
133 git_repo, 138 rejected,
134 repo_ref, 139 relay_results,
135 &git_state_refspecs, 140 old_state_event,
136 &proposal_refspecs, 141 new_state_event_id,
137 client, // &mut Client 142 ) = create_and_publish_events_and_proposals(
138 existing_state, 143 git_repo,
139 &term, 144 repo_ref,
140 title_description.as_ref(), 145 &git_state_refspecs,
141 &git_server_push_options, 146 &proposal_refspecs,
142 ) 147 client, // &mut Client
143 .await?; 148 existing_state,
149 &term,
150 title_description.as_ref(),
151 &git_server_push_options,
152 )
153 .await?;
144 154
145 if !rejected { 155 if !rejected {
146 for refspec in git_state_refspecs.iter().chain(proposal_refspecs.iter()) { 156 for refspec in git_state_refspecs.iter().chain(proposal_refspecs.iter()) {
@@ -167,12 +177,8 @@ pub async fn run_push(
167 .filter(|refspec| git_state_refspecs.contains(refspec)) 177 .filter(|refspec| git_state_refspecs.contains(refspec))
168 .cloned() 178 .cloned()
169 .collect::<Vec<String>>(); 179 .collect::<Vec<String>>();
170 if is_grasp_server_clone_url(&git_server_url) 180 if is_grasp_server_clone_url(&git_server_url) && !relay_results.is_empty() {
171 && !relay_results.is_empty() 181 if let Ok(relay_url) = format_grasp_server_url_as_relay_url(&git_server_url) {
172 {
173 if let Ok(relay_url) =
174 format_grasp_server_url_as_relay_url(&git_server_url)
175 {
176 let relay_failed = relay_results 182 let relay_failed = relay_results
177 .iter() 183 .iter()
178 .any(|(url, succeeded)| url == &relay_url && !succeeded); 184 .any(|(url, succeeded)| url == &relay_url && !succeeded);
@@ -190,19 +196,23 @@ pub async fn run_push(
190 196
191 // If all git servers were skipped and there were refspecs to push, 197 // If all git servers were skipped and there were refspecs to push,
192 // emit error lines for each ref using the git remote helper protocol 198 // emit error lines for each ref using the git remote helper protocol
199 // and roll back the state event in the local cache
193 if servers_to_push.is_empty() && !git_state_refspecs.is_empty() { 200 if servers_to_push.is_empty() && !git_state_refspecs.is_empty() {
194 for refspec in &git_state_refspecs { 201 for refspec in &git_state_refspecs {
195 let (_, to) = refspec_to_from_to(refspec)?; 202 let (_, to) = refspec_to_from_to(refspec)?;
196 println!( 203 println!("error {to} state event failed to reach any git server relay");
197 "error {to} state event failed to reach any git server relay" 204 }
198 ); 205 if let Some(new_id) = new_state_event_id {
206 rollback_state_event(git_repo.get_path()?, new_id, old_state_event.as_ref())
207 .await;
199 } 208 }
200 } else { 209 } else {
210 let mut any_push_succeeded = false;
201 for (git_server_url, server_refspecs) in &servers_to_push { 211 for (git_server_url, server_refspecs) in &servers_to_push {
202 if !server_refspecs.is_empty() { 212 if !server_refspecs.is_empty() {
203 let push_options_refs: Vec<&str> = 213 let push_options_refs: Vec<&str> =
204 git_server_push_options.iter().map(String::as_str).collect(); 214 git_server_push_options.iter().map(String::as_str).collect();
205 let _ = push_to_remote( 215 if push_to_remote(
206 git_repo, 216 git_repo,
207 git_server_url, 217 git_server_url,
208 &repo_ref.to_nostr_git_url(&None), 218 &repo_ref.to_nostr_git_url(&None),
@@ -210,7 +220,22 @@ pub async fn run_push(
210 &term, 220 &term,
211 is_grasp_server_clone_url(git_server_url), 221 is_grasp_server_clone_url(git_server_url),
212 &push_options_refs, 222 &push_options_refs,
213 ); 223 )
224 .is_ok()
225 {
226 any_push_succeeded = true;
227 }
228 }
229 }
230 // If every git server push failed, roll back the state event
231 if !any_push_succeeded && !git_state_refspecs.is_empty() {
232 if let Some(new_id) = new_state_event_id {
233 rollback_state_event(
234 git_repo.get_path()?,
235 new_id,
236 old_state_event.as_ref(),
237 )
238 .await;
214 } 239 }
215 } 240 }
216 } 241 }
@@ -221,6 +246,25 @@ pub async fn run_push(
221 Ok(()) 246 Ok(())
222} 247}
223 248
249/// Remove the newly-published state event from the local nostr cache and
250/// restore the previous state event (if any). This prevents a subsequent
251/// `ngit sync` or push from using a state that no git server ever accepted.
252async fn rollback_state_event(
253 git_repo_path: &std::path::Path,
254 new_state_event_id: EventId,
255 old_state_event: Option<&Event>,
256) {
257 if let Err(e) = delete_event_from_local_cache(git_repo_path, new_state_event_id).await {
258 eprintln!("WARNING: failed to roll back state event from local cache: {e}");
259 return;
260 }
261 if let Some(old_event) = old_state_event {
262 if let Err(e) = save_event_in_local_cache(git_repo_path, old_event).await {
263 eprintln!("WARNING: failed to restore previous state event in local cache: {e}");
264 }
265 }
266}
267
224#[allow(clippy::too_many_lines)] 268#[allow(clippy::too_many_lines)]
225#[allow(clippy::too_many_arguments)] 269#[allow(clippy::too_many_arguments)]
226async fn create_and_publish_events_and_proposals( 270async fn create_and_publish_events_and_proposals(
@@ -233,7 +277,13 @@ async fn create_and_publish_events_and_proposals(
233 term: &Term, 277 term: &Term,
234 title_description: Option<&(String, String)>, 278 title_description: Option<&(String, String)>,
235 git_server_push_options: &[String], 279 git_server_push_options: &[String],
236) -> Result<(Vec<String>, bool, Vec<(String, bool)>)> { 280) -> Result<(
281 Vec<String>,
282 bool,
283 Vec<(String, bool)>,
284 Option<Event>,
285 Option<EventId>,
286)> {
237 let (signer, mut user_ref, _) = load_existing_login( 287 let (signer, mut user_ref, _) = load_existing_login(
238 &Some(git_repo), 288 &Some(git_repo),
239 &None, 289 &None,
@@ -256,7 +306,7 @@ async fn create_and_publish_events_and_proposals(
256 ); 306 );
257 } 307 }
258 if proposal_refspecs.is_empty() { 308 if proposal_refspecs.is_empty() {
259 return Ok((vec![], true, vec![])); 309 return Ok((vec![], true, vec![], None, None));
260 } 310 }
261 } else if repo_ref 311 } else if repo_ref
262 .maintainers_without_annoucnement 312 .maintainers_without_annoucnement
@@ -274,6 +324,8 @@ async fn create_and_publish_events_and_proposals(
274 } 324 }
275 325
276 let mut events = vec![]; 326 let mut events = vec![];
327 let mut old_state_event: Option<Event> = None;
328 let mut new_state_event_id: Option<EventId> = None;
277 329
278 if !git_server_refspecs.is_empty() { 330 if !git_server_refspecs.is_empty() {
279 let new_state = generate_updated_state(git_repo, &existing_state, git_server_refspecs)?; 331 let new_state = generate_updated_state(git_repo, &existing_state, git_server_refspecs)?;
@@ -286,8 +338,22 @@ async fn create_and_publish_events_and_proposals(
286 }; 338 };
287 339
288 if store_state { 340 if store_state {
341 // Capture the existing state event before publishing the new one,
342 // so we can restore it if all git server pushes fail.
343 old_state_event = get_events_from_local_cache(
344 git_repo.get_path()?,
345 vec![get_filter_state_events(&repo_ref.coordinates(), true)],
346 )
347 .await
348 .ok()
349 .and_then(|mut events| {
350 events.sort_by_key(|e| std::cmp::Reverse(e.created_at));
351 events.into_iter().next()
352 });
353
289 let new_repo_state = 354 let new_repo_state =
290 RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?; 355 RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?;
356 new_state_event_id = Some(new_repo_state.event.id);
291 events.push(new_repo_state.event); 357 events.push(new_repo_state.event);
292 } 358 }
293 359
@@ -336,7 +402,9 @@ async fn create_and_publish_events_and_proposals(
336 402
337 // TODO check whether tip of each branch pushed is on at least one git server 403 // TODO check whether tip of each branch pushed is on at least one git server
338 // before broadcasting the nostr state 404 // before broadcasting the nostr state
339 let relay_results = if !events.is_empty() { 405 let relay_results = if events.is_empty() {
406 vec![]
407 } else {
340 send_events( 408 send_events(
341 client, 409 client,
342 Some(git_repo.get_path()?), 410 Some(git_repo.get_path()?),
@@ -347,10 +415,14 @@ async fn create_and_publish_events_and_proposals(
347 false, 415 false,
348 ) 416 )
349 .await? 417 .await?
350 } else {
351 vec![]
352 }; 418 };
353 Ok((rejected_proposal_refspecs, false, relay_results)) 419 Ok((
420 rejected_proposal_refspecs,
421 false,
422 relay_results,
423 old_state_event,
424 new_state_event_id,
425 ))
354} 426}
355 427
356#[allow(clippy::too_many_lines)] 428#[allow(clippy::too_many_lines)]