Files
ngit-grasp/closed/6af5-accept-noop-pushes.md
2026-02-03 17:06:09 +00:00

3.1 KiB

Accept No-Op Pushes (old_oid == new_oid)

ID: 6af5

Problem

When a user pushes refs where old_oid == new_oid (no actual change), ngit-grasp rejects the push with "No state event found in purgatory from authorized publishers". This creates a poor user experience because:

  1. Race condition: User fetches state, but between fetch and push, a state event arrives and is applied. Their push now has old_oid == new_oid but gets rejected.
  2. User confusion: The rejection makes users think their desired state was denied, when in fact it's already accepted.
  3. Git compatibility: Git accepts no-op pushes with "Everything up-to-date" - we should too.

Example from logs (work/tmp.md)

Feb 03 14:10:12 de1 ngit-grasp[6385]: 2026-02-03T14:10:12.937204Z DEBUG ngit_grasp::git::authorization: refs/heads/main 1c1f162929df98557fa58c4a7a392c66de862a87 -> 1c1f162929df98557fa58c4a7a392c66de862a87
Feb 03 14:10:12 de1 ngit-grasp[6385]: 2026-02-03T14:10:12.938617Z WARN ngit_grasp::git::handlers: Push rejected for f1-sunset-racing: No state event found in purgatory from authorized publishers

The push is trying to update main from 1c1f162... to 1c1f162... (same commit), but it's rejected because:

  • The state event was already applied (between fetch and push)
  • No state event remains in purgatory
  • Authorization requires a purgatory event

Plan

  • Phase 1: Detect no-op pushes in get_state_authorization_for_specific_owner_repo
    • Check if all pushed refs have old_oid == new_oid
    • Return success immediately without checking purgatory
    • Include appropriate reason message
  • Phase 2: Add tests for no-op push scenarios
    • Test single ref no-op
    • Test multiple refs all no-op
    • Test mixed (some no-op, some real changes) - should still require purgatory
  • Phase 3: Update documentation
    • Document no-op push behavior in architecture docs
    • Note Git compatibility

Progress

2026-02-03 [Session 14:30]

  • Created: Identified race condition from production logs
  • Root cause: User's push becomes no-op after state event is applied
  • Decision: Accept no-op pushes as successful without requiring purgatory event
  • Next: Implement detection in authorization.rs

Notes

Location

  • File: src/git/authorization.rs
  • Function: get_state_authorization_for_specific_owner_repo (line 531)
  • Before purgatory check (line 595)

Implementation Strategy

// Early detection: check if all refs are no-ops
let all_noop = pushed_refs
    .iter()
    .filter(|(_, _, name)| !name.starts_with("refs/nostr/"))
    .all(|(old_oid, new_oid, _)| old_oid == new_oid);

if all_noop && !pushed_refs.is_empty() {
    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![],
    });
}

Edge Cases

  • Empty push: Should not trigger no-op logic (no refs to check)
  • Mixed push: Some refs no-op, some real changes → require purgatory (normal flow)
  • refs/nostr/ refs: Should be excluded from no-op detection (different authorization path)