From c67ebe6f33bfa191f17eb0df24d3ee18092c74e1 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 7 Jan 2026 23:31:38 +0000 Subject: refactor: unify event processing logic Eliminates code duplication by extracting core event processing into reusable functions. All state and PR event processing now uses the same unified logic from src/git/process.rs. Changes: - Add src/git/process.rs with unified processing functions - process_state_with_git_data() for state events - process_pr_with_git_data() for PR events - Pure functions with comprehensive result types - Refactor policy handlers to use unified processing - src/nostr/policy/state.rs: Remove ~70 lines of duplicated logic - src/nostr/policy/pr_event.rs: Remove ~40 lines of duplicated logic - Refactor purgatory processing to use unified functions - src/git/sync.rs: Remove ~125 lines of duplicated logic - Make extract_owner_from_repo_path() public for reuse Benefits: - DRY: Single source of truth for event processing - Testable: Pure functions with clear contracts - Maintainable: Changes happen in one place - Consistent: All code paths use same logic All 217 unit tests + 40 integration tests pass (257/257). --- src/git/process.rs | 255 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 src/git/process.rs (limited to 'src/git/process.rs') diff --git a/src/git/process.rs b/src/git/process.rs new file mode 100644 index 0000000..d052c04 --- /dev/null +++ b/src/git/process.rs @@ -0,0 +1,255 @@ +//! Event Processing - Unified logic for processing state and PR events with git data +//! +//! This module provides the core processing logic used when events have git data available. +//! These functions are used in multiple scenarios: +//! - When events arrive with git data already available (policy handlers) +//! - When events are released from purgatory (purgatory sync) +//! - When git pushes trigger purgatory releases (receive-pack handler) + +use std::path::Path; +use nostr_sdk::Event; +use crate::git::authorization::{collect_authorized_maintainers, RepositoryData}; +use crate::git::sync::{align_repository_with_state, sync_pr_refs_to_tagged_owner_repos, copy_missing_oids_between_repos}; +use crate::git; +use crate::nostr::events::RepositoryState; + +/// Result of processing a state event with git data +#[derive(Debug, Default, Clone)] +pub struct ProcessStateResult { + /// Number of repositories synced (OIDs copied + refs aligned) + pub repos_synced: usize, + /// Number of refs created across all repos + pub refs_created: usize, + /// Number of refs updated across all repos + pub refs_updated: usize, + /// Number of refs deleted across all repos + pub refs_deleted: usize, + /// Errors encountered (non-fatal) + pub errors: Vec, +} + +/// Result of processing a PR event with git data +#[derive(Debug, Default, Clone)] +pub struct ProcessPrResult { + /// Number of repositories synced + pub repos_synced: usize, + /// Number of refs created across all repos + pub refs_created: usize, + /// Errors encountered (non-fatal) + pub errors: Vec, +} + +/// Process a single state event that has git data available. +/// +/// This is the core processing logic used when: +/// - A state event arrives with git data already available +/// - A state event is released from purgatory +/// +/// Does NOT save to database or notify subscribers - caller handles that. +/// +/// # Processing Steps +/// 1. Identify owner repos where state author is an authorized maintainer +/// 2. For each owner repo, check if this state is the latest authorized +/// 3. Copy missing OIDs from source repo to target repo +/// 4. Align refs (branches, tags, HEAD) with the state +/// +/// # Arguments +/// * `state` - The state event to process +/// * `source_repo_path` - Path to repo that has the git data +/// * `db_repo_data` - Repository data from database (announcements + states) +/// * `git_data_path` - Base path for git repositories +/// +/// # Returns +/// ProcessStateResult with statistics +pub fn process_state_with_git_data( + state: &RepositoryState, + source_repo_path: &Path, + db_repo_data: &RepositoryData, + git_data_path: &Path, +) -> ProcessStateResult { + let mut result = ProcessStateResult::default(); + + let state_author = state.event.pubkey.to_hex(); + + // Collect authorized maintainers per owner + let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); + + // Step 1: Identify owner repos that the state event author is maintainer for + let authorized_owners: Vec<&String> = by_owner + .iter() + .filter(|(_, maintainers)| maintainers.contains(&state_author)) + .map(|(owner, _)| owner) + .collect(); + + if authorized_owners.is_empty() { + tracing::debug!( + identifier = %state.identifier, + author = %state_author, + "State event author not authorized for any owner" + ); + return result; + } + + // Process each owner repo that authorizes this state event author + for owner in &authorized_owners { + let maintainers = by_owner.get(*owner).unwrap(); + + // Step 2: Check if this state event is the latest authorized for this owner + let is_latest = crate::git::sync::is_latest_authorized_state_public( + state, + maintainers, + &db_repo_data.states, + ); + + if !is_latest { + tracing::debug!( + identifier = %state.identifier, + owner = %owner, + "Skipping owner - newer authorized state exists" + ); + continue; + } + + // Find the announcement for this owner + let Some(announcement) = db_repo_data + .announcements + .iter() + .find(|a| a.event.pubkey.to_hex() == **owner) + else { + continue; + }; + + let target_repo_path = git_data_path.join(announcement.repo_path()); + + // Step 3: Check git repo exists for that owner + if !target_repo_path.exists() { + tracing::debug!( + identifier = %state.identifier, + owner = %owner, + repo_path = %target_repo_path.display(), + "Skipping owner - repository doesn't exist" + ); + continue; + } + + // Step 4: Copy all required OIDs to that repo (unless it's source_repo_path) + 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" + ); + result.errors.push(e); + continue; // Skip this owner repo + } + } + + // Step 5: Reset the git state in that repo to match the state event + let align_result = align_repository_with_state(&target_repo_path, state); + result.repos_synced += 1; + result.refs_created += align_result.refs_created; + result.refs_updated += align_result.refs_updated; + result.refs_deleted += align_result.refs_deleted; + + tracing::info!( + identifier = %state.identifier, + owner = %owner, + repo_path = %target_repo_path.display(), + refs_created = align_result.refs_created, + refs_updated = align_result.refs_updated, + refs_deleted = align_result.refs_deleted, + head_set = align_result.head_set, + "Aligned repository with state" + ); + } + + result +} + +/// Process a single PR event that has git data available. +/// +/// This is the core processing logic used when: +/// - A PR event arrives with git data already available +/// - A PR event is released from purgatory +/// +/// Does NOT save to database or notify subscribers - caller handles that. +/// +/// # Processing Steps +/// 1. Sync PR commit to owner repos using tagged maintainer logic +/// 2. Create refs/nostr/ ref in source repo (if missing) +/// 3. Create refs/nostr/ refs in all synced repos +/// +/// # Arguments +/// * `event` - The PR event to process +/// * `commit` - The commit hash from the PR event +/// * `source_repo_path` - Path to repo that has the commit +/// * `db_repo_data` - Repository data from database (announcements + states) +/// * `git_data_path` - Base path for git repositories +/// * `source_owner_pubkey` - Owner pubkey of source repo (to skip) +/// +/// # Returns +/// ProcessPrResult with statistics +pub fn process_pr_with_git_data( + event: &Event, + commit: &str, + source_repo_path: &Path, + db_repo_data: &RepositoryData, + git_data_path: &Path, + source_owner_pubkey: &str, +) -> ProcessPrResult { + let mut result = ProcessPrResult::default(); + + let event_id = event.id.to_hex(); + + // Sync PR ref to owner repos using tagged maintainer logic + let pr_refs = vec![(event_id.clone(), commit.to_string())]; + let pr_events = vec![event.clone()]; + + let sync_result = sync_pr_refs_to_tagged_owner_repos( + source_repo_path, + &pr_refs, + &pr_events, + db_repo_data, + git_data_path, + source_owner_pubkey, + ); + result.repos_synced += sync_result.repos_synced; + result.refs_created += sync_result.refs_created; + result.errors.extend( + sync_result + .errors + .into_iter() + .map(|(_, e)| e), + ); + + // Create the ref in the source repo if it doesn't exist + let ref_name = format!("refs/nostr/{}", event_id); + if git::get_ref_commit(source_repo_path, &ref_name).is_none() { + if let Err(e) = git::update_ref(source_repo_path, &ref_name, commit) { + tracing::warn!( + event_id = %event_id, + repo = %source_repo_path.display(), + error = %e, + "Failed to create PR ref in source repo" + ); + result.errors.push(e); + } else { + result.refs_created += 1; + tracing::info!( + event_id = %event_id, + commit = %commit, + repo = %source_repo_path.display(), + "Created PR ref in source repo" + ); + } + } + + result +} -- cgit v1.2.3