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-01-05 15:28:33 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-05 15:28:33 +0000
commit96203000f81a46976834971dd26e1a79465e6303 (patch)
tree1a95e1260b13ff50ad564e09ecc7f3e88b3d6d21 /src/git
parentb29c0362bfb881febf9848e40023ac588d1e9aa7 (diff)
sync PR refs to all relivant repos
Diffstat (limited to 'src/git')
-rw-r--r--src/git/handlers.rs59
-rw-r--r--src/git/sync.rs224
2 files changed, 211 insertions, 72 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index 23a15ba..2325106 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -16,7 +16,7 @@ use super::subprocess::GitSubprocess;
16use super::try_set_head_if_available; 16use super::try_set_head_if_available;
17 17
18use crate::git::authorization::{authorize_push, fetch_repository_data, parse_pushed_refs}; 18use crate::git::authorization::{authorize_push, fetch_repository_data, parse_pushed_refs};
19use crate::git::sync::{sync_pr_refs_to_owner_repos, sync_to_owner_repos}; 19use crate::git::sync::{sync_pr_refs_to_tagged_owner_repos, sync_to_owner_repos};
20use crate::nostr::builder::SharedDatabase; 20use crate::nostr::builder::SharedDatabase;
21use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE}; 21use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE};
22use crate::purgatory::Purgatory; 22use crate::purgatory::Purgatory;
@@ -389,36 +389,53 @@ pub async fn handle_receive_pack(
389 let pr_refs: Vec<(String, String)> = pushed_refs 389 let pr_refs: Vec<(String, String)> = pushed_refs
390 .iter() 390 .iter()
391 .filter_map(|(_, new_oid, ref_name)| { 391 .filter_map(|(_, new_oid, ref_name)| {
392 ref_name.strip_prefix("refs/nostr/").map(|event_id| { 392 ref_name
393 (event_id.to_string(), new_oid.clone()) 393 .strip_prefix("refs/nostr/")
394 }) 394 .map(|event_id| (event_id.to_string(), new_oid.clone()))
395 }) 395 })
396 .collect(); 396 .collect();
397 397
398 if !pr_refs.is_empty() { 398 if !pr_refs.is_empty() {
399 // Extract PR events from purgatory_events (filter for KIND_PR and KIND_PR_UPDATE)
400 let purgatory_pr_events: Vec<_> = auth_result
401 .purgatory_events
402 .iter()
403 .filter(|e| e.kind == Kind::from(KIND_PR) || e.kind == Kind::from(KIND_PR_UPDATE))
404 .cloned()
405 .collect();
406
399 match fetch_repository_data(&database, identifier).await { 407 match fetch_repository_data(&database, identifier).await {
400 Ok(db_repo_data) => { 408 Ok(db_repo_data) => {
401 let git_data_path_buf = std::path::PathBuf::from(git_data_path); 409 let git_data_path_buf = std::path::PathBuf::from(git_data_path);
402 let pr_sync_result = sync_pr_refs_to_owner_repos(
403 &repo_path,
404 &pr_refs,
405 &db_repo_data,
406 &git_data_path_buf,
407 owner_pubkey,
408 );
409 410
410 if pr_sync_result.repos_synced > 0 { 411 // sync to owner repos and repos of other owners that list them as maintainers
411 info!( 412 // This uses the `a` tags from PR events to find tagged owner repos
412 "Synced {} PR refs to {} other owner repositories for {}", 413 if !purgatory_pr_events.is_empty() {
413 pr_sync_result.refs_created, 414 let tagged_sync_result = sync_pr_refs_to_tagged_owner_repos(
414 pr_sync_result.repos_synced, 415 &repo_path,
415 identifier 416 &pr_refs,
417 &purgatory_pr_events,
418 &db_repo_data,
419 &git_data_path_buf,
420 owner_pubkey,
416 ); 421 );
417 }
418 422
419 if !pr_sync_result.errors.is_empty() { 423 if tagged_sync_result.repos_synced > 0 {
420 for (repo, error) in &pr_sync_result.errors { 424 info!(
421 warn!("Error syncing PR ref to {}: {}", repo, error); 425 "Synced {} PR refs to {} other owner repositories for {} (via tagged owners)",
426 tagged_sync_result.refs_created,
427 tagged_sync_result.repos_synced,
428 identifier
429 );
430 }
431
432 if !tagged_sync_result.errors.is_empty() {
433 for (repo, error) in &tagged_sync_result.errors {
434 warn!(
435 "Error syncing PR ref to {} (via tagged owner): {}",
436 repo, error
437 );
438 }
422 } 439 }
423 } 440 }
424 } 441 }
diff --git a/src/git/sync.rs b/src/git/sync.rs
index 998f490..9a8af5a 100644
--- a/src/git/sync.rs
+++ b/src/git/sync.rs
@@ -17,13 +17,15 @@
17//! authorizes a push, that push should be reflected in ALL owner repositories 17//! authorizes a push, that push should be reflected in ALL owner repositories
18//! that would authorize the same state. 18//! that would authorize the same state.
19 19
20use std::collections::HashMap; 20use std::collections::{HashMap, HashSet};
21use std::path::Path; 21use std::path::Path;
22use std::process::Command; 22use std::process::Command;
23use tracing::{debug, info, warn}; 23use tracing::{debug, info, warn};
24 24
25use crate::git::{self, oid_exists}; 25use nostr_sdk::Event;
26
26use crate::git::authorization::{collect_authorized_maintainers, RepositoryData}; 27use crate::git::authorization::{collect_authorized_maintainers, RepositoryData};
28use crate::git::{self, oid_exists};
27use crate::nostr::events::RepositoryState; 29use crate::nostr::events::RepositoryState;
28 30
29/// Result of syncing git data to owner repositories 31/// Result of syncing git data to owner repositories
@@ -67,27 +69,56 @@ pub struct PrSyncResult {
67 pub errors: Vec<(String, String)>, 69 pub errors: Vec<(String, String)>,
68} 70}
69 71
70/// Sync PR data (refs/nostr/<event-id>) from a source repository to all other 72/// Extract owner pubkeys from PR events' `a` tags.
71/// owner repositories that share maintainers.
72/// 73///
73/// This function: 74/// PR events reference repositories via `a` tags with format `30617:<owner_pubkey>:<identifier>`.
74/// 1. Collects all authorized maintainers per owner from announcements 75/// This function extracts all unique owner pubkeys from these tags.
75/// 2. For each owner that shares at least one maintainer with the source owner: 76///
76/// - Copies missing OIDs for the PR commits 77/// # Arguments
77/// - Creates the refs/nostr/<event-id> ref pointing to the same commit 78/// * `pr_events` - List of PR events to extract owner pubkeys from
79///
80/// # Returns
81/// A HashSet of owner pubkeys (hex strings) referenced by the PR events
82pub fn extract_tagged_owners_from_pr_events(pr_events: &[Event]) -> HashSet<String> {
83 let mut owners = HashSet::new();
84
85 for event in pr_events {
86 for tag in event.tags.iter() {
87 let tag_vec = tag.clone().to_vec();
88 if tag_vec.len() >= 2 && tag_vec[0] == "a" && tag_vec[1].starts_with("30617:") {
89 // Format: 30617:<owner_pubkey>:<identifier>
90 let parts: Vec<&str> = tag_vec[1].split(':').collect();
91 if parts.len() >= 2 {
92 owners.insert(parts[1].to_string());
93 }
94 }
95 }
96 }
97
98 owners
99}
100
101/// Sync PR data (refs/nostr/<event-id>) from a source repository to owner
102/// repositories that list any of the tagged owners as a maintainer.
103///
104/// This function is used when PR events from purgatory have been authorized.
105/// It extracts the owner pubkeys from the PR events' `a` tags and syncs to
106/// any owner repo that lists any of those owners as a maintainer.
78/// 107///
79/// # Arguments 108/// # Arguments
80/// * `source_repo_path` - Path to the repository that has the PR git data 109/// * `source_repo_path` - Path to the repository that has the PR git data
81/// * `pr_refs` - List of (event_id, commit_hash) tuples for PR refs that were pushed 110/// * `pr_refs` - List of (event_id, commit_hash) tuples for PR refs that were pushed
111/// * `purgatory_pr_events` - PR events from purgatory that authorized this push
82/// * `db_repo_data` - Repository data from database (announcements + states) 112/// * `db_repo_data` - Repository data from database (announcements + states)
83/// * `git_data_path` - Base path for git repositories 113/// * `git_data_path` - Base path for git repositories
84/// * `source_owner_pubkey` - The owner pubkey of the source repository 114/// * `source_owner_pubkey` - The owner pubkey of the source repository (to skip)
85/// 115///
86/// # Returns 116/// # Returns
87/// A `PrSyncResult` with statistics about what was synced 117/// A `PrSyncResult` with statistics about what was synced
88pub fn sync_pr_refs_to_owner_repos( 118pub fn sync_pr_refs_to_tagged_owner_repos(
89 source_repo_path: &Path, 119 source_repo_path: &Path,
90 pr_refs: &[(String, String)], // (event_id, commit_hash) 120 pr_refs: &[(String, String)], // (event_id, commit_hash)
121 purgatory_pr_events: &[Event],
91 db_repo_data: &RepositoryData, 122 db_repo_data: &RepositoryData,
92 git_data_path: &Path, 123 git_data_path: &Path,
93 source_owner_pubkey: &str, 124 source_owner_pubkey: &str,
@@ -98,42 +129,36 @@ pub fn sync_pr_refs_to_owner_repos(
98 return result; 129 return result;
99 } 130 }
100 131
101 // Collect authorized maintainers per owner 132 // Extract owner pubkeys from PR events' `a` tags
102 let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); 133 let tagged_owners = extract_tagged_owners_from_pr_events(purgatory_pr_events);
103 134
104 // Get the maintainer set for the source owner 135 if tagged_owners.is_empty() {
105 let source_maintainers = match by_owner.get(source_owner_pubkey) { 136 debug!("No tagged owners found in PR events");
106 Some(maintainers) => maintainers, 137 return result;
107 None => { 138 }
108 debug!(
109 "No maintainer set found for source owner {}",
110 source_owner_pubkey
111 );
112 return result;
113 }
114 };
115 139
116 debug!( 140 debug!(
117 source_owner = %source_owner_pubkey, 141 tagged_owners = ?tagged_owners,
118 pr_refs_count = pr_refs.len(), 142 pr_refs_count = pr_refs.len(),
119 owners = by_owner.len(), 143 "Syncing PR refs to owner repositories that list tagged owners as maintainers"
120 "Syncing PR refs to owner repositories"
121 ); 144 );
122 145
146 // Collect authorized maintainers per owner
147 let by_owner = collect_authorized_maintainers(&db_repo_data.announcements);
148
123 for (owner, maintainers) in &by_owner { 149 for (owner, maintainers) in &by_owner {
124 // Skip the source owner - we already have the data there 150 // Skip the source owner - we already have the data there
125 if owner == source_owner_pubkey { 151 if owner == source_owner_pubkey {
126 continue; 152 continue;
127 } 153 }
128 154
129 // Check if this owner shares any maintainers with the source owner 155 // Check if this owner's maintainer set includes any of the tagged owners
130 // (i.e., there's overlap in their maintainer sets) 156 let has_tagged_owner_as_maintainer = maintainers.iter().any(|m| tagged_owners.contains(m));
131 let has_shared_maintainer = maintainers.iter().any(|m| source_maintainers.contains(m));
132 157
133 if !has_shared_maintainer { 158 if !has_tagged_owner_as_maintainer {
134 debug!( 159 debug!(
135 owner = %owner, 160 owner = %owner,
136 "Skipping owner - no shared maintainers with source" 161 "Skipping owner - does not list any tagged owner as maintainer"
137 ); 162 );
138 continue; 163 continue;
139 } 164 }
@@ -164,9 +189,11 @@ pub fn sync_pr_refs_to_owner_repos(
164 for (event_id, commit_hash) in pr_refs { 189 for (event_id, commit_hash) in pr_refs {
165 // Copy the commit if missing 190 // Copy the commit if missing
166 if !oid_exists(&target_repo_path, commit_hash) { 191 if !oid_exists(&target_repo_path, commit_hash) {
167 if let Err(e) = 192 if let Err(e) = copy_single_commit_between_repos(
168 copy_single_commit_between_repos(source_repo_path, &target_repo_path, commit_hash) 193 source_repo_path,
169 { 194 &target_repo_path,
195 commit_hash,
196 ) {
170 warn!( 197 warn!(
171 event_id = %event_id, 198 event_id = %event_id,
172 source = %source_repo_path.display(), 199 source = %source_repo_path.display(),
@@ -189,7 +216,7 @@ pub fn sync_pr_refs_to_owner_repos(
189 event_id = %event_id, 216 event_id = %event_id,
190 commit = %commit_hash, 217 commit = %commit_hash,
191 target = %target_repo_path.display(), 218 target = %target_repo_path.display(),
192 "Created PR ref in target repository" 219 "Created PR ref in target repository (via tagged owner)"
193 ); 220 );
194 refs_created_for_owner += 1; 221 refs_created_for_owner += 1;
195 } 222 }
@@ -200,7 +227,9 @@ pub fn sync_pr_refs_to_owner_repos(
200 error = %e, 227 error = %e,
201 "Failed to create PR ref in target repository" 228 "Failed to create PR ref in target repository"
202 ); 229 );
203 result.errors.push((target_repo_path.display().to_string(), e)); 230 result
231 .errors
232 .push((target_repo_path.display().to_string(), e));
204 } 233 }
205 } 234 }
206 } 235 }
@@ -213,7 +242,7 @@ pub fn sync_pr_refs_to_owner_repos(
213 owner = %owner, 242 owner = %owner,
214 repo_path = %target_repo_path.display(), 243 repo_path = %target_repo_path.display(),
215 refs_created = refs_created_for_owner, 244 refs_created = refs_created_for_owner,
216 "Synced PR refs to owner repository" 245 "Synced PR refs to owner repository (via tagged owner)"
217 ); 246 );
218 } 247 }
219 } 248 }
@@ -222,7 +251,8 @@ pub fn sync_pr_refs_to_owner_repos(
222 repos_synced = result.repos_synced, 251 repos_synced = result.repos_synced,
223 refs_created = result.refs_created, 252 refs_created = result.refs_created,
224 errors = result.errors.len(), 253 errors = result.errors.len(),
225 "Completed PR ref sync to owner repositories" 254 tagged_owners = tagged_owners.len(),
255 "Completed PR ref sync to owner repositories via tagged owners"
226 ); 256 );
227 257
228 result 258 result
@@ -259,11 +289,7 @@ fn copy_single_commit_between_repos(
259 )); 289 ));
260 } 290 }
261 291
262 debug!( 292 debug!("Copied commit {} to {}", commit_hash, target_repo.display());
263 "Copied commit {} to {}",
264 commit_hash,
265 target_repo.display()
266 );
267 Ok(()) 293 Ok(())
268} 294}
269 295
@@ -359,7 +385,8 @@ pub fn sync_to_owner_repos(
359 385
360 // Copy missing OIDs from source repo to target repo if different 386 // Copy missing OIDs from source repo to target repo if different
361 if target_repo_path != source_repo_path { 387 if target_repo_path != source_repo_path {
362 if let Err(e) = copy_missing_oids_between_repos(source_repo_path, &target_repo_path, state) 388 if let Err(e) =
389 copy_missing_oids_between_repos(source_repo_path, &target_repo_path, state)
363 { 390 {
364 warn!( 391 warn!(
365 identifier = %state.identifier, 392 identifier = %state.identifier,
@@ -368,7 +395,9 @@ pub fn sync_to_owner_repos(
368 error = %e, 395 error = %e,
369 "Failed to copy OIDs between repos" 396 "Failed to copy OIDs between repos"
370 ); 397 );
371 result.errors.push((target_repo_path.display().to_string(), e)); 398 result
399 .errors
400 .push((target_repo_path.display().to_string(), e));
372 // Continue anyway - we'll try to align what we can 401 // Continue anyway - we'll try to align what we can
373 } 402 }
374 } 403 }
@@ -615,11 +644,7 @@ pub fn align_repository_with_state(repo_path: &Path, state: &RepositoryState) ->
615 if let Some(head_commit) = state.get_branch_commit(branch_name) { 644 if let Some(head_commit) = state.get_branch_commit(branch_name) {
616 match git::try_set_head_if_available(repo_path, head_ref, head_commit) { 645 match git::try_set_head_if_available(repo_path, head_ref, head_commit) {
617 Ok(true) => { 646 Ok(true) => {
618 info!( 647 info!("Set HEAD to {} in {}", head_ref, repo_path.display());
619 "Set HEAD to {} in {}",
620 head_ref,
621 repo_path.display()
622 );
623 result.head_set = true; 648 result.head_set = true;
624 } 649 }
625 Ok(false) => { 650 Ok(false) => {
@@ -671,4 +696,101 @@ mod tests {
671 assert_eq!(result.refs_created, 0); 696 assert_eq!(result.refs_created, 0);
672 assert!(result.errors.is_empty()); 697 assert!(result.errors.is_empty());
673 } 698 }
699
700 #[test]
701 fn test_extract_tagged_owners_from_pr_events_empty() {
702 let events: Vec<Event> = vec![];
703 let owners = extract_tagged_owners_from_pr_events(&events);
704 assert!(owners.is_empty());
705 }
706
707 #[test]
708 fn test_extract_tagged_owners_from_pr_events_with_a_tags() {
709 use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind};
710
711 let keys = Keys::generate();
712
713 // Create a PR event with `a` tags referencing repos
714 let tags = vec![
715 Tag::custom(
716 TagKind::Custom("a".into()),
717 vec!["30617:abc123def456:test-repo".to_string()],
718 ),
719 Tag::custom(
720 TagKind::Custom("a".into()),
721 vec!["30617:789xyz000111:another-repo".to_string()],
722 ),
723 Tag::custom(TagKind::Custom("c".into()), vec!["commit123".to_string()]),
724 ];
725
726 let event = EventBuilder::new(Kind::from(1618), "PR content")
727 .tags(tags)
728 .sign_with_keys(&keys)
729 .unwrap();
730
731 let owners = extract_tagged_owners_from_pr_events(&[event]);
732 assert_eq!(owners.len(), 2);
733 assert!(owners.contains("abc123def456"));
734 assert!(owners.contains("789xyz000111"));
735 }
736
737 #[test]
738 fn test_extract_tagged_owners_from_pr_events_deduplicates() {
739 use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind};
740
741 let keys = Keys::generate();
742
743 // Create two events with overlapping owners
744 let tags1 = vec![Tag::custom(
745 TagKind::Custom("a".into()),
746 vec!["30617:same_owner:repo1".to_string()],
747 )];
748
749 let tags2 = vec![Tag::custom(
750 TagKind::Custom("a".into()),
751 vec!["30617:same_owner:repo2".to_string()],
752 )];
753
754 let event1 = EventBuilder::new(Kind::from(1618), "PR 1")
755 .tags(tags1)
756 .sign_with_keys(&keys)
757 .unwrap();
758
759 let event2 = EventBuilder::new(Kind::from(1618), "PR 2")
760 .tags(tags2)
761 .sign_with_keys(&keys)
762 .unwrap();
763
764 let owners = extract_tagged_owners_from_pr_events(&[event1, event2]);
765 assert_eq!(owners.len(), 1);
766 assert!(owners.contains("same_owner"));
767 }
768
769 #[test]
770 fn test_extract_tagged_owners_ignores_non_30617_a_tags() {
771 use nostr_sdk::{EventBuilder, Keys, Kind, Tag, TagKind};
772
773 let keys = Keys::generate();
774
775 // Create a PR event with a non-30617 `a` tag
776 let tags = vec![
777 Tag::custom(
778 TagKind::Custom("a".into()),
779 vec!["30617:valid_owner:test-repo".to_string()],
780 ),
781 Tag::custom(
782 TagKind::Custom("a".into()),
783 vec!["30618:state_event:test-repo".to_string()], // Not 30617
784 ),
785 ];
786
787 let event = EventBuilder::new(Kind::from(1618), "PR content")
788 .tags(tags)
789 .sign_with_keys(&keys)
790 .unwrap();
791
792 let owners = extract_tagged_owners_from_pr_events(&[event]);
793 assert_eq!(owners.len(), 1);
794 assert!(owners.contains("valid_owner"));
795 }
674} 796}