From cf319efc6dcdc6c54564cb84e13218edbf3643fa Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 13 Feb 2024 14:52:24 +0000 Subject: feat!: nip34 make pr event optional use first patch as thread root if pr event isn't present. begin renaming pr event to cover letter. fix patch ordering upon creation. patches were in youngest first order which caused: - `PATCH n/t`to be in reverse order - the youngest patch was the marked root - oldest patch replied to the youngest fix finding most recent patch event. when a patch in a set is the most recent it will share a created_at with other patches. previously the first patch recieved from relay in the set would be used. now it finds the first patch with that created_at which isn't also a parent of another patch with the same created_at. --- src/git.rs | 5 +- src/sub_commands/prs/create.rs | 171 +++++++++++---- src/sub_commands/prs/list.rs | 215 +++++++++++------- src/sub_commands/pull.rs | 62 +----- src/sub_commands/push.rs | 64 ++---- tests/prs_create.rs | 483 +++++++++++++++++++++++++++++------------ tests/prs_list.rs | 241 +++++++++++++++++--- 7 files changed, 853 insertions(+), 388 deletions(-) diff --git a/src/git.rs b/src/git.rs index 24afe76..cd42724 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1265,6 +1265,7 @@ mod tests { &RepoRef::try_from(generate_repo_ref_event()).unwrap(), None, None, + None, ) } fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { @@ -1418,9 +1419,7 @@ mod tests { let git_repo = Repo::from_path(&original_repo.dir)?; let mut events = generate_pr_and_patch_events( - // Some(("test".to_string(), "test".to_string())), - "title", - "description", + Some(("test".to_string(), "test".to_string())), &git_repo, &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)], &TEST_KEY_1_KEYS, 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; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; +use super::list::tag_value; #[cfg(not(test))] use crate::client::Client; #[cfg(test)] @@ -32,6 +33,9 @@ pub struct SubCommandArgs { #[clap(long)] /// destination branch (defaults to main or master) to_branch: Option, + /// don't ask about a cover letter + #[arg(long, action)] + no_cover_letter: bool, } #[allow(clippy::too_many_lines)] @@ -42,7 +46,7 @@ pub async fn launch( ) -> Result<()> { let git_repo = Repo::discover().context("cannot find a git repository")?; - let (from_branch, to_branch, ahead, behind) = + let (from_branch, to_branch, mut ahead, behind) = identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; if ahead.is_empty() { @@ -79,34 +83,44 @@ pub async fn launch( ); } - let title = match &args.title { - Some(t) => t.clone(), - None => Interactor::default() - .input(PromptInputParms::default().with_prompt("title"))? - .clone(), + let title = if args.no_cover_letter { + None + } else { + match &args.title { + Some(t) => Some(t.clone()), + None => { + if Interactor::default().confirm( + PromptConfirmParms::default() + .with_default(false) + .with_prompt("include cover letter?"), + )? { + Some( + Interactor::default() + .input(PromptInputParms::default().with_prompt("title"))? + .clone(), + ) + } else { + None + } + } + } }; - let description = match &args.description { - Some(t) => t.clone(), - None => Interactor::default() - .input(PromptInputParms::default().with_prompt("description (Optional)"))?, + let cover_letter_title_description = if let Some(title) = title { + Some(( + title, + if let Some(t) = &args.description { + t.clone() + } else { + Interactor::default() + .input(PromptInputParms::default().with_prompt("cover letter description"))? + .clone() + }, + )) + } else { + None }; - // let cover_letter_title_description = if let Some(title) = title { - // Some(( - // title, - // if let Some(t) = &args.description { - // t.clone() - // } else { - // Interactor::default() - // .input(PromptInputParms::default().with_prompt("cover letter - // description"))? .clone() - // }, - // )) - // } else { - // None - // }; - #[cfg(not(test))] let mut client = Client::default(); #[cfg(test)] @@ -127,10 +141,11 @@ pub async fn launch( ) .await?; + // oldest first + ahead.reverse(); + let events = generate_pr_and_patch_events( - // cover_letter_title_description, - &title, - &description, + cover_letter_title_description.clone(), &git_repo, &ahead, &keys, @@ -138,8 +153,17 @@ pub async fn launch( )?; println!( - "posting 1 pull request with {} commits...", - events.len() - 1 + "posting {} patches {} a covering letter...", + if cover_letter_title_description.is_none() { + events.len() + } else { + events.len() - 1 + }, + if cover_letter_title_description.is_none() { + "without" + } else { + "with" + } ); send_events( @@ -329,9 +353,7 @@ pub static PR_KIND: u64 = 318; pub static PATCH_KIND: u64 = 1617; pub fn generate_pr_and_patch_events( - title: &str, - description: &str, - // cover_letter_title_description: Option<(String, String)>, + cover_letter_title_description: Option<(String, String)>, git_repo: &Repo, commits: &Vec, keys: &nostr::Keys, @@ -343,8 +365,7 @@ pub fn generate_pr_and_patch_events( let mut events = vec![]; - // if let Some((title, description)) = cover_letter_title_description { - if !title.is_empty() { + if let Some((title, description)) = cover_letter_title_description { events.push(EventBuilder::new( nostr::event::Kind::Custom(PR_KIND), format!( @@ -400,6 +421,15 @@ pub fn generate_pr_and_patch_events( } else { Some(((i + 1).try_into()?, commits.len().try_into()?)) }, + if events.is_empty() { + if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { + Some(branch_name) + } else { + None + } + } else { + None + }, ) .context("failed to generate patch event")?, ); @@ -410,36 +440,72 @@ pub fn generate_pr_and_patch_events( pub struct CoverLetter { pub title: String, pub description: String, - pub branch_name: Option, + pub branch_name: String, } -fn event_is_cover_letter(event: &nostr::Event) -> bool { +pub fn event_is_cover_letter(event: &nostr::Event) -> bool { event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) } pub fn event_to_cover_letter(event: &nostr::Event) -> Result { - if !event_is_cover_letter(event) { - bail!("event is not a cover letter") + if !event_is_patch_set_root(event) { + bail!("event is not a patch set root event (root patch or cover letter)") } let title_index = event .content .find("] ") - .context("event is not formatted as a cover letter patch")? + .context("event is not formatted as a patch or cover letter")? + 2; let description_index = event.content[title_index..] .find('\n') .unwrap_or(event.content.len() - 1 - title_index) + title_index; + let title = if let Ok(msg) = tag_value(event, "description") { + msg.split('\n').collect::>()[0].to_string() + } else { + event.content[title_index..description_index].to_string() + }; + + // note: if the description field is removed from patch events like in gitstr, + // then this will show entire patch. I'm not sure it is ever displayed though + let description = if let Ok(msg) = tag_value(event, "description") { + if let Some((_before, after)) = msg.split_once('\n') { + after.trim().to_string() + } else { + String::new() + } + } else { + event.content[description_index..].trim().to_string() + }; + Ok(CoverLetter { - title: event.content[title_index..description_index].to_string(), - description: event.content[description_index..].trim().to_string(), - branch_name: event - .iter_tags() - .find(|t| t.as_vec()[0].eq("branch-name")) - .map(|tag| tag.as_vec()[1].clone()), + title: title.clone(), + description, + // TODO should this be prefixed by format!("{}-"e.id.to_string()[..5]?) + branch_name: if let Ok(name) = tag_value(event, "branch-name") { + name + } else { + let s = title + .replace(' ', "-") + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c.eq(&'/') { + c + } else { + '-' + } + }) + .collect(); + s + }, }) } +pub fn event_is_patch_set_root(event: &nostr::Event) -> bool { + (event.kind.as_u64().eq(&PR_KIND) || event.kind.as_u64().eq(&PATCH_KIND)) + && event.iter_tags().any(|t| t.as_vec()[1].eq("root")) +} + #[allow(clippy::too_many_arguments)] pub fn generate_patch_event( git_repo: &Repo, @@ -450,6 +516,7 @@ pub fn generate_patch_event( repo_ref: &RepoRef, parent_patch_event_id: Option, series_count: Option<(u64, u64)>, + branch_name: Option, ) -> Result { let commit_parent = git_repo .get_commit_parent(commit) @@ -496,6 +563,18 @@ pub fn generate_patch_event( } else { vec![] }, + if let Some(branch_name) = branch_name { + if thread_event_id.is_none() { + vec![ + Tag::Generic( + TagKind::Custom("branch-name".to_string()), + vec![branch_name.to_string()], + ) + ] + } + else { vec![]} + } + else { vec![]}, // whilst it is in nip34 draft to tag the maintainers // I'm not sure it is a good idea because if they are // interested in all patches then their specialised diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs index bc85eed..36cbd02 100644 --- a/src/sub_commands/prs/list.rs +++ b/src/sub_commands/prs/list.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Context, Result}; +use super::create::event_is_patch_set_root; #[cfg(not(test))] use crate::client::Client; #[cfg(test)] @@ -8,8 +9,10 @@ use crate::{ cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, client::Connect, git::{Repo, RepoActions}, - repo_ref::{self}, - sub_commands::prs::create::{event_to_cover_letter, PATCH_KIND, PR_KIND}, + repo_ref::{self, RepoRef, REPO_REF_KIND}, + sub_commands::prs::create::{ + event_is_cover_letter, event_to_cover_letter, PATCH_KIND, PR_KIND, + }, Cli, }; @@ -51,40 +54,8 @@ pub async fn launch( println!("finding PRs..."); - let pr_events: Vec = client - .get_events( - repo_ref.relays.clone(), - vec![ - nostr::Filter::default() - .kind(nostr::Kind::Custom(PR_KIND)) - .reference(format!("{root_commit}")), - ], - ) - .await? - .iter() - .filter(|e| { - e.kind.as_u64() == PR_KIND - && e.tags - .iter() - .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}"))) - }) - .map(std::borrow::ToOwned::to_owned) - .collect(); - - // let pr_branch_names: Vec = pr_events - // .iter() - // .map(|e| { - // format!( - // "{}-{}", - // &e.id.to_string()[..5], - // if let Some(t) = e.tags.iter().find(|t| t.as_vec()[0] == - // "branch-name") { t.as_vec()[1].to_string() - // } else { - // "".to_string() - // } // git_repo.get_checked_out_branch_name(), - // ) - // }) - // .collect(); + let pr_events: Vec = + find_pr_events(&client, &repo_ref, &root_commit.to_string()).await?; let selected_index = Interactor::default().choice( PromptChoiceParms::default() @@ -95,6 +66,8 @@ pub async fn launch( .map(|e| { if let Ok(cl) = event_to_cover_letter(e) { cl.title + } else if let Ok(msg) = tag_value(e, "description") { + msg.split('\n').collect::>()[0].to_string() } else { e.id.to_string() } @@ -102,49 +75,20 @@ pub async fn launch( .collect(), ), )?; - // println!("prs:{:?}", &pr_events); println!("finding commits..."); - let commits_events: Vec = client - .get_events( - repo_ref.relays.clone(), - vec![ - nostr::Filter::default() - .kind(nostr::Kind::Custom(PATCH_KIND)) - .event(pr_events[selected_index].id), - ], - ) - .await? - .iter() - .filter(|e| { - e.kind.as_u64() == PATCH_KIND - && e.tags.iter().any(|t| { - t.as_vec().len() > 2 - && t.as_vec()[1].eq(&pr_events[selected_index].id.to_string()) - }) - }) - .map(std::borrow::ToOwned::to_owned) - .collect(); + let commits_events: Vec = + find_commits_for_pr_event(&client, &pr_events[selected_index], &repo_ref).await?; confirm_checkout(&git_repo)?; let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) .context("cannot get most recent patch for PR")?; - let branch_name: String = if let Ok(cl) = event_to_cover_letter(&pr_events[selected_index]) { - if let Some(name) = cl.branch_name { - name - } else { - cl.title - .replace(' ', "-") - .chars() - .filter(|c| c.is_ascii_alphanumeric() || c.eq(&'/')) - .collect() - } - } else { - bail!("Placeholder not a cover letter") - }; + let branch_name: String = event_to_cover_letter(&pr_events[selected_index]) + .context("cannot assign a branch name as event is not a patch set root")? + .branch_name; let applied = git_repo .apply_patch_chain(&branch_name, most_recent_pr_patch_chain) @@ -193,20 +137,139 @@ pub fn get_most_recent_patch_with_ancestors( ) -> Result> { patches.sort_by_key(|e| e.created_at); - let mut res = vec![]; + let first_patch = patches.first().context("no patches found")?; - let latest_commit_id = tag_value(patches.first().context("no patches found")?, "commit")?; + let patches_with_youngest_created_at: Vec<&nostr::Event> = patches + .iter() + .filter(|p| p.created_at.eq(&first_patch.created_at)) + .collect(); + + let latest_commit_id = tag_value( + // get the first patch which isn't a parent of a patch event created at the same + // time + patches_with_youngest_created_at + .clone() + .iter() + .find(|p| { + if let Ok(commit) = tag_value(p, "commit") { + !patches_with_youngest_created_at.iter().any(|p2| { + if let Ok(parent) = tag_value(p2, "parent-commit") { + commit.eq(&parent) + } else { + false // skip + } + }) + } else { + false // skip + } + }) + .context("cannot find patches_with_youngest_created_at")?, + "commit", + )?; + + let mut res = vec![]; let mut commit_id_to_search = latest_commit_id; while let Some(event) = patches.iter().find(|e| { - tag_value(e, "commit") - .context("patch event doesnt contain commit tag") - .unwrap() - .eq(&commit_id_to_search) + if let Ok(commit) = tag_value(e, "commit") { + commit.eq(&commit_id_to_search) + } else { + false // skip + } }) { res.push(event.clone()); commit_id_to_search = tag_value(event, "parent-commit")?; } Ok(res) } + +pub async fn find_pr_events( + #[cfg(test)] client: &crate::client::MockConnect, + #[cfg(not(test))] client: &Client, + repo_ref: &RepoRef, + root_commit: &str, +) -> Result> { + Ok(client + .get_events( + repo_ref.relays.clone(), + vec![ + nostr::Filter::default() + .kinds(vec![ + nostr::Kind::Custom(PR_KIND), + nostr::Kind::Custom(PATCH_KIND), + ]) + .custom_tag(nostr::Alphabet::T, vec!["root"]) + .identifiers( + repo_ref + .maintainers + .iter() + .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)), + ), + // also pick up prs from the same repo but no target at our maintainers repo events + nostr::Filter::default() + .kinds(vec![ + nostr::Kind::Custom(PR_KIND), + nostr::Kind::Custom(PATCH_KIND), + ]) + .custom_tag(nostr::Alphabet::T, vec!["root"]) + .reference(root_commit), + ], + ) + .await + .context("cannot get pr events")? + .iter() + .filter(|e| { + event_is_patch_set_root(e) + && (e + .tags + .iter() + .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(root_commit)) + || e.tags.iter().any(|t| { + t.as_vec().len() > 1 + && repo_ref + .maintainers + .iter() + .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)) + .any(|d| t.as_vec()[1].eq(&d)) + })) + }) + .map(std::borrow::ToOwned::to_owned) + .collect::>()) +} + +pub async fn find_commits_for_pr_event( + #[cfg(test)] client: &crate::client::MockConnect, + #[cfg(not(test))] client: &Client, + pr_event: &nostr::Event, + repo_ref: &RepoRef, +) -> Result> { + let mut patch_events: Vec = client + .get_events( + repo_ref.relays.clone(), + vec![ + nostr::Filter::default() + .kind(nostr::Kind::Custom(PATCH_KIND)) + // this requires every patch to reference the root event + // this will not pick up v2,v3 patch sets + // TODO: fetch commits for v2.. patch sets + .event(pr_event.id), + ], + ) + .await + .context("cannot fetch patch events")? + .iter() + .filter(|e| { + e.kind.as_u64() == PATCH_KIND + && e.tags + .iter() + .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string())) + }) + .map(std::borrow::ToOwned::to_owned) + .collect(); + + if !event_is_cover_letter(pr_event) { + patch_events.push(pr_event.clone()); + } + Ok(patch_events) +} diff --git a/src/sub_commands/pull.rs b/src/sub_commands/pull.rs index f3ae81f..2b20d3d 100644 --- a/src/sub_commands/pull.rs +++ b/src/sub_commands/pull.rs @@ -8,10 +8,8 @@ use crate::{ client::Connect, git::{Repo, RepoActions}, repo_ref, - repo_ref::REPO_REF_KIND, - sub_commands::prs::{ - create::{PATCH_KIND, PR_KIND}, - list::{get_most_recent_patch_with_ancestors, tag_value}, + sub_commands::{ + prs::list::get_most_recent_patch_with_ancestors, push::fetch_pr_and_most_recent_patch_chain, }, }; @@ -46,63 +44,15 @@ pub async fn launch() -> Result<()> { ) .await?; - println!("finding PR event..."); - - let pr_event: nostr::Event = client - .get_events( - repo_ref.relays.clone(), - vec![ - nostr::Filter::default() - .kind(nostr::Kind::Custom(PR_KIND)) - .identifiers( - repo_ref - .maintainers - .iter() - .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)), - ), - ], - ) - .await? - .iter() - .find(|e| { - e.kind.as_u64() == PR_KIND - && e.tags - .iter() - .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}"))) - && tag_value(e, "branch-name") - .unwrap_or_default() - .eq(&branch_name) - }) - .context("cannot find a PR event associated with the checked out branch name")? - .to_owned(); - - println!("found PR event. finding commits..."); - - let commits_events: Vec = client - .get_events( - repo_ref.relays.clone(), - vec![ - nostr::Filter::default() - .kind(nostr::Kind::Custom(PATCH_KIND)) - .event(pr_event.id), - ], - ) - .await? - .iter() - .filter(|e| { - e.kind.as_u64() == PATCH_KIND - && e.tags - .iter() - .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string())) - }) - .map(std::borrow::ToOwned::to_owned) - .collect(); + let (_pr_event, commit_events) = + fetch_pr_and_most_recent_patch_chain(&client, &repo_ref, &root_commit, &branch_name) + .await?; if git_repo.has_outstanding_changes()? { bail!("cannot pull changes when repository is not clean. discard changes and try again."); } - let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) + let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commit_events) .context("cannot get most recent patch for PR")?; let applied = git_repo diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs index 61d5d46..eb42699 100644 --- a/src/sub_commands/push.rs +++ b/src/sub_commands/push.rs @@ -9,10 +9,13 @@ use crate::{ client::Connect, git::{str_to_sha1, Repo, RepoActions}, login, - repo_ref::{self, RepoRef, REPO_REF_KIND}, + repo_ref::{self, RepoRef}, sub_commands::prs::{ - create::{generate_patch_event, send_events, PATCH_KIND, PR_KIND}, - list::{get_most_recent_patch_with_ancestors, tag_value}, + create::{event_to_cover_letter, generate_patch_event, send_events}, + list::{ + find_commits_for_pr_event, find_pr_events, get_most_recent_patch_with_ancestors, + tag_value, + }, }, Cli, }; @@ -111,6 +114,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> { &repo_ref, patch_events.last().map(nostr::Event::id), None, + None, ) .context("cannot make patch event from commit")?, ); @@ -131,7 +135,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> { Ok(()) } -async fn fetch_pr_and_most_recent_patch_chain( +pub async fn fetch_pr_and_most_recent_patch_chain( #[cfg(test)] client: &crate::client::MockConnect, #[cfg(not(test))] client: &Client, repo_ref: &RepoRef, @@ -140,54 +144,24 @@ async fn fetch_pr_and_most_recent_patch_chain( ) -> Result<(nostr::Event, Vec)> { println!("finding PR event..."); - let pr_event: nostr::Event = client - .get_events( - repo_ref.relays.clone(), - vec![ - nostr::Filter::default() - .kind(nostr::Kind::Custom(PR_KIND)) - .identifiers( - repo_ref - .maintainers - .iter() - .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)), - ), - ], - ) - .await? + let pr_events: Vec = find_pr_events(client, repo_ref, &root_commit.to_string()) + .await + .context("cannot get pr events for repo")?; + + let pr_event: nostr::Event = pr_events .iter() .find(|e| { - e.kind.as_u64() == PR_KIND - && e.tags - .iter() - .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}"))) - && tag_value(e, "branch-name") - .unwrap_or_default() - .eq(branch_name) + event_to_cover_letter(e).is_ok_and(|cl| cl.branch_name.eq(branch_name)) + // TODO remove the dependancy on same branch name and replace with + // references stored in .git/ngit }) .context("cannot find a PR event associated with the checked out branch name")? .to_owned(); println!("found PR event. finding commits..."); - let commits_events: Vec = client - .get_events( - repo_ref.relays.clone(), - vec![ - nostr::Filter::default() - .kind(nostr::Kind::Custom(PATCH_KIND)) - .event(pr_event.id), - ], - ) - .await? - .iter() - .filter(|e| { - e.kind.as_u64() == PATCH_KIND - && e.tags - .iter() - .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string())) - }) - .map(std::borrow::ToOwned::to_owned) - .collect(); + let commits_events: Vec = + find_commits_for_pr_event(client, &pr_event, repo_ref).await?; + Ok((pr_event, commits_events)) } diff --git a/tests/prs_create.rs b/tests/prs_create.rs index 6272ccd..316c9fe 100644 --- a/tests/prs_create.rs +++ b/tests/prs_create.rs @@ -1,6 +1,7 @@ use anyhow::Result; +use futures::join; use serial_test::serial; -use test_utils::{git::GitTestRepo, *}; +use test_utils::{git::GitTestRepo, relay::Relay, *}; #[test] fn when_to_branch_doesnt_exist_return_error() -> Result<()> { @@ -150,121 +151,133 @@ fn is_patch(event: &nostr::Event) -> bool { && !event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) } -mod sends_pr_and_2_patches_to_3_relays { - use futures::join; - use test_utils::relay::Relay; - - use super::*; +fn prep_git_repo() -> Result { + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + // create feature branch with 2 commit ahead + test_repo.create_branch("feature")?; + test_repo.checkout("feature")?; + std::fs::write(test_repo.dir.join("t3.md"), "some content")?; + test_repo.stage_and_commit("add t3.md")?; + std::fs::write(test_repo.dir.join("t4.md"), "some content")?; + test_repo.stage_and_commit("add t4.md")?; + Ok(test_repo) +} - fn prep_git_repo() -> Result { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch with 2 commit ahead - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - test_repo.stage_and_commit("add t4.md")?; - Ok(test_repo) +fn cli_tester_create_pr(git_repo: &GitTestRepo, include_cover_letter: bool) -> CliTester { + let mut args = vec![ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "prs", + "create", + ]; + if include_cover_letter { + for arg in [ + "--title", + "exampletitle", + "--description", + "exampledescription", + ] { + args.push(arg); + } + } else { + args.push("--no-cover-letter"); } + CliTester::new_from_dir(&git_repo.dir, args) +} - fn cli_tester_create_pr(git_repo: &GitTestRepo) -> CliTester { - CliTester::new_from_dir( - &git_repo.dir, - [ - "--nsec", - TEST_KEY_1_NSEC, - "--password", - TEST_PASSWORD, - "--disable-cli-spinners", - "prs", - "create", - "--title", - "exampletitle", - "--description", - "exampledescription", - ], - ) - } +fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> { + p.expect("creating patch for 2 commits from 'head' that can be merged into 'main'\r\n")?; + p.expect("searching for your details...\r\n")?; + p.expect("\r")?; + p.expect("logged in as fred\r\n")?; + p.expect(format!( + "posting 2 patches {} a covering letter...\r\n", + if include_cover_letter { + "with" + } else { + "without" + } + ))?; + Ok(()) +} - fn expect_msgs_first(p: &mut CliTester) -> Result<()> { - p.expect("creating patch for 2 commits from 'head' that can be merged into 'main'\r\n")?; - p.expect("searching for your details...\r\n")?; - p.expect("\r")?; - p.expect("logged in as fred\r\n")?; - p.expect("posting 1 pull request with 2 commits...\r\n")?; +async fn prep_run_create_pr( + include_cover_letter: bool, +) -> Result<( + Relay<'static>, + Relay<'static>, + Relay<'static>, + Relay<'static>, + Relay<'static>, +)> { + let git_repo = prep_git_repo()?; + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new( + 8051, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], + )?; + Ok(()) + }), + ), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![generate_repo_ref_event()], + )?; + Ok(()) + }), + ), + Relay::new(8056, None, None), + ); + + // // check relay had the right number of events + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let mut p = cli_tester_create_pr(&git_repo, include_cover_letter); + p.expect_end_eventually()?; + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } Ok(()) - } + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok((r51, r52, r53, r55, r56)) +} - async fn prep_run_create_pr() -> Result<( - Relay<'static>, - Relay<'static>, - Relay<'static>, - Relay<'static>, - Relay<'static>, - )> { - let git_repo = prep_git_repo()?; - // fallback (51,52) user write (53, 55) repo (55, 56) - let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( - Relay::new( - 8051, - None, - Some(&|relay, client_id, subscription_id, _| -> Result<()> { - relay.respond_events( - client_id, - &subscription_id, - &vec![ - generate_test_key_1_metadata_event("fred"), - generate_test_key_1_relay_list_event(), - ], - )?; - Ok(()) - }), - ), - Relay::new(8052, None, None), - Relay::new(8053, None, None), - Relay::new( - 8055, - None, - Some(&|relay, client_id, subscription_id, _| -> Result<()> { - relay.respond_events( - client_id, - &subscription_id, - &vec![generate_repo_ref_event()], - )?; - Ok(()) - }), - ), - Relay::new(8056, None, None), - ); - - // // check relay had the right number of events - let cli_tester_handle = std::thread::spawn(move || -> Result<()> { - let mut p = cli_tester_create_pr(&git_repo); - p.expect_end_eventually()?; - for p in [51, 52, 53, 55, 56] { - relay::shutdown_relay(8000 + p)?; - } - Ok(()) - }); - - // launch relay - let _ = join!( - r51.listen_until_close(), - r52.listen_until_close(), - r53.listen_until_close(), - r55.listen_until_close(), - r56.listen_until_close(), - ); - cli_tester_handle.join().unwrap()?; - Ok((r51, r52, r53, r55, r56)) - } +mod sends_cover_letter_and_2_patches_to_3_relays { + use super::*; #[tokio::test] #[serial] async fn only_1_pr_kind_event_sent_to_each_relay() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { assert_eq!( relay.events.iter().filter(|e| is_cover_letter(e)).count(), @@ -277,7 +290,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn only_1_pr_kind_event_sent_to_user_relays() -> Result<()> { - let (_, _, r53, r55, _) = prep_run_create_pr().await?; + let (_, _, r53, r55, _) = prep_run_create_pr(true).await?; for relay in [&r53, &r55] { assert_eq!( relay.events.iter().filter(|e| is_cover_letter(e)).count(), @@ -290,7 +303,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn only_1_pr_kind_event_sent_to_repo_relays() -> Result<()> { - let (_, _, _, r55, r56) = prep_run_create_pr().await?; + let (_, _, _, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r55, &r56] { assert_eq!( relay.events.iter().filter(|e| is_cover_letter(e)).count(), @@ -303,7 +316,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn pr_not_sent_to_fallback_relay() -> Result<()> { - let (r51, r52, _, _, _) = prep_run_create_pr().await?; + let (r51, r52, _, _, _) = prep_run_create_pr(true).await?; for relay in [&r51, &r52] { assert_eq!( relay.events.iter().filter(|e| is_cover_letter(e)).count(), @@ -316,7 +329,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn only_2_patch_kind_events_sent_to_each_relay() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2,); } @@ -327,18 +340,18 @@ mod sends_pr_and_2_patches_to_3_relays { #[serial] async fn patch_content_contains_patch_in_email_format_with_patch_series_numbers() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let patch_events: Vec<&nostr::Event> = relay.events.iter().filter(|e| is_patch(e)).collect(); assert_eq!( - patch_events[0].content, + patch_events[1].content, "\ From fe973a840fba2a8ab37dd505c154854a69a6505c Mon Sep 17 00:00:00 2001\n\ From: Joe Bloggs \n\ Date: Thu, 1 Jan 1970 00:00:00 +0000\n\ - Subject: [PATCH 1/2] add t4.md\n\ + Subject: [PATCH 2/2] add t4.md\n\ \n\ ---\n \ t4.md | 1 +\n \ @@ -359,12 +372,12 @@ mod sends_pr_and_2_patches_to_3_relays { ", ); assert_eq!( - patch_events[1].content, + patch_events[0].content, "\ From 232efb37ebc67692c9e9ff58b83c0d3d63971a0a Mon Sep 17 00:00:00 2001\n\ From: Joe Bloggs \n\ Date: Thu, 1 Jan 1970 00:00:00 +0000\n\ - Subject: [PATCH 2/2] add t3.md\n\ + Subject: [PATCH 1/2] add t3.md\n\ \n\ ---\n \ t3.md | 1 +\n \ @@ -394,7 +407,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn root_commit_as_r() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let pr_event: &nostr::Event = relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); @@ -414,7 +427,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn a_tag_for_repo_event() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let pr_event: &nostr::Event = relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); @@ -436,7 +449,7 @@ mod sends_pr_and_2_patches_to_3_relays { .unwrap() .as_vec() .clone()[1..]; - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { for m in maintainers { let pr_event: &nostr::Event = @@ -454,7 +467,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn t_tag_cover_letter() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let pr_event: &nostr::Event = relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); @@ -470,7 +483,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn t_tag_root() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let pr_event: &nostr::Event = relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); @@ -486,7 +499,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn pr_tags_branch_name() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let pr_event: &nostr::Event = relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); @@ -509,14 +522,14 @@ mod sends_pr_and_2_patches_to_3_relays { use super::*; async fn prep() -> Result { - let (_, _, r53, _, _) = prep_run_create_pr().await?; + let (_, _, r53, _, _) = prep_run_create_pr(true).await?; Ok(r53.events.iter().find(|e| is_patch(e)).unwrap().clone()) } #[tokio::test] #[serial] async fn commit_and_commit_r() -> Result<()> { - static COMMIT_ID: &str = "fe973a840fba2a8ab37dd505c154854a69a6505c"; + static COMMIT_ID: &str = "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"; let most_recent_patch = prep().await?; assert!( most_recent_patch @@ -537,12 +550,16 @@ mod sends_pr_and_2_patches_to_3_relays { #[serial] async fn parent_commit() -> Result<()> { // commit parent 'r' and 'parent-commit' tag - static COMMIT_PARENT_ID: &str = "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"; + static COMMIT_PARENT_ID: &str = "431b84edc0d2fa118d63faa3c2db9c73d630a5ae"; let most_recent_patch = prep().await?; - assert!( - most_recent_patch.tags.iter().any( - |t| t.as_vec()[0].eq("parent-commit") && t.as_vec()[1].eq(COMMIT_PARENT_ID) - ) + assert_eq!( + most_recent_patch + .tags + .iter() + .find(|t| t.as_vec()[0].eq("parent-commit")) + .unwrap() + .as_vec()[1], + COMMIT_PARENT_ID, ); Ok(()) } @@ -599,7 +616,7 @@ mod sends_pr_and_2_patches_to_3_relays { .find(|t| t.as_vec()[0].eq("description")) .unwrap() .as_vec()[1], - "add t4.md" + "add t3.md" ); Ok(()) } @@ -639,7 +656,7 @@ mod sends_pr_and_2_patches_to_3_relays { #[tokio::test] #[serial] async fn patch_tags_pr_event_as_root() -> Result<()> { - let (_, _, r53, r55, r56) = prep_run_create_pr().await?; + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; for relay in [&r53, &r55, &r56] { let patch_events: Vec<&nostr::Event> = relay.events.iter().filter(|e| is_patch(e)).collect(); @@ -659,6 +676,43 @@ mod sends_pr_and_2_patches_to_3_relays { } Ok(()) } + + #[tokio::test] + #[serial] + async fn second_patch_tags_first_with_reply() -> Result<()> { + let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?; + for relay in [&r53, &r55, &r56] { + let patch_events = relay + .events + .iter() + .filter(|e| is_patch(e)) + .collect::>(); + assert_eq!( + patch_events[1] + .iter_tags() + .find(|t| t.as_vec()[0].eq("e") + && t.as_vec().len().eq(&4) + && t.as_vec()[3].eq("reply")) + .unwrap() + .as_vec()[1], + patch_events[0].id.to_string(), + ); + } + Ok(()) + } + + #[tokio::test] + #[serial] + async fn no_t_root_tag() -> Result<()> { + assert!( + !prep() + .await? + .tags + .iter() + .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root")) + ); + Ok(()) + } } mod cli_ouput { use super::*; @@ -701,8 +755,8 @@ mod sends_pr_and_2_patches_to_3_relays { // // check relay had the right number of events let cli_tester_handle = std::thread::spawn(move || -> Result<()> { - let mut p = cli_tester_create_pr(&git_repo); - expect_msgs_first(&mut p)?; + let mut p = cli_tester_create_pr(&git_repo, true); + expect_msgs_first(&mut p, true)?; relay::expect_send_with_progress( &mut p, vec![ @@ -790,7 +844,7 @@ mod sends_pr_and_2_patches_to_3_relays { // // check relay had the right number of events let cli_tester_handle = std::thread::spawn(move || -> Result<()> { - let mut p = cli_tester_create_pr(&git_repo); + let mut p = cli_tester_create_pr(&git_repo, true); p.expect_end_eventually()?; for p in [51, 52, 53, 55, 56] { relay::shutdown_relay(8000 + p)?; @@ -869,8 +923,8 @@ mod sends_pr_and_2_patches_to_3_relays { // // check relay had the right number of events let cli_tester_handle = std::thread::spawn(move || -> Result<()> { - let mut p = cli_tester_create_pr(&git_repo); - expect_msgs_first(&mut p)?; + let mut p = cli_tester_create_pr(&git_repo, true); + expect_msgs_first(&mut p, true)?; // p.expect_end_with("bla")?; relay::expect_send_with_progress( &mut p, @@ -915,7 +969,162 @@ mod sends_pr_and_2_patches_to_3_relays { } } -mod without_cover_letter { +mod sends_2_patches_without_cover_letter { use super::*; - // TODO + + mod cli_ouput { + use super::*; + + async fn run_test_async() -> Result<()> { + let git_repo = prep_git_repo()?; + + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new( + 8051, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![ + generate_test_key_1_metadata_event("fred"), + generate_test_key_1_relay_list_event(), + ], + )?; + Ok(()) + }), + ), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new( + 8055, + None, + Some(&|relay, client_id, subscription_id, _| -> Result<()> { + relay.respond_events( + client_id, + &subscription_id, + &vec![generate_repo_ref_event()], + )?; + Ok(()) + }), + ), + Relay::new(8056, None, None), + ); + + // // check relay had the right number of events + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let mut p = cli_tester_create_pr(&git_repo, false); + + expect_msgs_first(&mut p, false)?; + relay::expect_send_with_progress( + &mut p, + vec![ + (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), + (" [repo-relay] ws://localhost:8056", true, ""), + ], + 2, + )?; + p.expect_end_with_whitespace()?; + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + + #[tokio::test] + #[serial] + async fn check_cli_output() -> Result<()> { + run_test_async().await?; + Ok(()) + } + } + + #[tokio::test] + #[serial] + async fn no_cover_letter_event() -> Result<()> { + let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?; + for relay in [&r53, &r55, &r56] { + assert_eq!( + relay.events.iter().filter(|e| is_cover_letter(e)).count(), + 0, + ); + } + Ok(()) + } + + #[tokio::test] + #[serial] + async fn two_patch_events() -> Result<()> { + let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?; + for relay in [&r53, &r55, &r56] { + assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2); + } + Ok(()) + } + + #[tokio::test] + #[serial] + // TODO check this is the ancestor + async fn first_patch_with_root_t_tag() -> Result<()> { + let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?; + for relay in [&r53, &r55, &r56] { + let patch_events = relay + .events + .iter() + .filter(|e| is_patch(e)) + .collect::>(); + + // first patch tagged as root + assert!( + patch_events[0] + .iter_tags() + .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root")) + ); + // second patch not tagged as root + assert!( + !patch_events[1] + .iter_tags() + .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root")) + ); + } + Ok(()) + } + + #[tokio::test] + #[serial] + async fn second_patch_lists_first_as_root() -> Result<()> { + let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?; + for relay in [&r53, &r55, &r56] { + let patch_events = relay + .events + .iter() + .filter(|e| is_patch(e)) + .collect::>(); + + assert_eq!( + patch_events[1] + .iter_tags() + .find(|t| t.as_vec()[0].eq("e") + && t.as_vec().len().eq(&4) + && t.as_vec()[3].eq("root")) + .unwrap() + .as_vec()[1], + patch_events[0].id.to_string(), + ); + } + Ok(()) + } } diff --git a/tests/prs_list.rs b/tests/prs_list.rs index 75704f6..7c0d8ec 100644 --- a/tests/prs_list.rs +++ b/tests/prs_list.rs @@ -6,6 +6,7 @@ use test_utils::{git::GitTestRepo, relay::Relay, *}; static FEATURE_BRANCH_NAME_1: &str = "feature-example-t"; static FEATURE_BRANCH_NAME_2: &str = "feature-example-f"; static FEATURE_BRANCH_NAME_3: &str = "feature-example-c"; +static FEATURE_BRANCH_NAME_4: &str = "feature-example-d"; static PR_TITLE_1: &str = "pr a"; static PR_TITLE_2: &str = "pr b"; @@ -18,22 +19,19 @@ fn cli_tester_create_prs() -> Result { &git_repo, FEATURE_BRANCH_NAME_1, "a", - PR_TITLE_1, - "pr a description", + Some((PR_TITLE_1, "pr a description")), )?; cli_tester_create_pr( &git_repo, FEATURE_BRANCH_NAME_2, "b", - PR_TITLE_2, - "pr b description", + Some((PR_TITLE_2, "pr b description")), )?; cli_tester_create_pr( &git_repo, FEATURE_BRANCH_NAME_3, "c", - PR_TITLE_3, - "pr c description", + Some((PR_TITLE_3, "pr c description")), )?; Ok(git_repo) } @@ -66,28 +64,44 @@ fn cli_tester_create_pr( test_repo: &GitTestRepo, branch_name: &str, prefix: &str, - title: &str, - description: &str, + cover_letter_title_and_description: Option<(&str, &str)>, ) -> Result<()> { create_and_populate_branch(test_repo, branch_name, prefix, false)?; - let mut p = CliTester::new_from_dir( - &test_repo.dir, - [ - "--nsec", - TEST_KEY_1_NSEC, - "--password", - TEST_PASSWORD, - "--disable-cli-spinners", - "prs", - "create", - "--title", - format!("\"{title}\"").as_str(), - "--description", - format!("\"{description}\"").as_str(), - ], - ); - p.expect_end_eventually()?; + if let Some((title, description)) = cover_letter_title_and_description { + let mut p = CliTester::new_from_dir( + &test_repo.dir, + [ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "prs", + "create", + "--title", + format!("\"{title}\"").as_str(), + "--description", + format!("\"{description}\"").as_str(), + ], + ); + p.expect_end_eventually()?; + } else { + let mut p = CliTester::new_from_dir( + &test_repo.dir, + [ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "prs", + "create", + "--no-cover-letter", + ], + ); + p.expect_end_eventually()?; + } Ok(()) } @@ -432,6 +446,183 @@ mod when_main_branch_is_uptodate { Ok(()) } } + mod when_forth_pr_has_no_cover_letter { + use super::*; + + async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = + std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { + let originating_repo = cli_tester_create_prs()?; + cli_tester_create_pr( + &originating_repo, + FEATURE_BRANCH_NAME_4, + "d", + None, + )?; + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); + + p.expect("finding PRs...\r\n")?; + let mut c = p.expect_choice( + "All PRs", + vec![ + format!("\"{PR_TITLE_1}\""), + format!("\"{PR_TITLE_2}\""), + format!("\"{PR_TITLE_3}\""), + format!("add d3.md"), // commit msg title + ], + )?; + c.succeeds_with(3, true)?; + let mut confirm = + p.expect_confirm_eventually("check out branch?", Some(true))?; + confirm.succeeds_with(None)?; + p.expect_end_eventually_and_print()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok((originating_repo, test_repo)) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + let res = cli_tester_handle.join().unwrap()?; + + Ok(res) + } + + mod cli_prompts { + use super::*; + async fn run_async_prompts_to_choose_from_pr_titles() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let originating_repo = cli_tester_create_prs()?; + cli_tester_create_pr( + &originating_repo, + FEATURE_BRANCH_NAME_4, + "d", + None, + )?; + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); + + p.expect("finding PRs...\r\n")?; + let mut c = p.expect_choice( + "All PRs", + vec![ + format!("\"{PR_TITLE_1}\""), + format!("\"{PR_TITLE_2}\""), + format!("\"{PR_TITLE_3}\""), + format!("add d3.md"), // commit msg title + ], + )?; + c.succeeds_with(3, true)?; + p.expect("finding commits...\r\n")?; + let mut confirm = p.expect_confirm("check out branch?", Some(true))?; + confirm.succeeds_with(None)?; + p.expect("checked out PR branch. pulled 2 new commits\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + println!("{:?}", r55.events); + Ok(()) + } + + #[tokio::test] + #[serial] + async fn prompts_to_choose_from_pr_titles() -> Result<()> { + let _ = run_async_prompts_to_choose_from_pr_titles().await; + Ok(()) + } + } + + #[tokio::test] + #[serial] + async fn pr_branch_created_with_correct_name() -> Result<()> { + let (_, test_repo) = prep_and_run().await?; + assert_eq!( + vec![FEATURE_BRANCH_NAME_4, "main"], + test_repo.get_local_branch_names()? + ); + Ok(()) + } + + #[tokio::test] + #[serial] + async fn pr_branch_checked_out() -> Result<()> { + let (_, test_repo) = prep_and_run().await?; + assert_eq!( + FEATURE_BRANCH_NAME_4, + test_repo.get_checked_out_branch_name()?, + ); + Ok(()) + } + + #[tokio::test] + #[serial] + async fn pr_branch_tip_is_most_recent_patch() -> Result<()> { + let (originating_repo, test_repo) = prep_and_run().await?; + assert_eq!( + originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_4)?, + test_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_4)?, + ); + Ok(()) + } + } } } -- cgit v1.2.3