upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/sub_commands/prs
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
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')
-rw-r--r--src/sub_commands/prs/create.rs240
-rw-r--r--src/sub_commands/prs/list.rs28
2 files changed, 231 insertions, 37 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}
diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs
index 88b325b..bc85eed 100644
--- a/src/sub_commands/prs/list.rs
+++ b/src/sub_commands/prs/list.rs
@@ -8,8 +8,8 @@ use crate::{
8 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, 8 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
9 client::Connect, 9 client::Connect,
10 git::{Repo, RepoActions}, 10 git::{Repo, RepoActions},
11 repo_ref, 11 repo_ref::{self},
12 sub_commands::prs::create::{PATCH_KIND, PR_KIND}, 12 sub_commands::prs::create::{event_to_cover_letter, PATCH_KIND, PR_KIND},
13 Cli, 13 Cli,
14}; 14};
15 15
@@ -20,6 +20,7 @@ pub struct SubCommandArgs {
20 open_only: bool, 20 open_only: bool,
21} 21}
22 22
23#[allow(clippy::too_many_lines)]
23pub async fn launch( 24pub async fn launch(
24 _cli_args: &Cli, 25 _cli_args: &Cli,
25 _pr_args: &super::SubCommandArgs, 26 _pr_args: &super::SubCommandArgs,
@@ -56,7 +57,7 @@ pub async fn launch(
56 vec![ 57 vec![
57 nostr::Filter::default() 58 nostr::Filter::default()
58 .kind(nostr::Kind::Custom(PR_KIND)) 59 .kind(nostr::Kind::Custom(PR_KIND))
59 .reference(format!("r-{root_commit}")), 60 .reference(format!("{root_commit}")),
60 ], 61 ],
61 ) 62 )
62 .await? 63 .await?
@@ -65,7 +66,7 @@ pub async fn launch(
65 e.kind.as_u64() == PR_KIND 66 e.kind.as_u64() == PR_KIND
66 && e.tags 67 && e.tags
67 .iter() 68 .iter()
68 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("r-{root_commit}"))) 69 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
69 }) 70 })
70 .map(std::borrow::ToOwned::to_owned) 71 .map(std::borrow::ToOwned::to_owned)
71 .collect(); 72 .collect();
@@ -92,8 +93,8 @@ pub async fn launch(
92 pr_events 93 pr_events
93 .iter() 94 .iter()
94 .map(|e| { 95 .map(|e| {
95 if let Ok(name) = tag_value(e, "name") { 96 if let Ok(cl) = event_to_cover_letter(e) {
96 name 97 cl.title
97 } else { 98 } else {
98 e.id.to_string() 99 e.id.to_string()
99 } 100 }
@@ -131,7 +132,19 @@ pub async fn launch(
131 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) 132 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events)
132 .context("cannot get most recent patch for PR")?; 133 .context("cannot get most recent patch for PR")?;
133 134
134 let branch_name = tag_value(&pr_events[selected_index], "branch-name")?; 135 let branch_name: String = if let Ok(cl) = event_to_cover_letter(&pr_events[selected_index]) {
136 if let Some(name) = cl.branch_name {
137 name
138 } else {
139 cl.title
140 .replace(' ', "-")
141 .chars()
142 .filter(|c| c.is_ascii_alphanumeric() || c.eq(&'/'))
143 .collect()
144 }
145 } else {
146 bail!("Placeholder not a cover letter")
147 };
135 148
136 let applied = git_repo 149 let applied = git_repo
137 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain) 150 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain)
@@ -145,7 +158,6 @@ pub async fn launch(
145 applied.len(), 158 applied.len(),
146 ); 159 );
147 } 160 }
148
149 Ok(()) 161 Ok(())
150} 162}
151 163