upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/purgatory/helpers.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:20:59 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-23 15:20:59 +0000
commit113928aa84894ea8f65c247d9987527e792b32a9 (patch)
treeec967d6195d9f7ec4f061449596611afe3a0950f /src/purgatory/helpers.rs
parent26f608e5011b9d1ad6036da75b89272835e69695 (diff)
parente0ad39a489b3398f8208713bf728db0cb11475b0 (diff)
Merge master into 3ca0-announcements-purgatory
Diffstat (limited to 'src/purgatory/helpers.rs')
-rw-r--r--src/purgatory/helpers.rs204
1 files changed, 204 insertions, 0 deletions
diff --git a/src/purgatory/helpers.rs b/src/purgatory/helpers.rs
index 193ef99..a9f6e66 100644
--- a/src/purgatory/helpers.rs
+++ b/src/purgatory/helpers.rs
@@ -225,6 +225,117 @@ pub fn get_unpushed_refs(event: &Event, pushed_refs: &[RefPair]) -> Vec<RefPair>
225 .collect() 225 .collect()
226} 226}
227 227
228/// Diagnose why a state event doesn't match the push.
229///
230/// Returns a human-readable explanation of the mismatch between the state event
231/// and what would result from applying the push to local refs.
232///
233/// # Arguments
234/// * `event` - The state event to check
235/// * `pushed_updates` - Ref updates in the current push operation
236/// * `local_refs` - Refs already existing locally (ref_name -> SHA)
237///
238/// # Returns
239/// String explaining why the state doesn't match, or None if it matches
240pub fn diagnose_state_mismatch(
241 event: &Event,
242 pushed_updates: &[RefUpdate],
243 local_refs: &HashMap<String, String>,
244) -> Option<String> {
245 let state_refs = extract_refs_from_state(event);
246
247 // Filter local_refs to only branches and tags
248 let mut would_be_state: HashMap<String, String> = local_refs
249 .iter()
250 .filter(|(ref_name, _)| {
251 ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/")
252 })
253 .map(|(k, v)| (k.clone(), v.clone()))
254 .collect();
255
256 // Apply all pushed updates to create the would-be state
257 for update in pushed_updates {
258 // Only process branches and tags
259 if !update.ref_name.starts_with("refs/heads/") && !update.ref_name.starts_with("refs/tags/")
260 {
261 continue;
262 }
263
264 if update.is_deletion() {
265 would_be_state.remove(&update.ref_name);
266 } else {
267 would_be_state.insert(update.ref_name.clone(), update.new_oid.clone());
268 }
269 }
270
271 // Convert event's state refs to a HashMap for comparison
272 let declared_state: HashMap<String, String> = state_refs
273 .into_iter()
274 .map(|r| (r.ref_name, r.object_sha))
275 .collect();
276
277 // Check if they match
278 if would_be_state == declared_state {
279 return None; // No mismatch
280 }
281
282 // Build diagnostic message
283 let mut reasons = Vec::new();
284
285 // Check for refs in declared state but not in would-be state
286 for (ref_name, declared_sha) in &declared_state {
287 if let Some(would_be_sha) = would_be_state.get(ref_name) {
288 if would_be_sha != declared_sha {
289 let would_be_short = if would_be_sha.len() >= 8 {
290 &would_be_sha[..8]
291 } else {
292 would_be_sha.as_str()
293 };
294 let declared_short = if declared_sha.len() >= 8 {
295 &declared_sha[..8]
296 } else {
297 declared_sha.as_str()
298 };
299 reasons.push(format!(
300 "{} would be at {} but state declares {}",
301 ref_name, would_be_short, declared_short
302 ));
303 }
304 } else {
305 let declared_short = if declared_sha.len() >= 8 {
306 &declared_sha[..8]
307 } else {
308 declared_sha.as_str()
309 };
310 reasons.push(format!(
311 "{} missing (state declares {})",
312 ref_name, declared_short
313 ));
314 }
315 }
316
317 // Check for refs in would-be state but not in declared state
318 for (ref_name, would_be_sha) in &would_be_state {
319 if !declared_state.contains_key(ref_name) {
320 let would_be_short = if would_be_sha.len() >= 8 {
321 &would_be_sha[..8]
322 } else {
323 would_be_sha.as_str()
324 };
325 reasons.push(format!(
326 "{} would exist at {} but state doesn't declare it",
327 ref_name, would_be_short
328 ));
329 }
330 }
331
332 if reasons.is_empty() {
333 Some("Unknown mismatch".to_string())
334 } else {
335 Some(reasons.join("; "))
336 }
337}
338
228#[cfg(test)] 339#[cfg(test)]
229mod tests { 340mod tests {
230 use super::*; 341 use super::*;
@@ -695,4 +806,97 @@ mod tests {
695 // Should return true - real OID exists, symbolic ref skipped 806 // Should return true - real OID exists, symbolic ref skipped
696 assert!(can_apply_state(&event, repo_path)); 807 assert!(can_apply_state(&event, repo_path));
697 } 808 }
809
810 #[test]
811 fn test_diagnose_state_mismatch_missing_ref() {
812 // State declares both main and test branches
813 let event = create_test_state_event(
814 "test-repo",
815 vec![("refs/heads/main", "abc123"), ("refs/heads/test", "def456")],
816 );
817
818 // Push only creates test branch
819 let pushed_updates = vec![RefUpdate {
820 old_oid: "0000000000000000000000000000000000000000".to_string(),
821 new_oid: "def456".to_string(),
822 ref_name: "refs/heads/test".to_string(),
823 }];
824
825 // No local refs
826 let local_refs = HashMap::new();
827
828 let diagnosis = diagnose_state_mismatch(&event, &pushed_updates, &local_refs);
829 assert!(diagnosis.is_some());
830 let msg = diagnosis.unwrap();
831 assert!(msg.contains("refs/heads/main"));
832 assert!(msg.contains("missing"));
833 }
834
835 #[test]
836 fn test_diagnose_state_mismatch_wrong_sha() {
837 // State declares main at abc123
838 let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
839
840 // Push updates main to different SHA
841 let pushed_updates = vec![RefUpdate {
842 old_oid: "0000000000000000000000000000000000000000".to_string(),
843 new_oid: "wrong123".to_string(),
844 ref_name: "refs/heads/main".to_string(),
845 }];
846
847 let local_refs = HashMap::new();
848
849 let diagnosis = diagnose_state_mismatch(&event, &pushed_updates, &local_refs);
850 assert!(diagnosis.is_some());
851 let msg = diagnosis.unwrap();
852 assert!(msg.contains("refs/heads/main"));
853 assert!(msg.contains("would be at"));
854 assert!(msg.contains("state declares"));
855 }
856
857 #[test]
858 fn test_diagnose_state_mismatch_extra_ref() {
859 // State declares only main
860 let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
861
862 // Push creates both main and test
863 let pushed_updates = vec![
864 RefUpdate {
865 old_oid: "0000000000000000000000000000000000000000".to_string(),
866 new_oid: "abc123".to_string(),
867 ref_name: "refs/heads/main".to_string(),
868 },
869 RefUpdate {
870 old_oid: "0000000000000000000000000000000000000000".to_string(),
871 new_oid: "def456".to_string(),
872 ref_name: "refs/heads/test".to_string(),
873 },
874 ];
875
876 let local_refs = HashMap::new();
877
878 let diagnosis = diagnose_state_mismatch(&event, &pushed_updates, &local_refs);
879 assert!(diagnosis.is_some());
880 let msg = diagnosis.unwrap();
881 assert!(msg.contains("refs/heads/test"));
882 assert!(msg.contains("doesn't declare"));
883 }
884
885 #[test]
886 fn test_diagnose_state_mismatch_no_mismatch() {
887 // State declares main
888 let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
889
890 // Push creates main at correct SHA
891 let pushed_updates = vec![RefUpdate {
892 old_oid: "0000000000000000000000000000000000000000".to_string(),
893 new_oid: "abc123".to_string(),
894 ref_name: "refs/heads/main".to_string(),
895 }];
896
897 let local_refs = HashMap::new();
898
899 let diagnosis = diagnose_state_mismatch(&event, &pushed_updates, &local_refs);
900 assert!(diagnosis.is_none()); // No mismatch
901 }
698} 902}