From 3f50107062d55a15decc47e93fd4e9f473de86e8 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 5 Jan 2026 14:54:29 +0000 Subject: sync all repos when authorised state data push received --- src/purgatory/mod.rs | 344 +-------------------------------------------------- 1 file changed, 6 insertions(+), 338 deletions(-) (limited to 'src/purgatory') 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; use std::sync::Arc; use std::time::{Duration, Instant}; -use crate::git::authorization::{ - collect_authorized_maintainers, fetch_repository_data, pubkey_authorised_for_repo_owners, - RepositoryData, -}; +use crate::git::authorization::{fetch_repository_data, pubkey_authorised_for_repo_owners, RepositoryData}; use crate::git::oid_exists; +use crate::git::sync::sync_to_owner_repos; use crate::nostr::builder::SharedDatabase; use crate::nostr::events::RepositoryState; @@ -596,96 +594,14 @@ async fn sync_state_git_data( } // Now that we have all OIDs, sync to other owner repositories and align refs - let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); - let mut repo_count = 0; - - for (owner, maintainers) in &by_owner { - // Check if this state's author is authorized for this owner - if !maintainers.contains(&state.event.pubkey.to_hex()) { - continue; - } - - // Find the previous latest state for this owner's maintainer set - let previous_state = db_repo_data - .states - .iter() - .filter(|s| maintainers.contains(&s.event.pubkey.to_hex())) - .max_by_key(|s| s.event.created_at); - - // Only update if this state is newer than any existing state - // TODO: in event of a tie, the event with the biggest event id wins - if let Some(prev) = previous_state { - if state.event.created_at <= prev.event.created_at { - tracing::debug!( - identifier = %state.identifier, - owner = %owner, - "Skipping owner - existing state is newer or equal" - ); - continue; - } - } - - // Find the announcement for this owner - let announcement = db_repo_data - .announcements - .iter() - .find(|a| a.event.pubkey.to_hex() == *owner); - - let Some(announcement) = announcement else { - continue; - }; - - let target_repo_path = git_data_path.join(announcement.repo_path()); - - if !target_repo_path.exists() { - // Repository doesn't exist (e.g., announcement doesn't list this service) - tracing::debug!( - identifier = %state.identifier, - owner = %owner, - repo_path = %target_repo_path.display(), - "Skipping owner - repository doesn't exist" - ); - continue; - } - - // Copy missing OIDs from source repo to target repo if different - if target_repo_path != source_repo_path { - if let Err(e) = - copy_missing_oids_between_repos(&source_repo_path, &target_repo_path, &state) - { - tracing::warn!( - identifier = %state.identifier, - source = %source_repo_path.display(), - target = %target_repo_path.display(), - error = %e, - "Failed to copy OIDs between repos" - ); - // Continue anyway - we'll try to align what we can - } - } - - // Align refs with state - let result = align_repository_with_state(&target_repo_path, &state); - repo_count += 1; - - tracing::info!( - identifier = %state.identifier, - owner = %owner, - repo_path = %target_repo_path.display(), - refs_created = result.refs_created, - refs_updated = result.refs_updated, - refs_deleted = result.refs_deleted, - head_set = result.head_set, - "Aligned repository with state from purgatory sync" - ); - } + let sync_result = sync_to_owner_repos(&source_repo_path, &state, &db_repo_data, git_data_path); tracing::info!( identifier = %state.identifier, event_id = %state.event.id, - repo_count = repo_count, - "Synced git data and aligned {} repositories", - repo_count + repos_synced = sync_result.repos_synced, + "Synced git data and aligned {} repositories from purgatory", + sync_result.repos_synced ); // Save state event to database @@ -737,254 +653,6 @@ async fn sync_state_git_data( Ok(()) } -/// Copy missing OIDs from a source repository to a target repository. -/// -/// Identifies commits referenced in the state that are missing from the target -/// repository and copies them from the source repository using git fetch. -fn copy_missing_oids_between_repos( - source_repo: &Path, - target_repo: &Path, - state: &RepositoryState, -) -> Result<(), String> { - // Collect all commits referenced in the state - let mut commits_to_check = Vec::new(); - - for branch in &state.branches { - if !branch.commit.starts_with("ref: ") { - commits_to_check.push(&branch.commit); - } - } - - for tag in &state.tags { - if !tag.commit.starts_with("ref: ") { - commits_to_check.push(&tag.commit); - } - } - - // Identify missing commits - let mut missing_commits = Vec::new(); - for commit in commits_to_check { - if !oid_exists(target_repo, commit) { - missing_commits.push(commit); - } - } - - if missing_commits.is_empty() { - tracing::debug!( - "No missing commits to copy from {} to {}", - source_repo.display(), - target_repo.display() - ); - return Ok(()); - } - - tracing::info!( - "Copying {} missing commits from {} to {}", - missing_commits.len(), - source_repo.display(), - target_repo.display() - ); - - // Fetch each missing commit from source to target - for commit in &missing_commits { - let output = Command::new("git") - .args([ - "fetch", - source_repo.to_str().ok_or("Invalid source path")?, - commit, - ]) - .current_dir(target_repo) - .output() - .map_err(|e| format!("Failed to execute git fetch: {}", e))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!( - "git fetch failed for commit {}: {}", - commit, stderr - )); - } - - tracing::debug!("Copied commit {} to {}", commit, target_repo.display()); - } - - Ok(()) -} - -/// Result of aligning a repository with authorized state -#[derive(Debug, Default)] -struct SyncAlignmentResult { - /// Number of refs created - refs_created: usize, - /// Number of refs updated - refs_updated: usize, - /// Number of refs deleted - refs_deleted: usize, - /// Whether HEAD was set - head_set: bool, -} - -/// Align a repository's refs with the authorized state. -/// -/// This function: -/// 1. Deletes refs that are in the repo but not in the state (for refs/heads/ and refs/tags/) -/// 2. Updates refs that exist in state if we have the commit -/// 3. Sets HEAD if the HEAD branch's commit is available -fn align_repository_with_state(repo_path: &Path, state: &RepositoryState) -> SyncAlignmentResult { - use crate::git; - - let mut result = SyncAlignmentResult::default(); - - // Check if repository exists - if !repo_path.exists() { - tracing::debug!( - "Repository not found at {}, cannot align with state", - repo_path.display() - ); - return result; - } - - // Get current refs from the repository - let current_refs = match git::list_refs(repo_path) { - Ok(refs) => refs, - Err(e) => { - tracing::warn!("Failed to list refs in {}: {}", repo_path.display(), e); - return result; - } - }; - - // Build expected refs from state - let mut expected_refs: std::collections::HashMap = - std::collections::HashMap::new(); - - for branch in &state.branches { - let ref_name = format!("refs/heads/{}", branch.name); - expected_refs.insert(ref_name, branch.commit.clone()); - } - - for tag in &state.tags { - let ref_name = format!("refs/tags/{}", tag.name); - expected_refs.insert(ref_name, tag.commit.clone()); - } - - // Delete refs that exist in repo but not in state (only for refs/heads/ and refs/tags/) - for (ref_name, _current_commit) in ¤t_refs { - if (ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/")) - && !expected_refs.contains_key(ref_name) - { - match git::delete_ref(repo_path, ref_name) { - Ok(()) => { - tracing::info!( - "Deleted {} from {} (not in state)", - ref_name, - repo_path.display() - ); - result.refs_deleted += 1; - } - Err(e) => { - tracing::warn!( - "Failed to delete {} from {}: {}", - ref_name, - repo_path.display(), - e - ); - } - } - } - } - - // Update refs that exist in state (if we have the commit) - for (ref_name, expected_commit) in &expected_refs { - // Skip symbolic refs - if expected_commit.starts_with("ref: ") { - continue; - } - - // Check if we have the commit - if !git::oid_exists(repo_path, expected_commit) { - tracing::debug!( - "Commit {} not available for {} in {}", - expected_commit, - ref_name, - repo_path.display() - ); - continue; - } - - // Check current value - let current_commit = current_refs - .iter() - .find(|(r, _)| r == ref_name) - .map(|(_, c)| c.as_str()); - - if current_commit == Some(expected_commit.as_str()) { - // Already correct - continue; - } - - // Update or create the ref - match git::update_ref(repo_path, ref_name, expected_commit) { - Ok(()) => { - if current_commit.is_some() { - tracing::info!( - "Updated {} to {} in {}", - ref_name, - expected_commit, - repo_path.display() - ); - result.refs_updated += 1; - } else { - tracing::info!( - "Created {} at {} in {}", - ref_name, - expected_commit, - repo_path.display() - ); - result.refs_created += 1; - } - } - Err(e) => { - tracing::warn!( - "Failed to update {} in {}: {}", - ref_name, - repo_path.display(), - e - ); - } - } - } - - // Set HEAD if specified in state - if let Some(head_ref) = &state.head { - if let Some(branch_name) = state.get_head_branch() { - if let Some(head_commit) = state.get_branch_commit(branch_name) { - match git::try_set_head_if_available(repo_path, head_ref, head_commit) { - Ok(true) => { - tracing::info!( - "Set HEAD to {} in {} (from purgatory sync)", - head_ref, - repo_path.display() - ); - result.head_set = true; - } - Ok(false) => { - tracing::debug!( - "HEAD commit {} not available yet in {}", - head_commit, - repo_path.display() - ); - } - Err(e) => { - tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e); - } - } - } - } - } - - result -} - /// Fetch missing OIDs from a remote git server. /// /// Uses `git fetch` to retrieve specific commits from the server. -- cgit v1.2.3