upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs169
-rw-r--r--src/nostr/policy/state.rs107
-rw-r--r--tests/push_authorization.rs5
3 files changed, 255 insertions, 26 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index ef494da..8bcf0f7 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -766,32 +766,159 @@ impl PushAuthorizationTests {
766 /// 766 ///
767 /// GRASP-01: "respecting the recursive maintainer set" 767 /// GRASP-01: "respecting the recursive maintainer set"
768 /// 768 ///
769 /// This test verifies that when a maintainer publishes a state event, it updates 769 /// This test verifies that when a maintainer publishes a state event, the purgatory
770 /// the git repository state for all other maintainers' views. This ensures git 770 /// feature correctly copies git commits between repos so all authorized maintainers'
771 /// repositories always reflect the state according to nostr events (including state 771 /// repositories always reflect the state according to nostr events.
772 /// from recursive maintainers).
773 /// 772 ///
774 /// ## Implementation Note 773 /// ## Implementation
775 /// 774 ///
776 /// This test is a stub for the purgatory feature. It will be implemented as part 775 /// This test:
777 /// of GRASP-02 purgatory functionality. 776 /// 1. Uses RecursiveMaintainerStateDataPushed fixture which:
777 /// - Creates owner repo + state (ValidRepo, OwnerStateDataPushed)
778 /// - Creates maintainer announcement (separate repo for maintainer)
779 /// - Pushes recursive maintainer's git data to owner's repo
780 /// 2. Clones the maintainer's repository (not the owner's)
781 /// 3. Verifies that the maintainer's repo contains the recursive maintainer's state
778 /// 782 ///
779 /// ## Fixture Compatibility 783 /// This proves purgatory is working: git data was pushed to owner's repo, and purgatory
780 /// 784 /// synced it to the maintainer's repo based on the state event.
781 /// This test will use:
782 /// - `MaintainerStateDataPushed` - maintainer's state event with git data pushed
783 /// - Multiple maintainer clones to verify state propagation
784 #[allow(dead_code)]
785 pub async fn test_push_of_state_by_maintainer_updates_other_maintainer_repos( 785 pub async fn test_push_of_state_by_maintainer_updates_other_maintainer_repos(
786 _client: &AuditClient, 786 client: &AuditClient,
787 _relay_domain: &str, 787 relay_domain: &str,
788 ) -> TestResult { 788 ) -> TestResult {
789 TestResult::new( 789 let test_name = "test_push_of_state_by_maintainer_updates_other_maintainer_repos";
790 "test_push_of_state_by_maintainer_updates_other_maintainer_repos", 790 let ctx = TestContext::new(client);
791 "GRASP-01:git-http:purgatory", 791
792 "Maintainer state updates propagate to other maintainer repos", 792 // Get the RecursiveMaintainerStateDataPushed fixture which:
793 ) 793 // 1. Creates owner repo + owner state + git data
794 .fail("Not yet implemented - requires purgatory feature (GRASP-02)") 794 // 2. Creates maintainer announcement (separate repo)
795 // 3. Pushes recursive maintainer's git data to owner's repo
796 // 4. Purgatory should then sync to maintainer's repo
797 let recursive_state = match ctx
798 .get_fixture(FixtureKind::RecursiveMaintainerStateDataPushed)
799 .await
800 {
801 Ok(s) => s,
802 Err(e) => {
803 return TestResult::new(
804 test_name,
805 "GRASP-01:git-http:purgatory",
806 "Maintainer state updates propagate to other maintainer repos",
807 )
808 .fail(format!(
809 "Failed to get RecursiveMaintainerStateDataPushed fixture: {}",
810 e
811 ))
812 }
813 };
814
815 // Small delay to ensure state processing completes
816 tokio::time::sleep(Duration::from_millis(200)).await;
817
818 // Extract repo_id from the recursive maintainer's state event
819 let repo_id = match recursive_state
820 .tags
821 .iter()
822 .find(|t| t.kind() == TagKind::d())
823 .and_then(|t| t.content())
824 {
825 Some(id) => id.to_string(),
826 None => {
827 return TestResult::new(
828 test_name,
829 "GRASP-01:git-http:purgatory",
830 "Maintainer state updates propagate to other maintainer repos",
831 )
832 .fail("No repo identifier in recursive maintainer state event")
833 }
834 };
835
836 // Get the maintainer's npub
837 let maintainer_npub = match client.maintainer_keys().public_key().to_bech32() {
838 Ok(npub) => npub,
839 Err(e) => {
840 return TestResult::new(
841 test_name,
842 "GRASP-01:git-http:purgatory",
843 "Maintainer state updates propagate to other maintainer repos",
844 )
845 .fail(format!(
846 "Failed to convert maintainer pubkey to npub: {}",
847 e
848 ))
849 }
850 };
851
852 // Use the known recursive maintainer deterministic commit hash from the fixture
853 let expected_commit = crate::RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH;
854
855 // Clone the maintainer's repository (NOT the owner's)
856 // This is the key test: git data was pushed to owner's repo, does maintainer's repo have it?
857 let clone_path = match clone_repo(relay_domain, &maintainer_npub, &repo_id) {
858 Ok(path) => path,
859 Err(e) => {
860 return TestResult::new(
861 test_name,
862 "GRASP-01:git-http:purgatory",
863 "Maintainer state updates propagate to other maintainer repos",
864 )
865 .fail(format!("Failed to clone maintainer's repo: {}", e))
866 }
867 };
868
869 let cleanup = || {
870 let _ = fs::remove_dir_all(&clone_path);
871 };
872
873 // Verify that the maintainer's repo contains the recursive maintainer's commit
874 // This proves purgatory copied it from owner's repo
875 let commit_exists_output = Command::new("git")
876 .args(["cat-file", "-t", expected_commit])
877 .current_dir(&clone_path)
878 .output();
879
880 let commit_exists = match commit_exists_output {
881 Ok(output) => {
882 if output.status.success() {
883 let obj_type = String::from_utf8_lossy(&output.stdout);
884 obj_type.trim() == "commit"
885 } else {
886 false
887 }
888 }
889 Err(e) => {
890 cleanup();
891 return TestResult::new(
892 test_name,
893 "GRASP-01:git-http:purgatory",
894 "Maintainer state updates propagate to other maintainer repos",
895 )
896 .fail(format!("Failed to check if commit exists: {}", e));
897 }
898 };
899
900 cleanup();
901
902 if commit_exists {
903 TestResult::new(
904 test_name,
905 "GRASP-01:git-http:purgatory",
906 "Maintainer state updates propagate to other maintainer repos",
907 )
908 .pass()
909 } else {
910 TestResult::new(
911 test_name,
912 "GRASP-01:git-http:purgatory",
913 "Maintainer state updates propagate to other maintainer repos",
914 )
915 .fail(format!(
916 "Maintainer's repo does not contain recursive maintainer's commit {}. \
917 Git data was pushed to owner's repo, but purgatory did not copy it to \
918 maintainer's repo. This indicates the purgatory feature is not working correctly.",
919 expected_commit
920 ))
921 }
795 } 922 }
796 923
797 /// Test that non-maintainer state event is ignored 924 /// Test that non-maintainer state event is ignored
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs
index 1203890..48435ea 100644
--- a/src/nostr/policy/state.rs
+++ b/src/nostr/policy/state.rs
@@ -125,7 +125,27 @@ impl StatePolicy {
125 { 125 {
126 let repo_path = 126 let repo_path =
127 self.ctx.git_data_path.join(annoucement.repo_path().clone()); 127 self.ctx.git_data_path.join(annoucement.repo_path().clone());
128 // TODO - if repo_path != repo_with_git_data, pass as a datasource for missing data? 128
129 if !repo_path.exists() {
130 // eg if annoucement doesnt list repo (but stored as its in maintainer set)
131 continue;
132 }
133 // If repo_path != repo_with_git_data, copy missing oids first
134 if repo_path != repo_with_git_data {
135 if let Err(e) = self.copy_missing_oids(
136 &repo_with_git_data,
137 &repo_path,
138 &state,
139 ) {
140 tracing::warn!(
141 "Failed to copy oids from {} to {}: {}",
142 repo_with_git_data.display(),
143 repo_path.display(),
144 e
145 );
146 }
147 }
148
129 let result = self.align_repository_with_state(&repo_path, &state); 149 let result = self.align_repository_with_state(&repo_path, &state);
130 repo_count += 1; 150 repo_count += 1;
131 tracing::info!( 151 tracing::info!(
@@ -335,6 +355,91 @@ impl StatePolicy {
335 355
336 result 356 result
337 } 357 }
358
359 /// Copy missing OIDs from a source repository to a target repository
360 ///
361 /// Identifies commits referenced in the state that are missing from the target
362 /// repository and copies them from the source repository using git fetch.
363 ///
364 /// # Arguments
365 /// * `source_repo` - Path to repository containing the commits
366 /// * `target_repo` - Path to repository to receive the commits
367 /// * `state` - Repository state containing commit references
368 ///
369 /// # Returns
370 /// Ok(()) on success, Err with error message on failure
371 fn copy_missing_oids(
372 &self,
373 source_repo: &Path,
374 target_repo: &Path,
375 state: &RepositoryState,
376 ) -> Result<(), String> {
377 use std::process::Command;
378
379 // Collect all commits referenced in the state
380 let mut commits_to_check = Vec::new();
381
382 for branch in &state.branches {
383 if !branch.commit.starts_with("ref: ") {
384 commits_to_check.push(&branch.commit);
385 }
386 }
387
388 for tag in &state.tags {
389 if !tag.commit.starts_with("ref: ") {
390 commits_to_check.push(&tag.commit);
391 }
392 }
393
394 // Identify missing commits
395 let mut missing_commits = Vec::new();
396 for commit in commits_to_check {
397 if !git::oid_exists(target_repo, commit) {
398 missing_commits.push(commit);
399 }
400 }
401
402 if missing_commits.is_empty() {
403 tracing::debug!(
404 "No missing commits to copy from {} to {}",
405 source_repo.display(),
406 target_repo.display()
407 );
408 return Ok(());
409 }
410
411 tracing::info!(
412 "Copying {} missing commits from {} to {}",
413 missing_commits.len(),
414 source_repo.display(),
415 target_repo.display()
416 );
417
418 // Fetch each missing commit from source to target
419 for commit in &missing_commits {
420 let output = Command::new("git")
421 .args([
422 "fetch",
423 source_repo.to_str().ok_or("Invalid source path")?,
424 commit,
425 ])
426 .current_dir(target_repo)
427 .output()
428 .map_err(|e| format!("Failed to execute git fetch: {}", e))?;
429
430 if !output.status.success() {
431 let stderr = String::from_utf8_lossy(&output.stderr);
432 return Err(format!(
433 "git fetch failed for commit {}: {}",
434 commit, stderr
435 ));
436 }
437
438 tracing::debug!("Copied commit {} to {}", commit, target_repo.display());
439 }
440
441 Ok(())
442 }
338} 443}
339 444
340fn find_repo_with_git_data( 445fn find_repo_with_git_data(
diff --git a/tests/push_authorization.rs b/tests/push_authorization.rs
index 85b9a5d..7047010 100644
--- a/tests/push_authorization.rs
+++ b/tests/push_authorization.rs
@@ -72,7 +72,4 @@ isolated_push_test!(
72); 72);
73isolated_push_test!(test_head_set_after_state_event_with_existing_commit); 73isolated_push_test!(test_head_set_after_state_event_with_existing_commit);
74isolated_push_test!(test_head_set_after_git_push_with_required_oids); 74isolated_push_test!(test_head_set_after_git_push_with_required_oids);
75 75isolated_push_test!(test_push_of_state_by_maintainer_updates_other_maintainer_repos);
76// Note: test_push_of_state_by_maintainer_updates_other_maintainer_repos is not included
77// as it's a stub for the purgatory feature. It can be run manually once implemented:
78// isolated_push_test!(test_push_of_state_by_maintainer_updates_other_maintainer_repos);