upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/git/sync.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/git/sync.rs')
-rw-r--r--src/git/sync.rs221
1 files changed, 221 insertions, 0 deletions
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}