upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr/builder.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 17:04:43 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 17:04:43 +0000
commitd0592943867b7003b30a778acff8fcc43c041e34 (patch)
treecfb527a13edadc3b43bf8788bf5116e7bfd66753 /src/nostr/builder.rs
parent35199693690345039b2d2db2070bbd652e25328c (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.rs331
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)]
23struct 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 &current_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 );