diff options
Diffstat (limited to 'src/sub_commands/prs/create.rs')
| -rw-r--r-- | src/sub_commands/prs/create.rs | 240 |
1 files changed, 211 insertions, 29 deletions
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs index 8506303..83a3942 100644 --- a/src/sub_commands/prs/create.rs +++ b/src/sub_commands/prs/create.rs | |||
| @@ -21,10 +21,10 @@ use crate::{ | |||
| 21 | #[derive(Debug, clap::Args)] | 21 | #[derive(Debug, clap::Args)] |
| 22 | pub struct SubCommandArgs { | 22 | pub struct SubCommandArgs { |
| 23 | #[clap(short, long)] | 23 | #[clap(short, long)] |
| 24 | /// title of pull request (defaults to first line of first commit) | 24 | /// optional cover letter title |
| 25 | title: Option<String>, | 25 | title: Option<String>, |
| 26 | #[clap(short, long)] | 26 | #[clap(short, long)] |
| 27 | /// optional description | 27 | /// optional cover letter description |
| 28 | description: Option<String>, | 28 | description: Option<String>, |
| 29 | #[clap(long)] | 29 | #[clap(long)] |
| 30 | /// branch to get changes from (defaults to head) | 30 | /// branch to get changes from (defaults to head) |
| @@ -34,6 +34,7 @@ pub struct SubCommandArgs { | |||
| 34 | to_branch: Option<String>, | 34 | to_branch: Option<String>, |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | #[allow(clippy::too_many_lines)] | ||
| 37 | pub async fn launch( | 38 | pub async fn launch( |
| 38 | cli_args: &Cli, | 39 | cli_args: &Cli, |
| 39 | _pr_args: &super::SubCommandArgs, | 40 | _pr_args: &super::SubCommandArgs, |
| @@ -91,6 +92,21 @@ pub async fn launch( | |||
| 91 | .input(PromptInputParms::default().with_prompt("description (Optional)"))?, | 92 | .input(PromptInputParms::default().with_prompt("description (Optional)"))?, |
| 92 | }; | 93 | }; |
| 93 | 94 | ||
| 95 | // let cover_letter_title_description = if let Some(title) = title { | ||
| 96 | // Some(( | ||
| 97 | // title, | ||
| 98 | // if let Some(t) = &args.description { | ||
| 99 | // t.clone() | ||
| 100 | // } else { | ||
| 101 | // Interactor::default() | ||
| 102 | // .input(PromptInputParms::default().with_prompt("cover letter | ||
| 103 | // description"))? .clone() | ||
| 104 | // }, | ||
| 105 | // )) | ||
| 106 | // } else { | ||
| 107 | // None | ||
| 108 | // }; | ||
| 109 | |||
| 94 | #[cfg(not(test))] | 110 | #[cfg(not(test))] |
| 95 | let mut client = Client::default(); | 111 | let mut client = Client::default(); |
| 96 | #[cfg(test)] | 112 | #[cfg(test)] |
| @@ -111,8 +127,15 @@ pub async fn launch( | |||
| 111 | ) | 127 | ) |
| 112 | .await?; | 128 | .await?; |
| 113 | 129 | ||
| 114 | let events = | 130 | let events = generate_pr_and_patch_events( |
| 115 | generate_pr_and_patch_events(&title, &description, &git_repo, &ahead, &keys, &repo_ref)?; | 131 | // cover_letter_title_description, |
| 132 | &title, | ||
| 133 | &description, | ||
| 134 | &git_repo, | ||
| 135 | &ahead, | ||
| 136 | &keys, | ||
| 137 | &repo_ref, | ||
| 138 | )?; | ||
| 116 | 139 | ||
| 117 | println!( | 140 | println!( |
| 118 | "posting 1 pull request with {} commits...", | 141 | "posting 1 pull request with {} commits...", |
| @@ -308,6 +331,7 @@ pub static PATCH_KIND: u64 = 1617; | |||
| 308 | pub fn generate_pr_and_patch_events( | 331 | pub fn generate_pr_and_patch_events( |
| 309 | title: &str, | 332 | title: &str, |
| 310 | description: &str, | 333 | description: &str, |
| 334 | // cover_letter_title_description: Option<(String, String)>, | ||
| 311 | git_repo: &Repo, | 335 | git_repo: &Repo, |
| 312 | commits: &Vec<Sha1Hash>, | 336 | commits: &Vec<Sha1Hash>, |
| 313 | keys: &nostr::Keys, | 337 | keys: &nostr::Keys, |
| @@ -317,40 +341,57 @@ pub fn generate_pr_and_patch_events( | |||
| 317 | .get_root_commit() | 341 | .get_root_commit() |
| 318 | .context("failed to get root commit of the repository")?; | 342 | .context("failed to get root commit of the repository")?; |
| 319 | 343 | ||
| 320 | let mut pr_tags = vec![ | 344 | let mut events = vec![]; |
| 321 | Tag::Reference(format!("r-{root_commit}")), | ||
| 322 | Tag::Name(title.to_string()), | ||
| 323 | Tag::Description(description.to_string()), | ||
| 324 | ]; | ||
| 325 | |||
| 326 | if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { | ||
| 327 | pr_tags.push(Tag::Generic( | ||
| 328 | TagKind::Custom("branch-name".to_string()), | ||
| 329 | vec![branch_name], | ||
| 330 | )); | ||
| 331 | } | ||
| 332 | 345 | ||
| 333 | let pr_event = EventBuilder::new( | 346 | // if let Some((title, description)) = cover_letter_title_description { |
| 347 | if !title.is_empty() { | ||
| 348 | events.push(EventBuilder::new( | ||
| 334 | nostr::event::Kind::Custom(PR_KIND), | 349 | nostr::event::Kind::Custom(PR_KIND), |
| 335 | format!("{title}\r\n\r\n{description}"), | 350 | format!( |
| 336 | pr_tags, | 351 | "From {} Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/{}] {title}\n\n{description}", |
| 337 | // TODO: add Repo event as root | 352 | commits.last().unwrap(), |
| 338 | // TODO: people tag maintainers | 353 | commits.len() |
| 339 | // TODO: add relay tags | 354 | ), |
| 355 | [ | ||
| 356 | vec![ | ||
| 357 | // TODO: why not tag all maintainer identifiers? | ||
| 358 | Tag::A { | ||
| 359 | kind: nostr::Kind::Custom(REPO_REF_KIND), | ||
| 360 | public_key: *repo_ref.maintainers.first() | ||
| 361 | .context("repo reference should always have at least one maintainer - the issuer of the repo event") | ||
| 362 | ?, | ||
| 363 | identifier: repo_ref.identifier.to_string(), | ||
| 364 | relay_url: repo_ref.relays.first().map(nostr::UncheckedUrl::from).clone(), | ||
| 365 | }, | ||
| 366 | Tag::Reference(format!("{root_commit}")), | ||
| 367 | Tag::Hashtag("cover-letter".to_string()), | ||
| 368 | Tag::Hashtag("root".to_string()), | ||
| 369 | ], | ||
| 370 | if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { | ||
| 371 | vec![Tag::Generic( | ||
| 372 | TagKind::Custom("branch-name".to_string()), | ||
| 373 | vec![branch_name], | ||
| 374 | )] | ||
| 375 | } else { | ||
| 376 | vec![] | ||
| 377 | }, | ||
| 378 | repo_ref.maintainers | ||
| 379 | .iter() | ||
| 380 | .map(|pk| Tag::public_key(*pk)) | ||
| 381 | .collect(), | ||
| 382 | ].concat(), | ||
| 340 | ) | 383 | ) |
| 341 | .to_event(keys) | 384 | .to_event(keys) |
| 342 | .context("failed to create pr event")?; | 385 | .context("failed to create cover-letter event")?); |
| 343 | 386 | } | |
| 344 | let pr_event_id = pr_event.id; | ||
| 345 | 387 | ||
| 346 | let mut events = vec![pr_event]; | ||
| 347 | for (i, commit) in commits.iter().enumerate() { | 388 | for (i, commit) in commits.iter().enumerate() { |
| 348 | events.push( | 389 | events.push( |
| 349 | generate_patch_event( | 390 | generate_patch_event( |
| 350 | git_repo, | 391 | git_repo, |
| 351 | &root_commit, | 392 | &root_commit, |
| 352 | commit, | 393 | commit, |
| 353 | pr_event_id, | 394 | events.first().map(|event| event.id), |
| 354 | keys, | 395 | keys, |
| 355 | repo_ref, | 396 | repo_ref, |
| 356 | events.last().map(nostr::Event::id), | 397 | events.last().map(nostr::Event::id), |
| @@ -366,12 +407,45 @@ pub fn generate_pr_and_patch_events( | |||
| 366 | Ok(events) | 407 | Ok(events) |
| 367 | } | 408 | } |
| 368 | 409 | ||
| 410 | pub struct CoverLetter { | ||
| 411 | pub title: String, | ||
| 412 | pub description: String, | ||
| 413 | pub branch_name: Option<String>, | ||
| 414 | } | ||
| 415 | |||
| 416 | fn event_is_cover_letter(event: &nostr::Event) -> bool { | ||
| 417 | event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) | ||
| 418 | } | ||
| 419 | pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { | ||
| 420 | if !event_is_cover_letter(event) { | ||
| 421 | bail!("event is not a cover letter") | ||
| 422 | } | ||
| 423 | let title_index = event | ||
| 424 | .content | ||
| 425 | .find("] ") | ||
| 426 | .context("event is not formatted as a cover letter patch")? | ||
| 427 | + 2; | ||
| 428 | let description_index = event.content[title_index..] | ||
| 429 | .find('\n') | ||
| 430 | .unwrap_or(event.content.len() - 1 - title_index) | ||
| 431 | + title_index; | ||
| 432 | |||
| 433 | Ok(CoverLetter { | ||
| 434 | title: event.content[title_index..description_index].to_string(), | ||
| 435 | description: event.content[description_index..].trim().to_string(), | ||
| 436 | branch_name: event | ||
| 437 | .iter_tags() | ||
| 438 | .find(|t| t.as_vec()[0].eq("branch-name")) | ||
| 439 | .map(|tag| tag.as_vec()[1].clone()), | ||
| 440 | }) | ||
| 441 | } | ||
| 442 | |||
| 369 | #[allow(clippy::too_many_arguments)] | 443 | #[allow(clippy::too_many_arguments)] |
| 370 | pub fn generate_patch_event( | 444 | pub fn generate_patch_event( |
| 371 | git_repo: &Repo, | 445 | git_repo: &Repo, |
| 372 | root_commit: &Sha1Hash, | 446 | root_commit: &Sha1Hash, |
| 373 | commit: &Sha1Hash, | 447 | commit: &Sha1Hash, |
| 374 | thread_event_id: nostr::EventId, | 448 | thread_event_id: Option<nostr::EventId>, |
| 375 | keys: &nostr::Keys, | 449 | keys: &nostr::Keys, |
| 376 | repo_ref: &RepoRef, | 450 | repo_ref: &RepoRef, |
| 377 | parent_patch_event_id: Option<nostr::EventId>, | 451 | parent_patch_event_id: Option<nostr::EventId>, |
| @@ -404,10 +478,13 @@ pub fn generate_patch_event( | |||
| 404 | // the commit id is correct | 478 | // the commit id is correct |
| 405 | Tag::Reference(commit.to_string()), | 479 | Tag::Reference(commit.to_string()), |
| 406 | 480 | ||
| 407 | Tag::Event { | 481 | if let Some(thread_event_id) = thread_event_id { Tag::Event { |
| 408 | event_id: thread_event_id, | 482 | event_id: thread_event_id, |
| 409 | relay_url: relay_hint.clone(), | 483 | relay_url: relay_hint.clone(), |
| 410 | marker: Some(Marker::Root), | 484 | marker: Some(Marker::Root), |
| 485 | } } | ||
| 486 | else { | ||
| 487 | Tag::Hashtag("root".to_string()) | ||
| 411 | }, | 488 | }, |
| 412 | ], | 489 | ], |
| 413 | if let Some(id) = parent_patch_event_id { | 490 | if let Some(id) = parent_patch_event_id { |
| @@ -686,4 +763,109 @@ mod tests { | |||
| 686 | Ok(()) | 763 | Ok(()) |
| 687 | } | 764 | } |
| 688 | } | 765 | } |
| 766 | |||
| 767 | mod event_to_cover_letter { | ||
| 768 | use super::*; | ||
| 769 | |||
| 770 | fn generate_cover_letter(title: &str, description: &str) -> Result<nostr::Event> { | ||
| 771 | Ok(nostr::event::EventBuilder::new( | ||
| 772 | nostr::event::Kind::Custom(PR_KIND), | ||
| 773 | format!("From ea897e987ea9a7a98e7a987e97987ea98e7a3334 Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/2] {title}\n\n{description}"), | ||
| 774 | [ | ||
| 775 | Tag::Hashtag("cover-letter".to_string()), | ||
| 776 | Tag::Hashtag("root".to_string()), | ||
| 777 | ], | ||
| 778 | ) | ||
| 779 | .to_event(&nostr::Keys::generate())?) | ||
| 780 | } | ||
| 781 | |||
| 782 | #[test] | ||
| 783 | fn basic_title() -> Result<()> { | ||
| 784 | assert_eq!( | ||
| 785 | event_to_cover_letter(&generate_cover_letter("the title", "description here")?)? | ||
| 786 | .title, | ||
| 787 | "the title", | ||
| 788 | ); | ||
| 789 | Ok(()) | ||
| 790 | } | ||
| 791 | |||
| 792 | #[test] | ||
| 793 | fn basic_description() -> Result<()> { | ||
| 794 | assert_eq!( | ||
| 795 | event_to_cover_letter(&generate_cover_letter("the title", "description here")?)? | ||
| 796 | .description, | ||
| 797 | "description here", | ||
| 798 | ); | ||
| 799 | Ok(()) | ||
| 800 | } | ||
| 801 | |||
| 802 | #[test] | ||
| 803 | fn description_trimmed() -> Result<()> { | ||
| 804 | assert_eq!( | ||
| 805 | event_to_cover_letter(&generate_cover_letter( | ||
| 806 | "the title", | ||
| 807 | " \n \ndescription here\n\n " | ||
| 808 | )?)? | ||
| 809 | .description, | ||
| 810 | "description here", | ||
| 811 | ); | ||
| 812 | Ok(()) | ||
| 813 | } | ||
| 814 | |||
| 815 | #[test] | ||
| 816 | fn multi_line_description() -> Result<()> { | ||
| 817 | assert_eq!( | ||
| 818 | event_to_cover_letter(&generate_cover_letter( | ||
| 819 | "the title", | ||
| 820 | "description here\n\nmore here\nmore" | ||
| 821 | )?)? | ||
| 822 | .description, | ||
| 823 | "description here\n\nmore here\nmore", | ||
| 824 | ); | ||
| 825 | Ok(()) | ||
| 826 | } | ||
| 827 | |||
| 828 | #[test] | ||
| 829 | fn new_lines_in_title_forms_part_of_description() -> Result<()> { | ||
| 830 | assert_eq!( | ||
| 831 | event_to_cover_letter(&generate_cover_letter( | ||
| 832 | "the title\nwith new line", | ||
| 833 | "description here\n\nmore here\nmore" | ||
| 834 | )?)? | ||
| 835 | .title, | ||
| 836 | "the title", | ||
| 837 | ); | ||
| 838 | assert_eq!( | ||
| 839 | event_to_cover_letter(&generate_cover_letter( | ||
| 840 | "the title\nwith new line", | ||
| 841 | "description here\n\nmore here\nmore" | ||
| 842 | )?)? | ||
| 843 | .description, | ||
| 844 | "with new line\n\ndescription here\n\nmore here\nmore", | ||
| 845 | ); | ||
| 846 | Ok(()) | ||
| 847 | } | ||
| 848 | |||
| 849 | mod blank_description { | ||
| 850 | use super::*; | ||
| 851 | |||
| 852 | #[test] | ||
| 853 | fn title_correct() -> Result<()> { | ||
| 854 | assert_eq!( | ||
| 855 | event_to_cover_letter(&generate_cover_letter("the title", "")?)?.title, | ||
| 856 | "the title", | ||
| 857 | ); | ||
| 858 | Ok(()) | ||
| 859 | } | ||
| 860 | |||
| 861 | #[test] | ||
| 862 | fn description_is_empty_string() -> Result<()> { | ||
| 863 | assert_eq!( | ||
| 864 | event_to_cover_letter(&generate_cover_letter("the title", "")?)?.description, | ||
| 865 | "", | ||
| 866 | ); | ||
| 867 | Ok(()) | ||
| 868 | } | ||
| 869 | } | ||
| 870 | } | ||
| 689 | } | 871 | } |