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:
Diffstat (limited to 'src/sub_commands/prs/create.rs')
-rw-r--r--src/sub_commands/prs/create.rs171
1 files changed, 125 insertions, 46 deletions
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs
index 83a3942..e5a7c1e 100644
--- a/src/sub_commands/prs/create.rs
+++ b/src/sub_commands/prs/create.rs
@@ -5,6 +5,7 @@ use futures::future::join_all;
5use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 5use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
6use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; 6use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind};
7 7
8use super::list::tag_value;
8#[cfg(not(test))] 9#[cfg(not(test))]
9use crate::client::Client; 10use crate::client::Client;
10#[cfg(test)] 11#[cfg(test)]
@@ -32,6 +33,9 @@ pub struct SubCommandArgs {
32 #[clap(long)] 33 #[clap(long)]
33 /// destination branch (defaults to main or master) 34 /// destination branch (defaults to main or master)
34 to_branch: Option<String>, 35 to_branch: Option<String>,
36 /// don't ask about a cover letter
37 #[arg(long, action)]
38 no_cover_letter: bool,
35} 39}
36 40
37#[allow(clippy::too_many_lines)] 41#[allow(clippy::too_many_lines)]
@@ -42,7 +46,7 @@ pub async fn launch(
42) -> Result<()> { 46) -> Result<()> {
43 let git_repo = Repo::discover().context("cannot find a git repository")?; 47 let git_repo = Repo::discover().context("cannot find a git repository")?;
44 48
45 let (from_branch, to_branch, ahead, behind) = 49 let (from_branch, to_branch, mut ahead, behind) =
46 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; 50 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?;
47 51
48 if ahead.is_empty() { 52 if ahead.is_empty() {
@@ -79,34 +83,44 @@ pub async fn launch(
79 ); 83 );
80 } 84 }
81 85
82 let title = match &args.title { 86 let title = if args.no_cover_letter {
83 Some(t) => t.clone(), 87 None
84 None => Interactor::default() 88 } else {
85 .input(PromptInputParms::default().with_prompt("title"))? 89 match &args.title {
86 .clone(), 90 Some(t) => Some(t.clone()),
91 None => {
92 if Interactor::default().confirm(
93 PromptConfirmParms::default()
94 .with_default(false)
95 .with_prompt("include cover letter?"),
96 )? {
97 Some(
98 Interactor::default()
99 .input(PromptInputParms::default().with_prompt("title"))?
100 .clone(),
101 )
102 } else {
103 None
104 }
105 }
106 }
87 }; 107 };
88 108
89 let description = match &args.description { 109 let cover_letter_title_description = if let Some(title) = title {
90 Some(t) => t.clone(), 110 Some((
91 None => Interactor::default() 111 title,
92 .input(PromptInputParms::default().with_prompt("description (Optional)"))?, 112 if let Some(t) = &args.description {
113 t.clone()
114 } else {
115 Interactor::default()
116 .input(PromptInputParms::default().with_prompt("cover letter description"))?
117 .clone()
118 },
119 ))
120 } else {
121 None
93 }; 122 };
94 123
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
110 #[cfg(not(test))] 124 #[cfg(not(test))]
111 let mut client = Client::default(); 125 let mut client = Client::default();
112 #[cfg(test)] 126 #[cfg(test)]
@@ -127,10 +141,11 @@ pub async fn launch(
127 ) 141 )
128 .await?; 142 .await?;
129 143
144 // oldest first
145 ahead.reverse();
146
130 let events = generate_pr_and_patch_events( 147 let events = generate_pr_and_patch_events(
131 // cover_letter_title_description, 148 cover_letter_title_description.clone(),
132 &title,
133 &description,
134 &git_repo, 149 &git_repo,
135 &ahead, 150 &ahead,
136 &keys, 151 &keys,
@@ -138,8 +153,17 @@ pub async fn launch(
138 )?; 153 )?;
139 154
140 println!( 155 println!(
141 "posting 1 pull request with {} commits...", 156 "posting {} patches {} a covering letter...",
142 events.len() - 1 157 if cover_letter_title_description.is_none() {
158 events.len()
159 } else {
160 events.len() - 1
161 },
162 if cover_letter_title_description.is_none() {
163 "without"
164 } else {
165 "with"
166 }
143 ); 167 );
144 168
145 send_events( 169 send_events(
@@ -329,9 +353,7 @@ pub static PR_KIND: u64 = 318;
329pub static PATCH_KIND: u64 = 1617; 353pub static PATCH_KIND: u64 = 1617;
330 354
331pub fn generate_pr_and_patch_events( 355pub fn generate_pr_and_patch_events(
332 title: &str, 356 cover_letter_title_description: Option<(String, String)>,
333 description: &str,
334 // cover_letter_title_description: Option<(String, String)>,
335 git_repo: &Repo, 357 git_repo: &Repo,
336 commits: &Vec<Sha1Hash>, 358 commits: &Vec<Sha1Hash>,
337 keys: &nostr::Keys, 359 keys: &nostr::Keys,
@@ -343,8 +365,7 @@ pub fn generate_pr_and_patch_events(
343 365
344 let mut events = vec![]; 366 let mut events = vec![];
345 367
346 // if let Some((title, description)) = cover_letter_title_description { 368 if let Some((title, description)) = cover_letter_title_description {
347 if !title.is_empty() {
348 events.push(EventBuilder::new( 369 events.push(EventBuilder::new(
349 nostr::event::Kind::Custom(PR_KIND), 370 nostr::event::Kind::Custom(PR_KIND),
350 format!( 371 format!(
@@ -400,6 +421,15 @@ pub fn generate_pr_and_patch_events(
400 } else { 421 } else {
401 Some(((i + 1).try_into()?, commits.len().try_into()?)) 422 Some(((i + 1).try_into()?, commits.len().try_into()?))
402 }, 423 },
424 if events.is_empty() {
425 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() {
426 Some(branch_name)
427 } else {
428 None
429 }
430 } else {
431 None
432 },
403 ) 433 )
404 .context("failed to generate patch event")?, 434 .context("failed to generate patch event")?,
405 ); 435 );
@@ -410,36 +440,72 @@ pub fn generate_pr_and_patch_events(
410pub struct CoverLetter { 440pub struct CoverLetter {
411 pub title: String, 441 pub title: String,
412 pub description: String, 442 pub description: String,
413 pub branch_name: Option<String>, 443 pub branch_name: String,
414} 444}
415 445
416fn event_is_cover_letter(event: &nostr::Event) -> bool { 446pub 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")) 447 event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter"))
418} 448}
419pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { 449pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> {
420 if !event_is_cover_letter(event) { 450 if !event_is_patch_set_root(event) {
421 bail!("event is not a cover letter") 451 bail!("event is not a patch set root event (root patch or cover letter)")
422 } 452 }
423 let title_index = event 453 let title_index = event
424 .content 454 .content
425 .find("] ") 455 .find("] ")
426 .context("event is not formatted as a cover letter patch")? 456 .context("event is not formatted as a patch or cover letter")?
427 + 2; 457 + 2;
428 let description_index = event.content[title_index..] 458 let description_index = event.content[title_index..]
429 .find('\n') 459 .find('\n')
430 .unwrap_or(event.content.len() - 1 - title_index) 460 .unwrap_or(event.content.len() - 1 - title_index)
431 + title_index; 461 + title_index;
432 462
463 let title = if let Ok(msg) = tag_value(event, "description") {
464 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
465 } else {
466 event.content[title_index..description_index].to_string()
467 };
468
469 // note: if the description field is removed from patch events like in gitstr,
470 // then this will show entire patch. I'm not sure it is ever displayed though
471 let description = if let Ok(msg) = tag_value(event, "description") {
472 if let Some((_before, after)) = msg.split_once('\n') {
473 after.trim().to_string()
474 } else {
475 String::new()
476 }
477 } else {
478 event.content[description_index..].trim().to_string()
479 };
480
433 Ok(CoverLetter { 481 Ok(CoverLetter {
434 title: event.content[title_index..description_index].to_string(), 482 title: title.clone(),
435 description: event.content[description_index..].trim().to_string(), 483 description,
436 branch_name: event 484 // TODO should this be prefixed by format!("{}-"e.id.to_string()[..5]?)
437 .iter_tags() 485 branch_name: if let Ok(name) = tag_value(event, "branch-name") {
438 .find(|t| t.as_vec()[0].eq("branch-name")) 486 name
439 .map(|tag| tag.as_vec()[1].clone()), 487 } else {
488 let s = title
489 .replace(' ', "-")
490 .chars()
491 .map(|c| {
492 if c.is_ascii_alphanumeric() || c.eq(&'/') {
493 c
494 } else {
495 '-'
496 }
497 })
498 .collect();
499 s
500 },
440 }) 501 })
441} 502}
442 503
504pub fn event_is_patch_set_root(event: &nostr::Event) -> bool {
505 (event.kind.as_u64().eq(&PR_KIND) || event.kind.as_u64().eq(&PATCH_KIND))
506 && event.iter_tags().any(|t| t.as_vec()[1].eq("root"))
507}
508
443#[allow(clippy::too_many_arguments)] 509#[allow(clippy::too_many_arguments)]
444pub fn generate_patch_event( 510pub fn generate_patch_event(
445 git_repo: &Repo, 511 git_repo: &Repo,
@@ -450,6 +516,7 @@ pub fn generate_patch_event(
450 repo_ref: &RepoRef, 516 repo_ref: &RepoRef,
451 parent_patch_event_id: Option<nostr::EventId>, 517 parent_patch_event_id: Option<nostr::EventId>,
452 series_count: Option<(u64, u64)>, 518 series_count: Option<(u64, u64)>,
519 branch_name: Option<String>,
453) -> Result<nostr::Event> { 520) -> Result<nostr::Event> {
454 let commit_parent = git_repo 521 let commit_parent = git_repo
455 .get_commit_parent(commit) 522 .get_commit_parent(commit)
@@ -496,6 +563,18 @@ pub fn generate_patch_event(
496 } else { 563 } else {
497 vec![] 564 vec![]
498 }, 565 },
566 if let Some(branch_name) = branch_name {
567 if thread_event_id.is_none() {
568 vec![
569 Tag::Generic(
570 TagKind::Custom("branch-name".to_string()),
571 vec![branch_name.to_string()],
572 )
573 ]
574 }
575 else { vec![]}
576 }
577 else { vec![]},
499 // whilst it is in nip34 draft to tag the maintainers 578 // whilst it is in nip34 draft to tag the maintainers
500 // I'm not sure it is a good idea because if they are 579 // I'm not sure it is a good idea because if they are
501 // interested in all patches then their specialised 580 // interested in all patches then their specialised