upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-05 15:05:53 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-05 15:05:53 +0000
commitb29c0362bfb881febf9848e40023ac588d1e9aa7 (patch)
treeb8ab4a5f27a7ac458270985216c5cf0b4c7a65d4 /src
parent3f50107062d55a15decc47e93fd4e9f473de86e8 (diff)
sync PR refs (refs/nostr/<event-id>) to all owner repos when push received
When a push to refs/nostr/<event-id> is received (PR data), the git data is now synced to all other owner repositories that share maintainers with the source owner. This mirrors the behavior added for state event data. Changes: - Add sync_pr_refs_to_owner_repos() function in git/sync.rs - Add PrSyncResult struct to track sync statistics - Add copy_single_commit_between_repos() helper function - Call PR sync in handle_receive_pack after successful push - Add unit test for PrSyncResult default values
Diffstat (limited to 'src')
-rw-r--r--src/git/handlers.rs52
-rw-r--r--src/git/sync.rs221
2 files changed, 271 insertions, 2 deletions
diff --git a/src/git/handlers.rs b/src/git/handlers.rs
index e86d2a3..23a15ba 100644
--- a/src/git/handlers.rs
+++ b/src/git/handlers.rs
@@ -15,8 +15,8 @@ use super::protocol::{GitService, PktLine};
15use super::subprocess::GitSubprocess; 15use 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}; 18use crate::git::authorization::{authorize_push, fetch_repository_data, parse_pushed_refs};
19use crate::git::sync::sync_to_owner_repos; 19use crate::git::sync::{sync_pr_refs_to_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;
@@ -383,6 +383,54 @@ pub async fn handle_receive_pack(
383 } 383 }
384 } 384 }
385 385
386 // Sync PR data (refs/nostr/<event-id>) to other owner repositories
387 // Parse pushed refs to find refs/nostr/* refs
388 let pushed_refs = parse_pushed_refs(&request_body);
389 let pr_refs: Vec<(String, String)> = pushed_refs
390 .iter()
391 .filter_map(|(_, new_oid, ref_name)| {
392 ref_name.strip_prefix("refs/nostr/").map(|event_id| {
393 (event_id.to_string(), new_oid.clone())
394 })
395 })
396 .collect();
397
398 if !pr_refs.is_empty() {
399 match fetch_repository_data(&database, identifier).await {
400 Ok(db_repo_data) => {
401 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 if pr_sync_result.repos_synced > 0 {
411 info!(
412 "Synced {} PR refs to {} other owner repositories for {}",
413 pr_sync_result.refs_created,
414 pr_sync_result.repos_synced,
415 identifier
416 );
417 }
418
419 if !pr_sync_result.errors.is_empty() {
420 for (repo, error) in &pr_sync_result.errors {
421 warn!("Error syncing PR ref to {}: {}", repo, error);
422 }
423 }
424 }
425 Err(e) => {
426 warn!(
427 "Failed to fetch repository data for PR sync after push to {}: {}",
428 identifier, e
429 );
430 }
431 }
432 }
433
386 Ok(Response::builder() 434 Ok(Response::builder()
387 .status(StatusCode::OK) 435 .status(StatusCode::OK)
388 .header( 436 .header(
diff --git a/src/git/sync.rs b/src/git/sync.rs
index c99eb43..998f490 100644
--- a/src/git/sync.rs
+++ b/src/git/sync.rs
@@ -7,6 +7,8 @@
7//! copied to other owner repos that authorize the same state 7//! copied to other owner repos that authorize the same state
8//! 2. Purgatory sync fetches git data from remote - needs to distribute to all 8//! 2. Purgatory sync fetches git data from remote - needs to distribute to all
9//! authorized owner repos 9//! authorized owner repos
10//! 3. A push to refs/nostr/<event-id> (PR data) is received - needs to be synced
11//! to all other owner repos that share maintainers
10//! 12//!
11//! ## Architecture 13//! ## Architecture
12//! 14//!
@@ -54,6 +56,217 @@ pub struct AlignmentResult {
54 pub head_set: bool, 56 pub head_set: bool,
55} 57}
56 58
59/// Result of syncing PR refs to owner repositories
60#[derive(Debug, Default)]
61pub struct PrSyncResult {
62 /// Number of repositories synced
63 pub repos_synced: usize,
64 /// Number of refs created across all repos
65 pub refs_created: usize,
66 /// Errors encountered (repo path -> error message)
67 pub errors: Vec<(String, String)>,
68}
69
70/// Sync PR data (refs/nostr/<event-id>) from a source repository to all other
71/// owner repositories that share maintainers.
72///
73/// This function:
74/// 1. Collects all authorized maintainers per owner from announcements
75/// 2. For each owner that shares at least one maintainer with the source owner:
76/// - Copies missing OIDs for the PR commits
77/// - Creates the refs/nostr/<event-id> ref pointing to the same commit
78///
79/// # Arguments
80/// * `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
82/// * `db_repo_data` - Repository data from database (announcements + states)
83/// * `git_data_path` - Base path for git repositories
84/// * `source_owner_pubkey` - The owner pubkey of the source repository
85///
86/// # Returns
87/// A `PrSyncResult` with statistics about what was synced
88pub fn sync_pr_refs_to_owner_repos(
89 source_repo_path: &Path,
90 pr_refs: &[(String, String)], // (event_id, commit_hash)
91 db_repo_data: &RepositoryData,
92 git_data_path: &Path,
93 source_owner_pubkey: &str,
94) -> PrSyncResult {
95 let mut result = PrSyncResult::default();
96
97 if pr_refs.is_empty() {
98 return result;
99 }
100
101 // Collect authorized maintainers per owner
102 let by_owner = collect_authorized_maintainers(&db_repo_data.announcements);
103
104 // Get the maintainer set for the source owner
105 let source_maintainers = match by_owner.get(source_owner_pubkey) {
106 Some(maintainers) => maintainers,
107 None => {
108 debug!(
109 "No maintainer set found for source owner {}",
110 source_owner_pubkey
111 );
112 return result;
113 }
114 };
115
116 debug!(
117 source_owner = %source_owner_pubkey,
118 pr_refs_count = pr_refs.len(),
119 owners = by_owner.len(),
120 "Syncing PR refs to owner repositories"
121 );
122
123 for (owner, maintainers) in &by_owner {
124 // Skip the source owner - we already have the data there
125 if owner == source_owner_pubkey {
126 continue;
127 }
128
129 // Check if this owner shares any maintainers with the source owner
130 // (i.e., there's overlap in their maintainer sets)
131 let has_shared_maintainer = maintainers.iter().any(|m| source_maintainers.contains(m));
132
133 if !has_shared_maintainer {
134 debug!(
135 owner = %owner,
136 "Skipping owner - no shared maintainers with source"
137 );
138 continue;
139 }
140
141 // Find the announcement for this owner
142 let announcement = db_repo_data
143 .announcements
144 .iter()
145 .find(|a| a.event.pubkey.to_hex() == *owner);
146
147 let Some(announcement) = announcement else {
148 continue;
149 };
150
151 let target_repo_path = git_data_path.join(announcement.repo_path());
152
153 if !target_repo_path.exists() {
154 debug!(
155 owner = %owner,
156 repo_path = %target_repo_path.display(),
157 "Skipping owner - repository doesn't exist"
158 );
159 continue;
160 }
161
162 // Sync each PR ref
163 let mut refs_created_for_owner = 0;
164 for (event_id, commit_hash) in pr_refs {
165 // Copy the commit if missing
166 if !oid_exists(&target_repo_path, commit_hash) {
167 if let Err(e) =
168 copy_single_commit_between_repos(source_repo_path, &target_repo_path, commit_hash)
169 {
170 warn!(
171 event_id = %event_id,
172 source = %source_repo_path.display(),
173 target = %target_repo_path.display(),
174 error = %e,
175 "Failed to copy PR commit between repos"
176 );
177 result
178 .errors
179 .push((target_repo_path.display().to_string(), e));
180 continue;
181 }
182 }
183
184 // Create the refs/nostr/<event-id> ref
185 let ref_name = format!("refs/nostr/{}", event_id);
186 match git::update_ref(&target_repo_path, &ref_name, commit_hash) {
187 Ok(()) => {
188 info!(
189 event_id = %event_id,
190 commit = %commit_hash,
191 target = %target_repo_path.display(),
192 "Created PR ref in target repository"
193 );
194 refs_created_for_owner += 1;
195 }
196 Err(e) => {
197 warn!(
198 event_id = %event_id,
199 target = %target_repo_path.display(),
200 error = %e,
201 "Failed to create PR ref in target repository"
202 );
203 result.errors.push((target_repo_path.display().to_string(), e));
204 }
205 }
206 }
207
208 if refs_created_for_owner > 0 {
209 result.repos_synced += 1;
210 result.refs_created += refs_created_for_owner;
211
212 info!(
213 owner = %owner,
214 repo_path = %target_repo_path.display(),
215 refs_created = refs_created_for_owner,
216 "Synced PR refs to owner repository"
217 );
218 }
219 }
220
221 info!(
222 repos_synced = result.repos_synced,
223 refs_created = result.refs_created,
224 errors = result.errors.len(),
225 "Completed PR ref sync to owner repositories"
226 );
227
228 result
229}
230
231/// Copy a single commit from source repository to target repository
232fn copy_single_commit_between_repos(
233 source_repo: &Path,
234 target_repo: &Path,
235 commit_hash: &str,
236) -> Result<(), String> {
237 debug!(
238 "Copying commit {} from {} to {}",
239 commit_hash,
240 source_repo.display(),
241 target_repo.display()
242 );
243
244 let output = Command::new("git")
245 .args([
246 "fetch",
247 source_repo.to_str().ok_or("Invalid source path")?,
248 commit_hash,
249 ])
250 .current_dir(target_repo)
251 .output()
252 .map_err(|e| format!("Failed to execute git fetch: {}", e))?;
253
254 if !output.status.success() {
255 let stderr = String::from_utf8_lossy(&output.stderr);
256 return Err(format!(
257 "git fetch failed for commit {}: {}",
258 commit_hash, stderr
259 ));
260 }
261
262 debug!(
263 "Copied commit {} to {}",
264 commit_hash,
265 target_repo.display()
266 );
267 Ok(())
268}
269
57/// Sync git data from a source repository to all other owner repositories 270/// Sync git data from a source repository to all other owner repositories
58/// that authorize the given state event. 271/// that authorize the given state event.
59/// 272///
@@ -450,4 +663,12 @@ mod tests {
450 assert_eq!(result.refs_deleted, 0); 663 assert_eq!(result.refs_deleted, 0);
451 assert!(!result.head_set); 664 assert!(!result.head_set);
452 } 665 }
666
667 #[test]
668 fn test_pr_sync_result_default() {
669 let result = PrSyncResult::default();
670 assert_eq!(result.repos_synced, 0);
671 assert_eq!(result.refs_created, 0);
672 assert!(result.errors.is_empty());
673 }
453} 674}