diff options
Diffstat (limited to 'src/sub_commands/prs/create.rs')
| -rw-r--r-- | src/sub_commands/prs/create.rs | 171 |
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; | |||
| 5 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; | 5 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; |
| 6 | use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; | 6 | use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; |
| 7 | 7 | ||
| 8 | use super::list::tag_value; | ||
| 8 | #[cfg(not(test))] | 9 | #[cfg(not(test))] |
| 9 | use crate::client::Client; | 10 | use 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; | |||
| 329 | pub static PATCH_KIND: u64 = 1617; | 353 | pub static PATCH_KIND: u64 = 1617; |
| 330 | 354 | ||
| 331 | pub fn generate_pr_and_patch_events( | 355 | pub 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( | |||
| 410 | pub struct CoverLetter { | 440 | pub 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 | ||
| 416 | fn event_is_cover_letter(event: &nostr::Event) -> bool { | 446 | pub 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 | } |
| 419 | pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { | 449 | pub 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 | ||
| 504 | pub 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)] |
| 444 | pub fn generate_patch_event( | 510 | pub 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 |