diff options
Diffstat (limited to 'src/git')
| -rw-r--r-- | src/git/handlers.rs | 52 | ||||
| -rw-r--r-- | src/git/sync.rs | 221 |
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}; | |||
| 15 | use super::subprocess::GitSubprocess; | 15 | use super::subprocess::GitSubprocess; |
| 16 | use super::try_set_head_if_available; | 16 | use super::try_set_head_if_available; |
| 17 | 17 | ||
| 18 | use crate::git::authorization::{authorize_push, fetch_repository_data}; | 18 | use crate::git::authorization::{authorize_push, fetch_repository_data, parse_pushed_refs}; |
| 19 | use crate::git::sync::sync_to_owner_repos; | 19 | use crate::git::sync::{sync_pr_refs_to_owner_repos, sync_to_owner_repos}; |
| 20 | use crate::nostr::builder::SharedDatabase; | 20 | use crate::nostr::builder::SharedDatabase; |
| 21 | use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE}; | 21 | use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE}; |
| 22 | use crate::purgatory::Purgatory; | 22 | use 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)] | ||
| 61 | pub 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 | ||
| 88 | pub 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 | ||
| 232 | fn 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 | } |