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/sync.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/sync.rs')
| -rw-r--r-- | src/git/sync.rs | 190 |
1 files changed, 43 insertions, 147 deletions
diff --git a/src/git/sync.rs b/src/git/sync.rs index 2f43e6e..5e2d3f2 100644 --- a/src/git/sync.rs +++ b/src/git/sync.rs | |||
| @@ -908,12 +908,9 @@ async fn process_purgatory_state_events( | |||
| 908 | } | 908 | } |
| 909 | }; | 909 | }; |
| 910 | 910 | ||
| 911 | // Collect authorized maintainers per owner (computed once) | ||
| 912 | let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); | ||
| 913 | |||
| 914 | // Process each state event in chronological order | 911 | // Process each state event in chronological order |
| 915 | for entry in &purgatory_states { | 912 | for entry in &purgatory_states { |
| 916 | // Step 0: Check if we have all the git data needed to apply this state event | 913 | // Check if we have all the git data needed to apply this state event |
| 917 | if !can_apply_state(&entry.event, source_repo_path) { | 914 | if !can_apply_state(&entry.event, source_repo_path) { |
| 918 | debug!( | 915 | debug!( |
| 919 | identifier = %identifier, | 916 | identifier = %identifier, |
| @@ -940,122 +937,19 @@ async fn process_purgatory_state_events( | |||
| 940 | } | 937 | } |
| 941 | }; | 938 | }; |
| 942 | 939 | ||
| 943 | let state_author = state.event.pubkey.to_hex(); | 940 | // Use unified processing function |
| 944 | 941 | let process_result = crate::git::process::process_state_with_git_data( | |
| 945 | // Step 1: Identify owner repos that the state event author is maintainer for | 942 | &state, |
| 946 | let authorized_owners: Vec<&String> = by_owner | 943 | source_repo_path, |
| 947 | .iter() | 944 | &db_repo_data, |
| 948 | .filter(|(_, maintainers)| maintainers.contains(&state_author)) | 945 | git_data_path, |
| 949 | .map(|(owner, _)| owner) | 946 | ); |
| 950 | .collect(); | ||
| 951 | |||
| 952 | if authorized_owners.is_empty() { | ||
| 953 | debug!( | ||
| 954 | identifier = %identifier, | ||
| 955 | event_id = %entry.event.id, | ||
| 956 | pubkey = %state_author, | ||
| 957 | "State event author not authorized for any owner - skipping" | ||
| 958 | ); | ||
| 959 | continue; | ||
| 960 | } | ||
| 961 | |||
| 962 | // Track if we applied to at least one owner repo | ||
| 963 | let mut applied_to_any = false; | ||
| 964 | |||
| 965 | // Process each owner repo that authorizes this state event author | ||
| 966 | for owner in &authorized_owners { | ||
| 967 | let maintainers = by_owner.get(*owner).unwrap(); | ||
| 968 | |||
| 969 | // Step 2: Check if this state event is the latest authorized for this owner | ||
| 970 | // Only consider database states, not other purgatory states | ||
| 971 | let is_latest = is_latest_authorized_state( | ||
| 972 | &state, | ||
| 973 | maintainers, | ||
| 974 | &db_repo_data.states, | ||
| 975 | ); | ||
| 976 | |||
| 977 | if !is_latest { | ||
| 978 | debug!( | ||
| 979 | identifier = %identifier, | ||
| 980 | event_id = %entry.event.id, | ||
| 981 | owner = %owner, | ||
| 982 | "Skipping owner - a newer authorized state exists" | ||
| 983 | ); | ||
| 984 | continue; | ||
| 985 | } | ||
| 986 | |||
| 987 | // Find the announcement for this owner | ||
| 988 | let announcement = db_repo_data | ||
| 989 | .announcements | ||
| 990 | .iter() | ||
| 991 | .find(|a| a.event.pubkey.to_hex() == **owner); | ||
| 992 | |||
| 993 | let Some(announcement) = announcement else { | ||
| 994 | continue; | ||
| 995 | }; | ||
| 996 | |||
| 997 | let target_repo_path = git_data_path.join(announcement.repo_path()); | ||
| 998 | |||
| 999 | // Step 3: Check git repo exists for that owner | ||
| 1000 | if !target_repo_path.exists() { | ||
| 1001 | debug!( | ||
| 1002 | identifier = %identifier, | ||
| 1003 | owner = %owner, | ||
| 1004 | repo_path = %target_repo_path.display(), | ||
| 1005 | "Skipping owner - repository doesn't exist" | ||
| 1006 | ); | ||
| 1007 | continue; | ||
| 1008 | } | ||
| 1009 | |||
| 1010 | // Step 4: Copy all required OIDs to that repo (unless it's source_repo_path) | ||
| 1011 | if target_repo_path != source_repo_path { | ||
| 1012 | if let Err(e) = | ||
| 1013 | copy_missing_oids_between_repos(source_repo_path, &target_repo_path, &state) | ||
| 1014 | { | ||
| 1015 | warn!( | ||
| 1016 | identifier = %identifier, | ||
| 1017 | source = %source_repo_path.display(), | ||
| 1018 | target = %target_repo_path.display(), | ||
| 1019 | error = %e, | ||
| 1020 | "Failed to copy OIDs between repos" | ||
| 1021 | ); | ||
| 1022 | result | ||
| 1023 | .errors | ||
| 1024 | .push((target_repo_path.display().to_string(), e).1); | ||
| 1025 | // Continue anyway - we'll try to align what we can | ||
| 1026 | } | ||
| 1027 | } | ||
| 1028 | |||
| 1029 | // Step 5: Reset the git state in that repo to match the state event | ||
| 1030 | // (excluding refs/nostr/*) | ||
| 1031 | let align_result = align_repository_with_state(&target_repo_path, &state); | ||
| 1032 | result.repos_synced += 1; | ||
| 1033 | result.refs_created += align_result.refs_created; | ||
| 1034 | result.refs_updated += align_result.refs_updated; | ||
| 1035 | result.refs_deleted += align_result.refs_deleted; | ||
| 1036 | |||
| 1037 | info!( | ||
| 1038 | identifier = %identifier, | ||
| 1039 | owner = %owner, | ||
| 1040 | event_id = %entry.event.id, | ||
| 1041 | repo_path = %target_repo_path.display(), | ||
| 1042 | refs_created = align_result.refs_created, | ||
| 1043 | refs_updated = align_result.refs_updated, | ||
| 1044 | refs_deleted = align_result.refs_deleted, | ||
| 1045 | head_set = align_result.head_set, | ||
| 1046 | "Aligned repository with state from purgatory" | ||
| 1047 | ); | ||
| 1048 | |||
| 1049 | applied_to_any = true; | ||
| 1050 | } | ||
| 1051 | 947 | ||
| 1052 | // We have the git data now, so we should release from purgatory regardless of | 948 | result.repos_synced += process_result.repos_synced; |
| 1053 | // whether we applied to any repo. The question is: should we save to DB or just remove? | 949 | result.refs_created += process_result.refs_created; |
| 1054 | // | 950 | result.refs_updated += process_result.refs_updated; |
| 1055 | // - If there's a newer state event from the same author already in the DB, just remove | 951 | result.refs_deleted += process_result.refs_deleted; |
| 1056 | // (no point saving an older event that will never be used) | 952 | result.errors.extend(process_result.errors); |
| 1057 | // - Otherwise, save it to the DB (even if we didn't apply to any repo, because in the | ||
| 1058 | // future the currently-authorized state event might be deleted and this one should apply) | ||
| 1059 | 953 | ||
| 1060 | // Check if there's a newer state from the same author in the database | 954 | // Check if there's a newer state from the same author in the database |
| 1061 | let has_newer_from_same_author = db_repo_data.states.iter().any(|s| { | 955 | let has_newer_from_same_author = db_repo_data.states.iter().any(|s| { |
| @@ -1073,17 +967,16 @@ async fn process_purgatory_state_events( | |||
| 1073 | debug!( | 967 | debug!( |
| 1074 | identifier = %identifier, | 968 | identifier = %identifier, |
| 1075 | event_id = %entry.event.id, | 969 | event_id = %entry.event.id, |
| 1076 | author = %state_author, | ||
| 1077 | "Removed older state event from purgatory - newer event from same author exists in DB" | 970 | "Removed older state event from purgatory - newer event from same author exists in DB" |
| 1078 | ); | 971 | ); |
| 1079 | } else { | 972 | } else { |
| 1080 | // Save to database (even if we didn't apply to any repo) | 973 | // Save to database |
| 1081 | match database.save_event(&entry.event).await { | 974 | match database.save_event(&entry.event).await { |
| 1082 | Ok(_) => { | 975 | Ok(_) => { |
| 1083 | info!( | 976 | info!( |
| 1084 | identifier = %identifier, | 977 | identifier = %identifier, |
| 1085 | event_id = %entry.event.id, | 978 | event_id = %entry.event.id, |
| 1086 | applied_to_repos = applied_to_any, | 979 | repos_synced = process_result.repos_synced, |
| 1087 | "Saved purgatory state event to database" | 980 | "Saved purgatory state event to database" |
| 1088 | ); | 981 | ); |
| 1089 | 982 | ||
| @@ -1169,6 +1062,25 @@ fn is_latest_authorized_state( | |||
| 1169 | } | 1062 | } |
| 1170 | } | 1063 | } |
| 1171 | 1064 | ||
| 1065 | /// Check if a state event is the latest authorized state for a given maintainer set. | ||
| 1066 | /// | ||
| 1067 | /// Only considers states already in the database, not other purgatory states. | ||
| 1068 | /// | ||
| 1069 | /// # Arguments | ||
| 1070 | /// * `state` - The state event to check | ||
| 1071 | /// * `maintainers` - The set of authorized maintainers for the owner | ||
| 1072 | /// * `db_states` - State events from the database | ||
| 1073 | /// | ||
| 1074 | /// # Returns | ||
| 1075 | /// true if this state is the latest (or equal latest) among all authorized states in the DB | ||
| 1076 | pub fn is_latest_authorized_state_public( | ||
| 1077 | state: &RepositoryState, | ||
| 1078 | maintainers: &[String], | ||
| 1079 | db_states: &[RepositoryState], | ||
| 1080 | ) -> bool { | ||
| 1081 | is_latest_authorized_state(state, maintainers, db_states) | ||
| 1082 | } | ||
| 1083 | |||
| 1172 | /// Process PR events from purgatory that can now be satisfied. | 1084 | /// Process PR events from purgatory that can now be satisfied. |
| 1173 | async fn process_purgatory_pr_events( | 1085 | async fn process_purgatory_pr_events( |
| 1174 | identifier: &str, | 1086 | identifier: &str, |
| @@ -1224,39 +1136,23 @@ async fn process_purgatory_pr_events( | |||
| 1224 | continue; | 1136 | continue; |
| 1225 | } | 1137 | } |
| 1226 | 1138 | ||
| 1227 | // Sync PR ref to owner repos | 1139 | // Extract owner pubkey |
| 1228 | let pr_refs = vec![(event.id.to_hex(), entry.commit.clone())]; | ||
| 1229 | let pr_events = vec![event.clone()]; | ||
| 1230 | |||
| 1231 | // Get owner pubkey from source repo path | ||
| 1232 | let owner_pubkey = extract_owner_from_repo_path(source_repo_path, git_data_path) | 1140 | let owner_pubkey = extract_owner_from_repo_path(source_repo_path, git_data_path) |
| 1233 | .unwrap_or_default(); | 1141 | .unwrap_or_default(); |
| 1234 | 1142 | ||
| 1235 | let sync_result = sync_pr_refs_to_tagged_owner_repos( | 1143 | // Use unified processing function |
| 1144 | let process_result = crate::git::process::process_pr_with_git_data( | ||
| 1145 | event, | ||
| 1146 | &entry.commit, | ||
| 1236 | source_repo_path, | 1147 | source_repo_path, |
| 1237 | &pr_refs, | ||
| 1238 | &pr_events, | ||
| 1239 | &db_repo_data, | 1148 | &db_repo_data, |
| 1240 | git_data_path, | 1149 | git_data_path, |
| 1241 | &owner_pubkey, | 1150 | &owner_pubkey, |
| 1242 | ); | 1151 | ); |
| 1243 | result.repos_synced += sync_result.repos_synced; | ||
| 1244 | result.refs_created += sync_result.refs_created; | ||
| 1245 | 1152 | ||
| 1246 | // Create the ref in the source repo if it doesn't exist | 1153 | result.repos_synced += process_result.repos_synced; |
| 1247 | let ref_name = format!("refs/nostr/{}", event.id.to_hex()); | 1154 | result.refs_created += process_result.refs_created; |
| 1248 | if git::get_ref_commit(source_repo_path, &ref_name).is_none() { | 1155 | result.errors.extend(process_result.errors); |
| 1249 | if let Err(e) = git::update_ref(source_repo_path, &ref_name, &entry.commit) { | ||
| 1250 | warn!( | ||
| 1251 | identifier = %identifier, | ||
| 1252 | event_id = %event.id, | ||
| 1253 | error = %e, | ||
| 1254 | "Failed to create PR ref in source repo" | ||
| 1255 | ); | ||
| 1256 | } else { | ||
| 1257 | result.refs_created += 1; | ||
| 1258 | } | ||
| 1259 | } | ||
| 1260 | 1156 | ||
| 1261 | // Save event to database | 1157 | // Save event to database |
| 1262 | match database.save_event(event).await { | 1158 | match database.save_event(event).await { |
| @@ -1307,7 +1203,7 @@ async fn process_purgatory_pr_events( | |||
| 1307 | /// Extract owner pubkey from a repository path. | 1203 | /// Extract owner pubkey from a repository path. |
| 1308 | /// | 1204 | /// |
| 1309 | /// Given a path like `{git_data_path}/{npub}/{identifier}.git`, extracts the npub. | 1205 | /// Given a path like `{git_data_path}/{npub}/{identifier}.git`, extracts the npub. |
| 1310 | fn extract_owner_from_repo_path(repo_path: &Path, git_data_path: &Path) -> Option<String> { | 1206 | pub fn extract_owner_from_repo_path(repo_path: &Path, git_data_path: &Path) -> Option<String> { |
| 1311 | let relative = repo_path.strip_prefix(git_data_path).ok()?; | 1207 | let relative = repo_path.strip_prefix(git_data_path).ok()?; |
| 1312 | let components: Vec<_> = relative.components().collect(); | 1208 | let components: Vec<_> = relative.components().collect(); |
| 1313 | if !components.is_empty() { | 1209 | if !components.is_empty() { |