diff options
Diffstat (limited to 'src/lib/git_events.rs')
| -rw-r--r-- | src/lib/git_events.rs | 321 |
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 @@ | |||
| 1 | use std::{str::FromStr, sync::Arc}; | 1 | use std::{str::FromStr, sync::Arc}; |
| 2 | 2 | ||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; | 4 | use nostr::{ |
| 5 | event::UnsignedEvent, | ||
| 6 | nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}, | ||
| 7 | }; | ||
| 5 | use nostr_sdk::{ | 8 | use 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 | ||
| 64 | pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); | ||
| 65 | pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); | ||
| 66 | |||
| 61 | pub fn event_is_patch_set_root(event: &Event) -> bool { | 67 | pub 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 | ||
| 69 | pub fn event_is_revision_root(event: &Event) -> bool { | 75 | pub 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 | ||
| 77 | pub fn patch_supports_commit_ids(event: &Event) -> bool { | 88 | pub 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 | ||
| 96 | pub 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)] |
| 87 | pub async fn generate_patch_event( | 111 | pub async fn generate_patch_event( |
| @@ -326,6 +350,180 @@ pub fn event_tag_from_nip19_or_hex( | |||
| 326 | } | 350 | } |
| 327 | } | 351 | } |
| 328 | 352 | ||
| 353 | pub 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 | |||
| 499 | fn 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)] |
| 330 | pub async fn generate_cover_letter_and_patch_events( | 528 | pub 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 | ||
| 533 | pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { | 715 | pub 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 | ||
| 572 | pub fn get_most_recent_patch_with_ancestors( | 763 | pub 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 | |||
| 847 | pub 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)] |