diff options
| -rw-r--r-- | docs/explanation/inline-authorization.md | 4 | ||||
| -rw-r--r-- | src/git/authorization.rs | 22 |
2 files changed, 26 insertions, 0 deletions
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( | |||
| 352 | - If no event found, create placeholder (git-data-first scenario) | 352 | - If no event found, create placeholder (git-data-first scenario) |
| 353 | - Collect PR events from purgatory for post-push processing | 353 | - Collect PR events from purgatory for post-push processing |
| 354 | 354 | ||
| 355 | **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. | ||
| 356 | |||
| 357 | --- | ||
| 358 | |||
| 355 | ## State Event Authorization | 359 | ## State Event Authorization |
| 356 | 360 | ||
| 357 | State events (kind 30618) undergo authorization checks at three points (defense-in-depth): | 361 | 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( | |||
| 575 | owner_pubkey | 575 | owner_pubkey |
| 576 | ); | 576 | ); |
| 577 | 577 | ||
| 578 | // Accept pushes where all refs are already at the desired state (old_oid == new_oid) | ||
| 579 | // This handles race conditions where state events are applied between fetch and push | ||
| 580 | if !pushed_refs.is_empty() { | ||
| 581 | let all_refs_unchanged = pushed_refs | ||
| 582 | .iter() | ||
| 583 | .all(|(old_oid, new_oid, _)| old_oid == new_oid); | ||
| 584 | |||
| 585 | if all_refs_unchanged { | ||
| 586 | debug!( | ||
| 587 | "All pushed refs unchanged (old_oid == new_oid) for {} owned by {}, accepting without purgatory check", | ||
| 588 | identifier, owner_pubkey | ||
| 589 | ); | ||
| 590 | return Ok(AuthorizationResult { | ||
| 591 | authorized: true, | ||
| 592 | reason: "Push accepted: all refs already at desired state (no-op)".to_string(), | ||
| 593 | state: None, | ||
| 594 | maintainers: authorized.into_iter().collect(), | ||
| 595 | purgatory_events: vec![], | ||
| 596 | }); | ||
| 597 | } | ||
| 598 | } | ||
| 599 | |||
| 578 | // Check purgatory for matching state events | 600 | // Check purgatory for matching state events |
| 579 | // Convert pushed refs to RefUpdate (filter out refs/nostr/* refs) | 601 | // Convert pushed refs to RefUpdate (filter out refs/nostr/* refs) |
| 580 | let pushed_updates: Vec<RefUpdate> = pushed_refs | 602 | let pushed_updates: Vec<RefUpdate> = pushed_refs |