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:
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}