upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/sub_commands/prs/create.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-02-13 06:27:34 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-02-13 06:27:34 +0000
commit3112576195aef212622d27ad9164336796c1953e (patch)
tree0e897bcb8309c2d4c8f33d7c1590c2dcc0577508 /src/sub_commands/prs/create.rs
parent9cd3e43b899b23b7f6e75276fa3d19bf9550f8fd (diff)
feat(prs-create)!: pr to nip34-like cover letter
up the pr event type to a nip34-like cover letter format this sets the building blocks in place to enable simplier clients to use the 'cover letter' feature in `git format-patch` to create the experience as a pr event
Diffstat (limited to 'src/sub_commands/prs/create.rs')
-rw-r--r--src/sub_commands/prs/create.rs240
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)]
22pub struct SubCommandArgs { 22pub 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)]
37pub async fn launch( 38pub 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;
308pub fn generate_pr_and_patch_events( 331pub 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
410pub struct CoverLetter {
411 pub title: String,
412 pub description: String,
413 pub branch_name: Option<String>,
414}
415
416fn 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}
419pub 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)]
370pub fn generate_patch_event( 444pub 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}