upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/git
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-13 13:24:46 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-13 17:29:23 +0000
commit1d09e4bdea7e328cf2740818df9df660c5532a99 (patch)
treedcb758a70a2e9b84709df247cc685a2f6423094e /src/git
parenta2a99d5a4137b57e4141cf2840f2f51b38035cfa (diff)
feat: implement announcement purgatory core (breaks archive sync test)
Route new announcements to purgatory instead of accepting immediately. Announcements are promoted to the database when git data arrives, ensuring we only serve announcements for repos with actual content. Implemented: - AnnouncementPurgatoryEntry type and DashMap store - Route new announcements to purgatory (replacement announcements skip) - Promote announcements on git data arrival (process_purgatory_announcements) - Authorization checks purgatory announcements (fetch_repository_data_with_purgatory) - State policy uses purgatory announcements for maintainer validation - Cleanup task handles announcement expiry - Updated count()/cleanup() to 3-tuples Known broken: - test_archive_read_only_creates_bare_repo fails: sync module does not treat purgatory announcements as confirmed repos, so per-repo sync (state events, PRs) is never triggered for purgatory announcements - Announcement persistence (save/restore) not implemented - SyncLevel (StateOnly vs Full) not implemented - Soft expiry two-phase not implemented - Expiry extension on state event / git auth not wired up
Diffstat (limited to 'src/git')
-rw-r--r--src/git/authorization.rs38
-rw-r--r--src/git/sync.rs110
2 files changed, 145 insertions, 3 deletions
diff --git a/src/git/authorization.rs b/src/git/authorization.rs
index e174b51..9d53c4f 100644
--- a/src/git/authorization.rs
+++ b/src/git/authorization.rs
@@ -287,6 +287,39 @@ pub async fn fetch_repository_data(
287 }) 287 })
288} 288}
289 289
290/// Fetch repository data including announcements from purgatory
291///
292/// This combines database announcements with purgatory announcements,
293/// which is needed for authorization when the announcement hasn't been
294/// promoted yet (no git data has arrived).
295pub async fn fetch_repository_data_with_purgatory(
296 database: &SharedDatabase,
297 purgatory: &crate::purgatory::Purgatory,
298 identifier: &str,
299) -> Result<RepositoryData> {
300 // First, fetch from database
301 let mut repo_data = fetch_repository_data(database, identifier).await?;
302
303 // Then, add announcements from purgatory
304 let purgatory_announcements = purgatory.get_announcements_by_identifier(identifier);
305 let purgatory_count = purgatory_announcements.len();
306
307 for entry in purgatory_announcements {
308 if let Ok(announcement) = RepositoryAnnouncement::from_event(entry.event) {
309 repo_data.announcements.push(announcement);
310 }
311 }
312
313 debug!(
314 "Fetched repository data with purgatory: {} announcements ({} from purgatory), {} states",
315 repo_data.announcements.len(),
316 purgatory_count,
317 repo_data.states.len()
318 );
319
320 Ok(repo_data)
321}
322
290pub fn pubkey_authorised_for_repo_owners( 323pub fn pubkey_authorised_for_repo_owners(
291 pubkey: &PublicKey, 324 pubkey: &PublicKey,
292 db_repo_data: &RepositoryData, 325 db_repo_data: &RepositoryData,
@@ -539,8 +572,9 @@ pub async fn get_state_authorization_for_specific_owner_repo(
539 use crate::git::list_refs; 572 use crate::git::list_refs;
540 use crate::purgatory::RefUpdate; 573 use crate::purgatory::RefUpdate;
541 574
542 // Fetch announcements only - we don't need database states 575 // Fetch announcements from database AND purgatory - needed for authorization
543 let repo_data = fetch_repository_data(database, identifier).await?; 576 // when the announcement hasn't been promoted yet (no git data has arrived)
577 let repo_data = fetch_repository_data_with_purgatory(database, purgatory, identifier).await?;
544 578
545 if repo_data.announcements.is_empty() { 579 if repo_data.announcements.is_empty() {
546 return Ok(AuthorizationResult::denied( 580 return Ok(AuthorizationResult::denied(
diff --git a/src/git/sync.rs b/src/git/sync.rs
index e8e9655..13f30b6 100644
--- a/src/git/sync.rs
+++ b/src/git/sync.rs
@@ -51,6 +51,8 @@ use crate::purgatory::{can_apply_state, Purgatory};
51/// or from purgatory sync fetching OIDs from remote servers). 51/// or from purgatory sync fetching OIDs from remote servers).
52#[derive(Debug, Default, Clone)] 52#[derive(Debug, Default, Clone)]
53pub struct ProcessResult { 53pub struct ProcessResult {
54 /// Number of announcements released from purgatory
55 pub announcements_released: usize,
54 /// Number of state events released from purgatory 56 /// Number of state events released from purgatory
55 pub states_released: usize, 57 pub states_released: usize,
56 /// Number of PR events released from purgatory 58 /// Number of PR events released from purgatory
@@ -70,11 +72,12 @@ pub struct ProcessResult {
70impl ProcessResult { 72impl ProcessResult {
71 /// Check if any events were released 73 /// Check if any events were released
72 pub fn released_any(&self) -> bool { 74 pub fn released_any(&self) -> bool {
73 self.states_released > 0 || self.prs_released > 0 75 self.announcements_released > 0 || self.states_released > 0 || self.prs_released > 0
74 } 76 }
75 77
76 /// Merge another ProcessResult into this one 78 /// Merge another ProcessResult into this one
77 pub fn merge(&mut self, other: ProcessResult) { 79 pub fn merge(&mut self, other: ProcessResult) {
80 self.announcements_released += other.announcements_released;
78 self.states_released += other.states_released; 81 self.states_released += other.states_released;
79 self.prs_released += other.prs_released; 82 self.prs_released += other.prs_released;
80 self.repos_synced += other.repos_synced; 83 self.repos_synced += other.repos_synced;
@@ -836,6 +839,18 @@ pub async fn process_newly_available_git_data(
836 "Processing newly available git data" 839 "Processing newly available git data"
837 ); 840 );
838 841
842 // Process announcements from purgatory
843 let announcement_result = process_purgatory_announcements(
844 &identifier,
845 source_repo_path,
846 database,
847 local_relay,
848 purgatory,
849 git_data_path,
850 )
851 .await;
852 result.merge(announcement_result);
853
839 // Process state events from purgatory 854 // Process state events from purgatory
840 let state_result = process_purgatory_state_events( 855 let state_result = process_purgatory_state_events(
841 &identifier, 856 &identifier,
@@ -863,6 +878,7 @@ pub async fn process_newly_available_git_data(
863 if result.released_any() { 878 if result.released_any() {
864 info!( 879 info!(
865 identifier = %identifier, 880 identifier = %identifier,
881 announcements_released = result.announcements_released,
866 states_released = result.states_released, 882 states_released = result.states_released,
867 prs_released = result.prs_released, 883 prs_released = result.prs_released,
868 repos_synced = result.repos_synced, 884 repos_synced = result.repos_synced,
@@ -1250,6 +1266,90 @@ async fn process_purgatory_pr_events(
1250 result 1266 result
1251} 1267}
1252 1268
1269/// Process announcements from purgatory that can now be promoted.
1270///
1271/// When git data arrives for a repository, any announcements in purgatory
1272/// for that repository should be promoted to the database and served to clients.
1273async fn process_purgatory_announcements(
1274 identifier: &str,
1275 source_repo_path: &Path,
1276 database: &SharedDatabase,
1277 local_relay: Option<&nostr_relay_builder::LocalRelay>,
1278 purgatory: &Purgatory,
1279 git_data_path: &Path,
1280) -> ProcessResult {
1281 let mut result = ProcessResult::default();
1282
1283 // Extract owner pubkey from the source repo path
1284 let owner_pubkey = match extract_owner_from_repo_path(source_repo_path, git_data_path) {
1285 Some(npub) => npub,
1286 None => {
1287 debug!(
1288 identifier = %identifier,
1289 "Could not extract owner from repo path"
1290 );
1291 return result;
1292 }
1293 };
1294
1295 // Parse the npub back to PublicKey
1296 let owner = match nostr_sdk::PublicKey::parse(&owner_pubkey) {
1297 Ok(pk) => pk,
1298 Err(e) => {
1299 warn!(
1300 identifier = %identifier,
1301 owner_pubkey = %owner_pubkey,
1302 error = %e,
1303 "Failed to parse owner pubkey"
1304 );
1305 result.errors.push(format!("Failed to parse owner pubkey: {}", e));
1306 return result;
1307 }
1308 };
1309
1310 // Check if there's an announcement in purgatory for this owner and identifier
1311 let announcement_event = purgatory.promote_announcement(&owner, identifier);
1312
1313 if let Some(event) = announcement_event {
1314 // Save to database
1315 match database.save_event(&event).await {
1316 Ok(_) => {
1317 info!(
1318 identifier = %identifier,
1319 event_id = %event.id,
1320 "Promoted announcement from purgatory to database"
1321 );
1322
1323 // Notify WebSocket subscribers
1324 if let Some(relay) = local_relay {
1325 if relay.notify_event(event.clone()) {
1326 debug!(
1327 identifier = %identifier,
1328 event_id = %event.id,
1329 "Broadcast announcement event to WebSocket listeners"
1330 );
1331 }
1332 }
1333
1334 result.announcements_released += 1;
1335 }
1336 Err(e) => {
1337 warn!(
1338 identifier = %identifier,
1339 event_id = %event.id,
1340 error = %e,
1341 "Failed to save announcement to database"
1342 );
1343 result
1344 .errors
1345 .push(format!("Failed to save announcement: {}", e));
1346 }
1347 }
1348 }
1349
1350 result
1351}
1352
1253/// Extract owner pubkey from a repository path. 1353/// Extract owner pubkey from a repository path.
1254/// 1354///
1255/// Given a path like `{git_data_path}/{npub}/{identifier}.git`, extracts the npub. 1355/// Given a path like `{git_data_path}/{npub}/{identifier}.git`, extracts the npub.
@@ -1271,6 +1371,7 @@ mod tests {
1271 #[test] 1371 #[test]
1272 fn test_process_result_default() { 1372 fn test_process_result_default() {
1273 let result = ProcessResult::default(); 1373 let result = ProcessResult::default();
1374 assert_eq!(result.announcements_released, 0);
1274 assert_eq!(result.states_released, 0); 1375 assert_eq!(result.states_released, 0);
1275 assert_eq!(result.prs_released, 0); 1376 assert_eq!(result.prs_released, 0);
1276 assert_eq!(result.repos_synced, 0); 1377 assert_eq!(result.repos_synced, 0);
@@ -1282,6 +1383,10 @@ mod tests {
1282 let mut result = ProcessResult::default(); 1383 let mut result = ProcessResult::default();
1283 assert!(!result.released_any()); 1384 assert!(!result.released_any());
1284 1385
1386 result.announcements_released = 1;
1387 assert!(result.released_any());
1388
1389 result.announcements_released = 0;
1285 result.states_released = 1; 1390 result.states_released = 1;
1286 assert!(result.released_any()); 1391 assert!(result.released_any());
1287 1392
@@ -1293,6 +1398,7 @@ mod tests {
1293 #[test] 1398 #[test]
1294 fn test_process_result_merge() { 1399 fn test_process_result_merge() {
1295 let mut result1 = ProcessResult { 1400 let mut result1 = ProcessResult {
1401 announcements_released: 0,
1296 states_released: 1, 1402 states_released: 1,
1297 prs_released: 2, 1403 prs_released: 2,
1298 repos_synced: 3, 1404 repos_synced: 3,
@@ -1303,6 +1409,7 @@ mod tests {
1303 }; 1409 };
1304 1410
1305 let result2 = ProcessResult { 1411 let result2 = ProcessResult {
1412 announcements_released: 5,
1306 states_released: 10, 1413 states_released: 10,
1307 prs_released: 20, 1414 prs_released: 20,
1308 repos_synced: 30, 1415 repos_synced: 30,
@@ -1314,6 +1421,7 @@ mod tests {
1314 1421
1315 result1.merge(result2); 1422 result1.merge(result2);
1316 1423
1424 assert_eq!(result1.announcements_released, 5);
1317 assert_eq!(result1.states_released, 11); 1425 assert_eq!(result1.states_released, 11);
1318 assert_eq!(result1.prs_released, 22); 1426 assert_eq!(result1.prs_released, 22);
1319 assert_eq!(result1.repos_synced, 33); 1427 assert_eq!(result1.repos_synced, 33);