diff options
Diffstat (limited to 'src')
| -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 |
3 files changed, 128 insertions, 29 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, |