diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 23:31:38 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 23:31:38 +0000 |
| commit | c67ebe6f33bfa191f17eb0df24d3ee18092c74e1 (patch) | |
| tree | b86911bbb406f4aa0253b1cf1e0a82aed16c972b /src/git/process.rs | |
| parent | 4dc0ed66a0bd3b4b00804bb13adf93b207bb5fc4 (diff) | |
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).
Diffstat (limited to 'src/git/process.rs')
| -rw-r--r-- | src/git/process.rs | 255 |
1 files changed, 255 insertions, 0 deletions
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 @@ | |||
| 1 | //! Event Processing - Unified logic for processing state and PR events with git data | ||
| 2 | //! | ||
| 3 | //! This module provides the core processing logic used when events have git data available. | ||
| 4 | //! These functions are used in multiple scenarios: | ||
| 5 | //! - When events arrive with git data already available (policy handlers) | ||
| 6 | //! - When events are released from purgatory (purgatory sync) | ||
| 7 | //! - When git pushes trigger purgatory releases (receive-pack handler) | ||
| 8 | |||
| 9 | use std::path::Path; | ||
| 10 | use nostr_sdk::Event; | ||
| 11 | use crate::git::authorization::{collect_authorized_maintainers, RepositoryData}; | ||
| 12 | use crate::git::sync::{align_repository_with_state, sync_pr_refs_to_tagged_owner_repos, copy_missing_oids_between_repos}; | ||
| 13 | use crate::git; | ||
| 14 | use crate::nostr::events::RepositoryState; | ||
| 15 | |||
| 16 | /// Result of processing a state event with git data | ||
| 17 | #[derive(Debug, Default, Clone)] | ||
| 18 | pub struct ProcessStateResult { | ||
| 19 | /// Number of repositories synced (OIDs copied + refs aligned) | ||
| 20 | pub repos_synced: usize, | ||
| 21 | /// Number of refs created across all repos | ||
| 22 | pub refs_created: usize, | ||
| 23 | /// Number of refs updated across all repos | ||
| 24 | pub refs_updated: usize, | ||
| 25 | /// Number of refs deleted across all repos | ||
| 26 | pub refs_deleted: usize, | ||
| 27 | /// Errors encountered (non-fatal) | ||
| 28 | pub errors: Vec<String>, | ||
| 29 | } | ||
| 30 | |||
| 31 | /// Result of processing a PR event with git data | ||
| 32 | #[derive(Debug, Default, Clone)] | ||
| 33 | pub struct ProcessPrResult { | ||
| 34 | /// Number of repositories synced | ||
| 35 | pub repos_synced: usize, | ||
| 36 | /// Number of refs created across all repos | ||
| 37 | pub refs_created: usize, | ||
| 38 | /// Errors encountered (non-fatal) | ||
| 39 | pub errors: Vec<String>, | ||
| 40 | } | ||
| 41 | |||
| 42 | /// Process a single state event that has git data available. | ||
| 43 | /// | ||
| 44 | /// This is the core processing logic used when: | ||
| 45 | /// - A state event arrives with git data already available | ||
| 46 | /// - A state event is released from purgatory | ||
| 47 | /// | ||
| 48 | /// Does NOT save to database or notify subscribers - caller handles that. | ||
| 49 | /// | ||
| 50 | /// # Processing Steps | ||
| 51 | /// 1. Identify owner repos where state author is an authorized maintainer | ||
| 52 | /// 2. For each owner repo, check if this state is the latest authorized | ||
| 53 | /// 3. Copy missing OIDs from source repo to target repo | ||
| 54 | /// 4. Align refs (branches, tags, HEAD) with the state | ||
| 55 | /// | ||
| 56 | /// # Arguments | ||
| 57 | /// * `state` - The state event to process | ||
| 58 | /// * `source_repo_path` - Path to repo that has the git data | ||
| 59 | /// * `db_repo_data` - Repository data from database (announcements + states) | ||
| 60 | /// * `git_data_path` - Base path for git repositories | ||
| 61 | /// | ||
| 62 | /// # Returns | ||
| 63 | /// ProcessStateResult with statistics | ||
| 64 | pub fn process_state_with_git_data( | ||
| 65 | state: &RepositoryState, | ||
| 66 | source_repo_path: &Path, | ||
| 67 | db_repo_data: &RepositoryData, | ||
| 68 | git_data_path: &Path, | ||
| 69 | ) -> ProcessStateResult { | ||
| 70 | let mut result = ProcessStateResult::default(); | ||
| 71 | |||
| 72 | let state_author = state.event.pubkey.to_hex(); | ||
| 73 | |||
| 74 | // Collect authorized maintainers per owner | ||
| 75 | let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); | ||
| 76 | |||
| 77 | // Step 1: Identify owner repos that the state event author is maintainer for | ||
| 78 | let authorized_owners: Vec<&String> = by_owner | ||
| 79 | .iter() | ||
| 80 | .filter(|(_, maintainers)| maintainers.contains(&state_author)) | ||
| 81 | .map(|(owner, _)| owner) | ||
| 82 | .collect(); | ||
| 83 | |||
| 84 | if authorized_owners.is_empty() { | ||
| 85 | tracing::debug!( | ||
| 86 | identifier = %state.identifier, | ||
| 87 | author = %state_author, | ||
| 88 | "State event author not authorized for any owner" | ||
| 89 | ); | ||
| 90 | return result; | ||
| 91 | } | ||
| 92 | |||
| 93 | // Process each owner repo that authorizes this state event author | ||
| 94 | for owner in &authorized_owners { | ||
| 95 | let maintainers = by_owner.get(*owner).unwrap(); | ||
| 96 | |||
| 97 | // Step 2: Check if this state event is the latest authorized for this owner | ||
| 98 | let is_latest = crate::git::sync::is_latest_authorized_state_public( | ||
| 99 | state, | ||
| 100 | maintainers, | ||
| 101 | &db_repo_data.states, | ||
| 102 | ); | ||
| 103 | |||
| 104 | if !is_latest { | ||
| 105 | tracing::debug!( | ||
| 106 | identifier = %state.identifier, | ||
| 107 | owner = %owner, | ||
| 108 | "Skipping owner - newer authorized state exists" | ||
| 109 | ); | ||
| 110 | continue; | ||
| 111 | } | ||
| 112 | |||
| 113 | // Find the announcement for this owner | ||
| 114 | let Some(announcement) = db_repo_data | ||
| 115 | .announcements | ||
| 116 | .iter() | ||
| 117 | .find(|a| a.event.pubkey.to_hex() == **owner) | ||
| 118 | else { | ||
| 119 | continue; | ||
| 120 | }; | ||
| 121 | |||
| 122 | let target_repo_path = git_data_path.join(announcement.repo_path()); | ||
| 123 | |||
| 124 | // Step 3: Check git repo exists for that owner | ||
| 125 | if !target_repo_path.exists() { | ||
| 126 | tracing::debug!( | ||
| 127 | identifier = %state.identifier, | ||
| 128 | owner = %owner, | ||
| 129 | repo_path = %target_repo_path.display(), | ||
| 130 | "Skipping owner - repository doesn't exist" | ||
| 131 | ); | ||
| 132 | continue; | ||
| 133 | } | ||
| 134 | |||
| 135 | // Step 4: Copy all required OIDs to that repo (unless it's source_repo_path) | ||
| 136 | if target_repo_path != source_repo_path { | ||
| 137 | if let Err(e) = copy_missing_oids_between_repos( | ||
| 138 | source_repo_path, | ||
| 139 | &target_repo_path, | ||
| 140 | state, | ||
| 141 | ) { | ||
| 142 | tracing::warn!( | ||
| 143 | identifier = %state.identifier, | ||
| 144 | source = %source_repo_path.display(), | ||
| 145 | target = %target_repo_path.display(), | ||
| 146 | error = %e, | ||
| 147 | "Failed to copy OIDs between repos" | ||
| 148 | ); | ||
| 149 | result.errors.push(e); | ||
| 150 | continue; // Skip this owner repo | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | // Step 5: Reset the git state in that repo to match the state event | ||
| 155 | let align_result = align_repository_with_state(&target_repo_path, state); | ||
| 156 | result.repos_synced += 1; | ||
| 157 | result.refs_created += align_result.refs_created; | ||
| 158 | result.refs_updated += align_result.refs_updated; | ||
| 159 | result.refs_deleted += align_result.refs_deleted; | ||
| 160 | |||
| 161 | tracing::info!( | ||
| 162 | identifier = %state.identifier, | ||
| 163 | owner = %owner, | ||
| 164 | repo_path = %target_repo_path.display(), | ||
| 165 | refs_created = align_result.refs_created, | ||
| 166 | refs_updated = align_result.refs_updated, | ||
| 167 | refs_deleted = align_result.refs_deleted, | ||
| 168 | head_set = align_result.head_set, | ||
| 169 | "Aligned repository with state" | ||
| 170 | ); | ||
| 171 | } | ||
| 172 | |||
| 173 | result | ||
| 174 | } | ||
| 175 | |||
| 176 | /// Process a single PR event that has git data available. | ||
| 177 | /// | ||
| 178 | /// This is the core processing logic used when: | ||
| 179 | /// - A PR event arrives with git data already available | ||
| 180 | /// - A PR event is released from purgatory | ||
| 181 | /// | ||
| 182 | /// Does NOT save to database or notify subscribers - caller handles that. | ||
| 183 | /// | ||
| 184 | /// # Processing Steps | ||
| 185 | /// 1. Sync PR commit to owner repos using tagged maintainer logic | ||
| 186 | /// 2. Create refs/nostr/<event-id> ref in source repo (if missing) | ||
| 187 | /// 3. Create refs/nostr/<event-id> refs in all synced repos | ||
| 188 | /// | ||
| 189 | /// # Arguments | ||
| 190 | /// * `event` - The PR event to process | ||
| 191 | /// * `commit` - The commit hash from the PR event | ||
| 192 | /// * `source_repo_path` - Path to repo that has the commit | ||
| 193 | /// * `db_repo_data` - Repository data from database (announcements + states) | ||
| 194 | /// * `git_data_path` - Base path for git repositories | ||
| 195 | /// * `source_owner_pubkey` - Owner pubkey of source repo (to skip) | ||
| 196 | /// | ||
| 197 | /// # Returns | ||
| 198 | /// ProcessPrResult with statistics | ||
| 199 | pub fn process_pr_with_git_data( | ||
| 200 | event: &Event, | ||
| 201 | commit: &str, | ||
| 202 | source_repo_path: &Path, | ||
| 203 | db_repo_data: &RepositoryData, | ||
| 204 | git_data_path: &Path, | ||
| 205 | source_owner_pubkey: &str, | ||
| 206 | ) -> ProcessPrResult { | ||
| 207 | let mut result = ProcessPrResult::default(); | ||
| 208 | |||
| 209 | let event_id = event.id.to_hex(); | ||
| 210 | |||
| 211 | // Sync PR ref to owner repos using tagged maintainer logic | ||
| 212 | let pr_refs = vec![(event_id.clone(), commit.to_string())]; | ||
| 213 | let pr_events = vec![event.clone()]; | ||
| 214 | |||
| 215 | let sync_result = sync_pr_refs_to_tagged_owner_repos( | ||
| 216 | source_repo_path, | ||
| 217 | &pr_refs, | ||
| 218 | &pr_events, | ||
| 219 | db_repo_data, | ||
| 220 | git_data_path, | ||
| 221 | source_owner_pubkey, | ||
| 222 | ); | ||
| 223 | result.repos_synced += sync_result.repos_synced; | ||
| 224 | result.refs_created += sync_result.refs_created; | ||
| 225 | result.errors.extend( | ||
| 226 | sync_result | ||
| 227 | .errors | ||
| 228 | .into_iter() | ||
| 229 | .map(|(_, e)| e), | ||
| 230 | ); | ||
| 231 | |||
| 232 | // Create the ref in the source repo if it doesn't exist | ||
| 233 | let ref_name = format!("refs/nostr/{}", event_id); | ||
| 234 | if git::get_ref_commit(source_repo_path, &ref_name).is_none() { | ||
| 235 | if let Err(e) = git::update_ref(source_repo_path, &ref_name, commit) { | ||
| 236 | tracing::warn!( | ||
| 237 | event_id = %event_id, | ||
| 238 | repo = %source_repo_path.display(), | ||
| 239 | error = %e, | ||
| 240 | "Failed to create PR ref in source repo" | ||
| 241 | ); | ||
| 242 | result.errors.push(e); | ||
| 243 | } else { | ||
| 244 | result.refs_created += 1; | ||
| 245 | tracing::info!( | ||
| 246 | event_id = %event_id, | ||
| 247 | commit = %commit, | ||
| 248 | repo = %source_repo_path.display(), | ||
| 249 | "Created PR ref in source repo" | ||
| 250 | ); | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | result | ||
| 255 | } | ||