From d392f0bc14bcd687e918d4653ae016226496b4c4 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 3 Feb 2026 21:21:33 +0000 Subject: feat: add diagnostic logging for partial state event matches Improves observability when pushes are rejected due to state events that only partially match the pushed refs. Previously, logs only showed 'No state event found' even when state events existed but didn't match. Changes: - Add diagnose_state_mismatch() to explain why state events don't match - Log specific reasons: missing refs, wrong SHAs, or extra refs - Update rejection message to 'No matching state event found' (more accurate) - Add 4 unit tests for diagnostic function Example diagnostic output: WARN State event abc123 from authorized author doesn't match push: refs/heads/main missing (state declares 9cc3d93b) This addresses the issue where a push with only refs/heads/test was rejected because the state event also declared refs/heads/main, but logs didn't explain why the match failed. --- src/git/authorization.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) (limited to 'src/git/authorization.rs') diff --git a/src/git/authorization.rs b/src/git/authorization.rs index db2b992..27107db 100644 --- a/src/git/authorization.rs +++ b/src/git/authorization.rs @@ -666,12 +666,88 @@ pub async fn get_state_authorization_for_specific_owner_repo( debug!("Purgatory events found but none from authorized authors"); } } else { - debug!("No matching state events found in purgatory"); + // Check if there are ANY state events in purgatory for this identifier + let all_purgatory_states = purgatory.find_state(identifier); + + if !all_purgatory_states.is_empty() { + // There are state events but none match the push - diagnose why + debug!( + "Found {} state event(s) in purgatory for {} but none match the push", + all_purgatory_states.len(), + identifier + ); + + // Count authorized state events and collect diagnostic info + let mut authorized_count = 0; + let mut diagnostic_reasons = Vec::new(); + + // Diagnose why each authorized state event doesn't match + for entry in all_purgatory_states.iter() { + let author_hex = entry.event.pubkey.to_hex(); + if authorized.contains(&author_hex) { + authorized_count += 1; + if let Some(reason) = crate::purgatory::diagnose_state_mismatch( + &entry.event, + &pushed_updates, + &local_refs, + ) { + debug!( + "State event {} from authorized author {} doesn't match push: {}", + entry.event.id, + entry + .event + .pubkey + .to_bech32() + .unwrap_or_else(|_| author_hex.clone()), + reason + ); + diagnostic_reasons.push(reason); + } + } + } + + // Create concise WARN message summarizing the rejection + let summary = if authorized_count > 0 { + let reason_summary = if !diagnostic_reasons.is_empty() { + // Take the first diagnostic reason as representative + format!(" ({})", diagnostic_reasons[0]) + } else { + String::new() + }; + format!( + "{} state event{} in purgatory from authorized publisher{} but doesn't match push{}", + authorized_count, + if authorized_count == 1 { "" } else { "s" }, + if authorized_count == 1 { "" } else { "s" }, + reason_summary + ) + } else { + format!( + "{} state event{} in purgatory but none from authorized publishers", + all_purgatory_states.len(), + if all_purgatory_states.len() == 1 { + "" + } else { + "s" + } + ) + }; + + warn!("Push rejected for {}: {}", identifier, summary); + return Ok(AuthorizationResult::denied(summary)); + } else { + debug!("No state events found in purgatory for {}", identifier); + warn!( + "Push rejected for {}: No state events in purgatory", + identifier + ); + return Ok(AuthorizationResult::denied("No state events in purgatory")); + } } // No matching state found in purgatory Ok(AuthorizationResult::denied( - "No state event found in purgatory from authorized publishers", + "No matching state event found in purgatory from authorized publishers", )) } -- cgit v1.2.3