From c163d717147b92b16d89da2fbccef775647b5a07 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 3 Feb 2026 16:13:59 +0000 Subject: fix: accept no-op pushes where old_oid == new_oid Fixes race condition where user's push becomes no-op after state event is applied between fetch and push. Now accepts these as successful no-ops, matching Git's 'Everything up-to-date' behavior. - Add early detection in get_state_authorization_for_specific_owner_repo - Return success for all-noop pushes without requiring purgatory event - Document behavior in inline-authorization.md --- docs/explanation/inline-authorization.md | 4 ++++ src/git/authorization.rs | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/explanation/inline-authorization.md b/docs/explanation/inline-authorization.md index 7081f63..80bd98f 100644 --- a/docs/explanation/inline-authorization.md +++ b/docs/explanation/inline-authorization.md @@ -352,6 +352,10 @@ pub async fn authorize_push( - If no event found, create placeholder (git-data-first scenario) - Collect PR events from purgatory for post-push processing +**No-Op Push Acceptance:** Pushes where all refs have `old_oid == new_oid` are accepted without requiring a purgatory state event, matching Git's "Everything up-to-date" behavior and avoiding race condition rejections. + +--- + ## State Event Authorization State events (kind 30618) undergo authorization checks at three points (defense-in-depth): diff --git a/src/git/authorization.rs b/src/git/authorization.rs index e174b51..db2b992 100644 --- a/src/git/authorization.rs +++ b/src/git/authorization.rs @@ -575,6 +575,28 @@ pub async fn get_state_authorization_for_specific_owner_repo( owner_pubkey ); + // Accept pushes where all refs are already at the desired state (old_oid == new_oid) + // This handles race conditions where state events are applied between fetch and push + if !pushed_refs.is_empty() { + let all_refs_unchanged = pushed_refs + .iter() + .all(|(old_oid, new_oid, _)| old_oid == new_oid); + + if all_refs_unchanged { + debug!( + "All pushed refs unchanged (old_oid == new_oid) for {} owned by {}, accepting without purgatory check", + identifier, owner_pubkey + ); + return Ok(AuthorizationResult { + authorized: true, + reason: "Push accepted: all refs already at desired state (no-op)".to_string(), + state: None, + maintainers: authorized.into_iter().collect(), + purgatory_events: vec![], + }); + } + } + // Check purgatory for matching state events // Convert pushed refs to RefUpdate (filter out refs/nostr/* refs) let pushed_updates: Vec = pushed_refs -- cgit v1.2.3