diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-04-22 07:26:15 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-05-07 09:10:33 +0100 |
| commit | 10498b953d36304b441fcb162155c2487046206f (patch) | |
| tree | cea78cfbb83dbbe5ddae12ffde18df94ae7d6746 | |
| parent | c4853f69dc25408ff70cccbe923d2cd400385fc2 (diff) | |
feat(send): `in-reply-to` tags npubs and events
in addition to being used to create a new proposal revision,
in-reply-to can now be used to reference other events and npubs.
for example an issues or kind 1 threads where the proposal is relevant
the proposal will only be marked as a revision if the first parameter is
a reference to an existing proposal root
| -rw-r--r-- | src/git.rs | 2 | ||||
| -rw-r--r-- | src/sub_commands/push.rs | 3 | ||||
| -rw-r--r-- | src/sub_commands/send.rs | 152 | ||||
| -rw-r--r-- | test_utils/src/lib.rs | 6 | ||||
| -rw-r--r-- | tests/send.rs | 274 |
5 files changed, 401 insertions, 36 deletions
| @@ -1526,6 +1526,7 @@ mod tests { | |||
| 1526 | None, | 1526 | None, |
| 1527 | None, | 1527 | None, |
| 1528 | &None, | 1528 | &None, |
| 1529 | &[], | ||
| 1529 | ) | 1530 | ) |
| 1530 | } | 1531 | } |
| 1531 | fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { | 1532 | fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { |
| @@ -1687,6 +1688,7 @@ mod tests { | |||
| 1687 | &TEST_KEY_1_KEYS, | 1688 | &TEST_KEY_1_KEYS, |
| 1688 | &RepoRef::try_from(generate_repo_ref_event()).unwrap(), | 1689 | &RepoRef::try_from(generate_repo_ref_event()).unwrap(), |
| 1689 | &None, | 1690 | &None, |
| 1691 | &[], | ||
| 1690 | )?; | 1692 | )?; |
| 1691 | 1693 | ||
| 1692 | events.reverse(); | 1694 | events.reverse(); |
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs index bb50ee2..fefe102 100644 --- a/src/sub_commands/push.rs +++ b/src/sub_commands/push.rs | |||
| @@ -108,7 +108,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 108 | cli_args, | 108 | cli_args, |
| 109 | &sub_commands::send::SubCommandArgs { | 109 | &sub_commands::send::SubCommandArgs { |
| 110 | since_or_range: String::new(), | 110 | since_or_range: String::new(), |
| 111 | in_reply_to: Some(proposal_root_event.id.to_string()), | 111 | in_reply_to: vec![proposal_root_event.id.to_string()], |
| 112 | title: None, | 112 | title: None, |
| 113 | description: None, | 113 | description: None, |
| 114 | no_cover_letter: args.no_cover_letter, | 114 | no_cover_letter: args.no_cover_letter, |
| @@ -166,6 +166,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 166 | None, | 166 | None, |
| 167 | None, | 167 | None, |
| 168 | &None, | 168 | &None, |
| 169 | &[], | ||
| 169 | ) | 170 | ) |
| 170 | .context("cannot make patch event from commit")?, | 171 | .context("cannot make patch event from commit")?, |
| 171 | ); | 172 | ); |
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index 78d00b2..4639b01 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs | |||
| @@ -31,10 +31,10 @@ pub struct SubCommandArgs { | |||
| 31 | #[arg(default_value = "")] | 31 | #[arg(default_value = "")] |
| 32 | /// commits to send as proposal; like in `git format-patch` eg. HEAD~2 | 32 | /// commits to send as proposal; like in `git format-patch` eg. HEAD~2 |
| 33 | pub(crate) since_or_range: String, | 33 | pub(crate) since_or_range: String, |
| 34 | #[clap(long)] | 34 | #[clap(long, value_parser, num_args = 0.., value_delimiter = ' ')] |
| 35 | /// nevent or event id of an existing proposal for which this is a new | 35 | /// references to an existing proposal for which this is a new |
| 36 | /// version | 36 | /// version and/or events / npubs to tag as mentions |
| 37 | pub(crate) in_reply_to: Option<String>, | 37 | pub(crate) in_reply_to: Vec<String>, |
| 38 | /// don't prompt for a cover letter | 38 | /// don't prompt for a cover letter |
| 39 | #[arg(long, action)] | 39 | #[arg(long, action)] |
| 40 | pub(crate) no_cover_letter: bool, | 40 | pub(crate) no_cover_letter: bool, |
| @@ -50,14 +50,29 @@ pub struct SubCommandArgs { | |||
| 50 | pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | 50 | pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { |
| 51 | let git_repo = Repo::discover().context("cannot find a git repository")?; | 51 | let git_repo = Repo::discover().context("cannot find a git repository")?; |
| 52 | 52 | ||
| 53 | if let Some(id) = &args.in_reply_to { | ||
| 54 | println!("creating proposal revision for: {id}"); | ||
| 55 | } | ||
| 56 | |||
| 57 | let (main_branch_name, main_tip) = git_repo | 53 | let (main_branch_name, main_tip) = git_repo |
| 58 | .get_main_or_master_branch() | 54 | .get_main_or_master_branch() |
| 59 | .context("the default branches (main or master) do not exist")?; | 55 | .context("the default branches (main or master) do not exist")?; |
| 60 | 56 | ||
| 57 | #[cfg(not(test))] | ||
| 58 | let mut client = Client::default(); | ||
| 59 | #[cfg(test)] | ||
| 60 | let mut client = <MockConnect as std::default::Default>::default(); | ||
| 61 | |||
| 62 | let (root_proposal_id, mention_tags) = get_root_proposal_id_and_mentions_from_in_reply_to( | ||
| 63 | &client, | ||
| 64 | // TODO: user repo relays when when event cache is in place | ||
| 65 | client.get_fallback_relays(), | ||
| 66 | &args.in_reply_to, | ||
| 67 | ) | ||
| 68 | .await?; | ||
| 69 | |||
| 70 | if let Some(root_ref) = args.in_reply_to.first() { | ||
| 71 | if root_proposal_id.is_some() { | ||
| 72 | println!("creating proposal revision for: {root_ref}"); | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 61 | let mut commits: Vec<Sha1Hash> = { | 76 | let mut commits: Vec<Sha1Hash> = { |
| 62 | if args.since_or_range.is_empty() { | 77 | if args.since_or_range.is_empty() { |
| 63 | let branch_name = git_repo.get_checked_out_branch_name()?; | 78 | let branch_name = git_repo.get_checked_out_branch_name()?; |
| @@ -163,12 +178,6 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 163 | } else { | 178 | } else { |
| 164 | None | 179 | None |
| 165 | }; | 180 | }; |
| 166 | |||
| 167 | #[cfg(not(test))] | ||
| 168 | let mut client = Client::default(); | ||
| 169 | #[cfg(test)] | ||
| 170 | let mut client = <MockConnect as std::default::Default>::default(); | ||
| 171 | |||
| 172 | let (keys, user_ref) = login::launch(&cli_args.nsec, &cli_args.password, Some(&client)).await?; | 181 | let (keys, user_ref) = login::launch(&cli_args.nsec, &cli_args.password, Some(&client)).await?; |
| 173 | 182 | ||
| 174 | client.set_keys(&keys).await; | 183 | client.set_keys(&keys).await; |
| @@ -194,7 +203,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 194 | &commits, | 203 | &commits, |
| 195 | &keys, | 204 | &keys, |
| 196 | &repo_ref, | 205 | &repo_ref, |
| 197 | &args.in_reply_to, | 206 | &root_proposal_id, |
| 207 | &mention_tags, | ||
| 198 | )?; | 208 | )?; |
| 199 | 209 | ||
| 200 | println!( | 210 | println!( |
| @@ -227,7 +237,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 227 | ) | 237 | ) |
| 228 | .await?; | 238 | .await?; |
| 229 | 239 | ||
| 230 | if args.in_reply_to.is_none() { | 240 | if root_proposal_id.is_none() { |
| 231 | if let Some(event) = events.first() { | 241 | if let Some(event) = events.first() { |
| 232 | // TODO: add gitworkshop.dev to njump and remove direct gitworkshop link | 242 | // TODO: add gitworkshop.dev to njump and remove direct gitworkshop link |
| 233 | println!( | 243 | println!( |
| @@ -538,15 +548,77 @@ mod tests_unique_and_duplicate { | |||
| 538 | } | 548 | } |
| 539 | } | 549 | } |
| 540 | 550 | ||
| 551 | async fn get_root_proposal_id_and_mentions_from_in_reply_to( | ||
| 552 | #[cfg(test)] client: &crate::client::MockConnect, | ||
| 553 | #[cfg(not(test))] client: &Client, | ||
| 554 | repo_relays: &[String], | ||
| 555 | in_reply_to: &[String], | ||
| 556 | ) -> Result<(Option<String>, Vec<nostr::Tag>)> { | ||
| 557 | let root_proposal_id = if let Some(first) = in_reply_to.first() { | ||
| 558 | match event_tag_from_nip19_or_hex(first, "in-reply-to", nostr::Marker::Root, true, false)? { | ||
| 559 | Tag::Event { | ||
| 560 | event_id, | ||
| 561 | relay_url: _, | ||
| 562 | marker: _, | ||
| 563 | } => { | ||
| 564 | let events = client | ||
| 565 | .get_events( | ||
| 566 | repo_relays.to_vec(), | ||
| 567 | vec![nostr::Filter::new().id(event_id)], | ||
| 568 | ) | ||
| 569 | .await | ||
| 570 | .context("whilst getting events specified in --in-reply-to")?; | ||
| 571 | if let Some(first) = events.iter().find(|e| e.id.eq(&event_id)) { | ||
| 572 | if event_is_patch_set_root(first) { | ||
| 573 | Some(event_id.to_string()) | ||
| 574 | } else { | ||
| 575 | None | ||
| 576 | } | ||
| 577 | } else { | ||
| 578 | bail!( | ||
| 579 | "cannot find first event specified in --in-reply-to \"{}\"", | ||
| 580 | first, | ||
| 581 | ) | ||
| 582 | } | ||
| 583 | } | ||
| 584 | _ => None, | ||
| 585 | } | ||
| 586 | } else { | ||
| 587 | return Ok((None, vec![])); | ||
| 588 | }; | ||
| 589 | |||
| 590 | let mut mention_tags = vec![]; | ||
| 591 | for (i, reply_to) in in_reply_to.iter().enumerate() { | ||
| 592 | if i.ne(&0) || root_proposal_id.is_none() { | ||
| 593 | mention_tags.push( | ||
| 594 | event_tag_from_nip19_or_hex( | ||
| 595 | reply_to, | ||
| 596 | "in-reply-to", | ||
| 597 | nostr::Marker::Mention, | ||
| 598 | true, | ||
| 599 | false, | ||
| 600 | ) | ||
| 601 | .context(format!( | ||
| 602 | "{reply_to} in 'in-reply-to' not a valid nostr reference" | ||
| 603 | ))?, | ||
| 604 | ); | ||
| 605 | } | ||
| 606 | } | ||
| 607 | |||
| 608 | Ok((root_proposal_id, mention_tags)) | ||
| 609 | } | ||
| 610 | |||
| 541 | pub static PATCH_KIND: u64 = 1617; | 611 | pub static PATCH_KIND: u64 = 1617; |
| 542 | 612 | ||
| 613 | #[allow(clippy::too_many_lines)] | ||
| 543 | pub fn generate_cover_letter_and_patch_events( | 614 | pub fn generate_cover_letter_and_patch_events( |
| 544 | cover_letter_title_description: Option<(String, String)>, | 615 | cover_letter_title_description: Option<(String, String)>, |
| 545 | git_repo: &Repo, | 616 | git_repo: &Repo, |
| 546 | commits: &[Sha1Hash], | 617 | commits: &[Sha1Hash], |
| 547 | keys: &nostr::Keys, | 618 | keys: &nostr::Keys, |
| 548 | repo_ref: &RepoRef, | 619 | repo_ref: &RepoRef, |
| 549 | in_reply_to: &Option<String>, | 620 | root_proposal_id: &Option<String>, |
| 621 | mentions: &[nostr::Tag], | ||
| 550 | ) -> Result<Vec<nostr::Event>> { | 622 | ) -> Result<Vec<nostr::Event>> { |
| 551 | let root_commit = git_repo | 623 | let root_commit = git_repo |
| 552 | .get_root_commit() | 624 | .get_root_commit() |
| @@ -578,18 +650,19 @@ pub fn generate_cover_letter_and_patch_events( | |||
| 578 | Tag::Reference(format!("{root_commit}")), | 650 | Tag::Reference(format!("{root_commit}")), |
| 579 | Tag::Hashtag("cover-letter".to_string()), | 651 | Tag::Hashtag("cover-letter".to_string()), |
| 580 | ], | 652 | ], |
| 581 | if let Some(event_ref) = in_reply_to.clone() { | 653 | if let Some(event_ref) = root_proposal_id.clone() { |
| 582 | vec![ | 654 | vec![ |
| 583 | Tag::Hashtag("root".to_string()), | 655 | Tag::Hashtag("root".to_string()), |
| 584 | Tag::Hashtag("revision-root".to_string()), | 656 | Tag::Hashtag("revision-root".to_string()), |
| 585 | // TODO check if id is for a root proposal (perhaps its for an issue?) | 657 | // TODO check if id is for a root proposal (perhaps its for an issue?) |
| 586 | e_tag_from_nip19(&event_ref,"proposal",nostr::Marker::Reply)?, | 658 | event_tag_from_nip19_or_hex(&event_ref,"proposal",nostr::Marker::Reply, false, false)?, |
| 587 | ] | 659 | ] |
| 588 | } else { | 660 | } else { |
| 589 | vec![ | 661 | vec![ |
| 590 | Tag::Hashtag("root".to_string()), | 662 | Tag::Hashtag("root".to_string()), |
| 591 | ] | 663 | ] |
| 592 | }, | 664 | }, |
| 665 | mentions.to_vec(), | ||
| 593 | // this is not strictly needed but makes for prettier branch names | 666 | // this is not strictly needed but makes for prettier branch names |
| 594 | // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding | 667 | // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding |
| 595 | // a change like this, or the removal of this tag will require the actual branch name to be tracked | 668 | // a change like this, or the removal of this tag will require the actual branch name to be tracked |
| @@ -651,7 +724,8 @@ pub fn generate_cover_letter_and_patch_events( | |||
| 651 | } else { | 724 | } else { |
| 652 | None | 725 | None |
| 653 | }, | 726 | }, |
| 654 | in_reply_to, | 727 | root_proposal_id, |
| 728 | if events.is_empty() { mentions } else { &[] }, | ||
| 655 | ) | 729 | ) |
| 656 | .context("failed to generate patch event")?, | 730 | .context("failed to generate patch event")?, |
| 657 | ); | 731 | ); |
| @@ -659,19 +733,20 @@ pub fn generate_cover_letter_and_patch_events( | |||
| 659 | Ok(events) | 733 | Ok(events) |
| 660 | } | 734 | } |
| 661 | 735 | ||
| 662 | fn e_tag_from_nip19( | 736 | fn event_tag_from_nip19_or_hex( |
| 663 | reference: &str, | 737 | reference: &str, |
| 664 | reference_name: &str, | 738 | reference_name: &str, |
| 665 | marker: nostr::Marker, | 739 | marker: nostr::Marker, |
| 740 | allow_npub_reference: bool, | ||
| 741 | prompt_for_correction: bool, | ||
| 666 | ) -> Result<nostr::Tag> { | 742 | ) -> Result<nostr::Tag> { |
| 667 | let mut bech32 = reference.to_string(); | 743 | let mut bech32 = reference.to_string(); |
| 668 | loop { | 744 | loop { |
| 669 | if bech32.is_empty() { | 745 | if bech32.is_empty() { |
| 670 | bech32 = Interactor::default().input( | 746 | bech32 = Interactor::default().input( |
| 671 | PromptInputParms::default().with_prompt(&format!("{reference_name} nevent")), | 747 | PromptInputParms::default().with_prompt(&format!("{reference_name} reference")), |
| 672 | )?; | 748 | )?; |
| 673 | } | 749 | } |
| 674 | |||
| 675 | if let Ok(nip19) = Nip19::from_bech32(bech32.clone()) { | 750 | if let Ok(nip19) = Nip19::from_bech32(bech32.clone()) { |
| 676 | match nip19 { | 751 | match nip19 { |
| 677 | Nip19::Event(n) => { | 752 | Nip19::Event(n) => { |
| @@ -688,6 +763,22 @@ fn e_tag_from_nip19( | |||
| 688 | marker: Some(marker), | 763 | marker: Some(marker), |
| 689 | }); | 764 | }); |
| 690 | } | 765 | } |
| 766 | Nip19::Coordinate(coordinate) => { | ||
| 767 | break Ok(nostr::Tag::A { | ||
| 768 | coordinate, | ||
| 769 | relay_url: None, | ||
| 770 | }); | ||
| 771 | } | ||
| 772 | Nip19::Profile(profile) => { | ||
| 773 | if allow_npub_reference { | ||
| 774 | break Ok(nostr::Tag::public_key(profile.public_key)); | ||
| 775 | } | ||
| 776 | } | ||
| 777 | Nip19::Pubkey(public_key) => { | ||
| 778 | if allow_npub_reference { | ||
| 779 | break Ok(nostr::Tag::public_key(public_key)); | ||
| 780 | } | ||
| 781 | } | ||
| 691 | _ => {} | 782 | _ => {} |
| 692 | } | 783 | } |
| 693 | } | 784 | } |
| @@ -698,7 +789,11 @@ fn e_tag_from_nip19( | |||
| 698 | marker: Some(marker), | 789 | marker: Some(marker), |
| 699 | }); | 790 | }); |
| 700 | } | 791 | } |
| 701 | println!("not a valid {reference_name} event reference"); | 792 | if prompt_for_correction { |
| 793 | println!("not a valid {reference_name} event reference"); | ||
| 794 | } else { | ||
| 795 | bail!(format!("not a valid {reference_name} event reference")); | ||
| 796 | } | ||
| 702 | 797 | ||
| 703 | bech32 = String::new(); | 798 | bech32 = String::new(); |
| 704 | } | 799 | } |
| @@ -810,7 +905,8 @@ pub fn generate_patch_event( | |||
| 810 | parent_patch_event_id: Option<nostr::EventId>, | 905 | parent_patch_event_id: Option<nostr::EventId>, |
| 811 | series_count: Option<(u64, u64)>, | 906 | series_count: Option<(u64, u64)>, |
| 812 | branch_name: Option<String>, | 907 | branch_name: Option<String>, |
| 813 | in_reply_to: &Option<String>, | 908 | root_proposal_id: &Option<String>, |
| 909 | mentions: &[nostr::Tag], | ||
| 814 | ) -> Result<nostr::Event> { | 910 | ) -> Result<nostr::Event> { |
| 815 | let commit_parent = git_repo | 911 | let commit_parent = git_repo |
| 816 | .get_commit_parent(commit) | 912 | .get_commit_parent(commit) |
| @@ -849,19 +945,19 @@ pub fn generate_patch_event( | |||
| 849 | relay_url: relay_hint.clone(), | 945 | relay_url: relay_hint.clone(), |
| 850 | marker: Some(Marker::Root), | 946 | marker: Some(Marker::Root), |
| 851 | }] | 947 | }] |
| 852 | } else if let Some(event_ref) = in_reply_to.clone() { | 948 | } else if let Some(event_ref) = root_proposal_id.clone() { |
| 853 | vec![ | 949 | vec![ |
| 854 | Tag::Hashtag("root".to_string()), | 950 | Tag::Hashtag("root".to_string()), |
| 855 | Tag::Hashtag("revision-root".to_string()), | 951 | Tag::Hashtag("revision-root".to_string()), |
| 856 | // TODO check if id is for a root proposal (perhaps its for an issue?) | 952 | // TODO check if id is for a root proposal (perhaps its for an issue?) |
| 857 | e_tag_from_nip19(&event_ref,"proposal",nostr::Marker::Reply)?, | 953 | event_tag_from_nip19_or_hex(&event_ref,"proposal",nostr::Marker::Reply, false, false)?, |
| 858 | ] | 954 | ] |
| 859 | } else { | 955 | } else { |
| 860 | vec![ | 956 | vec![ |
| 861 | Tag::Hashtag("root".to_string()), | 957 | Tag::Hashtag("root".to_string()), |
| 862 | ] | 958 | ] |
| 863 | }, | 959 | }, |
| 864 | 960 | mentions.to_vec(), | |
| 865 | if let Some(id) = parent_patch_event_id { | 961 | if let Some(id) = parent_patch_event_id { |
| 866 | vec![Tag::Event { | 962 | vec![Tag::Event { |
| 867 | event_id: id, | 963 | event_id: id, |
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index ad187be..a693607 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs | |||
| @@ -4,6 +4,7 @@ use anyhow::{bail, ensure, Context, Result}; | |||
| 4 | use dialoguer::theme::{ColorfulTheme, Theme}; | 4 | use dialoguer::theme::{ColorfulTheme, Theme}; |
| 5 | use directories::ProjectDirs; | 5 | use directories::ProjectDirs; |
| 6 | use nostr::{self, Kind, Tag}; | 6 | use nostr::{self, Kind, Tag}; |
| 7 | use nostr_sdk::serde_json; | ||
| 7 | use once_cell::sync::Lazy; | 8 | use once_cell::sync::Lazy; |
| 8 | use rexpect::session::{Options, PtySession}; | 9 | use rexpect::session::{Options, PtySession}; |
| 9 | use strip_ansi_escapes::strip_str; | 10 | use strip_ansi_escapes::strip_str; |
| @@ -178,6 +179,11 @@ pub fn generate_repo_ref_event() -> nostr::Event { | |||
| 178 | .unwrap() | 179 | .unwrap() |
| 179 | } | 180 | } |
| 180 | 181 | ||
| 182 | /// enough to fool event_is_patch_set_root | ||
| 183 | pub fn get_pretend_proposal_root_event() -> nostr::Event { | ||
| 184 | serde_json::from_str(r#"{"id":"8cb75aa4cda10a3a0f3242dc49d36159d30b3185bf63414cf6ce17f5c14a73b1","pubkey":"f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768","created_at":1714984571,"kind":1617,"tags":[["t","root"]],"content":"","sig":"6c197314b8c4c61da696dff888198333004d1ecc5d7bae2c554857f2f2b0d3ecc09369a5d8ba089c1bf89e3c6f5be40ade873fd698438ef8b303ffc6df35eb3f"}"#).unwrap() | ||
| 185 | } | ||
| 186 | |||
| 181 | /// wrapper for a cli testing tool - currently wraps rexpect and dialoguer | 187 | /// wrapper for a cli testing tool - currently wraps rexpect and dialoguer |
| 182 | /// | 188 | /// |
| 183 | /// 1. allow more accurate articulation of expected behaviour | 189 | /// 1. allow more accurate articulation of expected behaviour |
diff --git a/tests/send.rs b/tests/send.rs index a38546a..8474c19 100644 --- a/tests/send.rs +++ b/tests/send.rs | |||
| @@ -1284,9 +1284,14 @@ mod when_range_ommited_prompts_for_selection_defaulting_ahead_of_main { | |||
| 1284 | } | 1284 | } |
| 1285 | } | 1285 | } |
| 1286 | 1286 | ||
| 1287 | mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specified { | 1287 | mod root_proposal_specified_using_in_reply_to_with_range_of_head_2_and_cover_letter_details_specified { |
| 1288 | |||
| 1289 | use nostr::ToBech32; | ||
| 1290 | |||
| 1288 | use super::*; | 1291 | use super::*; |
| 1292 | |||
| 1289 | fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester { | 1293 | fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester { |
| 1294 | let proposal_root_bech32 = get_pretend_proposal_root_event().id.to_bech32().unwrap(); | ||
| 1290 | let args = vec![ | 1295 | let args = vec![ |
| 1291 | "--nsec", | 1296 | "--nsec", |
| 1292 | TEST_KEY_1_NSEC, | 1297 | TEST_KEY_1_NSEC, |
| @@ -1296,7 +1301,8 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1296 | "send", | 1301 | "send", |
| 1297 | "HEAD~2", | 1302 | "HEAD~2", |
| 1298 | "--in-reply-to", | 1303 | "--in-reply-to", |
| 1299 | "nevent1qqsypm62fzw7qynvlc4gjl3tr0jw4vmh659nvr2cc5qyhdg92a5yy0qzypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesxygzam", | 1304 | &proposal_root_bech32, |
| 1305 | // "nevent1qqsged665nx6zz36puey9hzf6ds4n5ctxxzm7c6pfnmvu9l4c9988vgzyr6nuj7d02wdauzfeajx043c5yepjk9v6wm3avycy07kltdsy0tksh0zxyx", | ||
| 1300 | "--title", | 1306 | "--title", |
| 1301 | "exampletitle", | 1307 | "exampletitle", |
| 1302 | "--description", | 1308 | "--description", |
| @@ -1305,7 +1311,12 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1305 | CliTester::new_from_dir(&git_repo.dir, args) | 1311 | CliTester::new_from_dir(&git_repo.dir, args) |
| 1306 | } | 1312 | } |
| 1307 | fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> { | 1313 | fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> { |
| 1308 | p.expect("creating proposal revision for: nevent1qqsypm62fzw7qynvlc4gjl3tr0jw4vmh659nvr2cc5qyhdg92a5yy0qzypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesxygzam\r\n")?; | 1314 | let proposal_root_bech32 = get_pretend_proposal_root_event().id.to_bech32().unwrap(); |
| 1315 | p.expect(format!( | ||
| 1316 | "creating proposal revision for: {}\r\n", | ||
| 1317 | proposal_root_bech32, | ||
| 1318 | ))?; | ||
| 1319 | // p.expect("creating proposal revision for: nevent1qqsged665nx6zz36puey9hzf6ds4n5ctxxzm7c6pfnmvu9l4c9988vgzyr6nuj7d02wdauzfeajx043c5yepjk9v6wm3avycy07kltdsy0tksh0zxyx\r\n")?; | ||
| 1309 | p.expect("creating proposal from 2 commits:\r\n")?; | 1320 | p.expect("creating proposal from 2 commits:\r\n")?; |
| 1310 | p.expect("fe973a8 add t4.md\r\n")?; | 1321 | p.expect("fe973a8 add t4.md\r\n")?; |
| 1311 | p.expect("232efb3 add t3.md\r\n")?; | 1322 | p.expect("232efb3 add t3.md\r\n")?; |
| @@ -1343,6 +1354,7 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1343 | &vec![ | 1354 | &vec![ |
| 1344 | generate_test_key_1_metadata_event("fred"), | 1355 | generate_test_key_1_metadata_event("fred"), |
| 1345 | generate_test_key_1_relay_list_event(), | 1356 | generate_test_key_1_relay_list_event(), |
| 1357 | get_pretend_proposal_root_event(), | ||
| 1346 | ], | 1358 | ], |
| 1347 | )?; | 1359 | )?; |
| 1348 | Ok(()) | 1360 | Ok(()) |
| @@ -1357,7 +1369,7 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1357 | relay.respond_events( | 1369 | relay.respond_events( |
| 1358 | client_id, | 1370 | client_id, |
| 1359 | &subscription_id, | 1371 | &subscription_id, |
| 1360 | &vec![generate_repo_ref_event()], | 1372 | &vec![generate_repo_ref_event(), get_pretend_proposal_root_event()], |
| 1361 | )?; | 1373 | )?; |
| 1362 | Ok(()) | 1374 | Ok(()) |
| 1363 | }), | 1375 | }), |
| @@ -1405,6 +1417,7 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1405 | &vec![ | 1417 | &vec![ |
| 1406 | generate_test_key_1_metadata_event("fred"), | 1418 | generate_test_key_1_metadata_event("fred"), |
| 1407 | generate_test_key_1_relay_list_event(), | 1419 | generate_test_key_1_relay_list_event(), |
| 1420 | get_pretend_proposal_root_event(), | ||
| 1408 | ], | 1421 | ], |
| 1409 | )?; | 1422 | )?; |
| 1410 | Ok(()) | 1423 | Ok(()) |
| @@ -1419,7 +1432,7 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1419 | relay.respond_events( | 1432 | relay.respond_events( |
| 1420 | client_id, | 1433 | client_id, |
| 1421 | &subscription_id, | 1434 | &subscription_id, |
| 1422 | &vec![generate_repo_ref_event()], | 1435 | &vec![generate_repo_ref_event(), get_pretend_proposal_root_event()], |
| 1423 | )?; | 1436 | )?; |
| 1424 | Ok(()) | 1437 | Ok(()) |
| 1425 | }), | 1438 | }), |
| @@ -1430,7 +1443,6 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1430 | // // check relay had the right number of events | 1443 | // // check relay had the right number of events |
| 1431 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 1444 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 1432 | let mut p = cli_tester_create_proposal(&git_repo); | 1445 | let mut p = cli_tester_create_proposal(&git_repo); |
| 1433 | |||
| 1434 | expect_msgs_first(&mut p, true)?; | 1446 | expect_msgs_first(&mut p, true)?; |
| 1435 | relay::expect_send_with_progress( | 1447 | relay::expect_send_with_progress( |
| 1436 | &mut p, | 1448 | &mut p, |
| @@ -1516,7 +1528,7 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1516 | .unwrap() | 1528 | .unwrap() |
| 1517 | .as_vec()[1], | 1529 | .as_vec()[1], |
| 1518 | // id of state nevent | 1530 | // id of state nevent |
| 1519 | "40ef4a489de0126cfe2a897e2b1be4eab377d50b360d58c5004bb5055768423c", | 1531 | "8cb75aa4cda10a3a0f3242dc49d36159d30b3185bf63414cf6ce17f5c14a73b1", |
| 1520 | ); | 1532 | ); |
| 1521 | } | 1533 | } |
| 1522 | Ok(()) | 1534 | Ok(()) |
| @@ -1552,3 +1564,251 @@ mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specifie | |||
| 1552 | Ok(()) | 1564 | Ok(()) |
| 1553 | } | 1565 | } |
| 1554 | } | 1566 | } |
| 1567 | |||
| 1568 | mod in_reply_to_mentions_issue { | ||
| 1569 | use nostr::ToBech32; | ||
| 1570 | |||
| 1571 | use super::*; | ||
| 1572 | pub fn get_pretend_issue_event() -> nostr::Event { | ||
| 1573 | serde_json::from_str(r#"{"created_at":1709286372,"content":"please provide feedback\nthis is an example ngit issue to demonstrate gitworkshop.dev.\n\nplease provide feedback with in reply to this issue or by creating a new issue.","tags":[["r","26689f97810fc656c7134c76e2a37d33b2e40ce7"],["a","30617:a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d:ngit","wss://relay.damus.io","root"],["p","a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d"]],"kind":1621,"pubkey":"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d","id":"e944765d625ae7323d080da0df069c726a0e5490a17b452f854d85e18f781588","sig":"a1af9e89a35f1f7ef93e3de33986bd86cb7c4d7d9abb233c0c6405f32b5788171e47f84551afe8515b3107d12f03472721ea784b8791ff3f25e66a3169a54c20"}"#).unwrap() | ||
| 1574 | } | ||
| 1575 | |||
| 1576 | fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester { | ||
| 1577 | let proposal_root_bech32 = get_pretend_issue_event().id.to_bech32().unwrap(); | ||
| 1578 | let args = vec![ | ||
| 1579 | "--nsec", | ||
| 1580 | TEST_KEY_1_NSEC, | ||
| 1581 | "--password", | ||
| 1582 | TEST_PASSWORD, | ||
| 1583 | "--disable-cli-spinners", | ||
| 1584 | "send", | ||
| 1585 | "HEAD~2", | ||
| 1586 | "--in-reply-to", | ||
| 1587 | &proposal_root_bech32, | ||
| 1588 | // "note1a9z8vhtzttnny0ggpksd7p5uwf4qu4ys59a52tu9fkz7rrmczkyqc46ngg", | ||
| 1589 | "--title", | ||
| 1590 | "exampletitle", | ||
| 1591 | "--description", | ||
| 1592 | "exampledescription", | ||
| 1593 | ]; | ||
| 1594 | CliTester::new_from_dir(&git_repo.dir, args) | ||
| 1595 | } | ||
| 1596 | |||
| 1597 | async fn prep_run_create_proposal() -> Result<( | ||
| 1598 | Relay<'static>, | ||
| 1599 | Relay<'static>, | ||
| 1600 | Relay<'static>, | ||
| 1601 | Relay<'static>, | ||
| 1602 | Relay<'static>, | ||
| 1603 | )> { | ||
| 1604 | let git_repo = prep_git_repo()?; | ||
| 1605 | // fallback (51,52) user write (53, 55) repo (55, 56) | ||
| 1606 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 1607 | Relay::new( | ||
| 1608 | 8051, | ||
| 1609 | None, | ||
| 1610 | Some(&|relay, client_id, subscription_id, _| -> Result<()> { | ||
| 1611 | relay.respond_events( | ||
| 1612 | client_id, | ||
| 1613 | &subscription_id, | ||
| 1614 | &vec![ | ||
| 1615 | generate_test_key_1_metadata_event("fred"), | ||
| 1616 | generate_test_key_1_relay_list_event(), | ||
| 1617 | get_pretend_issue_event(), | ||
| 1618 | ], | ||
| 1619 | )?; | ||
| 1620 | Ok(()) | ||
| 1621 | }), | ||
| 1622 | ), | ||
| 1623 | Relay::new(8052, None, None), | ||
| 1624 | Relay::new(8053, None, None), | ||
| 1625 | Relay::new( | ||
| 1626 | 8055, | ||
| 1627 | None, | ||
| 1628 | Some(&|relay, client_id, subscription_id, _| -> Result<()> { | ||
| 1629 | relay.respond_events( | ||
| 1630 | client_id, | ||
| 1631 | &subscription_id, | ||
| 1632 | &vec![generate_repo_ref_event(), get_pretend_issue_event()], | ||
| 1633 | )?; | ||
| 1634 | Ok(()) | ||
| 1635 | }), | ||
| 1636 | ), | ||
| 1637 | Relay::new(8056, None, None), | ||
| 1638 | ); | ||
| 1639 | |||
| 1640 | // // check relay had the right number of events | ||
| 1641 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 1642 | let mut p = cli_tester_create_proposal(&git_repo); | ||
| 1643 | p.expect_end_eventually()?; | ||
| 1644 | for p in [51, 52, 53, 55, 56] { | ||
| 1645 | relay::shutdown_relay(8000 + p)?; | ||
| 1646 | } | ||
| 1647 | Ok(()) | ||
| 1648 | }); | ||
| 1649 | |||
| 1650 | // launch relay | ||
| 1651 | let _ = join!( | ||
| 1652 | r51.listen_until_close(), | ||
| 1653 | r52.listen_until_close(), | ||
| 1654 | r53.listen_until_close(), | ||
| 1655 | r55.listen_until_close(), | ||
| 1656 | r56.listen_until_close(), | ||
| 1657 | ); | ||
| 1658 | cli_tester_handle.join().unwrap()?; | ||
| 1659 | Ok((r51, r52, r53, r55, r56)) | ||
| 1660 | } | ||
| 1661 | |||
| 1662 | #[tokio::test] | ||
| 1663 | #[serial] | ||
| 1664 | async fn issue_event_mentioned_in_tagged_cover_letter() -> Result<()> { | ||
| 1665 | let (_, _, r53, r55, r56) = prep_run_create_proposal().await?; | ||
| 1666 | for relay in [&r53, &r55, &r56] { | ||
| 1667 | let cover_letter_event: &nostr::Event = | ||
| 1668 | relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); | ||
| 1669 | assert!(cover_letter_event.iter_tags().any(|t| { | ||
| 1670 | t.as_vec()[0].eq("e") | ||
| 1671 | && t.as_vec()[1].eq(&get_pretend_issue_event().id.to_hex()) | ||
| 1672 | && t.as_vec()[3].eq(&"mention") | ||
| 1673 | })); | ||
| 1674 | } | ||
| 1675 | Ok(()) | ||
| 1676 | } | ||
| 1677 | |||
| 1678 | #[tokio::test] | ||
| 1679 | #[serial] | ||
| 1680 | async fn isnt_tagged_as_revision() -> Result<()> { | ||
| 1681 | let (_, _, r53, r55, r56) = prep_run_create_proposal().await?; | ||
| 1682 | for relay in [&r53, &r55, &r56] { | ||
| 1683 | let cover_letter_event: &nostr::Event = | ||
| 1684 | relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); | ||
| 1685 | assert!( | ||
| 1686 | !cover_letter_event | ||
| 1687 | .iter_tags() | ||
| 1688 | .any(|t| { t.as_vec()[0].eq("t") && t.as_vec()[1].eq(&"revision-root") }) | ||
| 1689 | ); | ||
| 1690 | } | ||
| 1691 | Ok(()) | ||
| 1692 | } | ||
| 1693 | } | ||
| 1694 | mod in_reply_to_mentions_npub_and_nprofile_which_get_mentioned_in_proposal_root { | ||
| 1695 | use nostr::JsonUtil; | ||
| 1696 | |||
| 1697 | use super::*; | ||
| 1698 | |||
| 1699 | fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester { | ||
| 1700 | let args = vec![ | ||
| 1701 | "--nsec", | ||
| 1702 | TEST_KEY_1_NSEC, | ||
| 1703 | "--password", | ||
| 1704 | TEST_PASSWORD, | ||
| 1705 | "--disable-cli-spinners", | ||
| 1706 | "send", | ||
| 1707 | "HEAD~2", | ||
| 1708 | "--in-reply-to", | ||
| 1709 | // nsec1q3c5xnsm5m4wgsrhwnz04p0d5mevkryyggqgdpa9jwulpq9gldhswgtxvq | ||
| 1710 | "npub1knxeegzqg0xqflsryvg7l7x7nmpe7kd7pl7zazug0a7t99tdsphszuyapx", | ||
| 1711 | // nsec1nx5ulvcndhcuu8k6q8fenw50l6y75sec7pj8vr0r68l6a44w3lqspvj02k | ||
| 1712 | "nprofile1qqsvru3yqrec6dxjn06f8cjh79jcu9wyaxu4y6v47yzpsx7vjm4xcuc33z2n3", | ||
| 1713 | "--title", | ||
| 1714 | "exampletitle", | ||
| 1715 | "--description", | ||
| 1716 | "exampledescription", | ||
| 1717 | ]; | ||
| 1718 | CliTester::new_from_dir(&git_repo.dir, args) | ||
| 1719 | } | ||
| 1720 | |||
| 1721 | async fn prep_run_create_proposal() -> Result<( | ||
| 1722 | Relay<'static>, | ||
| 1723 | Relay<'static>, | ||
| 1724 | Relay<'static>, | ||
| 1725 | Relay<'static>, | ||
| 1726 | Relay<'static>, | ||
| 1727 | )> { | ||
| 1728 | let git_repo = prep_git_repo()?; | ||
| 1729 | // fallback (51,52) user write (53, 55) repo (55, 56) | ||
| 1730 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 1731 | Relay::new( | ||
| 1732 | 8051, | ||
| 1733 | None, | ||
| 1734 | Some(&|relay, client_id, subscription_id, _| -> Result<()> { | ||
| 1735 | relay.respond_events( | ||
| 1736 | client_id, | ||
| 1737 | &subscription_id, | ||
| 1738 | &vec![ | ||
| 1739 | generate_test_key_1_metadata_event("fred"), | ||
| 1740 | generate_test_key_1_relay_list_event(), | ||
| 1741 | ], | ||
| 1742 | )?; | ||
| 1743 | Ok(()) | ||
| 1744 | }), | ||
| 1745 | ), | ||
| 1746 | Relay::new(8052, None, None), | ||
| 1747 | Relay::new(8053, None, None), | ||
| 1748 | Relay::new( | ||
| 1749 | 8055, | ||
| 1750 | None, | ||
| 1751 | Some(&|relay, client_id, subscription_id, _| -> Result<()> { | ||
| 1752 | relay.respond_events( | ||
| 1753 | client_id, | ||
| 1754 | &subscription_id, | ||
| 1755 | &vec![generate_repo_ref_event()], | ||
| 1756 | )?; | ||
| 1757 | Ok(()) | ||
| 1758 | }), | ||
| 1759 | ), | ||
| 1760 | Relay::new(8056, None, None), | ||
| 1761 | ); | ||
| 1762 | |||
| 1763 | // // check relay had the right number of events | ||
| 1764 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 1765 | let mut p = cli_tester_create_proposal(&git_repo); | ||
| 1766 | p.expect_end_eventually()?; | ||
| 1767 | for p in [51, 52, 53, 55, 56] { | ||
| 1768 | relay::shutdown_relay(8000 + p)?; | ||
| 1769 | } | ||
| 1770 | Ok(()) | ||
| 1771 | }); | ||
| 1772 | |||
| 1773 | // launch relay | ||
| 1774 | let _ = join!( | ||
| 1775 | r51.listen_until_close(), | ||
| 1776 | r52.listen_until_close(), | ||
| 1777 | r53.listen_until_close(), | ||
| 1778 | r55.listen_until_close(), | ||
| 1779 | r56.listen_until_close(), | ||
| 1780 | ); | ||
| 1781 | cli_tester_handle.join().unwrap()?; | ||
| 1782 | Ok((r51, r52, r53, r55, r56)) | ||
| 1783 | } | ||
| 1784 | |||
| 1785 | #[tokio::test] | ||
| 1786 | #[serial] | ||
| 1787 | async fn npub_and_nprofile_mentioned_in_tagged_cover_letter() -> Result<()> { | ||
| 1788 | let (_, _, r53, r55, r56) = prep_run_create_proposal().await?; | ||
| 1789 | for relay in [&r53, &r55, &r56] { | ||
| 1790 | let cover_letter_event: &nostr::Event = | ||
| 1791 | relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); | ||
| 1792 | println!("{:?}", &cover_letter_event.as_json()); | ||
| 1793 | assert!(cover_letter_event.iter_tags().any(|t| { | ||
| 1794 | t.as_vec()[0].eq("p") | ||
| 1795 | && t.as_vec()[1].eq(&nostr::Keys::parse( | ||
| 1796 | "nsec1q3c5xnsm5m4wgsrhwnz04p0d5mevkryyggqgdpa9jwulpq9gldhswgtxvq", | ||
| 1797 | ) | ||
| 1798 | .unwrap() | ||
| 1799 | .public_key() | ||
| 1800 | .to_hex()) | ||
| 1801 | })); | ||
| 1802 | assert!(cover_letter_event.iter_tags().any(|t| { | ||
| 1803 | t.as_vec()[0].eq("p") | ||
| 1804 | && t.as_vec()[1].eq(&nostr::Keys::parse( | ||
| 1805 | "nsec1nx5ulvcndhcuu8k6q8fenw50l6y75sec7pj8vr0r68l6a44w3lqspvj02k", | ||
| 1806 | ) | ||
| 1807 | .unwrap() | ||
| 1808 | .public_key() | ||
| 1809 | .to_hex()) | ||
| 1810 | })); | ||
| 1811 | } | ||
| 1812 | Ok(()) | ||
| 1813 | } | ||
| 1814 | } | ||