upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/git.rs5
-rw-r--r--src/sub_commands/prs/create.rs171
-rw-r--r--src/sub_commands/prs/list.rs215
-rw-r--r--src/sub_commands/pull.rs62
-rw-r--r--src/sub_commands/push.rs64
-rw-r--r--tests/prs_create.rs483
-rw-r--r--tests/prs_list.rs241
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 {
1265 &RepoRef::try_from(generate_repo_ref_event()).unwrap(), 1265 &RepoRef::try_from(generate_repo_ref_event()).unwrap(),
1266 None, 1266 None,
1267 None, 1267 None,
1268 None,
1268 ) 1269 )
1269 } 1270 }
1270 fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { 1271 fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> {
@@ -1418,9 +1419,7 @@ mod tests {
1418 let git_repo = Repo::from_path(&original_repo.dir)?; 1419 let git_repo = Repo::from_path(&original_repo.dir)?;
1419 1420
1420 let mut events = generate_pr_and_patch_events( 1421 let mut events = generate_pr_and_patch_events(
1421 // Some(("test".to_string(), "test".to_string())), 1422 Some(("test".to_string(), "test".to_string())),
1422 "title",
1423 "description",
1424 &git_repo, 1423 &git_repo,
1425 &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)], 1424 &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)],
1426 &TEST_KEY_1_KEYS, 1425 &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;
5use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 5use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
6use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; 6use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind};
7 7
8use super::list::tag_value;
8#[cfg(not(test))] 9#[cfg(not(test))]
9use crate::client::Client; 10use crate::client::Client;
10#[cfg(test)] 11#[cfg(test)]
@@ -32,6 +33,9 @@ pub struct SubCommandArgs {
32 #[clap(long)] 33 #[clap(long)]
33 /// destination branch (defaults to main or master) 34 /// destination branch (defaults to main or master)
34 to_branch: Option<String>, 35 to_branch: Option<String>,
36 /// don't ask about a cover letter
37 #[arg(long, action)]
38 no_cover_letter: bool,
35} 39}
36 40
37#[allow(clippy::too_many_lines)] 41#[allow(clippy::too_many_lines)]
@@ -42,7 +46,7 @@ pub async fn launch(
42) -> Result<()> { 46) -> Result<()> {
43 let git_repo = Repo::discover().context("cannot find a git repository")?; 47 let git_repo = Repo::discover().context("cannot find a git repository")?;
44 48
45 let (from_branch, to_branch, ahead, behind) = 49 let (from_branch, to_branch, mut ahead, behind) =
46 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; 50 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?;
47 51
48 if ahead.is_empty() { 52 if ahead.is_empty() {
@@ -79,34 +83,44 @@ pub async fn launch(
79 ); 83 );
80 } 84 }
81 85
82 let title = match &args.title { 86 let title = if args.no_cover_letter {
83 Some(t) => t.clone(), 87 None
84 None => Interactor::default() 88 } else {
85 .input(PromptInputParms::default().with_prompt("title"))? 89 match &args.title {
86 .clone(), 90 Some(t) => Some(t.clone()),
91 None => {
92 if Interactor::default().confirm(
93 PromptConfirmParms::default()
94 .with_default(false)
95 .with_prompt("include cover letter?"),
96 )? {
97 Some(
98 Interactor::default()
99 .input(PromptInputParms::default().with_prompt("title"))?
100 .clone(),
101 )
102 } else {
103 None
104 }
105 }
106 }
87 }; 107 };
88 108
89 let description = match &args.description { 109 let cover_letter_title_description = if let Some(title) = title {
90 Some(t) => t.clone(), 110 Some((
91 None => Interactor::default() 111 title,
92 .input(PromptInputParms::default().with_prompt("description (Optional)"))?, 112 if let Some(t) = &args.description {
113 t.clone()
114 } else {
115 Interactor::default()
116 .input(PromptInputParms::default().with_prompt("cover letter description"))?
117 .clone()
118 },
119 ))
120 } else {
121 None
93 }; 122 };
94 123
95 // let cover_letter_title_description = if let Some(title) = title {
96 // Some((
97 // title,
98 // if let Some(t) = &args.description {
99 // t.clone()
100 // } else {
101 // Interactor::default()
102 // .input(PromptInputParms::default().with_prompt("cover letter
103 // description"))? .clone()
104 // },
105 // ))
106 // } else {
107 // None
108 // };
109
110 #[cfg(not(test))] 124 #[cfg(not(test))]
111 let mut client = Client::default(); 125 let mut client = Client::default();
112 #[cfg(test)] 126 #[cfg(test)]
@@ -127,10 +141,11 @@ pub async fn launch(
127 ) 141 )
128 .await?; 142 .await?;
129 143
144 // oldest first
145 ahead.reverse();
146
130 let events = generate_pr_and_patch_events( 147 let events = generate_pr_and_patch_events(
131 // cover_letter_title_description, 148 cover_letter_title_description.clone(),
132 &title,
133 &description,
134 &git_repo, 149 &git_repo,
135 &ahead, 150 &ahead,
136 &keys, 151 &keys,
@@ -138,8 +153,17 @@ pub async fn launch(
138 )?; 153 )?;
139 154
140 println!( 155 println!(
141 "posting 1 pull request with {} commits...", 156 "posting {} patches {} a covering letter...",
142 events.len() - 1 157 if cover_letter_title_description.is_none() {
158 events.len()
159 } else {
160 events.len() - 1
161 },
162 if cover_letter_title_description.is_none() {
163 "without"
164 } else {
165 "with"
166 }
143 ); 167 );
144 168
145 send_events( 169 send_events(
@@ -329,9 +353,7 @@ pub static PR_KIND: u64 = 318;
329pub static PATCH_KIND: u64 = 1617; 353pub static PATCH_KIND: u64 = 1617;
330 354
331pub fn generate_pr_and_patch_events( 355pub fn generate_pr_and_patch_events(
332 title: &str, 356 cover_letter_title_description: Option<(String, String)>,
333 description: &str,
334 // cover_letter_title_description: Option<(String, String)>,
335 git_repo: &Repo, 357 git_repo: &Repo,
336 commits: &Vec<Sha1Hash>, 358 commits: &Vec<Sha1Hash>,
337 keys: &nostr::Keys, 359 keys: &nostr::Keys,
@@ -343,8 +365,7 @@ pub fn generate_pr_and_patch_events(
343 365
344 let mut events = vec![]; 366 let mut events = vec![];
345 367
346 // if let Some((title, description)) = cover_letter_title_description { 368 if let Some((title, description)) = cover_letter_title_description {
347 if !title.is_empty() {
348 events.push(EventBuilder::new( 369 events.push(EventBuilder::new(
349 nostr::event::Kind::Custom(PR_KIND), 370 nostr::event::Kind::Custom(PR_KIND),
350 format!( 371 format!(
@@ -400,6 +421,15 @@ pub fn generate_pr_and_patch_events(
400 } else { 421 } else {
401 Some(((i + 1).try_into()?, commits.len().try_into()?)) 422 Some(((i + 1).try_into()?, commits.len().try_into()?))
402 }, 423 },
424 if events.is_empty() {
425 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() {
426 Some(branch_name)
427 } else {
428 None
429 }
430 } else {
431 None
432 },
403 ) 433 )
404 .context("failed to generate patch event")?, 434 .context("failed to generate patch event")?,
405 ); 435 );
@@ -410,36 +440,72 @@ pub fn generate_pr_and_patch_events(
410pub struct CoverLetter { 440pub struct CoverLetter {
411 pub title: String, 441 pub title: String,
412 pub description: String, 442 pub description: String,
413 pub branch_name: Option<String>, 443 pub branch_name: String,
414} 444}
415 445
416fn event_is_cover_letter(event: &nostr::Event) -> bool { 446pub fn event_is_cover_letter(event: &nostr::Event) -> bool {
417 event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) 447 event.kind.as_u64().eq(&PR_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter"))
418} 448}
419pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { 449pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> {
420 if !event_is_cover_letter(event) { 450 if !event_is_patch_set_root(event) {
421 bail!("event is not a cover letter") 451 bail!("event is not a patch set root event (root patch or cover letter)")
422 } 452 }
423 let title_index = event 453 let title_index = event
424 .content 454 .content
425 .find("] ") 455 .find("] ")
426 .context("event is not formatted as a cover letter patch")? 456 .context("event is not formatted as a patch or cover letter")?
427 + 2; 457 + 2;
428 let description_index = event.content[title_index..] 458 let description_index = event.content[title_index..]
429 .find('\n') 459 .find('\n')
430 .unwrap_or(event.content.len() - 1 - title_index) 460 .unwrap_or(event.content.len() - 1 - title_index)
431 + title_index; 461 + title_index;
432 462
463 let title = if let Ok(msg) = tag_value(event, "description") {
464 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
465 } else {
466 event.content[title_index..description_index].to_string()
467 };
468
469 // note: if the description field is removed from patch events like in gitstr,
470 // then this will show entire patch. I'm not sure it is ever displayed though
471 let description = if let Ok(msg) = tag_value(event, "description") {
472 if let Some((_before, after)) = msg.split_once('\n') {
473 after.trim().to_string()
474 } else {
475 String::new()
476 }
477 } else {
478 event.content[description_index..].trim().to_string()
479 };
480
433 Ok(CoverLetter { 481 Ok(CoverLetter {
434 title: event.content[title_index..description_index].to_string(), 482 title: title.clone(),
435 description: event.content[description_index..].trim().to_string(), 483 description,
436 branch_name: event 484 // TODO should this be prefixed by format!("{}-"e.id.to_string()[..5]?)
437 .iter_tags() 485 branch_name: if let Ok(name) = tag_value(event, "branch-name") {
438 .find(|t| t.as_vec()[0].eq("branch-name")) 486 name
439 .map(|tag| tag.as_vec()[1].clone()), 487 } else {
488 let s = title
489 .replace(' ', "-")
490 .chars()
491 .map(|c| {
492 if c.is_ascii_alphanumeric() || c.eq(&'/') {
493 c
494 } else {
495 '-'
496 }
497 })
498 .collect();
499 s
500 },
440 }) 501 })
441} 502}
442 503
504pub fn event_is_patch_set_root(event: &nostr::Event) -> bool {
505 (event.kind.as_u64().eq(&PR_KIND) || event.kind.as_u64().eq(&PATCH_KIND))
506 && event.iter_tags().any(|t| t.as_vec()[1].eq("root"))
507}
508
443#[allow(clippy::too_many_arguments)] 509#[allow(clippy::too_many_arguments)]
444pub fn generate_patch_event( 510pub fn generate_patch_event(
445 git_repo: &Repo, 511 git_repo: &Repo,
@@ -450,6 +516,7 @@ pub fn generate_patch_event(
450 repo_ref: &RepoRef, 516 repo_ref: &RepoRef,
451 parent_patch_event_id: Option<nostr::EventId>, 517 parent_patch_event_id: Option<nostr::EventId>,
452 series_count: Option<(u64, u64)>, 518 series_count: Option<(u64, u64)>,
519 branch_name: Option<String>,
453) -> Result<nostr::Event> { 520) -> Result<nostr::Event> {
454 let commit_parent = git_repo 521 let commit_parent = git_repo
455 .get_commit_parent(commit) 522 .get_commit_parent(commit)
@@ -496,6 +563,18 @@ pub fn generate_patch_event(
496 } else { 563 } else {
497 vec![] 564 vec![]
498 }, 565 },
566 if let Some(branch_name) = branch_name {
567 if thread_event_id.is_none() {
568 vec![
569 Tag::Generic(
570 TagKind::Custom("branch-name".to_string()),
571 vec![branch_name.to_string()],
572 )
573 ]
574 }
575 else { vec![]}
576 }
577 else { vec![]},
499 // whilst it is in nip34 draft to tag the maintainers 578 // whilst it is in nip34 draft to tag the maintainers
500 // I'm not sure it is a good idea because if they are 579 // I'm not sure it is a good idea because if they are
501 // interested in all patches then their specialised 580 // interested in all patches then their specialised
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 @@
1use anyhow::{bail, Context, Result}; 1use anyhow::{bail, Context, Result};
2 2
3use super::create::event_is_patch_set_root;
3#[cfg(not(test))] 4#[cfg(not(test))]
4use crate::client::Client; 5use crate::client::Client;
5#[cfg(test)] 6#[cfg(test)]
@@ -8,8 +9,10 @@ use crate::{
8 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, 9 cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
9 client::Connect, 10 client::Connect,
10 git::{Repo, RepoActions}, 11 git::{Repo, RepoActions},
11 repo_ref::{self}, 12 repo_ref::{self, RepoRef, REPO_REF_KIND},
12 sub_commands::prs::create::{event_to_cover_letter, PATCH_KIND, PR_KIND}, 13 sub_commands::prs::create::{
14 event_is_cover_letter, event_to_cover_letter, PATCH_KIND, PR_KIND,
15 },
13 Cli, 16 Cli,
14}; 17};
15 18
@@ -51,40 +54,8 @@ pub async fn launch(
51 54
52 println!("finding PRs..."); 55 println!("finding PRs...");
53 56
54 let pr_events: Vec<nostr::Event> = client 57 let pr_events: Vec<nostr::Event> =
55 .get_events( 58 find_pr_events(&client, &repo_ref, &root_commit.to_string()).await?;
56 repo_ref.relays.clone(),
57 vec![
58 nostr::Filter::default()
59 .kind(nostr::Kind::Custom(PR_KIND))
60 .reference(format!("{root_commit}")),
61 ],
62 )
63 .await?
64 .iter()
65 .filter(|e| {
66 e.kind.as_u64() == PR_KIND
67 && e.tags
68 .iter()
69 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
70 })
71 .map(std::borrow::ToOwned::to_owned)
72 .collect();
73
74 // let pr_branch_names: Vec<String> = pr_events
75 // .iter()
76 // .map(|e| {
77 // format!(
78 // "{}-{}",
79 // &e.id.to_string()[..5],
80 // if let Some(t) = e.tags.iter().find(|t| t.as_vec()[0] ==
81 // "branch-name") { t.as_vec()[1].to_string()
82 // } else {
83 // "".to_string()
84 // } // git_repo.get_checked_out_branch_name(),
85 // )
86 // })
87 // .collect();
88 59
89 let selected_index = Interactor::default().choice( 60 let selected_index = Interactor::default().choice(
90 PromptChoiceParms::default() 61 PromptChoiceParms::default()
@@ -95,6 +66,8 @@ pub async fn launch(
95 .map(|e| { 66 .map(|e| {
96 if let Ok(cl) = event_to_cover_letter(e) { 67 if let Ok(cl) = event_to_cover_letter(e) {
97 cl.title 68 cl.title
69 } else if let Ok(msg) = tag_value(e, "description") {
70 msg.split('\n').collect::<Vec<&str>>()[0].to_string()
98 } else { 71 } else {
99 e.id.to_string() 72 e.id.to_string()
100 } 73 }
@@ -102,49 +75,20 @@ pub async fn launch(
102 .collect(), 75 .collect(),
103 ), 76 ),
104 )?; 77 )?;
105 // println!("prs:{:?}", &pr_events);
106 78
107 println!("finding commits..."); 79 println!("finding commits...");
108 80
109 let commits_events: Vec<nostr::Event> = client 81 let commits_events: Vec<nostr::Event> =
110 .get_events( 82 find_commits_for_pr_event(&client, &pr_events[selected_index], &repo_ref).await?;
111 repo_ref.relays.clone(),
112 vec![
113 nostr::Filter::default()
114 .kind(nostr::Kind::Custom(PATCH_KIND))
115 .event(pr_events[selected_index].id),
116 ],
117 )
118 .await?
119 .iter()
120 .filter(|e| {
121 e.kind.as_u64() == PATCH_KIND
122 && e.tags.iter().any(|t| {
123 t.as_vec().len() > 2
124 && t.as_vec()[1].eq(&pr_events[selected_index].id.to_string())
125 })
126 })
127 .map(std::borrow::ToOwned::to_owned)
128 .collect();
129 83
130 confirm_checkout(&git_repo)?; 84 confirm_checkout(&git_repo)?;
131 85
132 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) 86 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events)
133 .context("cannot get most recent patch for PR")?; 87 .context("cannot get most recent patch for PR")?;
134 88
135 let branch_name: String = if let Ok(cl) = event_to_cover_letter(&pr_events[selected_index]) { 89 let branch_name: String = event_to_cover_letter(&pr_events[selected_index])
136 if let Some(name) = cl.branch_name { 90 .context("cannot assign a branch name as event is not a patch set root")?
137 name 91 .branch_name;
138 } else {
139 cl.title
140 .replace(' ', "-")
141 .chars()
142 .filter(|c| c.is_ascii_alphanumeric() || c.eq(&'/'))
143 .collect()
144 }
145 } else {
146 bail!("Placeholder not a cover letter")
147 };
148 92
149 let applied = git_repo 93 let applied = git_repo
150 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain) 94 .apply_patch_chain(&branch_name, most_recent_pr_patch_chain)
@@ -193,20 +137,139 @@ pub fn get_most_recent_patch_with_ancestors(
193) -> Result<Vec<nostr::Event>> { 137) -> Result<Vec<nostr::Event>> {
194 patches.sort_by_key(|e| e.created_at); 138 patches.sort_by_key(|e| e.created_at);
195 139
196 let mut res = vec![]; 140 let first_patch = patches.first().context("no patches found")?;
197 141
198 let latest_commit_id = tag_value(patches.first().context("no patches found")?, "commit")?; 142 let patches_with_youngest_created_at: Vec<&nostr::Event> = patches
143 .iter()
144 .filter(|p| p.created_at.eq(&first_patch.created_at))
145 .collect();
146
147 let latest_commit_id = tag_value(
148 // get the first patch which isn't a parent of a patch event created at the same
149 // time
150 patches_with_youngest_created_at
151 .clone()
152 .iter()
153 .find(|p| {
154 if let Ok(commit) = tag_value(p, "commit") {
155 !patches_with_youngest_created_at.iter().any(|p2| {
156 if let Ok(parent) = tag_value(p2, "parent-commit") {
157 commit.eq(&parent)
158 } else {
159 false // skip
160 }
161 })
162 } else {
163 false // skip
164 }
165 })
166 .context("cannot find patches_with_youngest_created_at")?,
167 "commit",
168 )?;
169
170 let mut res = vec![];
199 171
200 let mut commit_id_to_search = latest_commit_id; 172 let mut commit_id_to_search = latest_commit_id;
201 173
202 while let Some(event) = patches.iter().find(|e| { 174 while let Some(event) = patches.iter().find(|e| {
203 tag_value(e, "commit") 175 if let Ok(commit) = tag_value(e, "commit") {
204 .context("patch event doesnt contain commit tag") 176 commit.eq(&commit_id_to_search)
205 .unwrap() 177 } else {
206 .eq(&commit_id_to_search) 178 false // skip
179 }
207 }) { 180 }) {
208 res.push(event.clone()); 181 res.push(event.clone());
209 commit_id_to_search = tag_value(event, "parent-commit")?; 182 commit_id_to_search = tag_value(event, "parent-commit")?;
210 } 183 }
211 Ok(res) 184 Ok(res)
212} 185}
186
187pub async fn find_pr_events(
188 #[cfg(test)] client: &crate::client::MockConnect,
189 #[cfg(not(test))] client: &Client,
190 repo_ref: &RepoRef,
191 root_commit: &str,
192) -> Result<Vec<nostr::Event>> {
193 Ok(client
194 .get_events(
195 repo_ref.relays.clone(),
196 vec![
197 nostr::Filter::default()
198 .kinds(vec![
199 nostr::Kind::Custom(PR_KIND),
200 nostr::Kind::Custom(PATCH_KIND),
201 ])
202 .custom_tag(nostr::Alphabet::T, vec!["root"])
203 .identifiers(
204 repo_ref
205 .maintainers
206 .iter()
207 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
208 ),
209 // also pick up prs from the same repo but no target at our maintainers repo events
210 nostr::Filter::default()
211 .kinds(vec![
212 nostr::Kind::Custom(PR_KIND),
213 nostr::Kind::Custom(PATCH_KIND),
214 ])
215 .custom_tag(nostr::Alphabet::T, vec!["root"])
216 .reference(root_commit),
217 ],
218 )
219 .await
220 .context("cannot get pr events")?
221 .iter()
222 .filter(|e| {
223 event_is_patch_set_root(e)
224 && (e
225 .tags
226 .iter()
227 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(root_commit))
228 || e.tags.iter().any(|t| {
229 t.as_vec().len() > 1
230 && repo_ref
231 .maintainers
232 .iter()
233 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier))
234 .any(|d| t.as_vec()[1].eq(&d))
235 }))
236 })
237 .map(std::borrow::ToOwned::to_owned)
238 .collect::<Vec<nostr::Event>>())
239}
240
241pub async fn find_commits_for_pr_event(
242 #[cfg(test)] client: &crate::client::MockConnect,
243 #[cfg(not(test))] client: &Client,
244 pr_event: &nostr::Event,
245 repo_ref: &RepoRef,
246) -> Result<Vec<nostr::Event>> {
247 let mut patch_events: Vec<nostr::Event> = client
248 .get_events(
249 repo_ref.relays.clone(),
250 vec![
251 nostr::Filter::default()
252 .kind(nostr::Kind::Custom(PATCH_KIND))
253 // this requires every patch to reference the root event
254 // this will not pick up v2,v3 patch sets
255 // TODO: fetch commits for v2.. patch sets
256 .event(pr_event.id),
257 ],
258 )
259 .await
260 .context("cannot fetch patch events")?
261 .iter()
262 .filter(|e| {
263 e.kind.as_u64() == PATCH_KIND
264 && e.tags
265 .iter()
266 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
267 })
268 .map(std::borrow::ToOwned::to_owned)
269 .collect();
270
271 if !event_is_cover_letter(pr_event) {
272 patch_events.push(pr_event.clone());
273 }
274 Ok(patch_events)
275}
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::{
8 client::Connect, 8 client::Connect,
9 git::{Repo, RepoActions}, 9 git::{Repo, RepoActions},
10 repo_ref, 10 repo_ref,
11 repo_ref::REPO_REF_KIND, 11 sub_commands::{
12 sub_commands::prs::{ 12 prs::list::get_most_recent_patch_with_ancestors, push::fetch_pr_and_most_recent_patch_chain,
13 create::{PATCH_KIND, PR_KIND},
14 list::{get_most_recent_patch_with_ancestors, tag_value},
15 }, 13 },
16}; 14};
17 15
@@ -46,63 +44,15 @@ pub async fn launch() -> Result<()> {
46 ) 44 )
47 .await?; 45 .await?;
48 46
49 println!("finding PR event..."); 47 let (_pr_event, commit_events) =
50 48 fetch_pr_and_most_recent_patch_chain(&client, &repo_ref, &root_commit, &branch_name)
51 let pr_event: nostr::Event = client 49 .await?;
52 .get_events(
53 repo_ref.relays.clone(),
54 vec![
55 nostr::Filter::default()
56 .kind(nostr::Kind::Custom(PR_KIND))
57 .identifiers(
58 repo_ref
59 .maintainers
60 .iter()
61 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
62 ),
63 ],
64 )
65 .await?
66 .iter()
67 .find(|e| {
68 e.kind.as_u64() == PR_KIND
69 && e.tags
70 .iter()
71 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
72 && tag_value(e, "branch-name")
73 .unwrap_or_default()
74 .eq(&branch_name)
75 })
76 .context("cannot find a PR event associated with the checked out branch name")?
77 .to_owned();
78
79 println!("found PR event. finding commits...");
80
81 let commits_events: Vec<nostr::Event> = client
82 .get_events(
83 repo_ref.relays.clone(),
84 vec![
85 nostr::Filter::default()
86 .kind(nostr::Kind::Custom(PATCH_KIND))
87 .event(pr_event.id),
88 ],
89 )
90 .await?
91 .iter()
92 .filter(|e| {
93 e.kind.as_u64() == PATCH_KIND
94 && e.tags
95 .iter()
96 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
97 })
98 .map(std::borrow::ToOwned::to_owned)
99 .collect();
100 50
101 if git_repo.has_outstanding_changes()? { 51 if git_repo.has_outstanding_changes()? {
102 bail!("cannot pull changes when repository is not clean. discard changes and try again."); 52 bail!("cannot pull changes when repository is not clean. discard changes and try again.");
103 } 53 }
104 54
105 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) 55 let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commit_events)
106 .context("cannot get most recent patch for PR")?; 56 .context("cannot get most recent patch for PR")?;
107 57
108 let applied = git_repo 58 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::{
9 client::Connect, 9 client::Connect,
10 git::{str_to_sha1, Repo, RepoActions}, 10 git::{str_to_sha1, Repo, RepoActions},
11 login, 11 login,
12 repo_ref::{self, RepoRef, REPO_REF_KIND}, 12 repo_ref::{self, RepoRef},
13 sub_commands::prs::{ 13 sub_commands::prs::{
14 create::{generate_patch_event, send_events, PATCH_KIND, PR_KIND}, 14 create::{event_to_cover_letter, generate_patch_event, send_events},
15 list::{get_most_recent_patch_with_ancestors, tag_value}, 15 list::{
16 find_commits_for_pr_event, find_pr_events, get_most_recent_patch_with_ancestors,
17 tag_value,
18 },
16 }, 19 },
17 Cli, 20 Cli,
18}; 21};
@@ -111,6 +114,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> {
111 &repo_ref, 114 &repo_ref,
112 patch_events.last().map(nostr::Event::id), 115 patch_events.last().map(nostr::Event::id),
113 None, 116 None,
117 None,
114 ) 118 )
115 .context("cannot make patch event from commit")?, 119 .context("cannot make patch event from commit")?,
116 ); 120 );
@@ -131,7 +135,7 @@ pub async fn launch(cli_args: &Cli) -> Result<()> {
131 Ok(()) 135 Ok(())
132} 136}
133 137
134async fn fetch_pr_and_most_recent_patch_chain( 138pub async fn fetch_pr_and_most_recent_patch_chain(
135 #[cfg(test)] client: &crate::client::MockConnect, 139 #[cfg(test)] client: &crate::client::MockConnect,
136 #[cfg(not(test))] client: &Client, 140 #[cfg(not(test))] client: &Client,
137 repo_ref: &RepoRef, 141 repo_ref: &RepoRef,
@@ -140,54 +144,24 @@ async fn fetch_pr_and_most_recent_patch_chain(
140) -> Result<(nostr::Event, Vec<nostr::Event>)> { 144) -> Result<(nostr::Event, Vec<nostr::Event>)> {
141 println!("finding PR event..."); 145 println!("finding PR event...");
142 146
143 let pr_event: nostr::Event = client 147 let pr_events: Vec<nostr::Event> = find_pr_events(client, repo_ref, &root_commit.to_string())
144 .get_events( 148 .await
145 repo_ref.relays.clone(), 149 .context("cannot get pr events for repo")?;
146 vec![ 150
147 nostr::Filter::default() 151 let pr_event: nostr::Event = pr_events
148 .kind(nostr::Kind::Custom(PR_KIND))
149 .identifiers(
150 repo_ref
151 .maintainers
152 .iter()
153 .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
154 ),
155 ],
156 )
157 .await?
158 .iter() 152 .iter()
159 .find(|e| { 153 .find(|e| {
160 e.kind.as_u64() == PR_KIND 154 event_to_cover_letter(e).is_ok_and(|cl| cl.branch_name.eq(branch_name))
161 && e.tags 155 // TODO remove the dependancy on same branch name and replace with
162 .iter() 156 // references stored in .git/ngit
163 .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("{root_commit}")))
164 && tag_value(e, "branch-name")
165 .unwrap_or_default()
166 .eq(branch_name)
167 }) 157 })
168 .context("cannot find a PR event associated with the checked out branch name")? 158 .context("cannot find a PR event associated with the checked out branch name")?
169 .to_owned(); 159 .to_owned();
170 160
171 println!("found PR event. finding commits..."); 161 println!("found PR event. finding commits...");
172 162
173 let commits_events: Vec<nostr::Event> = client 163 let commits_events: Vec<nostr::Event> =
174 .get_events( 164 find_commits_for_pr_event(client, &pr_event, repo_ref).await?;
175 repo_ref.relays.clone(), 165
176 vec![
177 nostr::Filter::default()
178 .kind(nostr::Kind::Custom(PATCH_KIND))
179 .event(pr_event.id),
180 ],
181 )
182 .await?
183 .iter()
184 .filter(|e| {
185 e.kind.as_u64() == PATCH_KIND
186 && e.tags
187 .iter()
188 .any(|t| t.as_vec().len() > 2 && t.as_vec()[1].eq(&pr_event.id.to_string()))
189 })
190 .map(std::borrow::ToOwned::to_owned)
191 .collect();
192 Ok((pr_event, commits_events)) 166 Ok((pr_event, commits_events))
193} 167}
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 @@
1use anyhow::Result; 1use anyhow::Result;
2use futures::join;
2use serial_test::serial; 3use serial_test::serial;
3use test_utils::{git::GitTestRepo, *}; 4use test_utils::{git::GitTestRepo, relay::Relay, *};
4 5
5#[test] 6#[test]
6fn when_to_branch_doesnt_exist_return_error() -> Result<()> { 7fn when_to_branch_doesnt_exist_return_error() -> Result<()> {
@@ -150,121 +151,133 @@ fn is_patch(event: &nostr::Event) -> bool {
150 && !event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) 151 && !event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter"))
151} 152}
152 153
153mod sends_pr_and_2_patches_to_3_relays { 154fn prep_git_repo() -> Result<GitTestRepo> {
154 use futures::join; 155 let test_repo = GitTestRepo::default();
155 use test_utils::relay::Relay; 156 test_repo.populate()?;
156 157 // create feature branch with 2 commit ahead
157 use super::*; 158 test_repo.create_branch("feature")?;
159 test_repo.checkout("feature")?;
160 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
161 test_repo.stage_and_commit("add t3.md")?;
162 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
163 test_repo.stage_and_commit("add t4.md")?;
164 Ok(test_repo)
165}
158 166
159 fn prep_git_repo() -> Result<GitTestRepo> { 167fn cli_tester_create_pr(git_repo: &GitTestRepo, include_cover_letter: bool) -> CliTester {
160 let test_repo = GitTestRepo::default(); 168 let mut args = vec![
161 test_repo.populate()?; 169 "--nsec",
162 // create feature branch with 2 commit ahead 170 TEST_KEY_1_NSEC,
163 test_repo.create_branch("feature")?; 171 "--password",
164 test_repo.checkout("feature")?; 172 TEST_PASSWORD,
165 std::fs::write(test_repo.dir.join("t3.md"), "some content")?; 173 "--disable-cli-spinners",
166 test_repo.stage_and_commit("add t3.md")?; 174 "prs",
167 std::fs::write(test_repo.dir.join("t4.md"), "some content")?; 175 "create",
168 test_repo.stage_and_commit("add t4.md")?; 176 ];
169 Ok(test_repo) 177 if include_cover_letter {
178 for arg in [
179 "--title",
180 "exampletitle",
181 "--description",
182 "exampledescription",
183 ] {
184 args.push(arg);
185 }
186 } else {
187 args.push("--no-cover-letter");
170 } 188 }
189 CliTester::new_from_dir(&git_repo.dir, args)
190}
171 191
172 fn cli_tester_create_pr(git_repo: &GitTestRepo) -> CliTester { 192fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> {
173 CliTester::new_from_dir( 193 p.expect("creating patch for 2 commits from 'head' that can be merged into 'main'\r\n")?;
174 &git_repo.dir, 194 p.expect("searching for your details...\r\n")?;
175 [ 195 p.expect("\r")?;
176 "--nsec", 196 p.expect("logged in as fred\r\n")?;
177 TEST_KEY_1_NSEC, 197 p.expect(format!(
178 "--password", 198 "posting 2 patches {} a covering letter...\r\n",
179 TEST_PASSWORD, 199 if include_cover_letter {
180 "--disable-cli-spinners", 200 "with"
181 "prs", 201 } else {
182 "create", 202 "without"
183 "--title", 203 }
184 "exampletitle", 204 ))?;
185 "--description", 205 Ok(())
186 "exampledescription", 206}
187 ],
188 )
189 }
190 207
191 fn expect_msgs_first(p: &mut CliTester) -> Result<()> { 208async fn prep_run_create_pr(
192 p.expect("creating patch for 2 commits from 'head' that can be merged into 'main'\r\n")?; 209 include_cover_letter: bool,
193 p.expect("searching for your details...\r\n")?; 210) -> Result<(
194 p.expect("\r")?; 211 Relay<'static>,
195 p.expect("logged in as fred\r\n")?; 212 Relay<'static>,
196 p.expect("posting 1 pull request with 2 commits...\r\n")?; 213 Relay<'static>,
214 Relay<'static>,
215 Relay<'static>,
216)> {
217 let git_repo = prep_git_repo()?;
218 // fallback (51,52) user write (53, 55) repo (55, 56)
219 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
220 Relay::new(
221 8051,
222 None,
223 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
224 relay.respond_events(
225 client_id,
226 &subscription_id,
227 &vec![
228 generate_test_key_1_metadata_event("fred"),
229 generate_test_key_1_relay_list_event(),
230 ],
231 )?;
232 Ok(())
233 }),
234 ),
235 Relay::new(8052, None, None),
236 Relay::new(8053, None, None),
237 Relay::new(
238 8055,
239 None,
240 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
241 relay.respond_events(
242 client_id,
243 &subscription_id,
244 &vec![generate_repo_ref_event()],
245 )?;
246 Ok(())
247 }),
248 ),
249 Relay::new(8056, None, None),
250 );
251
252 // // check relay had the right number of events
253 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
254 let mut p = cli_tester_create_pr(&git_repo, include_cover_letter);
255 p.expect_end_eventually()?;
256 for p in [51, 52, 53, 55, 56] {
257 relay::shutdown_relay(8000 + p)?;
258 }
197 Ok(()) 259 Ok(())
198 } 260 });
261
262 // launch relay
263 let _ = join!(
264 r51.listen_until_close(),
265 r52.listen_until_close(),
266 r53.listen_until_close(),
267 r55.listen_until_close(),
268 r56.listen_until_close(),
269 );
270 cli_tester_handle.join().unwrap()?;
271 Ok((r51, r52, r53, r55, r56))
272}
199 273
200 async fn prep_run_create_pr() -> Result<( 274mod sends_cover_letter_and_2_patches_to_3_relays {
201 Relay<'static>,
202 Relay<'static>,
203 Relay<'static>,
204 Relay<'static>,
205 Relay<'static>,
206 )> {
207 let git_repo = prep_git_repo()?;
208 // fallback (51,52) user write (53, 55) repo (55, 56)
209 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
210 Relay::new(
211 8051,
212 None,
213 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
214 relay.respond_events(
215 client_id,
216 &subscription_id,
217 &vec![
218 generate_test_key_1_metadata_event("fred"),
219 generate_test_key_1_relay_list_event(),
220 ],
221 )?;
222 Ok(())
223 }),
224 ),
225 Relay::new(8052, None, None),
226 Relay::new(8053, None, None),
227 Relay::new(
228 8055,
229 None,
230 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
231 relay.respond_events(
232 client_id,
233 &subscription_id,
234 &vec![generate_repo_ref_event()],
235 )?;
236 Ok(())
237 }),
238 ),
239 Relay::new(8056, None, None),
240 );
241
242 // // check relay had the right number of events
243 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
244 let mut p = cli_tester_create_pr(&git_repo);
245 p.expect_end_eventually()?;
246 for p in [51, 52, 53, 55, 56] {
247 relay::shutdown_relay(8000 + p)?;
248 }
249 Ok(())
250 });
251
252 // launch relay
253 let _ = join!(
254 r51.listen_until_close(),
255 r52.listen_until_close(),
256 r53.listen_until_close(),
257 r55.listen_until_close(),
258 r56.listen_until_close(),
259 );
260 cli_tester_handle.join().unwrap()?;
261 Ok((r51, r52, r53, r55, r56))
262 }
263 275
276 use super::*;
264 #[tokio::test] 277 #[tokio::test]
265 #[serial] 278 #[serial]
266 async fn only_1_pr_kind_event_sent_to_each_relay() -> Result<()> { 279 async fn only_1_pr_kind_event_sent_to_each_relay() -> Result<()> {
267 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 280 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
268 for relay in [&r53, &r55, &r56] { 281 for relay in [&r53, &r55, &r56] {
269 assert_eq!( 282 assert_eq!(
270 relay.events.iter().filter(|e| is_cover_letter(e)).count(), 283 relay.events.iter().filter(|e| is_cover_letter(e)).count(),
@@ -277,7 +290,7 @@ mod sends_pr_and_2_patches_to_3_relays {
277 #[tokio::test] 290 #[tokio::test]
278 #[serial] 291 #[serial]
279 async fn only_1_pr_kind_event_sent_to_user_relays() -> Result<()> { 292 async fn only_1_pr_kind_event_sent_to_user_relays() -> Result<()> {
280 let (_, _, r53, r55, _) = prep_run_create_pr().await?; 293 let (_, _, r53, r55, _) = prep_run_create_pr(true).await?;
281 for relay in [&r53, &r55] { 294 for relay in [&r53, &r55] {
282 assert_eq!( 295 assert_eq!(
283 relay.events.iter().filter(|e| is_cover_letter(e)).count(), 296 relay.events.iter().filter(|e| is_cover_letter(e)).count(),
@@ -290,7 +303,7 @@ mod sends_pr_and_2_patches_to_3_relays {
290 #[tokio::test] 303 #[tokio::test]
291 #[serial] 304 #[serial]
292 async fn only_1_pr_kind_event_sent_to_repo_relays() -> Result<()> { 305 async fn only_1_pr_kind_event_sent_to_repo_relays() -> Result<()> {
293 let (_, _, _, r55, r56) = prep_run_create_pr().await?; 306 let (_, _, _, r55, r56) = prep_run_create_pr(true).await?;
294 for relay in [&r55, &r56] { 307 for relay in [&r55, &r56] {
295 assert_eq!( 308 assert_eq!(
296 relay.events.iter().filter(|e| is_cover_letter(e)).count(), 309 relay.events.iter().filter(|e| is_cover_letter(e)).count(),
@@ -303,7 +316,7 @@ mod sends_pr_and_2_patches_to_3_relays {
303 #[tokio::test] 316 #[tokio::test]
304 #[serial] 317 #[serial]
305 async fn pr_not_sent_to_fallback_relay() -> Result<()> { 318 async fn pr_not_sent_to_fallback_relay() -> Result<()> {
306 let (r51, r52, _, _, _) = prep_run_create_pr().await?; 319 let (r51, r52, _, _, _) = prep_run_create_pr(true).await?;
307 for relay in [&r51, &r52] { 320 for relay in [&r51, &r52] {
308 assert_eq!( 321 assert_eq!(
309 relay.events.iter().filter(|e| is_cover_letter(e)).count(), 322 relay.events.iter().filter(|e| is_cover_letter(e)).count(),
@@ -316,7 +329,7 @@ mod sends_pr_and_2_patches_to_3_relays {
316 #[tokio::test] 329 #[tokio::test]
317 #[serial] 330 #[serial]
318 async fn only_2_patch_kind_events_sent_to_each_relay() -> Result<()> { 331 async fn only_2_patch_kind_events_sent_to_each_relay() -> Result<()> {
319 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 332 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
320 for relay in [&r53, &r55, &r56] { 333 for relay in [&r53, &r55, &r56] {
321 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2,); 334 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2,);
322 } 335 }
@@ -327,18 +340,18 @@ mod sends_pr_and_2_patches_to_3_relays {
327 #[serial] 340 #[serial]
328 async fn patch_content_contains_patch_in_email_format_with_patch_series_numbers() -> Result<()> 341 async fn patch_content_contains_patch_in_email_format_with_patch_series_numbers() -> Result<()>
329 { 342 {
330 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 343 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
331 for relay in [&r53, &r55, &r56] { 344 for relay in [&r53, &r55, &r56] {
332 let patch_events: Vec<&nostr::Event> = 345 let patch_events: Vec<&nostr::Event> =
333 relay.events.iter().filter(|e| is_patch(e)).collect(); 346 relay.events.iter().filter(|e| is_patch(e)).collect();
334 347
335 assert_eq!( 348 assert_eq!(
336 patch_events[0].content, 349 patch_events[1].content,
337 "\ 350 "\
338 From fe973a840fba2a8ab37dd505c154854a69a6505c Mon Sep 17 00:00:00 2001\n\ 351 From fe973a840fba2a8ab37dd505c154854a69a6505c Mon Sep 17 00:00:00 2001\n\
339 From: Joe Bloggs <joe.bloggs@pm.me>\n\ 352 From: Joe Bloggs <joe.bloggs@pm.me>\n\
340 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\ 353 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\
341 Subject: [PATCH 1/2] add t4.md\n\ 354 Subject: [PATCH 2/2] add t4.md\n\
342 \n\ 355 \n\
343 ---\n \ 356 ---\n \
344 t4.md | 1 +\n \ 357 t4.md | 1 +\n \
@@ -359,12 +372,12 @@ mod sends_pr_and_2_patches_to_3_relays {
359 ", 372 ",
360 ); 373 );
361 assert_eq!( 374 assert_eq!(
362 patch_events[1].content, 375 patch_events[0].content,
363 "\ 376 "\
364 From 232efb37ebc67692c9e9ff58b83c0d3d63971a0a Mon Sep 17 00:00:00 2001\n\ 377 From 232efb37ebc67692c9e9ff58b83c0d3d63971a0a Mon Sep 17 00:00:00 2001\n\
365 From: Joe Bloggs <joe.bloggs@pm.me>\n\ 378 From: Joe Bloggs <joe.bloggs@pm.me>\n\
366 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\ 379 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\
367 Subject: [PATCH 2/2] add t3.md\n\ 380 Subject: [PATCH 1/2] add t3.md\n\
368 \n\ 381 \n\
369 ---\n \ 382 ---\n \
370 t3.md | 1 +\n \ 383 t3.md | 1 +\n \
@@ -394,7 +407,7 @@ mod sends_pr_and_2_patches_to_3_relays {
394 #[tokio::test] 407 #[tokio::test]
395 #[serial] 408 #[serial]
396 async fn root_commit_as_r() -> Result<()> { 409 async fn root_commit_as_r() -> Result<()> {
397 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 410 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
398 for relay in [&r53, &r55, &r56] { 411 for relay in [&r53, &r55, &r56] {
399 let pr_event: &nostr::Event = 412 let pr_event: &nostr::Event =
400 relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); 413 relay.events.iter().find(|e| is_cover_letter(e)).unwrap();
@@ -414,7 +427,7 @@ mod sends_pr_and_2_patches_to_3_relays {
414 #[tokio::test] 427 #[tokio::test]
415 #[serial] 428 #[serial]
416 async fn a_tag_for_repo_event() -> Result<()> { 429 async fn a_tag_for_repo_event() -> Result<()> {
417 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 430 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
418 for relay in [&r53, &r55, &r56] { 431 for relay in [&r53, &r55, &r56] {
419 let pr_event: &nostr::Event = 432 let pr_event: &nostr::Event =
420 relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); 433 relay.events.iter().find(|e| is_cover_letter(e)).unwrap();
@@ -436,7 +449,7 @@ mod sends_pr_and_2_patches_to_3_relays {
436 .unwrap() 449 .unwrap()
437 .as_vec() 450 .as_vec()
438 .clone()[1..]; 451 .clone()[1..];
439 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 452 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
440 for relay in [&r53, &r55, &r56] { 453 for relay in [&r53, &r55, &r56] {
441 for m in maintainers { 454 for m in maintainers {
442 let pr_event: &nostr::Event = 455 let pr_event: &nostr::Event =
@@ -454,7 +467,7 @@ mod sends_pr_and_2_patches_to_3_relays {
454 #[tokio::test] 467 #[tokio::test]
455 #[serial] 468 #[serial]
456 async fn t_tag_cover_letter() -> Result<()> { 469 async fn t_tag_cover_letter() -> Result<()> {
457 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 470 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
458 for relay in [&r53, &r55, &r56] { 471 for relay in [&r53, &r55, &r56] {
459 let pr_event: &nostr::Event = 472 let pr_event: &nostr::Event =
460 relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); 473 relay.events.iter().find(|e| is_cover_letter(e)).unwrap();
@@ -470,7 +483,7 @@ mod sends_pr_and_2_patches_to_3_relays {
470 #[tokio::test] 483 #[tokio::test]
471 #[serial] 484 #[serial]
472 async fn t_tag_root() -> Result<()> { 485 async fn t_tag_root() -> Result<()> {
473 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 486 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
474 for relay in [&r53, &r55, &r56] { 487 for relay in [&r53, &r55, &r56] {
475 let pr_event: &nostr::Event = 488 let pr_event: &nostr::Event =
476 relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); 489 relay.events.iter().find(|e| is_cover_letter(e)).unwrap();
@@ -486,7 +499,7 @@ mod sends_pr_and_2_patches_to_3_relays {
486 #[tokio::test] 499 #[tokio::test]
487 #[serial] 500 #[serial]
488 async fn pr_tags_branch_name() -> Result<()> { 501 async fn pr_tags_branch_name() -> Result<()> {
489 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 502 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
490 for relay in [&r53, &r55, &r56] { 503 for relay in [&r53, &r55, &r56] {
491 let pr_event: &nostr::Event = 504 let pr_event: &nostr::Event =
492 relay.events.iter().find(|e| is_cover_letter(e)).unwrap(); 505 relay.events.iter().find(|e| is_cover_letter(e)).unwrap();
@@ -509,14 +522,14 @@ mod sends_pr_and_2_patches_to_3_relays {
509 use super::*; 522 use super::*;
510 523
511 async fn prep() -> Result<nostr::Event> { 524 async fn prep() -> Result<nostr::Event> {
512 let (_, _, r53, _, _) = prep_run_create_pr().await?; 525 let (_, _, r53, _, _) = prep_run_create_pr(true).await?;
513 Ok(r53.events.iter().find(|e| is_patch(e)).unwrap().clone()) 526 Ok(r53.events.iter().find(|e| is_patch(e)).unwrap().clone())
514 } 527 }
515 528
516 #[tokio::test] 529 #[tokio::test]
517 #[serial] 530 #[serial]
518 async fn commit_and_commit_r() -> Result<()> { 531 async fn commit_and_commit_r() -> Result<()> {
519 static COMMIT_ID: &str = "fe973a840fba2a8ab37dd505c154854a69a6505c"; 532 static COMMIT_ID: &str = "232efb37ebc67692c9e9ff58b83c0d3d63971a0a";
520 let most_recent_patch = prep().await?; 533 let most_recent_patch = prep().await?;
521 assert!( 534 assert!(
522 most_recent_patch 535 most_recent_patch
@@ -537,12 +550,16 @@ mod sends_pr_and_2_patches_to_3_relays {
537 #[serial] 550 #[serial]
538 async fn parent_commit() -> Result<()> { 551 async fn parent_commit() -> Result<()> {
539 // commit parent 'r' and 'parent-commit' tag 552 // commit parent 'r' and 'parent-commit' tag
540 static COMMIT_PARENT_ID: &str = "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"; 553 static COMMIT_PARENT_ID: &str = "431b84edc0d2fa118d63faa3c2db9c73d630a5ae";
541 let most_recent_patch = prep().await?; 554 let most_recent_patch = prep().await?;
542 assert!( 555 assert_eq!(
543 most_recent_patch.tags.iter().any( 556 most_recent_patch
544 |t| t.as_vec()[0].eq("parent-commit") && t.as_vec()[1].eq(COMMIT_PARENT_ID) 557 .tags
545 ) 558 .iter()
559 .find(|t| t.as_vec()[0].eq("parent-commit"))
560 .unwrap()
561 .as_vec()[1],
562 COMMIT_PARENT_ID,
546 ); 563 );
547 Ok(()) 564 Ok(())
548 } 565 }
@@ -599,7 +616,7 @@ mod sends_pr_and_2_patches_to_3_relays {
599 .find(|t| t.as_vec()[0].eq("description")) 616 .find(|t| t.as_vec()[0].eq("description"))
600 .unwrap() 617 .unwrap()
601 .as_vec()[1], 618 .as_vec()[1],
602 "add t4.md" 619 "add t3.md"
603 ); 620 );
604 Ok(()) 621 Ok(())
605 } 622 }
@@ -639,7 +656,7 @@ mod sends_pr_and_2_patches_to_3_relays {
639 #[tokio::test] 656 #[tokio::test]
640 #[serial] 657 #[serial]
641 async fn patch_tags_pr_event_as_root() -> Result<()> { 658 async fn patch_tags_pr_event_as_root() -> Result<()> {
642 let (_, _, r53, r55, r56) = prep_run_create_pr().await?; 659 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
643 for relay in [&r53, &r55, &r56] { 660 for relay in [&r53, &r55, &r56] {
644 let patch_events: Vec<&nostr::Event> = 661 let patch_events: Vec<&nostr::Event> =
645 relay.events.iter().filter(|e| is_patch(e)).collect(); 662 relay.events.iter().filter(|e| is_patch(e)).collect();
@@ -659,6 +676,43 @@ mod sends_pr_and_2_patches_to_3_relays {
659 } 676 }
660 Ok(()) 677 Ok(())
661 } 678 }
679
680 #[tokio::test]
681 #[serial]
682 async fn second_patch_tags_first_with_reply() -> Result<()> {
683 let (_, _, r53, r55, r56) = prep_run_create_pr(true).await?;
684 for relay in [&r53, &r55, &r56] {
685 let patch_events = relay
686 .events
687 .iter()
688 .filter(|e| is_patch(e))
689 .collect::<Vec<&nostr::Event>>();
690 assert_eq!(
691 patch_events[1]
692 .iter_tags()
693 .find(|t| t.as_vec()[0].eq("e")
694 && t.as_vec().len().eq(&4)
695 && t.as_vec()[3].eq("reply"))
696 .unwrap()
697 .as_vec()[1],
698 patch_events[0].id.to_string(),
699 );
700 }
701 Ok(())
702 }
703
704 #[tokio::test]
705 #[serial]
706 async fn no_t_root_tag() -> Result<()> {
707 assert!(
708 !prep()
709 .await?
710 .tags
711 .iter()
712 .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root"))
713 );
714 Ok(())
715 }
662 } 716 }
663 mod cli_ouput { 717 mod cli_ouput {
664 use super::*; 718 use super::*;
@@ -701,8 +755,8 @@ mod sends_pr_and_2_patches_to_3_relays {
701 755
702 // // check relay had the right number of events 756 // // check relay had the right number of events
703 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 757 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
704 let mut p = cli_tester_create_pr(&git_repo); 758 let mut p = cli_tester_create_pr(&git_repo, true);
705 expect_msgs_first(&mut p)?; 759 expect_msgs_first(&mut p, true)?;
706 relay::expect_send_with_progress( 760 relay::expect_send_with_progress(
707 &mut p, 761 &mut p,
708 vec![ 762 vec![
@@ -790,7 +844,7 @@ mod sends_pr_and_2_patches_to_3_relays {
790 844
791 // // check relay had the right number of events 845 // // check relay had the right number of events
792 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 846 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
793 let mut p = cli_tester_create_pr(&git_repo); 847 let mut p = cli_tester_create_pr(&git_repo, true);
794 p.expect_end_eventually()?; 848 p.expect_end_eventually()?;
795 for p in [51, 52, 53, 55, 56] { 849 for p in [51, 52, 53, 55, 56] {
796 relay::shutdown_relay(8000 + p)?; 850 relay::shutdown_relay(8000 + p)?;
@@ -869,8 +923,8 @@ mod sends_pr_and_2_patches_to_3_relays {
869 923
870 // // check relay had the right number of events 924 // // check relay had the right number of events
871 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 925 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
872 let mut p = cli_tester_create_pr(&git_repo); 926 let mut p = cli_tester_create_pr(&git_repo, true);
873 expect_msgs_first(&mut p)?; 927 expect_msgs_first(&mut p, true)?;
874 // p.expect_end_with("bla")?; 928 // p.expect_end_with("bla")?;
875 relay::expect_send_with_progress( 929 relay::expect_send_with_progress(
876 &mut p, 930 &mut p,
@@ -915,7 +969,162 @@ mod sends_pr_and_2_patches_to_3_relays {
915 } 969 }
916} 970}
917 971
918mod without_cover_letter { 972mod sends_2_patches_without_cover_letter {
919 use super::*; 973 use super::*;
920 // TODO 974
975 mod cli_ouput {
976 use super::*;
977
978 async fn run_test_async() -> Result<()> {
979 let git_repo = prep_git_repo()?;
980
981 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
982 Relay::new(
983 8051,
984 None,
985 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
986 relay.respond_events(
987 client_id,
988 &subscription_id,
989 &vec![
990 generate_test_key_1_metadata_event("fred"),
991 generate_test_key_1_relay_list_event(),
992 ],
993 )?;
994 Ok(())
995 }),
996 ),
997 Relay::new(8052, None, None),
998 Relay::new(8053, None, None),
999 Relay::new(
1000 8055,
1001 None,
1002 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1003 relay.respond_events(
1004 client_id,
1005 &subscription_id,
1006 &vec![generate_repo_ref_event()],
1007 )?;
1008 Ok(())
1009 }),
1010 ),
1011 Relay::new(8056, None, None),
1012 );
1013
1014 // // check relay had the right number of events
1015 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1016 let mut p = cli_tester_create_pr(&git_repo, false);
1017
1018 expect_msgs_first(&mut p, false)?;
1019 relay::expect_send_with_progress(
1020 &mut p,
1021 vec![
1022 (" [my-relay] [repo-relay] ws://localhost:8055", true, ""),
1023 (" [my-relay] ws://localhost:8053", true, ""),
1024 (" [repo-relay] ws://localhost:8056", true, ""),
1025 ],
1026 2,
1027 )?;
1028 p.expect_end_with_whitespace()?;
1029 for p in [51, 52, 53, 55, 56] {
1030 relay::shutdown_relay(8000 + p)?;
1031 }
1032 Ok(())
1033 });
1034
1035 // launch relay
1036 let _ = join!(
1037 r51.listen_until_close(),
1038 r52.listen_until_close(),
1039 r53.listen_until_close(),
1040 r55.listen_until_close(),
1041 r56.listen_until_close(),
1042 );
1043 cli_tester_handle.join().unwrap()?;
1044 Ok(())
1045 }
1046
1047 #[tokio::test]
1048 #[serial]
1049 async fn check_cli_output() -> Result<()> {
1050 run_test_async().await?;
1051 Ok(())
1052 }
1053 }
1054
1055 #[tokio::test]
1056 #[serial]
1057 async fn no_cover_letter_event() -> Result<()> {
1058 let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?;
1059 for relay in [&r53, &r55, &r56] {
1060 assert_eq!(
1061 relay.events.iter().filter(|e| is_cover_letter(e)).count(),
1062 0,
1063 );
1064 }
1065 Ok(())
1066 }
1067
1068 #[tokio::test]
1069 #[serial]
1070 async fn two_patch_events() -> Result<()> {
1071 let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?;
1072 for relay in [&r53, &r55, &r56] {
1073 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2);
1074 }
1075 Ok(())
1076 }
1077
1078 #[tokio::test]
1079 #[serial]
1080 // TODO check this is the ancestor
1081 async fn first_patch_with_root_t_tag() -> Result<()> {
1082 let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?;
1083 for relay in [&r53, &r55, &r56] {
1084 let patch_events = relay
1085 .events
1086 .iter()
1087 .filter(|e| is_patch(e))
1088 .collect::<Vec<&nostr::Event>>();
1089
1090 // first patch tagged as root
1091 assert!(
1092 patch_events[0]
1093 .iter_tags()
1094 .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root"))
1095 );
1096 // second patch not tagged as root
1097 assert!(
1098 !patch_events[1]
1099 .iter_tags()
1100 .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root"))
1101 );
1102 }
1103 Ok(())
1104 }
1105
1106 #[tokio::test]
1107 #[serial]
1108 async fn second_patch_lists_first_as_root() -> Result<()> {
1109 let (_, _, r53, r55, r56) = prep_run_create_pr(false).await?;
1110 for relay in [&r53, &r55, &r56] {
1111 let patch_events = relay
1112 .events
1113 .iter()
1114 .filter(|e| is_patch(e))
1115 .collect::<Vec<&nostr::Event>>();
1116
1117 assert_eq!(
1118 patch_events[1]
1119 .iter_tags()
1120 .find(|t| t.as_vec()[0].eq("e")
1121 && t.as_vec().len().eq(&4)
1122 && t.as_vec()[3].eq("root"))
1123 .unwrap()
1124 .as_vec()[1],
1125 patch_events[0].id.to_string(),
1126 );
1127 }
1128 Ok(())
1129 }
921} 1130}
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, *};
6static FEATURE_BRANCH_NAME_1: &str = "feature-example-t"; 6static FEATURE_BRANCH_NAME_1: &str = "feature-example-t";
7static FEATURE_BRANCH_NAME_2: &str = "feature-example-f"; 7static FEATURE_BRANCH_NAME_2: &str = "feature-example-f";
8static FEATURE_BRANCH_NAME_3: &str = "feature-example-c"; 8static FEATURE_BRANCH_NAME_3: &str = "feature-example-c";
9static FEATURE_BRANCH_NAME_4: &str = "feature-example-d";
9 10
10static PR_TITLE_1: &str = "pr a"; 11static PR_TITLE_1: &str = "pr a";
11static PR_TITLE_2: &str = "pr b"; 12static PR_TITLE_2: &str = "pr b";
@@ -18,22 +19,19 @@ fn cli_tester_create_prs() -> Result<GitTestRepo> {
18 &git_repo, 19 &git_repo,
19 FEATURE_BRANCH_NAME_1, 20 FEATURE_BRANCH_NAME_1,
20 "a", 21 "a",
21 PR_TITLE_1, 22 Some((PR_TITLE_1, "pr a description")),
22 "pr a description",
23 )?; 23 )?;
24 cli_tester_create_pr( 24 cli_tester_create_pr(
25 &git_repo, 25 &git_repo,
26 FEATURE_BRANCH_NAME_2, 26 FEATURE_BRANCH_NAME_2,
27 "b", 27 "b",
28 PR_TITLE_2, 28 Some((PR_TITLE_2, "pr b description")),
29 "pr b description",
30 )?; 29 )?;
31 cli_tester_create_pr( 30 cli_tester_create_pr(
32 &git_repo, 31 &git_repo,
33 FEATURE_BRANCH_NAME_3, 32 FEATURE_BRANCH_NAME_3,
34 "c", 33 "c",
35 PR_TITLE_3, 34 Some((PR_TITLE_3, "pr c description")),
36 "pr c description",
37 )?; 35 )?;
38 Ok(git_repo) 36 Ok(git_repo)
39} 37}
@@ -66,28 +64,44 @@ fn cli_tester_create_pr(
66 test_repo: &GitTestRepo, 64 test_repo: &GitTestRepo,
67 branch_name: &str, 65 branch_name: &str,
68 prefix: &str, 66 prefix: &str,
69 title: &str, 67 cover_letter_title_and_description: Option<(&str, &str)>,
70 description: &str,
71) -> Result<()> { 68) -> Result<()> {
72 create_and_populate_branch(test_repo, branch_name, prefix, false)?; 69 create_and_populate_branch(test_repo, branch_name, prefix, false)?;
73 70
74 let mut p = CliTester::new_from_dir( 71 if let Some((title, description)) = cover_letter_title_and_description {
75 &test_repo.dir, 72 let mut p = CliTester::new_from_dir(
76 [ 73 &test_repo.dir,
77 "--nsec", 74 [
78 TEST_KEY_1_NSEC, 75 "--nsec",
79 "--password", 76 TEST_KEY_1_NSEC,
80 TEST_PASSWORD, 77 "--password",
81 "--disable-cli-spinners", 78 TEST_PASSWORD,
82 "prs", 79 "--disable-cli-spinners",
83 "create", 80 "prs",
84 "--title", 81 "create",
85 format!("\"{title}\"").as_str(), 82 "--title",
86 "--description", 83 format!("\"{title}\"").as_str(),
87 format!("\"{description}\"").as_str(), 84 "--description",
88 ], 85 format!("\"{description}\"").as_str(),
89 ); 86 ],
90 p.expect_end_eventually()?; 87 );
88 p.expect_end_eventually()?;
89 } else {
90 let mut p = CliTester::new_from_dir(
91 &test_repo.dir,
92 [
93 "--nsec",
94 TEST_KEY_1_NSEC,
95 "--password",
96 TEST_PASSWORD,
97 "--disable-cli-spinners",
98 "prs",
99 "create",
100 "--no-cover-letter",
101 ],
102 );
103 p.expect_end_eventually()?;
104 }
91 Ok(()) 105 Ok(())
92} 106}
93 107
@@ -432,6 +446,183 @@ mod when_main_branch_is_uptodate {
432 Ok(()) 446 Ok(())
433 } 447 }
434 } 448 }
449 mod when_forth_pr_has_no_cover_letter {
450 use super::*;
451
452 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
453 // fallback (51,52) user write (53, 55) repo (55, 56)
454 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
455 Relay::new(8051, None, None),
456 Relay::new(8052, None, None),
457 Relay::new(8053, None, None),
458 Relay::new(8055, None, None),
459 Relay::new(8056, None, None),
460 );
461
462 r51.events.push(generate_test_key_1_relay_list_event());
463 r51.events.push(generate_test_key_1_metadata_event("fred"));
464 r51.events.push(generate_repo_ref_event());
465
466 r55.events.push(generate_repo_ref_event());
467 r55.events.push(generate_test_key_1_metadata_event("fred"));
468 r55.events.push(generate_test_key_1_relay_list_event());
469
470 let cli_tester_handle =
471 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
472 let originating_repo = cli_tester_create_prs()?;
473 cli_tester_create_pr(
474 &originating_repo,
475 FEATURE_BRANCH_NAME_4,
476 "d",
477 None,
478 )?;
479 let test_repo = GitTestRepo::default();
480 test_repo.populate()?;
481 let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]);
482
483 p.expect("finding PRs...\r\n")?;
484 let mut c = p.expect_choice(
485 "All PRs",
486 vec![
487 format!("\"{PR_TITLE_1}\""),
488 format!("\"{PR_TITLE_2}\""),
489 format!("\"{PR_TITLE_3}\""),
490 format!("add d3.md"), // commit msg title
491 ],
492 )?;
493 c.succeeds_with(3, true)?;
494 let mut confirm =
495 p.expect_confirm_eventually("check out branch?", Some(true))?;
496 confirm.succeeds_with(None)?;
497 p.expect_end_eventually_and_print()?;
498
499 for p in [51, 52, 53, 55, 56] {
500 relay::shutdown_relay(8000 + p)?;
501 }
502 Ok((originating_repo, test_repo))
503 });
504
505 // launch relay
506 let _ = join!(
507 r51.listen_until_close(),
508 r52.listen_until_close(),
509 r53.listen_until_close(),
510 r55.listen_until_close(),
511 r56.listen_until_close(),
512 );
513 let res = cli_tester_handle.join().unwrap()?;
514
515 Ok(res)
516 }
517
518 mod cli_prompts {
519 use super::*;
520 async fn run_async_prompts_to_choose_from_pr_titles() -> Result<()> {
521 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
522 Relay::new(8051, None, None),
523 Relay::new(8052, None, None),
524 Relay::new(8053, None, None),
525 Relay::new(8055, None, None),
526 Relay::new(8056, None, None),
527 );
528
529 r51.events.push(generate_test_key_1_relay_list_event());
530 r51.events.push(generate_test_key_1_metadata_event("fred"));
531 r51.events.push(generate_repo_ref_event());
532
533 r55.events.push(generate_repo_ref_event());
534 r55.events.push(generate_test_key_1_metadata_event("fred"));
535 r55.events.push(generate_test_key_1_relay_list_event());
536
537 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
538 let originating_repo = cli_tester_create_prs()?;
539 cli_tester_create_pr(
540 &originating_repo,
541 FEATURE_BRANCH_NAME_4,
542 "d",
543 None,
544 )?;
545 let test_repo = GitTestRepo::default();
546 test_repo.populate()?;
547 let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]);
548
549 p.expect("finding PRs...\r\n")?;
550 let mut c = p.expect_choice(
551 "All PRs",
552 vec![
553 format!("\"{PR_TITLE_1}\""),
554 format!("\"{PR_TITLE_2}\""),
555 format!("\"{PR_TITLE_3}\""),
556 format!("add d3.md"), // commit msg title
557 ],
558 )?;
559 c.succeeds_with(3, true)?;
560 p.expect("finding commits...\r\n")?;
561 let mut confirm = p.expect_confirm("check out branch?", Some(true))?;
562 confirm.succeeds_with(None)?;
563 p.expect("checked out PR branch. pulled 2 new commits\r\n")?;
564 p.expect_end()?;
565
566 for p in [51, 52, 53, 55, 56] {
567 relay::shutdown_relay(8000 + p)?;
568 }
569 Ok(())
570 });
571
572 // launch relay
573 let _ = join!(
574 r51.listen_until_close(),
575 r52.listen_until_close(),
576 r53.listen_until_close(),
577 r55.listen_until_close(),
578 r56.listen_until_close(),
579 );
580 cli_tester_handle.join().unwrap()?;
581 println!("{:?}", r55.events);
582 Ok(())
583 }
584
585 #[tokio::test]
586 #[serial]
587 async fn prompts_to_choose_from_pr_titles() -> Result<()> {
588 let _ = run_async_prompts_to_choose_from_pr_titles().await;
589 Ok(())
590 }
591 }
592
593 #[tokio::test]
594 #[serial]
595 async fn pr_branch_created_with_correct_name() -> Result<()> {
596 let (_, test_repo) = prep_and_run().await?;
597 assert_eq!(
598 vec![FEATURE_BRANCH_NAME_4, "main"],
599 test_repo.get_local_branch_names()?
600 );
601 Ok(())
602 }
603
604 #[tokio::test]
605 #[serial]
606 async fn pr_branch_checked_out() -> Result<()> {
607 let (_, test_repo) = prep_and_run().await?;
608 assert_eq!(
609 FEATURE_BRANCH_NAME_4,
610 test_repo.get_checked_out_branch_name()?,
611 );
612 Ok(())
613 }
614
615 #[tokio::test]
616 #[serial]
617 async fn pr_branch_tip_is_most_recent_patch() -> Result<()> {
618 let (originating_repo, test_repo) = prep_and_run().await?;
619 assert_eq!(
620 originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_4)?,
621 test_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_4)?,
622 );
623 Ok(())
624 }
625 }
435 } 626 }
436 } 627 }
437 628