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..d0e1070 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, }, }; @@ -48,61 +46,15 @@ pub async fn launch() -> Result<()> { 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..912e6d1 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,27 @@ 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")? + .context(format!( + "out of {} prs for this repo. one matching this branch name cannot be found", + pr_events.len() + ))? .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(()) + } + } } }