upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 12:58:01 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 12:58:01 +0000
commit37c9d3e0d195b0789f9e6407b81973cf50222b76 (patch)
treed4d14e833a2fc78bfb7418cfe0ac5d4d80366493 /src
parentd78d3a86ba81a5b59cde527a448f5c9d131db8d6 (diff)
purgatory: improve process_newly_available_git_data state event sync
Diffstat (limited to 'src')
-rw-r--r--src/git/sync.rs38
-rw-r--r--src/purgatory/helpers.rs250
-rw-r--r--src/purgatory/mod.rs2
3 files changed, 264 insertions, 26 deletions
diff --git a/src/git/sync.rs b/src/git/sync.rs
index e57a0cc..cf6e93d 100644
--- a/src/git/sync.rs
+++ b/src/git/sync.rs
@@ -43,7 +43,7 @@ use crate::git::authorization::{
43use crate::git::{self, oid_exists}; 43use crate::git::{self, oid_exists};
44use crate::nostr::builder::SharedDatabase; 44use crate::nostr::builder::SharedDatabase;
45use crate::nostr::events::RepositoryState; 45use crate::nostr::events::RepositoryState;
46use crate::purgatory::{can_satisfy_state, Purgatory}; 46use crate::purgatory::{can_apply_state, Purgatory};
47 47
48/// Result of processing newly available git data. 48/// Result of processing newly available git data.
49/// 49///
@@ -837,15 +837,9 @@ pub async fn process_newly_available_git_data(
837 "Processing newly available git data" 837 "Processing newly available git data"
838 ); 838 );
839 839
840 // Get current refs from the repository for state matching
841 let current_refs: HashMap<String, String> = git::list_refs(source_repo_path)
842 .unwrap_or_default()
843 .into_iter()
844 .collect();
845
846 // Process state events from purgatory 840 // Process state events from purgatory
847 let state_result = 841 let state_result =
848 process_purgatory_state_events(&identifier, source_repo_path, &current_refs, database, local_relay, purgatory, git_data_path).await; 842 process_purgatory_state_events(&identifier, source_repo_path, database, local_relay, purgatory, git_data_path).await;
849 result.merge(state_result); 843 result.merge(state_result);
850 844
851 // Process PR events from purgatory 845 // Process PR events from purgatory
@@ -866,11 +860,15 @@ pub async fn process_newly_available_git_data(
866 Ok(result) 860 Ok(result)
867} 861}
868 862
869/// Process state events from purgatory that can now be satisfied. 863/// Process state events from purgatory that can now be applied.
864///
865/// This checks if we have all the git OIDs needed to apply each state event.
866/// Unlike push authorization (which uses `can_satisfy_state` to check if a push
867/// would transform refs correctly), this uses `can_apply_state` to simply check
868/// if the required git data is available.
870async fn process_purgatory_state_events( 869async fn process_purgatory_state_events(
871 identifier: &str, 870 identifier: &str,
872 source_repo_path: &Path, 871 source_repo_path: &Path,
873 current_refs: &HashMap<String, String>,
874 database: &SharedDatabase, 872 database: &SharedDatabase,
875 local_relay: Option<&nostr_relay_builder::LocalRelay>, 873 local_relay: Option<&nostr_relay_builder::LocalRelay>,
876 purgatory: &Purgatory, 874 purgatory: &Purgatory,
@@ -887,27 +885,17 @@ async fn process_purgatory_state_events(
887 debug!( 885 debug!(
888 identifier = %identifier, 886 identifier = %identifier,
889 purgatory_states_count = purgatory_states.len(), 887 purgatory_states_count = purgatory_states.len(),
890 "Checking purgatory state events for satisfaction" 888 "Checking purgatory state events for available git data"
891 ); 889 );
892 890
893 // Build ref updates from current refs (treating all as "creations" for matching purposes) 891 // Check which state events can be applied (have all required OIDs)
894 let ref_updates: Vec<crate::purgatory::RefUpdate> = current_refs
895 .iter()
896 .map(|(ref_name, commit)| crate::purgatory::RefUpdate {
897 old_oid: "0000000000000000000000000000000000000000".to_string(),
898 new_oid: commit.clone(),
899 ref_name: ref_name.clone(),
900 })
901 .collect();
902
903 // Check which state events can be satisfied
904 for entry in &purgatory_states { 892 for entry in &purgatory_states {
905 // Check if this state event can be satisfied with current refs 893 // Check if we have all the git data needed to apply this state event
906 if !can_satisfy_state(&entry.event, &ref_updates, current_refs) { 894 if !can_apply_state(&entry.event, source_repo_path) {
907 debug!( 895 debug!(
908 identifier = %identifier, 896 identifier = %identifier,
909 event_id = %entry.event.id, 897 event_id = %entry.event.id,
910 "State event cannot be satisfied with current refs" 898 "State event cannot be applied - missing git OIDs"
911 ); 899 );
912 continue; 900 continue;
913 } 901 }
diff --git a/src/purgatory/helpers.rs b/src/purgatory/helpers.rs
index 5df6cc8..2e53778 100644
--- a/src/purgatory/helpers.rs
+++ b/src/purgatory/helpers.rs
@@ -3,10 +3,23 @@
3//! These functions handle the late-binding extraction and matching of git refs 3//! These functions handle the late-binding extraction and matching of git refs
4//! from state events. Refs are extracted at git push time rather than event 4//! from state events. Refs are extracted at git push time rather than event
5//! arrival time to enable flexible matching logic. 5//! arrival time to enable flexible matching logic.
6//!
7//! ## Key Functions
8//!
9//! - [`can_satisfy_state`]: Used for **push authorization** - checks if a push
10//! would transform the current refs into the declared state. This validates
11//! that the pushed refspecs match what the state event declares.
12//!
13//! - [`can_apply_state`]: Used for **purgatory processing** - checks if we have
14//! all the git OIDs needed to apply a state event. This validates that the
15//! git data is available locally, regardless of current ref state.
6 16
7use super::{RefPair, RefUpdate}; 17use super::{RefPair, RefUpdate};
8use nostr_sdk::prelude::*; 18use nostr_sdk::prelude::*;
9use std::collections::HashMap; 19use std::collections::HashMap;
20use std::path::Path;
21
22use crate::git::oid_exists;
10 23
11/// Extract ref pairs from a state event (kind 30618). 24/// Extract ref pairs from a state event (kind 30618).
12/// 25///
@@ -61,8 +74,56 @@ pub fn extract_refs_from_state(event: &Event) -> Vec<RefPair> {
61 .collect() 74 .collect()
62} 75}
63 76
77/// Check if a state event can be applied given the available git data.
78///
79/// This is used for **purgatory processing** to determine if we have all the
80/// git objects needed to apply a state event. Unlike `can_satisfy_state` which
81/// validates push authorization, this function only checks OID availability.
82///
83/// Returns true if all OIDs referenced in the state event exist in the repository.
84/// Symbolic refs (starting with "ref: ") are skipped as they don't require OID lookup.
85///
86/// # Arguments
87/// * `event` - The state event to check
88/// * `repo_path` - Path to the git repository to check OIDs against
89///
90/// # Returns
91/// true if all required OIDs exist in the repository, false otherwise
92///
93/// # Example
94/// ```ignore
95/// // State event declares:
96/// // refs/heads/main -> abc123
97/// // refs/heads/dev -> def456
98/// // refs/heads/symlink -> ref: refs/heads/main (symbolic)
99/// //
100/// // If abc123 and def456 exist in repo: returns true
101/// // If abc123 exists but def456 doesn't: returns false
102/// // The symbolic ref doesn't require an OID check
103/// ```
104pub fn can_apply_state(event: &Event, repo_path: &Path) -> bool {
105 let state_refs = extract_refs_from_state(event);
106
107 for ref_pair in state_refs {
108 // Skip symbolic refs (they don't require OID lookup)
109 if ref_pair.object_sha.starts_with("ref: ") {
110 continue;
111 }
112
113 // Check if the OID exists in the repository
114 if !oid_exists(repo_path, &ref_pair.object_sha) {
115 return false;
116 }
117 }
118
119 true
120}
121
64/// Check if a state event can be satisfied by ref updates plus local refs. 122/// Check if a state event can be satisfied by ref updates plus local refs.
65/// 123///
124/// This is used for **push authorization** to validate that a push would
125/// transform the current refs into the declared state.
126///
66/// Returns true if applying the ref updates to local state results in exactly 127/// Returns true if applying the ref updates to local state results in exactly
67/// the state declared in the event. This means: 128/// the state declared in the event. This means:
68/// 1. Filter local_refs to only branches (refs/heads/*) and tags (refs/tags/*) 129/// 1. Filter local_refs to only branches (refs/heads/*) and tags (refs/tags/*)
@@ -432,4 +493,193 @@ mod tests {
432 assert_eq!(unpushed[0].ref_name, "refs/heads/main"); 493 assert_eq!(unpushed[0].ref_name, "refs/heads/main");
433 assert_eq!(unpushed[0].object_sha, "abc123"); 494 assert_eq!(unpushed[0].object_sha, "abc123");
434 } 495 }
496
497 // =========================================================================
498 // can_apply_state tests
499 // =========================================================================
500
501 /// Helper to create a temporary bare git repository with a commit.
502 /// Returns (temp_dir, commit_hash) where commit_hash is Some if a commit was created.
503 fn create_test_repo_with_commit() -> (tempfile::TempDir, Option<String>) {
504 use std::process::Command;
505
506 let temp_dir = tempfile::tempdir().unwrap();
507 let bare_path = temp_dir.path();
508
509 // Initialize bare repo
510 Command::new("git")
511 .args(["init", "--bare"])
512 .current_dir(bare_path)
513 .output()
514 .expect("Failed to init bare git repo");
515
516 // Create a working repo to generate a commit
517 let work_dir = tempfile::tempdir().unwrap();
518
519 Command::new("git")
520 .args(["init"])
521 .current_dir(work_dir.path())
522 .output()
523 .expect("Failed to init work repo");
524
525 Command::new("git")
526 .args(["config", "user.email", "test@test.com"])
527 .current_dir(work_dir.path())
528 .output()
529 .expect("Failed to set email");
530
531 Command::new("git")
532 .args(["config", "user.name", "Test"])
533 .current_dir(work_dir.path())
534 .output()
535 .expect("Failed to set name");
536
537 // Create a commit
538 std::fs::write(work_dir.path().join("file.txt"), "content").unwrap();
539 Command::new("git")
540 .args(["add", "."])
541 .current_dir(work_dir.path())
542 .output()
543 .expect("Failed to add");
544
545 Command::new("git")
546 .args(["commit", "-m", "test"])
547 .current_dir(work_dir.path())
548 .output()
549 .expect("Failed to commit");
550
551 // Get the commit hash from the working repo
552 let output = Command::new("git")
553 .args(["rev-parse", "HEAD"])
554 .current_dir(work_dir.path())
555 .output()
556 .expect("Failed to get commit hash");
557
558 let commit_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
559
560 // Push to bare repo
561 Command::new("git")
562 .args(["push", bare_path.to_str().unwrap(), "HEAD:refs/heads/main"])
563 .current_dir(work_dir.path())
564 .output()
565 .expect("Failed to push");
566
567 (temp_dir, Some(commit_hash))
568 }
569
570 /// Helper to create an empty bare git repository (no commits).
571 fn create_empty_test_repo() -> tempfile::TempDir {
572 use std::process::Command;
573
574 let temp_dir = tempfile::tempdir().unwrap();
575
576 Command::new("git")
577 .args(["init", "--bare"])
578 .current_dir(temp_dir.path())
579 .output()
580 .expect("Failed to init bare git repo");
581
582 temp_dir
583 }
584
585 #[test]
586 fn test_can_apply_state_with_existing_oid() {
587 // Create a repo with a real commit
588 let (temp_repo, commit_hash) = create_test_repo_with_commit();
589 let repo_path = temp_repo.path();
590 let commit_hash = commit_hash.expect("Should have a commit");
591
592 // Create a state event referencing that commit
593 let event = create_test_state_event(
594 "test-repo",
595 vec![("refs/heads/main", &commit_hash)],
596 );
597
598 // Should return true since the OID exists
599 assert!(can_apply_state(&event, repo_path));
600 }
601
602 #[test]
603 fn test_can_apply_state_with_missing_oid() {
604 // Create an empty repo
605 let temp_repo = create_empty_test_repo();
606 let repo_path = temp_repo.path();
607
608 // Create a state event referencing a non-existent commit
609 let event = create_test_state_event(
610 "test-repo",
611 vec![("refs/heads/main", "0000000000000000000000000000000000000000")],
612 );
613
614 // Should return false since the OID doesn't exist
615 assert!(!can_apply_state(&event, repo_path));
616 }
617
618 #[test]
619 fn test_can_apply_state_with_symbolic_ref() {
620 // Create an empty repo (no commits needed for symbolic refs)
621 let temp_repo = create_empty_test_repo();
622 let repo_path = temp_repo.path();
623
624 // Create a state event with only a symbolic ref
625 let event = create_test_state_event(
626 "test-repo",
627 vec![("refs/heads/main", "ref: refs/heads/other")],
628 );
629
630 // Should return true - symbolic refs don't require OID lookup
631 assert!(can_apply_state(&event, repo_path));
632 }
633
634 #[test]
635 fn test_can_apply_state_mixed_existing_and_missing() {
636 // Create a repo with a real commit
637 let (temp_repo, commit_hash) = create_test_repo_with_commit();
638 let repo_path = temp_repo.path();
639 let commit_hash = commit_hash.expect("Should have a commit");
640
641 // Create a state event with one existing and one missing OID
642 let event = create_test_state_event(
643 "test-repo",
644 vec![
645 ("refs/heads/main", &commit_hash), // exists
646 ("refs/heads/dev", "0000000000000000000000000000000000000000"), // doesn't exist
647 ],
648 );
649
650 // Should return false since one OID is missing
651 assert!(!can_apply_state(&event, repo_path));
652 }
653
654 #[test]
655 fn test_can_apply_state_empty_event() {
656 let temp_repo = create_empty_test_repo();
657 let repo_path = temp_repo.path();
658
659 // Empty state event (no refs declared)
660 let event = create_test_state_event("test-repo", vec![]);
661
662 // Should return true - nothing to check
663 assert!(can_apply_state(&event, repo_path));
664 }
665
666 #[test]
667 fn test_can_apply_state_mixed_symbolic_and_real() {
668 // Create a repo with a real commit
669 let (temp_repo, commit_hash) = create_test_repo_with_commit();
670 let repo_path = temp_repo.path();
671 let commit_hash = commit_hash.expect("Should have a commit");
672
673 // Create a state event with both a real OID and a symbolic ref
674 let event = create_test_state_event(
675 "test-repo",
676 vec![
677 ("refs/heads/main", &commit_hash), // real OID that exists
678 ("refs/heads/alias", "ref: refs/heads/main"), // symbolic ref
679 ],
680 );
681
682 // Should return true - real OID exists, symbolic ref skipped
683 assert!(can_apply_state(&event, repo_path));
684 }
435} 685}
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index 11fe41f..499e534 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -16,7 +16,7 @@ pub mod sync;
16mod types; 16mod types;
17 17
18use anyhow::{bail, Result}; 18use anyhow::{bail, Result};
19pub use helpers::{can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; 19pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs};
20pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; 20pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry};
21 21
22use dashmap::DashMap; 22use dashmap::DashMap;