upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib/git_events.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/git_events.rs')
-rw-r--r--src/lib/git_events.rs321
1 files changed, 284 insertions, 37 deletions
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index 69406c1..2e1f215 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -1,7 +1,10 @@
1use std::{str::FromStr, sync::Arc}; 1use std::{str::FromStr, sync::Arc};
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; 4use nostr::{
5 event::UnsignedEvent,
6 nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19},
7};
5use nostr_sdk::{ 8use nostr_sdk::{
6 Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, 9 Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind,
7 TagStandard, hashes::sha1::Hash as Sha1Hash, 10 TagStandard, hashes::sha1::Hash as Sha1Hash,
@@ -58,6 +61,9 @@ pub fn status_kinds() -> Vec<Kind> {
58 ] 61 ]
59} 62}
60 63
64pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618);
65pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619);
66
61pub fn event_is_patch_set_root(event: &Event) -> bool { 67pub fn event_is_patch_set_root(event: &Event) -> bool {
62 event.kind.eq(&Kind::GitPatch) 68 event.kind.eq(&Kind::GitPatch)
63 && event 69 && event
@@ -67,11 +73,16 @@ pub fn event_is_patch_set_root(event: &Event) -> bool {
67} 73}
68 74
69pub fn event_is_revision_root(event: &Event) -> bool { 75pub fn event_is_revision_root(event: &Event) -> bool {
70 event.kind.eq(&Kind::GitPatch) 76 (event.kind.eq(&Kind::GitPatch)
71 && event 77 && event
72 .tags 78 .tags
73 .iter() 79 .iter()
74 .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root")) 80 .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root")))
81 || (event.kind.eq(&KIND_PULL_REQUEST)
82 && event
83 .tags
84 .iter()
85 .any(|t| t.as_slice().len() > 1 && t.as_slice()[0].eq("e")))
75} 86}
76 87
77pub fn patch_supports_commit_ids(event: &Event) -> bool { 88pub fn patch_supports_commit_ids(event: &Event) -> bool {
@@ -82,6 +93,19 @@ pub fn patch_supports_commit_ids(event: &Event) -> bool {
82 .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig")) 93 .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig"))
83} 94}
84 95
96pub fn event_is_valid_pr_or_pr_update(event: &Event) -> bool {
97 [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind)
98 && event.tags.iter().any(|t| {
99 t.as_slice().len().gt(&1)
100 && t.as_slice()[0].eq("c")
101 && git2::Oid::from_str(&t.as_slice()[1]).is_ok()
102 })
103 && event
104 .tags
105 .iter()
106 .any(|t| t.as_slice().len().gt(&1) && t.as_slice()[0].eq("clone"))
107}
108
85#[allow(clippy::too_many_arguments)] 109#[allow(clippy::too_many_arguments)]
86#[allow(clippy::too_many_lines)] 110#[allow(clippy::too_many_lines)]
87pub async fn generate_patch_event( 111pub async fn generate_patch_event(
@@ -326,6 +350,180 @@ pub fn event_tag_from_nip19_or_hex(
326 } 350 }
327} 351}
328 352
353pub fn generate_unsigned_pr_or_update_event(
354 git_repo: &Repo,
355 repo_ref: &RepoRef,
356 signing_public_key: &PublicKey,
357 root_proposal: Option<&Event>,
358 commit: &Sha1Hash,
359 clone_url_hint: &[&str],
360 mentions: &[nostr::Tag],
361) -> Result<UnsignedEvent> {
362 let root_patch_cover_letter = if let Some(root_proposal) = root_proposal {
363 if root_proposal.kind.eq(&Kind::GitPatch) {
364 Some(event_to_cover_letter(root_proposal)?)
365 } else {
366 None
367 }
368 } else {
369 None
370 };
371
372 let title = if let Some(cl) = &root_patch_cover_letter {
373 cl.title.clone()
374 } else {
375 git_repo.get_commit_message_summary(commit)?
376 };
377
378 let description = if let Some(cl) = &root_patch_cover_letter {
379 cl.description.clone()
380 } else {
381 let mut description = git_repo.get_commit_message(commit)?.trim().to_string();
382 if let Some(remaining_description) = description.strip_prefix(&title) {
383 description = remaining_description.trim().to_string();
384 }
385 description
386 };
387
388 let root_commit = git_repo
389 .get_root_commit()
390 .context("failed to get root commit of the repository")?;
391
392 let pr_update_specific_tags = |root_proposal: &Event| {
393 vec![
394 Tag::custom(
395 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
396 vec![format!("git Pull Request Update")],
397 ),
398 Tag::custom(
399 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")),
400 vec![root_proposal.id],
401 ),
402 Tag::custom(
403 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")),
404 vec![root_proposal.pubkey],
405 ),
406 ]
407 };
408 let pr_specific_tags = || {
409 [
410 vec![
411 Tag::from_standardized(TagStandard::Subject(title.clone())),
412 Tag::custom(
413 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
414 vec![format!("git Pull Request: {}", title.clone())],
415 ),
416 ],
417 if let Some(cl) = &root_patch_cover_letter {
418 vec![
419 Tag::custom(
420 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")),
421 vec![root_proposal.unwrap().id],
422 ),
423 Tag::custom(
424 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")),
425 vec![cl.branch_name_without_id_or_prefix.clone()],
426 ),
427 Tag::public_key(root_proposal.unwrap().pubkey),
428 ]
429 } else if let Some(branch_name_tag) =
430 make_branch_name_tag_from_check_out_branch(git_repo)
431 {
432 vec![branch_name_tag]
433 } else {
434 vec![]
435 },
436 ]
437 .concat()
438 };
439
440 Ok(
441 if root_proposal.is_some() && root_patch_cover_letter.is_none() {
442 EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "")
443 } else {
444 EventBuilder::new(KIND_PULL_REQUEST, description)
445 }
446 .tags(
447 [
448 repo_ref
449 .maintainers
450 .iter()
451 .map(|m| {
452 Tag::from_standardized(TagStandard::Coordinate {
453 coordinate: Coordinate {
454 kind: nostr::Kind::GitRepoAnnouncement,
455 public_key: *m,
456 identifier: repo_ref.identifier.to_string(),
457 },
458 relay_url: repo_ref.relays.first().cloned(),
459 uppercase: false,
460 })
461 })
462 .collect::<Vec<Tag>>(),
463 mentions.to_vec(),
464 if let Some(root_proposal) = root_proposal {
465 if root_patch_cover_letter.is_none() {
466 pr_update_specific_tags(root_proposal)
467 } else {
468 pr_specific_tags()
469 }
470 } else {
471 pr_specific_tags()
472 },
473 vec![
474 Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))),
475 Tag::custom(
476 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")),
477 vec![format!("{commit}")],
478 ),
479 Tag::custom(
480 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")),
481 clone_url_hint
482 .iter()
483 .map(|s| s.to_string())
484 .collect::<Vec<String>>(),
485 ),
486 ],
487 repo_ref
488 .maintainers
489 .iter()
490 .map(|pk| Tag::public_key(*pk))
491 .collect(),
492 ]
493 .concat(),
494 )
495 .build(*signing_public_key),
496 )
497}
498
499fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option<Tag> {
500 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() {
501 if !branch_name.eq("main")
502 && !branch_name.eq("master")
503 && !branch_name.eq("origin/main")
504 && !branch_name.eq("origin/master")
505 {
506 Some(Tag::custom(
507 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")),
508 vec![
509 if let Some(branch_name) = branch_name.strip_prefix("pr/") {
510 branch_name.to_string()
511 } else {
512 branch_name
513 }
514 .chars()
515 .take(60)
516 .collect::<String>(),
517 ],
518 ))
519 } else {
520 None
521 }
522 } else {
523 None
524 }
525}
526
329#[allow(clippy::too_many_lines)] 527#[allow(clippy::too_many_lines)]
330pub async fn generate_cover_letter_and_patch_events( 528pub async fn generate_cover_letter_and_patch_events(
331 cover_letter_title_description: Option<(String, String)>, 529 cover_letter_title_description: Option<(String, String)>,
@@ -388,24 +586,8 @@ pub async fn generate_cover_letter_and_patch_events(
388 // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding 586 // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding
389 // a change like this, or the removal of this tag will require the actual branch name to be tracked 587 // a change like this, or the removal of this tag will require the actual branch name to be tracked
390 // so pulling and pushing still work 588 // so pulling and pushing still work
391 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { 589 if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) {
392 if !branch_name.eq("main") 590 vec![branch_name_tag]
393 && !branch_name.eq("master")
394 && !branch_name.eq("origin/main")
395 && !branch_name.eq("origin/master")
396 {
397 vec![
398 Tag::custom(
399 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")),
400 vec![if let Some(branch_name) = branch_name.strip_prefix("pr/") {
401 branch_name.to_string()
402 } else {
403 branch_name
404 }.chars().take(60).collect::<String>()],
405 ),
406 ]
407 }
408 else { vec![] }
409 } else { 591 } else {
410 vec![] 592 vec![]
411 }, 593 },
@@ -531,13 +713,22 @@ pub fn commit_msg_from_patch_oneliner(patch: &nostr::Event) -> Result<String> {
531} 713}
532 714
533pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { 715pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> {
534 if !event_is_patch_set_root(event) { 716 if !event.kind.eq(&KIND_PULL_REQUEST) && !event_is_patch_set_root(event) {
535 bail!("event is not a patch set root event (root patch or cover letter)") 717 bail!("event is not a patch set root event (root patch or cover letter)")
536 } 718 }
537 719
538 let title = commit_msg_from_patch_oneliner(event)?; 720 let title = if event.kind.eq(&KIND_PULL_REQUEST) {
539 let full = commit_msg_from_patch(event)?; 721 tag_value(event, "subject").unwrap_or("untitled".to_owned())
540 let description = full[title.len()..].trim().to_string(); 722 } else {
723 commit_msg_from_patch_oneliner(event)?
724 };
725 let description = if event.kind.eq(&KIND_PULL_REQUEST) {
726 event.content.clone()
727 } else {
728 commit_msg_from_patch(event)?[title.len()..]
729 .trim()
730 .to_string()
731 };
541 732
542 Ok(CoverLetter { 733 Ok(CoverLetter {
543 title: title.clone(), 734 title: title.clone(),
@@ -569,25 +760,25 @@ fn safe_branch_name_for_pr(s: &str) -> String {
569 .collect() 760 .collect()
570} 761}
571 762
572pub fn get_most_recent_patch_with_ancestors( 763pub fn get_pr_tip_event_or_most_recent_patch_with_ancestors(
573 mut patches: Vec<nostr::Event>, 764 mut proposal_events: Vec<nostr::Event>,
574) -> Result<Vec<nostr::Event>> { 765) -> Result<Vec<nostr::Event>> {
575 patches.sort_by_key(|e| e.created_at); 766 proposal_events.sort_by_key(|e| e.created_at);
576 767
577 let youngest_patch = patches.last().context("no patches found")?; 768 let youngest = proposal_events.last().context("no proposal events found")?;
578 769
579 let patches_with_youngest_created_at: Vec<&nostr::Event> = patches 770 let events_with_youngest_created_at: Vec<&nostr::Event> = proposal_events
580 .iter() 771 .iter()
581 .filter(|p| p.created_at.eq(&youngest_patch.created_at)) 772 .filter(|p| p.created_at.eq(&youngest.created_at))
582 .collect(); 773 .collect();
583 774
584 let mut res = vec![]; 775 let mut res = vec![];
585 776
586 let mut event_id_to_search = patches_with_youngest_created_at 777 let mut event_id_to_search = events_with_youngest_created_at
587 .clone() 778 .clone()
588 .iter() 779 .iter()
589 .find(|p| { 780 .find(|p| {
590 !patches_with_youngest_created_at.iter().any(|p2| { 781 !events_with_youngest_created_at.iter().any(|p2| {
591 if let Ok(reply_to) = get_event_parent_id(p2) { 782 if let Ok(reply_to) = get_event_parent_id(p2) {
592 reply_to.eq(&p.id.to_string()) 783 reply_to.eq(&p.id.to_string())
593 } else { 784 } else {
@@ -595,16 +786,18 @@ pub fn get_most_recent_patch_with_ancestors(
595 } 786 }
596 }) 787 })
597 }) 788 })
598 .context("failed to find patches_with_youngest_created_at")? 789 .context("failed to find events_with_youngest_created_at")?
599 .id 790 .id
600 .to_string(); 791 .to_string();
601 792
602 while let Some(event) = patches 793 while let Some(event) = proposal_events
603 .iter() 794 .iter()
604 .find(|e| e.id.to_string().eq(&event_id_to_search)) 795 .find(|e| e.id.to_string().eq(&event_id_to_search))
605 { 796 {
606 res.push(event.clone()); 797 res.push(event.clone());
607 if event_is_patch_set_root(event) { 798 if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind)
799 || event_is_patch_set_root(event)
800 {
608 break; 801 break;
609 } 802 }
610 event_id_to_search = get_event_parent_id(event).unwrap_or_default(); 803 event_id_to_search = get_event_parent_id(event).unwrap_or_default();
@@ -642,7 +835,61 @@ pub fn is_event_proposal_root_for_branch(
642 || cl 835 || cl
643 .get_branch_name_with_pr_prefix_and_shorthand_id() 836 .get_branch_name_with_pr_prefix_and_shorthand_id()
644 .is_ok_and(|s| s.eq(&branch_name)) 837 .is_ok_and(|s| s.eq(&branch_name))
645 }) && !event_is_revision_root(e)) 838 }) && (
839 // If we wanted to treat to list Pull Requests that revise a Patch we would do this:
840 // Note: whilst this the the case elsewhere event_is_revision_root is used, there is more to
841 // think about here?
842 // e.kind.eq(&KIND_PULL_REQUEST) ||
843 !event_is_revision_root(e)
844 ))
845}
846
847pub fn get_status(
848 proposal: &Event,
849 repo_ref: &RepoRef,
850 all_status_in_repo: &[Event],
851 all_pr_roots_in_repo: &[Event],
852) -> Kind {
853 let get_direct_status = |proposal: &Event| {
854 if let Some(e) = all_status_in_repo
855 .iter()
856 .filter(|e| {
857 status_kinds().contains(&e.kind)
858 && e.tags.iter().any(|t| {
859 t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string())
860 })
861 && (proposal.pubkey.eq(&e.pubkey) || repo_ref.maintainers.contains(&e.pubkey))
862 })
863 .collect::<Vec<&nostr::Event>>()
864 .first()
865 {
866 e.kind
867 } else {
868 Kind::GitStatusOpen
869 }
870 };
871 let is_proposal_pr_revision_of_patch = |proposal: &Event, patch: &Event| {
872 proposal.kind.eq(&KIND_PULL_REQUEST)
873 && proposal.tags.clone().into_iter().any(|t| {
874 t.as_slice().len() > 1
875 && t.as_slice()[0].eq("e")
876 && t.as_slice()[1].eq(&patch.id.to_string())
877 })
878 };
879
880 let direct_status = get_direct_status(proposal);
881 if direct_status.eq(&Kind::GitStatusClosed) && proposal.kind.eq(&Kind::GitPatch) {
882 if let Some(pr_revision) = all_pr_roots_in_repo
883 .iter()
884 .find(|p| is_proposal_pr_revision_of_patch(p, proposal))
885 {
886 get_direct_status(pr_revision)
887 } else {
888 direct_status
889 }
890 } else {
891 direct_status
892 }
646} 893}
647 894
648#[cfg(test)] 895#[cfg(test)]