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

77 lines
3.1 KiB
Markdown

# 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
```rust
// 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)