upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-03-08 14:37:56 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-03-08 14:39:21 +0000
commit6b3aecbcbde669859533716225e9c3bbfd2023b2 (patch)
tree06258c93893c57ae1ef3b3d709c364f00365fdb5
parent5622e384447fba354548aca0e39dcee3d95951c3 (diff)
feat(send): select commits from a list
when since_or_range isn't specified adds resilience as assuming master..HEAD can cause some issues eg when master is not up-to-date with origin/master
-rw-r--r--src/cli_interactor.rs44
-rw-r--r--src/git.rs74
-rw-r--r--src/sub_commands/push.rs2
-rw-r--r--src/sub_commands/send.rs216
-rw-r--r--test_utils/src/lib.rs85
-rw-r--r--tests/list.rs3
-rw-r--r--tests/pull.rs3
-rw-r--r--tests/push.rs22
-rw-r--r--tests/send.rs380
9 files changed, 438 insertions, 391 deletions
diff --git a/src/cli_interactor.rs b/src/cli_interactor.rs
index dc15c87..4cf6357 100644
--- a/src/cli_interactor.rs
+++ b/src/cli_interactor.rs
@@ -14,6 +14,7 @@ pub trait InteractorPrompt {
14 fn password(&self, parms: PromptPasswordParms) -> Result<String>; 14 fn password(&self, parms: PromptPasswordParms) -> Result<String>;
15 fn confirm(&self, params: PromptConfirmParms) -> Result<bool>; 15 fn confirm(&self, params: PromptConfirmParms) -> Result<bool>;
16 fn choice(&self, params: PromptChoiceParms) -> Result<usize>; 16 fn choice(&self, params: PromptChoiceParms) -> Result<usize>;
17 fn multi_choice(&self, params: PromptMultiChoiceParms) -> Result<Vec<usize>>;
17} 18}
18impl InteractorPrompt for Interactor { 19impl InteractorPrompt for Interactor {
19 fn input(&self, parms: PromptInputParms) -> Result<String> { 20 fn input(&self, parms: PromptInputParms) -> Result<String> {
@@ -53,6 +54,18 @@ impl InteractorPrompt for Interactor {
53 } 54 }
54 choice.interact().context("failed to get choice") 55 choice.interact().context("failed to get choice")
55 } 56 }
57 fn multi_choice(&self, parms: PromptMultiChoiceParms) -> Result<Vec<usize>> {
58 // the colorful theme is not very clear so falling back to default
59 let mut choice = dialoguer::MultiSelect::default();
60 choice
61 .with_prompt(parms.prompt)
62 .report(parms.report)
63 .items(&parms.choices);
64 if let Some(defaults) = parms.defaults {
65 choice.defaults(&defaults);
66 }
67 choice.interact().context("failed to get choice")
68 }
56} 69}
57 70
58#[derive(Default)] 71#[derive(Default)]
@@ -140,3 +153,34 @@ impl PromptChoiceParms {
140 self 153 self
141 } 154 }
142} 155}
156
157#[derive(Default)]
158pub struct PromptMultiChoiceParms {
159 pub prompt: String,
160 pub choices: Vec<String>,
161 pub defaults: Option<Vec<bool>>,
162 pub report: bool,
163}
164
165impl PromptMultiChoiceParms {
166 pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
167 self.prompt = prompt.into();
168 self.report = true;
169 self
170 }
171
172 pub fn dont_report(mut self) -> Self {
173 self.report = false;
174 self
175 }
176
177 pub fn with_choices(mut self, choices: Vec<String>) -> Self {
178 self.choices = choices;
179 self
180 }
181
182 pub fn with_defaults(mut self, defaults: Vec<bool>) -> Self {
183 self.defaults = Some(defaults);
184 self
185 }
186}
diff --git a/src/git.rs b/src/git.rs
index ef14ab1..42297be 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -41,6 +41,7 @@ pub trait RepoActions {
41 fn get_head_commit(&self) -> Result<Sha1Hash>; 41 fn get_head_commit(&self) -> Result<Sha1Hash>;
42 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>; 42 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>;
43 fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String>; 43 fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String>;
44 fn get_commit_message_summary(&self, commit: &Sha1Hash) -> Result<String>;
44 /// returns vector ["name", "email", "unixtime", "offset"] 45 /// returns vector ["name", "email", "unixtime", "offset"]
45 /// eg ["joe bloggs", "joe@pm.me", "12176","-300"] 46 /// eg ["joe bloggs", "joe@pm.me", "12176","-300"]
46 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>>; 47 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
@@ -52,6 +53,7 @@ pub trait RepoActions {
52 base_commit: &Sha1Hash, 53 base_commit: &Sha1Hash,
53 latest_commit: &Sha1Hash, 54 latest_commit: &Sha1Hash,
54 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>; 55 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>;
56 fn get_refs(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
55 // including (un)staged changes and (un)tracked files 57 // including (un)staged changes and (un)tracked files
56 fn has_outstanding_changes(&self) -> Result<bool>; 58 fn has_outstanding_changes(&self) -> Result<bool>;
57 fn make_patch_from_commit( 59 fn make_patch_from_commit(
@@ -199,6 +201,22 @@ impl RepoActions for Repo {
199 .to_string()) 201 .to_string())
200 } 202 }
201 203
204 fn get_commit_message_summary(&self, commit: &Sha1Hash) -> Result<String> {
205 Ok(self
206 .git_repo
207 .find_commit(sha1_to_oid(commit)?)
208 .context(format!("could not find commit {commit}"))?
209 .message_raw()
210 .context("commit message has unusual characters in (not valid utf-8)")?
211 .split('\r')
212 .collect::<Vec<&str>>()[0]
213 .split('\n')
214 .collect::<Vec<&str>>()[0]
215 .to_string()
216 .trim()
217 .to_string())
218 }
219
202 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>> { 220 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
203 let commit = self 221 let commit = self
204 .git_repo 222 .git_repo
@@ -217,6 +235,25 @@ impl RepoActions for Repo {
217 Ok(git_sig_to_tag_vec(&sig)) 235 Ok(git_sig_to_tag_vec(&sig))
218 } 236 }
219 237
238 fn get_refs(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
239 Ok(self
240 .git_repo
241 .references()?
242 .filter(|r| {
243 if let Ok(r) = r {
244 if let Ok(ref_tip) = r.peel_to_commit() {
245 ref_tip.id().to_string().eq(&commit.to_string())
246 } else {
247 false
248 }
249 } else {
250 false
251 }
252 })
253 .map(|r| r.unwrap().shorthand().unwrap().to_string())
254 .collect::<Vec<String>>())
255 }
256
220 fn make_patch_from_commit( 257 fn make_patch_from_commit(
221 &self, 258 &self,
222 commit: &Sha1Hash, 259 commit: &Sha1Hash,
@@ -744,6 +781,43 @@ mod tests {
744 } 781 }
745 } 782 }
746 783
784 mod get_commit_message_summary {
785 use super::*;
786 fn run(message: &str, summary: &str) -> Result<()> {
787 let test_repo = GitTestRepo::default();
788 test_repo.populate()?;
789 std::fs::write(test_repo.dir.join("t100.md"), "some content")?;
790 let oid = test_repo.stage_and_commit(message)?;
791
792 let git_repo = Repo::from_path(&test_repo.dir)?;
793
794 assert_eq!(
795 summary,
796 git_repo.get_commit_message_summary(&oid_to_sha1(&oid))?,
797 );
798 Ok(())
799 }
800 #[test]
801 fn one_liner() -> Result<()> {
802 run("add t100.md", "add t100.md")
803 }
804
805 #[test]
806 fn multiline() -> Result<()> {
807 run("add t100.md\r\nanother line\r\nthird line", "add t100.md")
808 }
809
810 #[test]
811 fn trailing_newlines() -> Result<()> {
812 run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n", "add t100.md")
813 }
814
815 #[test]
816 fn unicode_characters() -> Result<()> {
817 run("add t100.md ❤️", "add t100.md ❤️")
818 }
819 }
820
747 mod get_commit_author { 821 mod get_commit_author {
748 use super::*; 822 use super::*;
749 823
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs
index bcac178..fdaab8e 100644
--- a/src/sub_commands/push.rs
+++ b/src/sub_commands/push.rs
@@ -107,7 +107,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
107 sub_commands::send::launch( 107 sub_commands::send::launch(
108 cli_args, 108 cli_args,
109 &sub_commands::send::SubCommandArgs { 109 &sub_commands::send::SubCommandArgs {
110 since_or_revision_range: String::new(), 110 since_or_range: String::new(),
111 in_reply_to: Some(proposal_root_event.id.to_string()), 111 in_reply_to: Some(proposal_root_event.id.to_string()),
112 title: None, 112 title: None,
113 description: None, 113 description: None,
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs
index 16f10c4..9b44cc3 100644
--- a/src/sub_commands/send.rs
+++ b/src/sub_commands/send.rs
@@ -1,6 +1,7 @@
1use std::{str::FromStr, time::Duration}; 1use std::{str::FromStr, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use console::Style;
4use futures::future::join_all; 5use futures::future::join_all;
5use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 6use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
6use nostr::{ 7use nostr::{
@@ -14,7 +15,9 @@ use crate::client::Client;
14#[cfg(test)] 15#[cfg(test)]
15use crate::client::MockConnect; 16use crate::client::MockConnect;
16use crate::{ 17use crate::{
17 cli_interactor::{Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms}, 18 cli_interactor::{
19 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms,
20 },
18 client::Connect, 21 client::Connect,
19 git::{Repo, RepoActions}, 22 git::{Repo, RepoActions},
20 login, 23 login,
@@ -24,9 +27,9 @@ use crate::{
24 27
25#[derive(Debug, clap::Args)] 28#[derive(Debug, clap::Args)]
26pub struct SubCommandArgs { 29pub struct SubCommandArgs {
27 #[arg(default_value = "master..HEAD")] 30 #[arg(default_value = "")]
28 /// commits to send as proposal; like in `git format-patch` 31 /// commits to send as proposal; like in `git format-patch` eg. HEAD~2
29 pub(crate) since_or_revision_range: String, 32 pub(crate) since_or_range: String,
30 #[clap(long)] 33 #[clap(long)]
31 /// nevent or event id of an existing proposal for which this is a new 34 /// nevent or event id of an existing proposal for which this is a new
32 /// version 35 /// version
@@ -46,67 +49,83 @@ pub struct SubCommandArgs {
46pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { 49pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
47 let git_repo = Repo::discover().context("cannot find a git repository")?; 50 let git_repo = Repo::discover().context("cannot find a git repository")?;
48 51
52 if let Some(id) = &args.in_reply_to {
53 println!("creating proposal revision for: {id}");
54 }
55
49 let mut commits: Vec<Sha1Hash> = { 56 let mut commits: Vec<Sha1Hash> = {
50 if args.since_or_revision_range.is_empty() 57 if args.since_or_range.is_empty() {
51 || args.since_or_revision_range.eq("master..HEAD")
52 {
53 let branch_name = git_repo.get_checked_out_branch_name()?; 58 let branch_name = git_repo.get_checked_out_branch_name()?;
54 let (main_branch_name, main_tip) = git_repo 59 let (main_branch_name, main_tip) = git_repo
55 .get_main_or_master_branch() 60 .get_main_or_master_branch()
56 .context("the default branches (main or master) do not exist")?; 61 .context("the default branches (main or master) do not exist")?;
57 if branch_name.eq(main_branch_name) { 62
58 println!("creating 1 patch from latest commit"); 63 let proposed_commits = if branch_name.eq(main_branch_name) {
59 vec![main_tip] 64 vec![main_tip]
60 } else { 65 } else {
61 let (from_branch, to_branch, ahead, behind) = 66 let (_, _, ahead, _) = identify_ahead_behind(&git_repo, &None, &None)?;
62 identify_ahead_behind(&git_repo, &None, &None)?;
63
64 if ahead.is_empty() {
65 bail!(format!(
66 "'{from_branch}' is 0 commits ahead of '{to_branch}' so no patches were created"
67 ));
68 }
69
70 if behind.is_empty() {
71 println!(
72 "creating patch for {} commits from '{from_branch}' that can be merged into '{to_branch}'",
73 ahead.len(),
74 );
75 } else {
76 if !Interactor::default().confirm(
77 PromptConfirmParms::default()
78 .with_prompt(
79 format!(
80 "'{from_branch}' is {} commits behind '{to_branch}' and {} ahead. Consider rebasing before sending patches. Proceed anyway?",
81 behind.len(),
82 ahead.len(),
83 )
84 )
85 .with_default(false)
86 ).context("failed to get confirmation response from interactor confirm")? {
87 bail!("aborting so branch can be rebased");
88 }
89 println!(
90 "creating patch for {} commit{} from '{from_branch}' that {} {} behind '{to_branch}'",
91 ahead.len(),
92 if ahead.len() > 1 { "s" } else { "" },
93 if ahead.len() > 1 { "are" } else { "is" },
94 behind.len(),
95 );
96 }
97 ahead 67 ahead
98 } 68 };
69 choose_commits(&git_repo, proposed_commits)?
99 } else { 70 } else {
100 let ahead = git_repo 71 git_repo
101 .parse_starting_commits(&args.since_or_revision_range) 72 .parse_starting_commits(&args.since_or_range)
102 .context("cannot parse specified starting commit or range")?; 73 .context("cannot parse specified starting commit or range")?
103 println!("creating patch for {} commits", ahead.len(),);
104 ahead
105 } 74 }
106 }; 75 };
107 76
108 if let Some(id) = &args.in_reply_to { 77 if commits.is_empty() {
109 println!("as a revision to proposal: {id}"); 78 bail!("no commits selected");
79 }
80 println!("creating proposal from {} commits:", commits.len());
81
82 let dim = Style::new().color256(247);
83 for commit in &commits {
84 println!(
85 "{} {}",
86 dim.apply_to(commit.to_string().chars().take(7).collect::<String>()),
87 git_repo.get_commit_message_summary(commit)?
88 );
89 }
90
91 let (main_branch_name, main_tip) = git_repo
92 .get_main_or_master_branch()
93 .context("the default branches (main or master) do not exist")?;
94 let (first_commit_ahead, behind) =
95 git_repo.get_commits_ahead_behind(&main_tip, commits.last().context("no commits")?)?;
96
97 // check proposal ahead of origin/main
98 if first_commit_ahead.len().gt(&1) && !Interactor::default().confirm(
99 PromptConfirmParms::default()
100 .with_prompt(
101 format!("proposal builds on a commit {} ahead of '{main_branch_name}' - do you want to continue?", first_commit_ahead.len() - 1)
102 )
103 .with_default(false)
104 ).context("failed to get confirmation response from interactor confirm")? {
105 bail!("aborting because selected commits were ahead of origin/master");
106 }
107
108 // check if a selected commit is already in origin
109 if commits.iter().any(|c| c.eq(&main_tip)) {
110 if !Interactor::default().confirm(
111 PromptConfirmParms::default()
112 .with_prompt(
113 format!("proposal contains commit(s) already in '{main_branch_name}'. proceed anyway?")
114 )
115 .with_default(false)
116 ).context("failed to get confirmation response from interactor confirm")? {
117 bail!("aborting as proposal contains commit(s) already in '{main_branch_name}'");
118 }
119 }
120 // check proposal isn't behind origin/main
121 else if !behind.is_empty() && !Interactor::default().confirm(
122 PromptConfirmParms::default()
123 .with_prompt(
124 format!("proposal is {} behind '{main_branch_name}'. consider rebasing before submission. proceed anyway?", behind.len())
125 )
126 .with_default(false)
127 ).context("failed to get confirmation response from interactor confirm")? {
128 bail!("aborting so commits can be rebased");
110 } 129 }
111 130
112 let title = if args.no_cover_letter { 131 let title = if args.no_cover_letter {
@@ -359,6 +378,97 @@ where
359 (vec1_u, vec2_u, dup, all) 378 (vec1_u, vec2_u, dup, all)
360} 379}
361 380
381fn choose_commits(git_repo: &Repo, proposed_commits: Vec<Sha1Hash>) -> Result<Vec<Sha1Hash>> {
382 let mut proposed_commits = if proposed_commits.len().gt(&10) {
383 vec![]
384 } else {
385 proposed_commits
386 };
387
388 let tip_of_head = git_repo.get_tip_of_local_branch(&git_repo.get_checked_out_branch_name()?)?;
389 let most_recent_commit = proposed_commits.first().unwrap_or(&tip_of_head);
390
391 let mut last_15_commits = vec![*most_recent_commit];
392
393 while last_15_commits.len().lt(&15) {
394 if let Ok(parent_commit) = git_repo.get_commit_parent(last_15_commits.last().unwrap()) {
395 last_15_commits.push(parent_commit);
396 } else {
397 break;
398 }
399 }
400
401 let term = console::Term::stderr();
402 let mut printed_error_line = false;
403
404 let selected_commits = 'outer: loop {
405 let selected = Interactor::default().multi_choice(
406 PromptMultiChoiceParms::default()
407 .with_prompt("select commits for proposal")
408 .dont_report()
409 .with_choices(
410 last_15_commits
411 .iter()
412 .map(|h| summarise_commit_for_selection(git_repo, h).unwrap())
413 .collect(),
414 )
415 .with_defaults(
416 last_15_commits
417 .iter()
418 .map(|h| proposed_commits.iter().any(|c| c.eq(h)))
419 .collect(),
420 ),
421 )?;
422 proposed_commits = selected.iter().map(|i| last_15_commits[*i]).collect();
423
424 if printed_error_line {
425 term.clear_last_lines(1)?;
426 }
427
428 if proposed_commits.is_empty() {
429 term.write_line("no commits selected")?;
430 printed_error_line = true;
431 continue;
432 }
433 for (i, selected_i) in selected.iter().enumerate() {
434 if i.gt(&0) && selected_i.ne(&(selected[i - 1] + 1)) {
435 term.write_line("commits must be consecutive. try again.")?;
436 printed_error_line = true;
437 continue 'outer;
438 }
439 }
440
441 break proposed_commits;
442 };
443 Ok(selected_commits)
444}
445
446fn summarise_commit_for_selection(git_repo: &Repo, commit: &Sha1Hash) -> Result<String> {
447 let references = git_repo.get_refs(commit)?;
448 let dim = Style::new().color256(247);
449 let prefix = format!("({})", git_repo.get_commit_author(commit)?[0],);
450 let references_string = if references.is_empty() {
451 String::new()
452 } else {
453 format!(
454 " {}",
455 references
456 .iter()
457 .map(|r| format!("[{r}]"))
458 .collect::<Vec<String>>()
459 .join(" ")
460 )
461 };
462
463 Ok(format!(
464 "{} {}{} {}",
465 dim.apply_to(prefix),
466 git_repo.get_commit_message_summary(commit)?,
467 Style::new().magenta().apply_to(references_string),
468 dim.apply_to(commit.to_string().chars().take(7).collect::<String>(),),
469 ))
470}
471
362mod tests_unique_and_duplicate { 472mod tests_unique_and_duplicate {
363 473
364 #[test] 474 #[test]
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs
index b4f0360..2edbc60 100644
--- a/test_utils/src/lib.rs
+++ b/test_utils/src/lib.rs
@@ -1,6 +1,6 @@
1use std::{ffi::OsStr, path::PathBuf}; 1use std::{ffi::OsStr, path::PathBuf};
2 2
3use anyhow::{ensure, Context, Result}; 3use anyhow::{bail, ensure, Context, Result};
4use dialoguer::theme::{ColorfulTheme, Theme}; 4use dialoguer::theme::{ColorfulTheme, Theme};
5use directories::ProjectDirs; 5use directories::ProjectDirs;
6use nostr::{self, prelude::FromSkStr, Kind, Tag}; 6use nostr::{self, prelude::FromSkStr, Kind, Tag};
@@ -259,6 +259,20 @@ impl CliTester {
259 i.prompt(false).context("initial confirm prompt")?; 259 i.prompt(false).context("initial confirm prompt")?;
260 Ok(i) 260 Ok(i)
261 } 261 }
262
263 pub fn expect_multi_select(
264 &mut self,
265 prompt: &str,
266 choices: Vec<String>,
267 ) -> Result<CliTesterMultiSelectPrompt> {
268 let mut i = CliTesterMultiSelectPrompt {
269 tester: self,
270 prompt: prompt.to_string(),
271 choices,
272 };
273 i.prompt(false).context("initial confirm prompt")?;
274 Ok(i)
275 }
262} 276}
263 277
264pub struct CliTesterInputPrompt<'a> { 278pub struct CliTesterInputPrompt<'a> {
@@ -448,6 +462,75 @@ impl CliTesterConfirmPrompt<'_> {
448 } 462 }
449} 463}
450 464
465pub struct CliTesterMultiSelectPrompt<'a> {
466 tester: &'a mut CliTester,
467 prompt: String,
468 choices: Vec<String>,
469}
470
471impl CliTesterMultiSelectPrompt<'_> {
472 fn prompt(&mut self, eventually: bool) -> Result<&mut Self> {
473 if eventually {
474 self.tester
475 .expect_eventually(format!("{}:\r\n", self.prompt))
476 .context("expect multi-select prompt eventually")?;
477 } else {
478 self.tester
479 .expect(format!("{}:\r\n", self.prompt))
480 .context("expect multi-select prompt")?;
481 }
482 Ok(self)
483 }
484
485 pub fn succeeds_with(
486 &mut self,
487 chosen_indexes: Vec<usize>,
488 report: bool,
489 default_indexes: Vec<usize>,
490 ) -> Result<&mut Self> {
491 if report {
492 bail!("TODO: add support for report")
493 }
494
495 fn show_options(
496 tester: &mut CliTester,
497 choices: &[String],
498 active_index: usize,
499 selected_indexes: &[usize],
500 ) -> Result<()> {
501 for (index, item) in choices.iter().enumerate() {
502 tester.expect(format!(
503 "{}{}{}\r\n",
504 if active_index.eq(&index) { "> " } else { " " },
505 if selected_indexes.iter().any(|i| i.eq(&index)) {
506 "[x] "
507 } else {
508 "[ ] "
509 },
510 item,
511 ))?;
512 }
513 Ok(())
514 }
515
516 show_options(self.tester, &self.choices, 0, &default_indexes)?;
517
518 if default_indexes.eq(&chosen_indexes) {
519 self.tester.send("\r\n")?;
520 } else {
521 bail!("TODO: add support changing options");
522 }
523
524 for _ in self.choices.iter() {
525 self.tester.expect("\r")?;
526 }
527 // one for removing prompt maybe?
528 self.tester.expect("\r")?;
529
530 Ok(self)
531 }
532}
533
451pub struct CliTesterChoicePrompt<'a> { 534pub struct CliTesterChoicePrompt<'a> {
452 tester: &'a mut CliTester, 535 tester: &'a mut CliTester,
453 prompt: String, 536 prompt: String,
diff --git a/tests/list.rs b/tests/list.rs
index 7e12dc1..4f55645 100644
--- a/tests/list.rs
+++ b/tests/list.rs
@@ -83,6 +83,7 @@ fn cli_tester_create_proposal(
83 TEST_PASSWORD, 83 TEST_PASSWORD,
84 "--disable-cli-spinners", 84 "--disable-cli-spinners",
85 "send", 85 "send",
86 "HEAD~2",
86 "--no-cover-letter", 87 "--no-cover-letter",
87 "--in-reply-to", 88 "--in-reply-to",
88 in_reply_to.as_str(), 89 in_reply_to.as_str(),
@@ -99,6 +100,7 @@ fn cli_tester_create_proposal(
99 TEST_PASSWORD, 100 TEST_PASSWORD,
100 "--disable-cli-spinners", 101 "--disable-cli-spinners",
101 "send", 102 "send",
103 "HEAD~2",
102 "--title", 104 "--title",
103 format!("\"{title}\"").as_str(), 105 format!("\"{title}\"").as_str(),
104 "--description", 106 "--description",
@@ -116,6 +118,7 @@ fn cli_tester_create_proposal(
116 TEST_PASSWORD, 118 TEST_PASSWORD,
117 "--disable-cli-spinners", 119 "--disable-cli-spinners",
118 "send", 120 "send",
121 "HEAD~2",
119 "--no-cover-letter", 122 "--no-cover-letter",
120 ], 123 ],
121 ); 124 );
diff --git a/tests/pull.rs b/tests/pull.rs
index 77ff87b..50dddbf 100644
--- a/tests/pull.rs
+++ b/tests/pull.rs
@@ -81,6 +81,7 @@ fn cli_tester_create_proposal(
81 TEST_PASSWORD, 81 TEST_PASSWORD,
82 "--disable-cli-spinners", 82 "--disable-cli-spinners",
83 "send", 83 "send",
84 "HEAD~2",
84 "--no-cover-letter", 85 "--no-cover-letter",
85 "--in-reply-to", 86 "--in-reply-to",
86 in_reply_to.as_str(), 87 in_reply_to.as_str(),
@@ -97,6 +98,7 @@ fn cli_tester_create_proposal(
97 TEST_PASSWORD, 98 TEST_PASSWORD,
98 "--disable-cli-spinners", 99 "--disable-cli-spinners",
99 "send", 100 "send",
101 "HEAD~2",
100 "--title", 102 "--title",
101 format!("\"{title}\"").as_str(), 103 format!("\"{title}\"").as_str(),
102 "--description", 104 "--description",
@@ -114,6 +116,7 @@ fn cli_tester_create_proposal(
114 TEST_PASSWORD, 116 TEST_PASSWORD,
115 "--disable-cli-spinners", 117 "--disable-cli-spinners",
116 "send", 118 "send",
119 "HEAD~2",
117 "--no-cover-letter", 120 "--no-cover-letter",
118 ], 121 ],
119 ); 122 );
diff --git a/tests/push.rs b/tests/push.rs
index db7a8b8..5fe1f15 100644
--- a/tests/push.rs
+++ b/tests/push.rs
@@ -80,6 +80,7 @@ fn cli_tester_create_proposal(
80 TEST_PASSWORD, 80 TEST_PASSWORD,
81 "--disable-cli-spinners", 81 "--disable-cli-spinners",
82 "send", 82 "send",
83 "HEAD~2",
83 "--title", 84 "--title",
84 format!("\"{title}\"").as_str(), 85 format!("\"{title}\"").as_str(),
85 "--description", 86 "--description",
@@ -571,13 +572,26 @@ mod when_branch_is_checked_out {
571 p.expect("finding proposal root event...\r\n")?; 572 p.expect("finding proposal root event...\r\n")?;
572 p.expect("found proposal root event. finding commits...\r\n")?; 573 p.expect("found proposal root event. finding commits...\r\n")?;
573 p.expect("preparing to force push proposal revision...\r\n")?; 574 p.expect("preparing to force push proposal revision...\r\n")?;
574
575 // standard output from `ngit send` 575 // standard output from `ngit send`
576 p.expect(format!("creating patch for 2 commits from '{FEATURE_BRANCH_NAME_1}' that can be merged into 'main'\r\n"))?; 576 p.expect("creating proposal revision for: ")?;
577 p.expect("as a revision to proposal: ")?;
578 // proposal id will be printed in this gap 577 // proposal id will be printed in this gap
579 p.expect_eventually("\r\n")?; 578 p.expect_eventually("\r\n")?;
580 579 let mut selector = p.expect_multi_select(
580 "select commits for proposal",
581 vec![
582 "(Joe Bloggs) add a4.md [feature-example-t] 355bdf1".to_string(),
583 "(Joe Bloggs) add a3.md dbd1115".to_string(),
584 "(Joe Bloggs) commit for rebasing on top of [main] 1aa2cfe"
585 .to_string(),
586 "(Joe Bloggs) add t2.md 431b84e".to_string(),
587 "(Joe Bloggs) add t1.md af474d8".to_string(),
588 "(Joe Bloggs) Initial commit 9ee507f".to_string(),
589 ],
590 )?;
591 selector.succeeds_with(vec![0, 1], false, vec![0, 1])?;
592 p.expect("creating proposal from 2 commits:\r\n")?;
593 p.expect("355bdf1 add a4.md\r\n")?;
594 p.expect("dbd1115 add a3.md\r\n")?;
581 p.expect("searching for profile and relay updates...\r\n")?; 595 p.expect("searching for profile and relay updates...\r\n")?;
582 p.expect("\r")?; 596 p.expect("\r")?;
583 p.expect("logged in as fred\r\n")?; 597 p.expect("logged in as fred\r\n")?;
diff --git a/tests/send.rs b/tests/send.rs
index 3c619a4..538f38a 100644
--- a/tests/send.rs
+++ b/tests/send.rs
@@ -12,19 +12,8 @@ fn when_no_main_or_master_branch_return_error() -> Result<()> {
12 Ok(()) 12 Ok(())
13} 13}
14 14
15#[test] 15// TODO when commits ahead of origin/master - test ask to proceed
16fn when_no_commits_ahead_of_main_return_error() -> Result<()> { 16// TODO when commits in origin/master - test ask to proceed
17 let test_repo = GitTestRepo::default();
18 test_repo.populate()?;
19 // create feature branch with 1 commit ahead
20 test_repo.create_branch("feature")?;
21 test_repo.checkout("feature")?;
22
23 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send"]);
24 p.expect("Error: 'feature' is 0 commits ahead of 'main' so no patches were created")?;
25 Ok(())
26}
27
28mod when_commits_behind_ask_to_proceed { 17mod when_commits_behind_ask_to_proceed {
29 use super::*; 18 use super::*;
30 19
@@ -46,16 +35,13 @@ mod when_commits_behind_ask_to_proceed {
46 test_repo.checkout("feature")?; 35 test_repo.checkout("feature")?;
47 Ok(test_repo) 36 Ok(test_repo)
48 } 37 }
49 static BEHIND_LEN: u8 = 1; 38
50 static AHEAD_LEN: u8 = 2; 39 fn expect_confirm_prompt(p: &mut CliTester) -> Result<CliTesterConfirmPrompt> {
51 40 p.expect("creating proposal from 2 commits:\r\n")?;
52 fn expect_confirm_prompt( 41 p.expect("fe973a8 add t4.md\r\n")?;
53 p: &mut CliTester, 42 p.expect("232efb3 add t3.md\r\n")?;
54 behind: u8,
55 ahead: u8,
56 ) -> Result<CliTesterConfirmPrompt> {
57 p.expect_confirm( 43 p.expect_confirm(
58 format!("'feature' is {behind} commits behind 'main' and {ahead} ahead. Consider rebasing before sending patches. Proceed anyway?").as_str(), 44 "proposal is 1 behind 'main'. consider rebasing before submission. proceed anyway?",
59 Some(false), 45 Some(false),
60 ) 46 )
61 } 47 }
@@ -64,8 +50,8 @@ mod when_commits_behind_ask_to_proceed {
64 fn asked_with_default_no() -> Result<()> { 50 fn asked_with_default_no() -> Result<()> {
65 let test_repo = prep_test_repo()?; 51 let test_repo = prep_test_repo()?;
66 52
67 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send"]); 53 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]);
68 expect_confirm_prompt(&mut p, BEHIND_LEN, AHEAD_LEN)?; 54 expect_confirm_prompt(&mut p)?;
69 p.exit()?; 55 p.exit()?;
70 Ok(()) 56 Ok(())
71 } 57 }
@@ -74,11 +60,11 @@ mod when_commits_behind_ask_to_proceed {
74 fn when_response_is_false_aborts() -> Result<()> { 60 fn when_response_is_false_aborts() -> Result<()> {
75 let test_repo = prep_test_repo()?; 61 let test_repo = prep_test_repo()?;
76 62
77 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send"]); 63 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]);
78 64
79 expect_confirm_prompt(&mut p, BEHIND_LEN, AHEAD_LEN)?.succeeds_with(Some(false))?; 65 expect_confirm_prompt(&mut p)?.succeeds_with(Some(false))?;
80 66
81 p.expect_end_with("Error: aborting so branch can be rebased\r\n")?; 67 p.expect_end_with("Error: aborting so commits can be rebased\r\n")?;
82 68
83 Ok(()) 69 Ok(())
84 } 70 }
@@ -87,37 +73,14 @@ mod when_commits_behind_ask_to_proceed {
87 fn when_response_is_true_proceeds() -> Result<()> { 73 fn when_response_is_true_proceeds() -> Result<()> {
88 let test_repo = prep_test_repo()?; 74 let test_repo = prep_test_repo()?;
89 75
90 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send"]); 76 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send", "HEAD~2"]);
91 expect_confirm_prompt(&mut p, BEHIND_LEN, AHEAD_LEN)?.succeeds_with(Some(true))?; 77 expect_confirm_prompt(&mut p)?.succeeds_with(Some(true))?;
92 p.expect( 78 p.expect("? include cover letter")?;
93 format!("creating patch for {AHEAD_LEN} commits from 'feature' that are {BEHIND_LEN} behind 'main'",)
94 .as_str(),
95 )?;
96 p.exit()?; 79 p.exit()?;
97 Ok(()) 80 Ok(())
98 } 81 }
99} 82}
100 83
101#[test]
102#[serial]
103fn cli_message_creating_patches() -> Result<()> {
104 let test_repo = GitTestRepo::default();
105 test_repo.populate()?;
106 // create feature branch with 2 commit ahead
107 test_repo.create_branch("feature")?;
108 test_repo.checkout("feature")?;
109 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
110 test_repo.stage_and_commit("add t3.md")?;
111 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
112 test_repo.stage_and_commit("add t4.md")?;
113
114 let mut p = CliTester::new_from_dir(&test_repo.dir, ["send"]);
115
116 p.expect("creating patch for 2 commits from 'feature' that can be merged into 'main'")?;
117 p.exit()?;
118 Ok(())
119}
120
121fn is_cover_letter(event: &nostr::Event) -> bool { 84fn is_cover_letter(event: &nostr::Event) -> bool {
122 event.kind.as_u64().eq(&PATCH_KIND) 85 event.kind.as_u64().eq(&PATCH_KIND)
123 && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter")) 86 && event.iter_tags().any(|t| t.as_vec()[1].eq("cover-letter"))
@@ -149,6 +112,7 @@ fn cli_tester_create_proposal(git_repo: &GitTestRepo, include_cover_letter: bool
149 TEST_PASSWORD, 112 TEST_PASSWORD,
150 "--disable-cli-spinners", 113 "--disable-cli-spinners",
151 "send", 114 "send",
115 "HEAD~2",
152 ]; 116 ];
153 if include_cover_letter { 117 if include_cover_letter {
154 for arg in [ 118 for arg in [
@@ -166,7 +130,9 @@ fn cli_tester_create_proposal(git_repo: &GitTestRepo, include_cover_letter: bool
166} 130}
167 131
168fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> { 132fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> {
169 p.expect("creating patch for 2 commits from 'feature' that can be merged into 'main'\r\n")?; 133 p.expect("creating proposal from 2 commits:\r\n")?;
134 p.expect("fe973a8 add t4.md\r\n")?;
135 p.expect("232efb3 add t3.md\r\n")?;
170 p.expect("searching for profile and relay updates...\r\n")?; 136 p.expect("searching for profile and relay updates...\r\n")?;
171 p.expect("\r")?; 137 p.expect("\r")?;
172 p.expect("logged in as fred\r\n")?; 138 p.expect("logged in as fred\r\n")?;
@@ -247,7 +213,7 @@ async fn prep_run_create_proposal(
247 Ok((r51, r52, r53, r55, r56)) 213 Ok((r51, r52, r53, r55, r56))
248} 214}
249 215
250mod sends_cover_letter_and_2_patches_to_3_relays { 216mod when_cover_letter_details_specified_with_range_of_head_2_sends_cover_letter_and_2_patches_to_3_relays {
251 217
252 use super::*; 218 use super::*;
253 #[tokio::test] 219 #[tokio::test]
@@ -937,7 +903,7 @@ mod sends_cover_letter_and_2_patches_to_3_relays {
937 } 903 }
938} 904}
939 905
940mod sends_2_patches_without_cover_letter { 906mod when_no_cover_letter_flag_set_with_range_of_head_2_sends_2_patches_without_cover_letter {
941 use super::*; 907 use super::*;
942 908
943 mod cli_ouput { 909 mod cli_ouput {
@@ -1118,206 +1084,9 @@ mod sends_2_patches_without_cover_letter {
1118 } 1084 }
1119} 1085}
1120 1086
1121mod when_on_main_branch_defaults_to_last_commit { 1087mod when_range_ommited_prompts_for_selection_defaulting_ahead_of_main {
1122 use super::*;
1123
1124 fn prep_git_repo() -> Result<GitTestRepo> {
1125 let test_repo = GitTestRepo::default();
1126 test_repo.populate()?;
1127 // dont checkout feature branch
1128 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1129 test_repo.stage_and_commit("add t3.md")?;
1130 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1131 test_repo.stage_and_commit("add t4.md")?;
1132 Ok(test_repo)
1133 }
1134
1135 fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester {
1136 let args = vec![
1137 "--nsec",
1138 TEST_KEY_1_NSEC,
1139 "--password",
1140 TEST_PASSWORD,
1141 "--disable-cli-spinners",
1142 "send",
1143 "--no-cover-letter",
1144 ];
1145 CliTester::new_from_dir(&git_repo.dir, args)
1146 }
1147 fn expect_msgs_first(p: &mut CliTester) -> Result<()> {
1148 p.expect("creating 1 patch from latest commit\r\n")?;
1149 p.expect("searching for profile and relay updates...\r\n")?;
1150 p.expect("\r")?;
1151 p.expect("logged in as fred\r\n")?;
1152 p.expect("posting 1 patch without a covering letter...\r\n")?;
1153 Ok(())
1154 }
1155 async fn prep_run_create_proposal() -> Result<(
1156 Relay<'static>,
1157 Relay<'static>,
1158 Relay<'static>,
1159 Relay<'static>,
1160 Relay<'static>,
1161 )> {
1162 let git_repo = prep_git_repo()?;
1163
1164 // fallback (51,52) user write (53, 55) repo (55, 56)
1165 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
1166 Relay::new(
1167 8051,
1168 None,
1169 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1170 relay.respond_events(
1171 client_id,
1172 &subscription_id,
1173 &vec![
1174 generate_test_key_1_metadata_event("fred"),
1175 generate_test_key_1_relay_list_event(),
1176 ],
1177 )?;
1178 Ok(())
1179 }),
1180 ),
1181 Relay::new(8052, None, None),
1182 Relay::new(8053, None, None),
1183 Relay::new(
1184 8055,
1185 None,
1186 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1187 relay.respond_events(
1188 client_id,
1189 &subscription_id,
1190 &vec![generate_repo_ref_event()],
1191 )?;
1192 Ok(())
1193 }),
1194 ),
1195 Relay::new(8056, None, None),
1196 );
1197
1198 // // check relay had the right number of events
1199 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1200 let mut p = cli_tester_create_proposal(&git_repo);
1201 p.expect_end_eventually()?;
1202 for p in [51, 52, 53, 55, 56] {
1203 relay::shutdown_relay(8000 + p)?;
1204 }
1205 Ok(())
1206 });
1207
1208 // launch relay
1209 let _ = join!(
1210 r51.listen_until_close(),
1211 r52.listen_until_close(),
1212 r53.listen_until_close(),
1213 r55.listen_until_close(),
1214 r56.listen_until_close(),
1215 );
1216 cli_tester_handle.join().unwrap()?;
1217 Ok((r51, r52, r53, r55, r56))
1218 }
1219 mod cli_ouput {
1220 use super::*;
1221
1222 #[tokio::test]
1223 #[serial]
1224 async fn check_cli_output() -> Result<()> {
1225 let git_repo = prep_git_repo()?;
1226
1227 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
1228 Relay::new(
1229 8051,
1230 None,
1231 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1232 relay.respond_events(
1233 client_id,
1234 &subscription_id,
1235 &vec![
1236 generate_test_key_1_metadata_event("fred"),
1237 generate_test_key_1_relay_list_event(),
1238 ],
1239 )?;
1240 Ok(())
1241 }),
1242 ),
1243 Relay::new(8052, None, None),
1244 Relay::new(8053, None, None),
1245 Relay::new(
1246 8055,
1247 None,
1248 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1249 relay.respond_events(
1250 client_id,
1251 &subscription_id,
1252 &vec![generate_repo_ref_event()],
1253 )?;
1254 Ok(())
1255 }),
1256 ),
1257 Relay::new(8056, None, None),
1258 );
1259
1260 // // check relay had the right number of events
1261 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1262 let mut p = cli_tester_create_proposal(&git_repo);
1263
1264 expect_msgs_first(&mut p)?;
1265 relay::expect_send_with_progress(
1266 &mut p,
1267 vec![
1268 (" [my-relay] [repo-relay] ws://localhost:8055", true, ""),
1269 (" [my-relay] ws://localhost:8053", true, ""),
1270 (" [repo-relay] ws://localhost:8056", true, ""),
1271 (" [default] ws://localhost:8051", true, ""),
1272 (" [default] ws://localhost:8052", true, ""),
1273 ],
1274 1,
1275 )?;
1276 p.expect_end_with_whitespace()?;
1277 for p in [51, 52, 53, 55, 56] {
1278 relay::shutdown_relay(8000 + p)?;
1279 }
1280 Ok(())
1281 });
1282
1283 // launch relay
1284 let _ = join!(
1285 r51.listen_until_close(),
1286 r52.listen_until_close(),
1287 r53.listen_until_close(),
1288 r55.listen_until_close(),
1289 r56.listen_until_close(),
1290 );
1291 cli_tester_handle.join().unwrap()?;
1292 Ok(())
1293 }
1294 }
1295
1296 #[tokio::test]
1297 #[serial]
1298 async fn one_patch_event_sent() -> Result<()> {
1299 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?;
1300 for relay in [&r53, &r55, &r56] {
1301 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 1);
1302 }
1303 Ok(())
1304 }
1305}
1306
1307mod specify_starting_commits_whist_on_main_branch {
1308 use super::*; 1088 use super::*;
1309 1089
1310 fn prep_git_repo() -> Result<GitTestRepo> {
1311 let test_repo = GitTestRepo::default();
1312 test_repo.populate()?;
1313 // dont checkout feature branch
1314 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1315 test_repo.stage_and_commit("add t3.md")?;
1316 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1317 test_repo.stage_and_commit("add t4.md")?;
1318 Ok(test_repo)
1319 }
1320
1321 fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester { 1090 fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester {
1322 let args = vec![ 1091 let args = vec![
1323 "--nsec", 1092 "--nsec",
@@ -1326,17 +1095,29 @@ mod specify_starting_commits_whist_on_main_branch {
1326 TEST_PASSWORD, 1095 TEST_PASSWORD,
1327 "--disable-cli-spinners", 1096 "--disable-cli-spinners",
1328 "send", 1097 "send",
1329 "HEAD~3",
1330 "--no-cover-letter", 1098 "--no-cover-letter",
1331 ]; 1099 ];
1332 CliTester::new_from_dir(&git_repo.dir, args) 1100 CliTester::new_from_dir(&git_repo.dir, args)
1333 } 1101 }
1334 fn expect_msgs_first(p: &mut CliTester) -> Result<()> { 1102 fn expect_msgs_first(p: &mut CliTester) -> Result<()> {
1335 p.expect("creating patch for 3 commits\r\n")?; 1103 let mut selector = p.expect_multi_select(
1104 "select commits for proposal",
1105 vec![
1106 "(Joe Bloggs) add t4.md [feature] fe973a8".to_string(),
1107 "(Joe Bloggs) add t3.md 232efb3".to_string(),
1108 "(Joe Bloggs) add t2.md [main] 431b84e".to_string(),
1109 "(Joe Bloggs) add t1.md af474d8".to_string(),
1110 "(Joe Bloggs) Initial commit 9ee507f".to_string(),
1111 ],
1112 )?;
1113 selector.succeeds_with(vec![0, 1], false, vec![0, 1])?;
1114 p.expect("creating proposal from 2 commits:\r\n")?;
1115 p.expect("fe973a8 add t4.md\r\n")?;
1116 p.expect("232efb3 add t3.md\r\n")?;
1336 p.expect("searching for profile and relay updates...\r\n")?; 1117 p.expect("searching for profile and relay updates...\r\n")?;
1337 p.expect("\r")?; 1118 p.expect("\r")?;
1338 p.expect("logged in as fred\r\n")?; 1119 p.expect("logged in as fred\r\n")?;
1339 p.expect("posting 3 patches without a covering letter...\r\n")?; 1120 p.expect("posting 2 patches without a covering letter...\r\n")?;
1340 Ok(()) 1121 Ok(())
1341 } 1122 }
1342 async fn prep_run_create_proposal() -> Result<( 1123 async fn prep_run_create_proposal() -> Result<(
@@ -1385,6 +1166,7 @@ mod specify_starting_commits_whist_on_main_branch {
1385 // // check relay had the right number of events 1166 // // check relay had the right number of events
1386 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 1167 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1387 let mut p = cli_tester_create_proposal(&git_repo); 1168 let mut p = cli_tester_create_proposal(&git_repo);
1169 expect_msgs_first(&mut p)?;
1388 p.expect_end_eventually()?; 1170 p.expect_end_eventually()?;
1389 for p in [51, 52, 53, 55, 56] { 1171 for p in [51, 52, 53, 55, 56] {
1390 relay::shutdown_relay(8000 + p)?; 1172 relay::shutdown_relay(8000 + p)?;
@@ -1444,7 +1226,6 @@ mod specify_starting_commits_whist_on_main_branch {
1444 Relay::new(8056, None, None), 1226 Relay::new(8056, None, None),
1445 ); 1227 );
1446 1228
1447 // // check relay had the right number of events
1448 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 1229 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1449 let mut p = cli_tester_create_proposal(&git_repo); 1230 let mut p = cli_tester_create_proposal(&git_repo);
1450 1231
@@ -1458,7 +1239,7 @@ mod specify_starting_commits_whist_on_main_branch {
1458 (" [default] ws://localhost:8051", true, ""), 1239 (" [default] ws://localhost:8051", true, ""),
1459 (" [default] ws://localhost:8052", true, ""), 1240 (" [default] ws://localhost:8052", true, ""),
1460 ], 1241 ],
1461 3, 1242 2,
1462 )?; 1243 )?;
1463 p.expect_end_with_whitespace()?; 1244 p.expect_end_with_whitespace()?;
1464 for p in [51, 52, 53, 55, 56] { 1245 for p in [51, 52, 53, 55, 56] {
@@ -1482,84 +1263,16 @@ mod specify_starting_commits_whist_on_main_branch {
1482 1263
1483 #[tokio::test] 1264 #[tokio::test]
1484 #[serial] 1265 #[serial]
1485 async fn three_patch_events() -> Result<()> { 1266 async fn two_patch_events_sent() -> Result<()> {
1486 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?; 1267 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?;
1487 for relay in [&r53, &r55, &r56] { 1268 for relay in [&r53, &r55, &r56] {
1488 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 3); 1269 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 2);
1489 }
1490 Ok(())
1491 }
1492
1493 #[tokio::test]
1494 #[serial]
1495 async fn root_patch_doesnt_have_a_branch_name_tag() -> Result<()> {
1496 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?;
1497 for relay in [&r53, &r55, &r56] {
1498 let patch_events = relay
1499 .events
1500 .iter()
1501 .filter(|e| is_patch(e))
1502 .collect::<Vec<&nostr::Event>>();
1503
1504 assert!(
1505 !patch_events[0]
1506 .iter_tags()
1507 .any(|t| t.as_vec()[0].eq("branch-name"))
1508 );
1509 }
1510 Ok(())
1511 }
1512
1513 #[tokio::test]
1514 #[serial]
1515 async fn first_patch_is_ancestor_and_root_others_in_correct_order() -> Result<()> {
1516 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?;
1517 for relay in [&r53, &r55, &r56] {
1518 let patch_events = relay
1519 .events
1520 .iter()
1521 .filter(|e| is_patch(e))
1522 .collect::<Vec<&nostr::Event>>();
1523
1524 // first patch tagged as root
1525 assert!(
1526 patch_events[0]
1527 .iter_tags()
1528 .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root"))
1529 );
1530 // first patch is ancestor
1531 assert_eq!(
1532 patch_events[0]
1533 .iter_tags()
1534 .find(|t| t.as_vec()[0].eq("commit"))
1535 .unwrap()
1536 .as_vec()[1],
1537 "431b84edc0d2fa118d63faa3c2db9c73d630a5ae"
1538 );
1539 // second patch not tagged as root
1540 assert_eq!(
1541 patch_events[1]
1542 .iter_tags()
1543 .find(|t| t.as_vec()[0].eq("commit"))
1544 .unwrap()
1545 .as_vec()[1],
1546 "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"
1547 );
1548 // second patch not tagged as root
1549 assert_eq!(
1550 patch_events[2]
1551 .iter_tags()
1552 .find(|t| t.as_vec()[0].eq("commit"))
1553 .unwrap()
1554 .as_vec()[1],
1555 "fe973a840fba2a8ab37dd505c154854a69a6505c"
1556 );
1557 } 1270 }
1558 Ok(()) 1271 Ok(())
1559 } 1272 }
1560} 1273}
1561 1274
1562mod specify_in_reply_to { 1275mod in_reply_to_specified_with_range_of_head_2_and_cover_letter_details_specified {
1563 use super::*; 1276 use super::*;
1564 fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester { 1277 fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester {
1565 let args = vec![ 1278 let args = vec![
@@ -1569,6 +1282,7 @@ mod specify_in_reply_to {
1569 TEST_PASSWORD, 1282 TEST_PASSWORD,
1570 "--disable-cli-spinners", 1283 "--disable-cli-spinners",
1571 "send", 1284 "send",
1285 "HEAD~2",
1572 "--in-reply-to", 1286 "--in-reply-to",
1573 "nevent1qqsypm62fzw7qynvlc4gjl3tr0jw4vmh659nvr2cc5qyhdg92a5yy0qzypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesxygzam", 1287 "nevent1qqsypm62fzw7qynvlc4gjl3tr0jw4vmh659nvr2cc5qyhdg92a5yy0qzypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesxygzam",
1574 "--title", 1288 "--title",
@@ -1579,8 +1293,10 @@ mod specify_in_reply_to {
1579 CliTester::new_from_dir(&git_repo.dir, args) 1293 CliTester::new_from_dir(&git_repo.dir, args)
1580 } 1294 }
1581 fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> { 1295 fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> {
1582 p.expect("creating patch for 2 commits from 'feature' that can be merged into 'main'\r\n")?; 1296 p.expect("creating proposal revision for: nevent1qqsypm62fzw7qynvlc4gjl3tr0jw4vmh659nvr2cc5qyhdg92a5yy0qzypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesxygzam\r\n")?;
1583 p.expect("as a revision to proposal: nevent1qqsypm62fzw7qynvlc4gjl3tr0jw4vmh659nvr2cc5qyhdg92a5yy0qzypumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesxygzam\r\n")?; 1297 p.expect("creating proposal from 2 commits:\r\n")?;
1298 p.expect("fe973a8 add t4.md\r\n")?;
1299 p.expect("232efb3 add t3.md\r\n")?;
1584 p.expect("searching for profile and relay updates...\r\n")?; 1300 p.expect("searching for profile and relay updates...\r\n")?;
1585 p.expect("\r")?; 1301 p.expect("\r")?;
1586 p.expect("logged in as fred\r\n")?; 1302 p.expect("logged in as fred\r\n")?;