From 71cd982e5ca3e6f60fdf33fd41d0db3eabdbf39f Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 27 Feb 2026 15:51:01 +0000 Subject: feat: ngit sync --force republishes state event even with no ref changes allows users to repair repos whose state event is missing ^{} peeled refs for annotated tags (or any other corruption) without needing to push a new ref. the new event is signed with a fresh timestamp and broadcast to all repo relays and the user's write relays. --- CHANGELOG.md | 1 + src/bin/ngit/sub_commands/sync.rs | 60 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f52d353..f91429b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `ngit sync --force` now republishes the state event with a fresh timestamp even when no refs have changed, allowing users to repair repos with a corrupt or incomplete state event (e.g. missing `^{}` peeled refs for annotated tags) without needing to push a new ref - git server push option passthrough, enabling `-o secret-scanning.skip` for grasp servers - `ngit sync` now publishes the current state event to grasp server relays that are missing it or have a stale version before attempting git pushes, preventing rejections; per-relay state visibility is captured during the nostr fetch and surfaced via `FetchReport::state_per_relay` - Fetch filters now request kind-5 deletion events for cached state and repo announcement events by `#e` tag (NIP-09), in addition to the existing `#a`-tagged filter; ensures deletions of these events are received even from clients that do not embed a repo coordinate in their deletion event diff --git a/src/bin/ngit/sub_commands/sync.rs b/src/bin/ngit/sub_commands/sync.rs index 4d7e799..146bcbc 100644 --- a/src/bin/ngit/sub_commands/sync.rs +++ b/src/bin/ngit/sub_commands/sync.rs @@ -78,6 +78,64 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { let nostr_state = get_state_from_cache(Some(git_repo_path), &repo_ref).await?; + // When --force is given, rebuild and republish the state event even if + // nothing has changed. This lets users repair repos whose state event is + // missing ^{} peeled refs for annotated tags (or any other corruption) + // without needing to push a new ref. A fresh event is signed (new + // created_at) and broadcast to all repo relays and the user's write relays. + if args.force { + let (signer, user_ref, _) = load_existing_login( + &Some(&git_repo), + &None, + &None, + &None, + Some(&client), + false, // not silent — we need the user to authenticate if required + false, // prompt_for_password + false, // fetch_profile_updates + ) + .await + .context("authentication required to republish state; run 'ngit account login' first")?; + client.set_signer(signer.clone()).await; + // Backfill any missing ^{} peeled refs before rebuilding — the existing + // state event may predate the fix that started storing them. + let mut state = nostr_state.state.clone(); + let tag_refs: Vec<(String, String)> = state + .iter() + .filter(|(k, _)| k.starts_with("refs/tags/") && !k.ends_with("^{}")) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + for (ref_name, tag_oid) in tag_refs { + let peeled_key = format!("{ref_name}^{{}}"); + if state.contains_key(&peeled_key) { + continue; + } + if let Ok(oid) = git2::Oid::from_str(&tag_oid) { + if git_repo + .git_repo + .find_object(oid, Some(git2::ObjectType::Tag)) + .is_ok() + { + if let Ok(commit_oid) = git_repo.get_commit_or_tip_of_reference(&ref_name) { + state.insert(peeled_key, commit_oid.to_string()); + } + } + } + } + let new_state = RepoState::build(repo_ref.identifier.clone(), state, &signer).await?; + send_events( + &client, + Some(git_repo_path), + vec![new_state.event], + user_ref.relays.write(), + repo_ref.relays.clone(), + true, + false, + ) + .await?; + println!("state event republished"); + } + // Publish the current state event to any grasp server relays that are // missing it or have a stale version. Grasp servers reject git pushes // unless the state event is already present on their relay, so we must @@ -129,7 +187,7 @@ pub async fn launch(args: &SubCommandArgs) -> Result<()> { ) .await { - client.set_signer(signer).await; + client.set_signer(signer.clone()).await; } // Send only to the specific grasp relays that are missing or have a // stale state event — no user write relays. -- cgit v1.2.3