diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 17:04:43 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 17:04:43 +0000 |
| commit | d0592943867b7003b30a778acff8fcc43c041e34 (patch) | |
| tree | cfb527a13edadc3b43bf8788bf5116e7bfd66753 /src/nostr/builder.rs | |
| parent | 35199693690345039b2d2db2070bbd652e25328c (diff) | |
try and add / update / delete refs on state update
if we have the OIDs
Diffstat (limited to 'src/nostr/builder.rs')
| -rw-r--r-- | src/nostr/builder.rs | 331 |
1 files changed, 242 insertions, 89 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 97fd17e..2f182ea 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -18,6 +18,19 @@ use crate::nostr::events::{ | |||
| 18 | KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, | 18 | KIND_PR_UPDATE, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE, |
| 19 | }; | 19 | }; |
| 20 | 20 | ||
| 21 | /// Result of aligning a repository with authorized state | ||
| 22 | #[derive(Debug, Default)] | ||
| 23 | struct AlignmentResult { | ||
| 24 | /// Number of refs created | ||
| 25 | refs_created: usize, | ||
| 26 | /// Number of refs updated | ||
| 27 | refs_updated: usize, | ||
| 28 | /// Number of refs deleted | ||
| 29 | refs_deleted: usize, | ||
| 30 | /// Whether HEAD was set | ||
| 31 | head_set: bool, | ||
| 32 | } | ||
| 33 | |||
| 21 | /// NIP-34 Write Policy with Full GRASP-01 Event Validation | 34 | /// NIP-34 Write Policy with Full GRASP-01 Event Validation |
| 22 | /// | 35 | /// |
| 23 | /// Validates all events according to GRASP-01 specification: | 36 | /// Validates all events according to GRASP-01 specification: |
| @@ -185,56 +198,16 @@ impl Nip34WritePolicy { | |||
| 185 | } | 198 | } |
| 186 | } | 199 | } |
| 187 | 200 | ||
| 188 | /// Try to set repository HEAD for all authorized announcement owners | 201 | /// Identify all owner repositories for which this state event is the latest authorized state |
| 189 | /// | ||
| 190 | /// Per GRASP-01: "MUST set repository HEAD per repository state announcement | ||
| 191 | /// as soon as the git data related to that branch has been received." | ||
| 192 | /// | ||
| 193 | /// This function: | ||
| 194 | /// 1. Checks if this state event is the latest for the identifier | ||
| 195 | /// 2. Finds all announcements where the state author is authorized | ||
| 196 | /// 3. Updates HEAD in each relevant repository | ||
| 197 | /// | 202 | /// |
| 198 | /// Returns Ok(count) with the number of repositories updated. | 203 | /// Returns a list of (announcement, repo_path) pairs where: |
| 199 | async fn try_set_head_for_authorized_repos( | 204 | /// - The state author is authorized (owner or maintainer) |
| 205 | /// - This state event is the latest for the identifier in that context | ||
| 206 | async fn identify_owner_repositories( | ||
| 200 | &self, | 207 | &self, |
| 201 | database: &Arc<MemoryDatabase>, | 208 | database: &Arc<MemoryDatabase>, |
| 202 | state: &RepositoryState, | 209 | state: &RepositoryState, |
| 203 | ) -> Result<usize, String> { | 210 | ) -> Result<Vec<(RepositoryAnnouncement, std::path::PathBuf)>, String> { |
| 204 | // Check if state has a HEAD reference | ||
| 205 | let head_ref = match &state.head { | ||
| 206 | Some(h) => h, | ||
| 207 | None => { | ||
| 208 | tracing::debug!("State event for {} has no HEAD reference", state.identifier); | ||
| 209 | return Ok(0); | ||
| 210 | } | ||
| 211 | }; | ||
| 212 | |||
| 213 | // Get the branch name and commit | ||
| 214 | let branch_name = match state.get_head_branch() { | ||
| 215 | Some(b) => b, | ||
| 216 | None => { | ||
| 217 | tracing::debug!( | ||
| 218 | "State event for {} has invalid HEAD format: {}", | ||
| 219 | state.identifier, | ||
| 220 | head_ref | ||
| 221 | ); | ||
| 222 | return Ok(0); | ||
| 223 | } | ||
| 224 | }; | ||
| 225 | |||
| 226 | let head_commit = match state.get_branch_commit(branch_name) { | ||
| 227 | Some(c) => c, | ||
| 228 | None => { | ||
| 229 | tracing::debug!( | ||
| 230 | "State event for {} HEAD branch {} has no commit in state", | ||
| 231 | state.identifier, | ||
| 232 | branch_name | ||
| 233 | ); | ||
| 234 | return Ok(0); | ||
| 235 | } | ||
| 236 | }; | ||
| 237 | |||
| 238 | // Find all announcements where state author is authorized | 211 | // Find all announcements where state author is authorized |
| 239 | let announcements = | 212 | let announcements = |
| 240 | Self::find_authorized_announcements(database, &state.identifier, &state.event.pubkey) | 213 | Self::find_authorized_announcements(database, &state.identifier, &state.event.pubkey) |
| @@ -246,12 +219,12 @@ impl Nip34WritePolicy { | |||
| 246 | state.identifier, | 219 | state.identifier, |
| 247 | state.event.pubkey.to_hex() | 220 | state.event.pubkey.to_hex() |
| 248 | ); | 221 | ); |
| 249 | return Ok(0); | 222 | return Ok(Vec::new()); |
| 250 | } | 223 | } |
| 251 | 224 | ||
| 252 | // Update HEAD in each authorized announcement's repository | 225 | let mut owner_repos = Vec::new(); |
| 253 | let mut updated_count = 0; | 226 | |
| 254 | for announcement in &announcements { | 227 | for announcement in announcements { |
| 255 | // Build the list of authorized pubkeys for this specific announcement | 228 | // Build the list of authorized pubkeys for this specific announcement |
| 256 | // (owner + maintainers) | 229 | // (owner + maintainers) |
| 257 | let mut authorized_pubkeys = vec![announcement.event.pubkey]; | 230 | let mut authorized_pubkeys = vec![announcement.event.pubkey]; |
| @@ -262,10 +235,9 @@ impl Nip34WritePolicy { | |||
| 262 | } | 235 | } |
| 263 | 236 | ||
| 264 | // Check if this is the latest state event for THIS announcement's context | 237 | // Check if this is the latest state event for THIS announcement's context |
| 265 | // Different owners with the same identifier should not interfere | ||
| 266 | if !Self::is_latest_state_for_identifier(database, state, &authorized_pubkeys).await? { | 238 | if !Self::is_latest_state_for_identifier(database, state, &authorized_pubkeys).await? { |
| 267 | tracing::debug!( | 239 | tracing::debug!( |
| 268 | "Skipping HEAD update for {} in {}'s repo - not the latest state event for this context", | 240 | "Skipping {} in {}'s repo - not the latest state event for this context", |
| 269 | state.identifier, | 241 | state.identifier, |
| 270 | announcement.event.pubkey.to_hex() | 242 | announcement.event.pubkey.to_hex() |
| 271 | ); | 243 | ); |
| @@ -274,31 +246,186 @@ impl Nip34WritePolicy { | |||
| 274 | 246 | ||
| 275 | // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git | 247 | // Build repository path: <git_data_path>/<owner_npub>/<identifier>.git |
| 276 | let repo_path = self.git_data_path.join(announcement.repo_path().clone()); | 248 | let repo_path = self.git_data_path.join(announcement.repo_path().clone()); |
| 249 | owner_repos.push((announcement, repo_path)); | ||
| 250 | } | ||
| 277 | 251 | ||
| 278 | match git::try_set_head_if_available(&repo_path, head_ref, head_commit) { | 252 | Ok(owner_repos) |
| 279 | Ok(true) => { | 253 | } |
| 280 | tracing::info!( | 254 | |
| 281 | "Set HEAD to {} in repository {} (from state by {})", | 255 | /// Align an owner repository's refs with the authorized state |
| 282 | head_ref, | 256 | /// |
| 283 | repo_path.display(), | 257 | /// This function: |
| 284 | state.event.pubkey.to_hex() | 258 | /// 1. Deletes refs that are in the repo but not in the state (for refs/heads/ and refs/tags/) |
| 285 | ); | 259 | /// 2. Updates refs that exist in state if we have the commit (for refs/heads/ and refs/tags/) |
| 286 | updated_count += 1; | 260 | /// 3. Sets HEAD if the HEAD branch's commit is available |
| 261 | /// | ||
| 262 | /// Per GRASP-01: "MUST set repository HEAD per repository state announcement | ||
| 263 | /// as soon as the git data related to that branch has been received." | ||
| 264 | /// | ||
| 265 | /// Returns a summary of actions taken. | ||
| 266 | fn align_owner_repository_with_state( | ||
| 267 | &self, | ||
| 268 | repo_path: &std::path::Path, | ||
| 269 | state: &RepositoryState, | ||
| 270 | ) -> AlignmentResult { | ||
| 271 | let mut result = AlignmentResult::default(); | ||
| 272 | |||
| 273 | // Check if repository exists | ||
| 274 | if !repo_path.exists() { | ||
| 275 | tracing::debug!( | ||
| 276 | "Repository not found at {}, cannot align with state", | ||
| 277 | repo_path.display() | ||
| 278 | ); | ||
| 279 | return result; | ||
| 280 | } | ||
| 281 | |||
| 282 | // Get current refs from the repository | ||
| 283 | let current_refs = match git::list_refs(repo_path) { | ||
| 284 | Ok(refs) => refs, | ||
| 285 | Err(e) => { | ||
| 286 | tracing::warn!("Failed to list refs in {}: {}", repo_path.display(), e); | ||
| 287 | return result; | ||
| 288 | } | ||
| 289 | }; | ||
| 290 | |||
| 291 | // Build expected refs from state | ||
| 292 | let mut expected_refs: std::collections::HashMap<String, String> = | ||
| 293 | std::collections::HashMap::new(); | ||
| 294 | |||
| 295 | for branch in &state.branches { | ||
| 296 | let ref_name = format!("refs/heads/{}", branch.name); | ||
| 297 | expected_refs.insert(ref_name, branch.commit.clone()); | ||
| 298 | } | ||
| 299 | |||
| 300 | for tag in &state.tags { | ||
| 301 | let ref_name = format!("refs/tags/{}", tag.name); | ||
| 302 | expected_refs.insert(ref_name, tag.commit.clone()); | ||
| 303 | } | ||
| 304 | |||
| 305 | // Process current refs: update or delete as needed | ||
| 306 | for (ref_name, current_commit) in ¤t_refs { | ||
| 307 | // Only process refs/heads/ and refs/tags/ | ||
| 308 | if !ref_name.starts_with("refs/heads/") && !ref_name.starts_with("refs/tags/") { | ||
| 309 | continue; | ||
| 310 | } | ||
| 311 | |||
| 312 | match expected_refs.get(ref_name) { | ||
| 313 | Some(expected_commit) => { | ||
| 314 | // Ref should exist - check if commit matches | ||
| 315 | if current_commit != expected_commit { | ||
| 316 | // Check if we have the expected commit | ||
| 317 | if git::commit_exists(repo_path, expected_commit) { | ||
| 318 | // Update the ref | ||
| 319 | match git::update_ref(repo_path, ref_name, expected_commit) { | ||
| 320 | Ok(()) => { | ||
| 321 | tracing::info!( | ||
| 322 | "Updated {} from {} to {} in {}", | ||
| 323 | ref_name, | ||
| 324 | current_commit, | ||
| 325 | expected_commit, | ||
| 326 | repo_path.display() | ||
| 327 | ); | ||
| 328 | result.refs_updated += 1; | ||
| 329 | } | ||
| 330 | Err(e) => { | ||
| 331 | tracing::warn!( | ||
| 332 | "Failed to update {} in {}: {}", | ||
| 333 | ref_name, | ||
| 334 | repo_path.display(), | ||
| 335 | e | ||
| 336 | ); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } else { | ||
| 340 | tracing::debug!( | ||
| 341 | "Commit {} not available for {} in {}", | ||
| 342 | expected_commit, | ||
| 343 | ref_name, | ||
| 344 | repo_path.display() | ||
| 345 | ); | ||
| 346 | } | ||
| 347 | } | ||
| 287 | } | 348 | } |
| 288 | Ok(false) => { | 349 | None => { |
| 289 | tracing::debug!( | 350 | // Ref should not exist - delete it |
| 290 | "HEAD commit {} not available yet in {}", | 351 | match git::delete_ref(repo_path, ref_name) { |
| 291 | head_commit, | 352 | Ok(()) => { |
| 292 | repo_path.display() | 353 | tracing::info!( |
| 293 | ); | 354 | "Deleted {} (not in state) from {}", |
| 355 | ref_name, | ||
| 356 | repo_path.display() | ||
| 357 | ); | ||
| 358 | result.refs_deleted += 1; | ||
| 359 | } | ||
| 360 | Err(e) => { | ||
| 361 | tracing::warn!( | ||
| 362 | "Failed to delete {} from {}: {}", | ||
| 363 | ref_name, | ||
| 364 | repo_path.display(), | ||
| 365 | e | ||
| 366 | ); | ||
| 367 | } | ||
| 368 | } | ||
| 294 | } | 369 | } |
| 295 | Err(e) => { | 370 | } |
| 296 | tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e); | 371 | } |
| 372 | |||
| 373 | // Add refs that exist in state but not in repo (if we have the commit) | ||
| 374 | for (ref_name, expected_commit) in &expected_refs { | ||
| 375 | let exists = current_refs.iter().any(|(r, _)| r == ref_name); | ||
| 376 | if !exists && git::commit_exists(repo_path, expected_commit) { | ||
| 377 | match git::update_ref(repo_path, ref_name, expected_commit) { | ||
| 378 | Ok(()) => { | ||
| 379 | tracing::info!( | ||
| 380 | "Created {} at {} in {}", | ||
| 381 | ref_name, | ||
| 382 | expected_commit, | ||
| 383 | repo_path.display() | ||
| 384 | ); | ||
| 385 | result.refs_created += 1; | ||
| 386 | } | ||
| 387 | Err(e) => { | ||
| 388 | tracing::warn!( | ||
| 389 | "Failed to create {} in {}: {}", | ||
| 390 | ref_name, | ||
| 391 | repo_path.display(), | ||
| 392 | e | ||
| 393 | ); | ||
| 394 | } | ||
| 297 | } | 395 | } |
| 298 | } | 396 | } |
| 299 | } | 397 | } |
| 300 | 398 | ||
| 301 | Ok(updated_count) | 399 | // Set HEAD if specified in state |
| 400 | if let Some(head_ref) = &state.head { | ||
| 401 | if let Some(branch_name) = state.get_head_branch() { | ||
| 402 | if let Some(head_commit) = state.get_branch_commit(branch_name) { | ||
| 403 | match git::try_set_head_if_available(repo_path, head_ref, head_commit) { | ||
| 404 | Ok(true) => { | ||
| 405 | tracing::info!( | ||
| 406 | "Set HEAD to {} in {} (from state by {})", | ||
| 407 | head_ref, | ||
| 408 | repo_path.display(), | ||
| 409 | state.event.pubkey.to_hex() | ||
| 410 | ); | ||
| 411 | result.head_set = true; | ||
| 412 | } | ||
| 413 | Ok(false) => { | ||
| 414 | tracing::debug!( | ||
| 415 | "HEAD commit {} not available yet in {}", | ||
| 416 | head_commit, | ||
| 417 | repo_path.display() | ||
| 418 | ); | ||
| 419 | } | ||
| 420 | Err(e) => { | ||
| 421 | tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e); | ||
| 422 | } | ||
| 423 | } | ||
| 424 | } | ||
| 425 | } | ||
| 426 | } | ||
| 427 | |||
| 428 | result | ||
| 302 | } | 429 | } |
| 303 | 430 | ||
| 304 | /// Extract all reference tags from an event (a, A, q, e, E) | 431 | /// Extract all reference tags from an event (a, A, q, e, E) |
| @@ -763,28 +890,54 @@ impl WritePolicy for Nip34WritePolicy { | |||
| 763 | // Parse state to get HEAD and branch info | 890 | // Parse state to get HEAD and branch info |
| 764 | match RepositoryState::from_event(event.clone()) { | 891 | match RepositoryState::from_event(event.clone()) { |
| 765 | Ok(state) => { | 892 | Ok(state) => { |
| 766 | // Try to set HEAD for all authorized repos if this is the latest state | 893 | // Identify owner repositories for which this is the latest authorized state |
| 767 | match self | 894 | match self.identify_owner_repositories(&database, &state).await { |
| 768 | .try_set_head_for_authorized_repos(&database, &state) | 895 | Ok(owner_repos) => { |
| 769 | .await | 896 | let repo_count = owner_repos.len(); |
| 770 | { | 897 | let mut total_aligned = 0; |
| 771 | Ok(count) if count > 0 => { | 898 | |
| 772 | tracing::info!( | 899 | // Align each owner repository with the authorized state |
| 773 | "Set HEAD from state event {} for {} repo(s) with identifier {}", | 900 | for (_announcement, repo_path) in owner_repos { |
| 774 | event_id_str, | 901 | let result = self.align_owner_repository_with_state( |
| 775 | count, | 902 | &repo_path, &state, |
| 776 | state.identifier | 903 | ); |
| 777 | ); | 904 | |
| 778 | } | 905 | if result.refs_created > 0 |
| 779 | Ok(_) => { | 906 | || result.refs_updated > 0 |
| 780 | tracing::debug!( | 907 | || result.refs_deleted > 0 |
| 781 | "HEAD not set from state {} - git data not available yet or not latest", | 908 | || result.head_set |
| 782 | event_id_str | 909 | { |
| 783 | ); | 910 | tracing::info!( |
| 911 | "Aligned {} with state {}: created={}, updated={}, deleted={}, head_set={}", | ||
| 912 | repo_path.display(), | ||
| 913 | event_id_str, | ||
| 914 | result.refs_created, | ||
| 915 | result.refs_updated, | ||
| 916 | result.refs_deleted, | ||
| 917 | result.head_set | ||
| 918 | ); | ||
| 919 | total_aligned += 1; | ||
| 920 | } | ||
| 921 | } | ||
| 922 | |||
| 923 | if repo_count > 0 { | ||
| 924 | tracing::info!( | ||
| 925 | "Processed state event {} for {} repo(s) ({} aligned) with identifier {}", | ||
| 926 | event_id_str, | ||
| 927 | repo_count, | ||
| 928 | total_aligned, | ||
| 929 | state.identifier | ||
| 930 | ); | ||
| 931 | } else { | ||
| 932 | tracing::debug!( | ||
| 933 | "No owner repos to align for state {} - git data not available yet or not latest", | ||
| 934 | event_id_str | ||
| 935 | ); | ||
| 936 | } | ||
| 784 | } | 937 | } |
| 785 | Err(e) => { | 938 | Err(e) => { |
| 786 | tracing::warn!( | 939 | tracing::warn!( |
| 787 | "Failed to process HEAD from state {}: {}", | 940 | "Failed to identify owner repositories for state {}: {}", |
| 788 | event_id_str, | 941 | event_id_str, |
| 789 | e | 942 | e |
| 790 | ); | 943 | ); |