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 | |
| 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).
| -rw-r--r-- | src/git/mod.rs | 1 | ||||
| -rw-r--r-- | src/git/process.rs | 255 | ||||
| -rw-r--r-- | src/git/sync.rs | 190 | ||||
| -rw-r--r-- | src/nostr/policy/pr_event.rs | 132 | ||||
| -rw-r--r-- | src/nostr/policy/state.rs | 194 |
5 files changed, 381 insertions, 391 deletions
diff --git a/src/git/mod.rs b/src/git/mod.rs index fb17c53..205e3bc 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs | |||
| @@ -19,6 +19,7 @@ | |||
| 19 | 19 | ||
| 20 | pub mod authorization; | 20 | pub mod authorization; |
| 21 | pub mod handlers; | 21 | pub mod handlers; |
| 22 | pub mod process; | ||
| 22 | pub mod protocol; | 23 | pub mod protocol; |
| 23 | pub mod subprocess; | 24 | pub mod subprocess; |
| 24 | pub mod sync; | 25 | pub mod sync; |
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 | } | ||
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() { |
diff --git a/src/nostr/policy/pr_event.rs b/src/nostr/policy/pr_event.rs index ff3bade..9942a6a 100644 --- a/src/nostr/policy/pr_event.rs +++ b/src/nostr/policy/pr_event.rs | |||
| @@ -27,7 +27,7 @@ impl PrEventPolicy { | |||
| 27 | /// 2. Commit existence in referenced repositories | 27 | /// 2. Commit existence in referenced repositories |
| 28 | /// 3. Deletion of incorrect refs/nostr/<event-id> refs | 28 | /// 3. Deletion of incorrect refs/nostr/<event-id> refs |
| 29 | /// 4. Deletion of incorrect placeholders | 29 | /// 4. Deletion of incorrect placeholders |
| 30 | /// 5. Copying git data to all referenced repositories when found | 30 | /// 5. Processing PR event with unified function |
| 31 | /// | 31 | /// |
| 32 | /// # Returns | 32 | /// # Returns |
| 33 | /// - `Ok(true)` if git data ready (commit exists and is synced to all repos) | 33 | /// - `Ok(true)` if git data ready (commit exists and is synced to all repos) |
| @@ -64,7 +64,6 @@ impl PrEventPolicy { | |||
| 64 | ); | 64 | ); |
| 65 | // Remove placeholder - event processing will continue normally | 65 | // Remove placeholder - event processing will continue normally |
| 66 | self.ctx.purgatory.remove_pr(&event_id); | 66 | self.ctx.purgatory.remove_pr(&event_id); |
| 67 | // Continue to validate and sync refs across all repos | ||
| 68 | } else { | 67 | } else { |
| 69 | // Placeholder has different commit - incoming event supersedes | 68 | // Placeholder has different commit - incoming event supersedes |
| 70 | tracing::info!( | 69 | tracing::info!( |
| @@ -75,8 +74,7 @@ impl PrEventPolicy { | |||
| 75 | ); | 74 | ); |
| 76 | // Remove incorrect placeholder | 75 | // Remove incorrect placeholder |
| 77 | self.ctx.purgatory.remove_pr(&event_id); | 76 | self.ctx.purgatory.remove_pr(&event_id); |
| 78 | // Delete incorrect git data (refs/nostr/<event-id>) from all repos | 77 | // Delete incorrect git data (refs/nostr/<event-id>) will be handled below |
| 79 | // This will be handled below when we validate refs | ||
| 80 | } | 78 | } |
| 81 | } | 79 | } |
| 82 | 80 | ||
| @@ -87,9 +85,8 @@ impl PrEventPolicy { | |||
| 87 | return Ok(false); | 85 | return Ok(false); |
| 88 | } | 86 | } |
| 89 | 87 | ||
| 90 | // delete incorrect refs/nostr/<event-id> | 88 | // Delete incorrect refs/nostr/<event-id> |
| 91 | for repo_path in &repo_paths { | 89 | for repo_path in &repo_paths { |
| 92 | // First, validate/delete any incorrect refs/nostr/<event-id> | ||
| 93 | match git::validate_nostr_ref(repo_path, &event_id, &commit) { | 90 | match git::validate_nostr_ref(repo_path, &event_id, &commit) { |
| 94 | Ok(true) => { | 91 | Ok(true) => { |
| 95 | tracing::info!( | 92 | tracing::info!( |
| @@ -110,10 +107,9 @@ impl PrEventPolicy { | |||
| 110 | } | 107 | } |
| 111 | } | 108 | } |
| 112 | 109 | ||
| 113 | // find location of correct git data (if exists) | 110 | // Find location of correct git data (if exists) |
| 114 | let mut source_repo: Option<std::path::PathBuf> = None; | 111 | let mut source_repo: Option<std::path::PathBuf> = None; |
| 115 | for repo_path in &repo_paths { | 112 | for repo_path in &repo_paths { |
| 116 | // Check if commit exists in this repository | ||
| 117 | if git::commit_exists(repo_path, &commit) { | 113 | if git::commit_exists(repo_path, &commit) { |
| 118 | source_repo = Some(repo_path.clone()); | 114 | source_repo = Some(repo_path.clone()); |
| 119 | tracing::debug!( | 115 | tracing::debug!( |
| @@ -125,59 +121,50 @@ impl PrEventPolicy { | |||
| 125 | } | 121 | } |
| 126 | } | 122 | } |
| 127 | 123 | ||
| 128 | // Copy commit to all other referenced repositories | ||
| 129 | if let Some(source_repo) = source_repo { | 124 | if let Some(source_repo) = source_repo { |
| 130 | for repo_path in &repo_paths { | 125 | // Extract identifier |
| 131 | if repo_path == &source_repo { | 126 | let identifier = crate::git::sync::extract_identifier_from_pr_event(event) |
| 132 | // Skip source repo | 127 | .ok_or_else(|| anyhow::anyhow!("No identifier in PR event"))?; |
| 133 | continue; | 128 | |
| 134 | } | 129 | // Fetch repository data |
| 130 | let db_repo_data = fetch_repository_data(&self.ctx.database, &identifier).await?; | ||
| 131 | |||
| 132 | // Extract owner pubkey from source repo path | ||
| 133 | let owner_pubkey = crate::git::sync::extract_owner_from_repo_path( | ||
| 134 | &source_repo, | ||
| 135 | &self.ctx.git_data_path, | ||
| 136 | ) | ||
| 137 | .unwrap_or_default(); | ||
| 138 | |||
| 139 | // Use unified processing function | ||
| 140 | let result = crate::git::process::process_pr_with_git_data( | ||
| 141 | event, | ||
| 142 | &commit, | ||
| 143 | &source_repo, | ||
| 144 | &db_repo_data, | ||
| 145 | &self.ctx.git_data_path, | ||
| 146 | &owner_pubkey, | ||
| 147 | ); | ||
| 135 | 148 | ||
| 136 | // Check if repository exists | 149 | tracing::info!( |
| 137 | if !repo_path.exists() { | 150 | identifier = %identifier, |
| 138 | tracing::debug!( | 151 | event_id = %event_id, |
| 139 | "Repository {} does not exist, skipping sync", | 152 | repos_synced = result.repos_synced, |
| 140 | repo_path.display() | 153 | refs_created = result.refs_created, |
| 141 | ); | 154 | "Processed PR event with git data already available" |
| 142 | continue; | 155 | ); |
| 143 | } | ||
| 144 | 156 | ||
| 145 | // Check if commit already exists | 157 | if !result.errors.is_empty() { |
| 146 | if git::commit_exists(repo_path, &commit) { | 158 | for error in &result.errors { |
| 147 | tracing::debug!( | 159 | tracing::warn!( |
| 148 | "Commit {} already exists in {}, skipping sync", | 160 | identifier = %identifier, |
| 149 | commit, | 161 | event_id = %event_id, |
| 150 | repo_path.display() | 162 | error = %error, |
| 163 | "Error processing PR event" | ||
| 151 | ); | 164 | ); |
| 152 | continue; | ||
| 153 | } | ||
| 154 | |||
| 155 | // Fetch commit from source repo to target repo | ||
| 156 | tracing::info!( | ||
| 157 | "Syncing commit {} from {} to {}", | ||
| 158 | commit, | ||
| 159 | source_repo.display(), | ||
| 160 | repo_path.display() | ||
| 161 | ); | ||
| 162 | |||
| 163 | match self.copy_commit(&source_repo, repo_path, &commit).await { | ||
| 164 | Ok(()) => { | ||
| 165 | tracing::info!( | ||
| 166 | "Successfully synced commit {} to {}", | ||
| 167 | commit, | ||
| 168 | repo_path.display() | ||
| 169 | ); | ||
| 170 | } | ||
| 171 | Err(e) => { | ||
| 172 | tracing::warn!( | ||
| 173 | "Failed to sync commit {} to {}: {}", | ||
| 174 | commit, | ||
| 175 | repo_path.display(), | ||
| 176 | e | ||
| 177 | ); | ||
| 178 | } | ||
| 179 | } | 165 | } |
| 180 | } | 166 | } |
| 167 | |||
| 181 | Ok(true) | 168 | Ok(true) |
| 182 | } else { | 169 | } else { |
| 183 | tracing::debug!( | 170 | tracing::debug!( |
| @@ -250,40 +237,5 @@ impl PrEventPolicy { | |||
| 250 | 237 | ||
| 251 | Ok(repo_paths) | 238 | Ok(repo_paths) |
| 252 | } | 239 | } |
| 253 | /// Copy a commit from source repository to target repository | ||
| 254 | /// | ||
| 255 | /// Uses `git fetch` to copy a specific commit between local repositories. | ||
| 256 | /// | ||
| 257 | /// # Arguments | ||
| 258 | /// * `source_repo` - Path to repository containing the commit | ||
| 259 | /// * `target_repo` - Path to repository to receive the commit | ||
| 260 | /// * `commit` - Commit hash to copy | ||
| 261 | /// | ||
| 262 | /// # Returns | ||
| 263 | /// Ok(()) on success, Err with error message on failure | ||
| 264 | async fn copy_commit( | ||
| 265 | &self, | ||
| 266 | source_repo: &std::path::Path, | ||
| 267 | target_repo: &std::path::Path, | ||
| 268 | commit: &str, | ||
| 269 | ) -> Result<(), String> { | ||
| 270 | use std::process::Command; | ||
| 271 | |||
| 272 | let output = Command::new("git") | ||
| 273 | .args([ | ||
| 274 | "fetch", | ||
| 275 | source_repo.to_str().ok_or("Invalid source path")?, | ||
| 276 | commit, | ||
| 277 | ]) | ||
| 278 | .current_dir(target_repo) | ||
| 279 | .output() | ||
| 280 | .map_err(|e| format!("Failed to execute git fetch: {}", e))?; | ||
| 281 | 240 | ||
| 282 | if !output.status.success() { | ||
| 283 | let stderr = String::from_utf8_lossy(&output.stderr); | ||
| 284 | return Err(format!("git fetch failed: {}", stderr)); | ||
| 285 | } | ||
| 286 | |||
| 287 | Ok(()) | ||
| 288 | } | ||
| 289 | } | 241 | } |
diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index 7d69d7d..68b1e97 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs | |||
| @@ -9,9 +9,8 @@ use nostr_relay_builder::builder::WritePolicyResult; | |||
| 9 | use nostr_relay_builder::prelude::Event; | 9 | use nostr_relay_builder::prelude::Event; |
| 10 | 10 | ||
| 11 | use super::PolicyContext; | 11 | use super::PolicyContext; |
| 12 | use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data}; | 12 | use crate::git::authorization::fetch_repository_data; |
| 13 | use crate::git::sync::align_repository_with_state; | 13 | use crate::git; |
| 14 | use crate::git::{self}; | ||
| 15 | use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; | 14 | use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; |
| 16 | 15 | ||
| 17 | /// Result of state policy evaluation | 16 | /// Result of state policy evaluation |
| @@ -50,7 +49,7 @@ impl StatePolicy { | |||
| 50 | let state = | 49 | let state = |
| 51 | RepositoryState::from_event(event.clone()).context("Failed to parse state event")?; | 50 | RepositoryState::from_event(event.clone()).context("Failed to parse state event")?; |
| 52 | 51 | ||
| 53 | // duplicate check in purgatory | 52 | // Duplicate check in purgatory |
| 54 | if self | 53 | if self |
| 55 | .ctx | 54 | .ctx |
| 56 | .purgatory | 55 | .purgatory |
| @@ -63,95 +62,65 @@ impl StatePolicy { | |||
| 63 | event.id, | 62 | event.id, |
| 64 | ); | 63 | ); |
| 65 | return Ok(WritePolicyResult::Reject { | 64 | return Ok(WritePolicyResult::Reject { |
| 66 | status: true, // Client sees OK | 65 | status: true, |
| 67 | message: "duplicate: in purgatory".into(), | 66 | message: "duplicate: in purgatory".into(), |
| 68 | }); | 67 | }); |
| 69 | } | 68 | } |
| 70 | // get all repositories and state events from db with identifier | 69 | |
| 70 | // Get all repositories and state events from db with identifier | ||
| 71 | let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?; | 71 | let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?; |
| 72 | 72 | ||
| 73 | // duplicate check in db | 73 | // Duplicate check in db |
| 74 | if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) { | 74 | if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) { |
| 75 | tracing::debug!("processed state event duplicate (in db): {}", event.id,); | 75 | tracing::debug!("processed state event duplicate (in db): {}", event.id); |
| 76 | return Ok(WritePolicyResult::Reject { | 76 | return Ok(WritePolicyResult::Reject { |
| 77 | status: true, // Client sees OK | 77 | status: true, |
| 78 | message: "duplicate".into(), | 78 | message: "duplicate".into(), |
| 79 | }); | 79 | }); |
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | // check if git data is avialable | 82 | // Check if git data is available |
| 83 | if let Some(repo_with_git_data) = | 83 | if let Some(repo_with_git_data) = |
| 84 | find_repo_with_git_data(&db_repo_data.announcements, &state, &self.ctx.git_data_path) | 84 | find_repo_with_git_data(&db_repo_data.announcements, &state, &self.ctx.git_data_path) |
| 85 | { | 85 | { |
| 86 | tracing::debug!( | 86 | tracing::debug!( |
| 87 | "processing state event git as data already available: {}", | 87 | "processing state event as git data already available: {}", |
| 88 | event.id, | 88 | event.id, |
| 89 | ); | 89 | ); |
| 90 | // find repos for which this state is authorised and align the git refs to this state | ||
| 91 | let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); | ||
| 92 | let mut repo_count = 0; | ||
| 93 | for (owner, maintainers) in by_owner { | ||
| 94 | if maintainers.contains(&event.pubkey.to_string()) { | ||
| 95 | if let Some(previous_state) = db_repo_data | ||
| 96 | .states | ||
| 97 | .iter() | ||
| 98 | .filter(|e| maintainers.contains(&e.event.pubkey.to_string())) | ||
| 99 | .max_by_key(|e| e.event.created_at) | ||
| 100 | { | ||
| 101 | // TODO in event of a tie the event with the biggest event id wins | ||
| 102 | if state.event.created_at > previous_state.event.created_at { | ||
| 103 | if let Some(annoucement) = db_repo_data | ||
| 104 | .announcements | ||
| 105 | .iter() | ||
| 106 | .find(|a| a.event.pubkey.to_string().eq(&owner)) | ||
| 107 | { | ||
| 108 | let repo_path = | ||
| 109 | self.ctx.git_data_path.join(annoucement.repo_path().clone()); | ||
| 110 | 90 | ||
| 111 | if !repo_path.exists() { | 91 | // Use unified processing function |
| 112 | // eg if annoucement doesnt list repo (but stored as its in maintainer set) | 92 | let result = crate::git::process::process_state_with_git_data( |
| 113 | continue; | 93 | &state, |
| 114 | } | 94 | &repo_with_git_data, |
| 115 | // If repo_path != repo_with_git_data, copy missing oids first | 95 | &db_repo_data, |
| 116 | if repo_path != repo_with_git_data { | 96 | &self.ctx.git_data_path, |
| 117 | if let Err(e) = self.copy_missing_oids( | 97 | ); |
| 118 | &repo_with_git_data, | 98 | |
| 119 | &repo_path, | 99 | tracing::info!( |
| 120 | &state, | 100 | identifier = %state.identifier, |
| 121 | ) { | 101 | event_id = %event.id, |
| 122 | tracing::warn!( | 102 | repos_synced = result.repos_synced, |
| 123 | "Failed to copy oids from {} to {}: {}", | 103 | refs_created = result.refs_created, |
| 124 | repo_with_git_data.display(), | 104 | refs_updated = result.refs_updated, |
| 125 | repo_path.display(), | 105 | refs_deleted = result.refs_deleted, |
| 126 | e | 106 | "Processed state event with git data already available" |
| 127 | ); | 107 | ); |
| 128 | } | ||
| 129 | } | ||
| 130 | 108 | ||
| 131 | let result = align_repository_with_state(&repo_path, &state); | 109 | if !result.errors.is_empty() { |
| 132 | repo_count += 1; | 110 | for error in &result.errors { |
| 133 | tracing::info!( | 111 | tracing::warn!( |
| 134 | "Aligned {} with state: created={}, updated={}, deleted={}, head_set={}", | 112 | identifier = %state.identifier, |
| 135 | repo_path.display(), | 113 | event_id = %event.id, |
| 136 | result.refs_created, | 114 | error = %error, |
| 137 | result.refs_updated, | 115 | "Error processing state event" |
| 138 | result.refs_deleted, | 116 | ); |
| 139 | result.head_set | ||
| 140 | ); | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | 117 | } |
| 145 | } | 118 | } |
| 146 | 119 | ||
| 147 | tracing::info!( | 120 | // Event will be saved and broadcast by relay builder |
| 148 | "immediately accepting state event. Was latest authorised state and git data updated for {repo_count} repositories: eventid: {}", | 121 | Ok(WritePolicyResult::Accept) |
| 149 | state.event.id, | ||
| 150 | ); | ||
| 151 | // immediately accept the event, bypassing purgatory | ||
| 152 | Ok(WritePolicyResult::Accept) // event should be saved and broadcast | ||
| 153 | } else { | 122 | } else { |
| 154 | // if no git data - add to purgatory | 123 | // If no git data - add to purgatory |
| 155 | // (add_state automatically enqueues for background sync) | 124 | // (add_state automatically enqueues for background sync) |
| 156 | self.ctx | 125 | self.ctx |
| 157 | .purgatory | 126 | .purgatory |
| @@ -163,96 +132,13 @@ impl StatePolicy { | |||
| 163 | state.identifier, | 132 | state.identifier, |
| 164 | ); | 133 | ); |
| 165 | Ok(WritePolicyResult::Reject { | 134 | Ok(WritePolicyResult::Reject { |
| 166 | status: true, // Client sees OK | 135 | status: true, |
| 167 | message: "purgatory: won't be served until git data arrives".into(), | 136 | message: "purgatory: won't be served until git data arrives".into(), |
| 168 | }) | 137 | }) |
| 169 | } | 138 | } |
| 170 | } | 139 | } |
| 171 | 140 | ||
| 172 | /// Copy missing OIDs from a source repository to a target repository | ||
| 173 | /// | ||
| 174 | /// Identifies commits referenced in the state that are missing from the target | ||
| 175 | /// repository and copies them from the source repository using git fetch. | ||
| 176 | /// | ||
| 177 | /// # Arguments | ||
| 178 | /// * `source_repo` - Path to repository containing the commits | ||
| 179 | /// * `target_repo` - Path to repository to receive the commits | ||
| 180 | /// * `state` - Repository state containing commit references | ||
| 181 | /// | ||
| 182 | /// # Returns | ||
| 183 | /// Ok(()) on success, Err with error message on failure | ||
| 184 | fn copy_missing_oids( | ||
| 185 | &self, | ||
| 186 | source_repo: &Path, | ||
| 187 | target_repo: &Path, | ||
| 188 | state: &RepositoryState, | ||
| 189 | ) -> Result<(), String> { | ||
| 190 | use std::process::Command; | ||
| 191 | |||
| 192 | // Collect all commits referenced in the state | ||
| 193 | let mut commits_to_check = Vec::new(); | ||
| 194 | |||
| 195 | for branch in &state.branches { | ||
| 196 | if !branch.commit.starts_with("ref: ") { | ||
| 197 | commits_to_check.push(&branch.commit); | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | for tag in &state.tags { | ||
| 202 | if !tag.commit.starts_with("ref: ") { | ||
| 203 | commits_to_check.push(&tag.commit); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | // Identify missing commits | ||
| 208 | let mut missing_commits = Vec::new(); | ||
| 209 | for commit in commits_to_check { | ||
| 210 | if !git::oid_exists(target_repo, commit) { | ||
| 211 | missing_commits.push(commit); | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | if missing_commits.is_empty() { | ||
| 216 | tracing::debug!( | ||
| 217 | "No missing commits to copy from {} to {}", | ||
| 218 | source_repo.display(), | ||
| 219 | target_repo.display() | ||
| 220 | ); | ||
| 221 | return Ok(()); | ||
| 222 | } | ||
| 223 | |||
| 224 | tracing::info!( | ||
| 225 | "Copying {} missing commits from {} to {}", | ||
| 226 | missing_commits.len(), | ||
| 227 | source_repo.display(), | ||
| 228 | target_repo.display() | ||
| 229 | ); | ||
| 230 | |||
| 231 | // Fetch each missing commit from source to target | ||
| 232 | for commit in &missing_commits { | ||
| 233 | let output = Command::new("git") | ||
| 234 | .args([ | ||
| 235 | "fetch", | ||
| 236 | source_repo.to_str().ok_or("Invalid source path")?, | ||
| 237 | commit, | ||
| 238 | ]) | ||
| 239 | .current_dir(target_repo) | ||
| 240 | .output() | ||
| 241 | .map_err(|e| format!("Failed to execute git fetch: {}", e))?; | ||
| 242 | |||
| 243 | if !output.status.success() { | ||
| 244 | let stderr = String::from_utf8_lossy(&output.stderr); | ||
| 245 | return Err(format!( | ||
| 246 | "git fetch failed for commit {}: {}", | ||
| 247 | commit, stderr | ||
| 248 | )); | ||
| 249 | } | ||
| 250 | |||
| 251 | tracing::debug!("Copied commit {} to {}", commit, target_repo.display()); | ||
| 252 | } | ||
| 253 | 141 | ||
| 254 | Ok(()) | ||
| 255 | } | ||
| 256 | } | 142 | } |
| 257 | 143 | ||
| 258 | fn find_repo_with_git_data( | 144 | fn find_repo_with_git_data( |