diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 12:58:01 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 12:58:01 +0000 |
| commit | 37c9d3e0d195b0789f9e6407b81973cf50222b76 (patch) | |
| tree | d4d14e833a2fc78bfb7418cfe0ac5d4d80366493 /src | |
| parent | d78d3a86ba81a5b59cde527a448f5c9d131db8d6 (diff) | |
purgatory: improve process_newly_available_git_data state event sync
Diffstat (limited to 'src')
| -rw-r--r-- | src/git/sync.rs | 38 | ||||
| -rw-r--r-- | src/purgatory/helpers.rs | 250 | ||||
| -rw-r--r-- | src/purgatory/mod.rs | 2 |
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::{ | |||
| 43 | use crate::git::{self, oid_exists}; | 43 | use crate::git::{self, oid_exists}; |
| 44 | use crate::nostr::builder::SharedDatabase; | 44 | use crate::nostr::builder::SharedDatabase; |
| 45 | use crate::nostr::events::RepositoryState; | 45 | use crate::nostr::events::RepositoryState; |
| 46 | use crate::purgatory::{can_satisfy_state, Purgatory}; | 46 | use 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, ¤t_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. | ||
| 870 | async fn process_purgatory_state_events( | 869 | async 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 | ||
| 7 | use super::{RefPair, RefUpdate}; | 17 | use super::{RefPair, RefUpdate}; |
| 8 | use nostr_sdk::prelude::*; | 18 | use nostr_sdk::prelude::*; |
| 9 | use std::collections::HashMap; | 19 | use std::collections::HashMap; |
| 20 | use std::path::Path; | ||
| 21 | |||
| 22 | use 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 | /// ``` | ||
| 104 | pub 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; | |||
| 16 | mod types; | 16 | mod types; |
| 17 | 17 | ||
| 18 | use anyhow::{bail, Result}; | 18 | use anyhow::{bail, Result}; |
| 19 | pub use helpers::{can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; | 19 | pub use helpers::{can_apply_state, can_satisfy_state, extract_refs_from_state, get_unpushed_refs}; |
| 20 | pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; | 20 | pub use types::{PrPurgatoryEntry, RefPair, RefUpdate, StatePurgatoryEntry}; |
| 21 | 21 | ||
| 22 | use dashmap::DashMap; | 22 | use dashmap::DashMap; |