upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/purgatory/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/purgatory/mod.rs')
-rw-r--r--src/purgatory/mod.rs344
1 files changed, 6 insertions, 338 deletions
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index f15d6bd..88377fb 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -26,11 +26,9 @@ use std::process::Command;
26use std::sync::Arc; 26use std::sync::Arc;
27use std::time::{Duration, Instant}; 27use std::time::{Duration, Instant};
28 28
29use crate::git::authorization::{ 29use crate::git::authorization::{fetch_repository_data, pubkey_authorised_for_repo_owners, RepositoryData};
30 collect_authorized_maintainers, fetch_repository_data, pubkey_authorised_for_repo_owners,
31 RepositoryData,
32};
33use crate::git::oid_exists; 30use crate::git::oid_exists;
31use crate::git::sync::sync_to_owner_repos;
34use crate::nostr::builder::SharedDatabase; 32use crate::nostr::builder::SharedDatabase;
35use crate::nostr::events::RepositoryState; 33use crate::nostr::events::RepositoryState;
36 34
@@ -596,96 +594,14 @@ async fn sync_state_git_data(
596 } 594 }
597 595
598 // Now that we have all OIDs, sync to other owner repositories and align refs 596 // Now that we have all OIDs, sync to other owner repositories and align refs
599 let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); 597 let sync_result = sync_to_owner_repos(&source_repo_path, &state, &db_repo_data, git_data_path);
600 let mut repo_count = 0;
601
602 for (owner, maintainers) in &by_owner {
603 // Check if this state's author is authorized for this owner
604 if !maintainers.contains(&state.event.pubkey.to_hex()) {
605 continue;
606 }
607
608 // Find the previous latest state for this owner's maintainer set
609 let previous_state = db_repo_data
610 .states
611 .iter()
612 .filter(|s| maintainers.contains(&s.event.pubkey.to_hex()))
613 .max_by_key(|s| s.event.created_at);
614
615 // Only update if this state is newer than any existing state
616 // TODO: in event of a tie, the event with the biggest event id wins
617 if let Some(prev) = previous_state {
618 if state.event.created_at <= prev.event.created_at {
619 tracing::debug!(
620 identifier = %state.identifier,
621 owner = %owner,
622 "Skipping owner - existing state is newer or equal"
623 );
624 continue;
625 }
626 }
627
628 // Find the announcement for this owner
629 let announcement = db_repo_data
630 .announcements
631 .iter()
632 .find(|a| a.event.pubkey.to_hex() == *owner);
633
634 let Some(announcement) = announcement else {
635 continue;
636 };
637
638 let target_repo_path = git_data_path.join(announcement.repo_path());
639
640 if !target_repo_path.exists() {
641 // Repository doesn't exist (e.g., announcement doesn't list this service)
642 tracing::debug!(
643 identifier = %state.identifier,
644 owner = %owner,
645 repo_path = %target_repo_path.display(),
646 "Skipping owner - repository doesn't exist"
647 );
648 continue;
649 }
650
651 // Copy missing OIDs from source repo to target repo if different
652 if target_repo_path != source_repo_path {
653 if let Err(e) =
654 copy_missing_oids_between_repos(&source_repo_path, &target_repo_path, &state)
655 {
656 tracing::warn!(
657 identifier = %state.identifier,
658 source = %source_repo_path.display(),
659 target = %target_repo_path.display(),
660 error = %e,
661 "Failed to copy OIDs between repos"
662 );
663 // Continue anyway - we'll try to align what we can
664 }
665 }
666
667 // Align refs with state
668 let result = align_repository_with_state(&target_repo_path, &state);
669 repo_count += 1;
670
671 tracing::info!(
672 identifier = %state.identifier,
673 owner = %owner,
674 repo_path = %target_repo_path.display(),
675 refs_created = result.refs_created,
676 refs_updated = result.refs_updated,
677 refs_deleted = result.refs_deleted,
678 head_set = result.head_set,
679 "Aligned repository with state from purgatory sync"
680 );
681 }
682 598
683 tracing::info!( 599 tracing::info!(
684 identifier = %state.identifier, 600 identifier = %state.identifier,
685 event_id = %state.event.id, 601 event_id = %state.event.id,
686 repo_count = repo_count, 602 repos_synced = sync_result.repos_synced,
687 "Synced git data and aligned {} repositories", 603 "Synced git data and aligned {} repositories from purgatory",
688 repo_count 604 sync_result.repos_synced
689 ); 605 );
690 606
691 // Save state event to database 607 // Save state event to database
@@ -737,254 +653,6 @@ async fn sync_state_git_data(
737 Ok(()) 653 Ok(())
738} 654}
739 655
740/// Copy missing OIDs from a source repository to a target repository.
741///
742/// Identifies commits referenced in the state that are missing from the target
743/// repository and copies them from the source repository using git fetch.
744fn copy_missing_oids_between_repos(
745 source_repo: &Path,
746 target_repo: &Path,
747 state: &RepositoryState,
748) -> Result<(), String> {
749 // Collect all commits referenced in the state
750 let mut commits_to_check = Vec::new();
751
752 for branch in &state.branches {
753 if !branch.commit.starts_with("ref: ") {
754 commits_to_check.push(&branch.commit);
755 }
756 }
757
758 for tag in &state.tags {
759 if !tag.commit.starts_with("ref: ") {
760 commits_to_check.push(&tag.commit);
761 }
762 }
763
764 // Identify missing commits
765 let mut missing_commits = Vec::new();
766 for commit in commits_to_check {
767 if !oid_exists(target_repo, commit) {
768 missing_commits.push(commit);
769 }
770 }
771
772 if missing_commits.is_empty() {
773 tracing::debug!(
774 "No missing commits to copy from {} to {}",
775 source_repo.display(),
776 target_repo.display()
777 );
778 return Ok(());
779 }
780
781 tracing::info!(
782 "Copying {} missing commits from {} to {}",
783 missing_commits.len(),
784 source_repo.display(),
785 target_repo.display()
786 );
787
788 // Fetch each missing commit from source to target
789 for commit in &missing_commits {
790 let output = Command::new("git")
791 .args([
792 "fetch",
793 source_repo.to_str().ok_or("Invalid source path")?,
794 commit,
795 ])
796 .current_dir(target_repo)
797 .output()
798 .map_err(|e| format!("Failed to execute git fetch: {}", e))?;
799
800 if !output.status.success() {
801 let stderr = String::from_utf8_lossy(&output.stderr);
802 return Err(format!(
803 "git fetch failed for commit {}: {}",
804 commit, stderr
805 ));
806 }
807
808 tracing::debug!("Copied commit {} to {}", commit, target_repo.display());
809 }
810
811 Ok(())
812}
813
814/// Result of aligning a repository with authorized state
815#[derive(Debug, Default)]
816struct SyncAlignmentResult {
817 /// Number of refs created
818 refs_created: usize,
819 /// Number of refs updated
820 refs_updated: usize,
821 /// Number of refs deleted
822 refs_deleted: usize,
823 /// Whether HEAD was set
824 head_set: bool,
825}
826
827/// Align a repository's refs with the authorized state.
828///
829/// This function:
830/// 1. Deletes refs that are in the repo but not in the state (for refs/heads/ and refs/tags/)
831/// 2. Updates refs that exist in state if we have the commit
832/// 3. Sets HEAD if the HEAD branch's commit is available
833fn align_repository_with_state(repo_path: &Path, state: &RepositoryState) -> SyncAlignmentResult {
834 use crate::git;
835
836 let mut result = SyncAlignmentResult::default();
837
838 // Check if repository exists
839 if !repo_path.exists() {
840 tracing::debug!(
841 "Repository not found at {}, cannot align with state",
842 repo_path.display()
843 );
844 return result;
845 }
846
847 // Get current refs from the repository
848 let current_refs = match git::list_refs(repo_path) {
849 Ok(refs) => refs,
850 Err(e) => {
851 tracing::warn!("Failed to list refs in {}: {}", repo_path.display(), e);
852 return result;
853 }
854 };
855
856 // Build expected refs from state
857 let mut expected_refs: std::collections::HashMap<String, String> =
858 std::collections::HashMap::new();
859
860 for branch in &state.branches {
861 let ref_name = format!("refs/heads/{}", branch.name);
862 expected_refs.insert(ref_name, branch.commit.clone());
863 }
864
865 for tag in &state.tags {
866 let ref_name = format!("refs/tags/{}", tag.name);
867 expected_refs.insert(ref_name, tag.commit.clone());
868 }
869
870 // Delete refs that exist in repo but not in state (only for refs/heads/ and refs/tags/)
871 for (ref_name, _current_commit) in &current_refs {
872 if (ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/"))
873 && !expected_refs.contains_key(ref_name)
874 {
875 match git::delete_ref(repo_path, ref_name) {
876 Ok(()) => {
877 tracing::info!(
878 "Deleted {} from {} (not in state)",
879 ref_name,
880 repo_path.display()
881 );
882 result.refs_deleted += 1;
883 }
884 Err(e) => {
885 tracing::warn!(
886 "Failed to delete {} from {}: {}",
887 ref_name,
888 repo_path.display(),
889 e
890 );
891 }
892 }
893 }
894 }
895
896 // Update refs that exist in state (if we have the commit)
897 for (ref_name, expected_commit) in &expected_refs {
898 // Skip symbolic refs
899 if expected_commit.starts_with("ref: ") {
900 continue;
901 }
902
903 // Check if we have the commit
904 if !git::oid_exists(repo_path, expected_commit) {
905 tracing::debug!(
906 "Commit {} not available for {} in {}",
907 expected_commit,
908 ref_name,
909 repo_path.display()
910 );
911 continue;
912 }
913
914 // Check current value
915 let current_commit = current_refs
916 .iter()
917 .find(|(r, _)| r == ref_name)
918 .map(|(_, c)| c.as_str());
919
920 if current_commit == Some(expected_commit.as_str()) {
921 // Already correct
922 continue;
923 }
924
925 // Update or create the ref
926 match git::update_ref(repo_path, ref_name, expected_commit) {
927 Ok(()) => {
928 if current_commit.is_some() {
929 tracing::info!(
930 "Updated {} to {} in {}",
931 ref_name,
932 expected_commit,
933 repo_path.display()
934 );
935 result.refs_updated += 1;
936 } else {
937 tracing::info!(
938 "Created {} at {} in {}",
939 ref_name,
940 expected_commit,
941 repo_path.display()
942 );
943 result.refs_created += 1;
944 }
945 }
946 Err(e) => {
947 tracing::warn!(
948 "Failed to update {} in {}: {}",
949 ref_name,
950 repo_path.display(),
951 e
952 );
953 }
954 }
955 }
956
957 // Set HEAD if specified in state
958 if let Some(head_ref) = &state.head {
959 if let Some(branch_name) = state.get_head_branch() {
960 if let Some(head_commit) = state.get_branch_commit(branch_name) {
961 match git::try_set_head_if_available(repo_path, head_ref, head_commit) {
962 Ok(true) => {
963 tracing::info!(
964 "Set HEAD to {} in {} (from purgatory sync)",
965 head_ref,
966 repo_path.display()
967 );
968 result.head_set = true;
969 }
970 Ok(false) => {
971 tracing::debug!(
972 "HEAD commit {} not available yet in {}",
973 head_commit,
974 repo_path.display()
975 );
976 }
977 Err(e) => {
978 tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e);
979 }
980 }
981 }
982 }
983 }
984
985 result
986}
987
988/// Fetch missing OIDs from a remote git server. 656/// Fetch missing OIDs from a remote git server.
989/// 657///
990/// Uses `git fetch` to retrieve specific commits from the server. 658/// Uses `git fetch` to retrieve specific commits from the server.