diff options
| -rw-r--r-- | src/cli_interactor.rs | 44 | ||||
| -rw-r--r-- | src/git.rs | 74 | ||||
| -rw-r--r-- | src/sub_commands/push.rs | 2 | ||||
| -rw-r--r-- | src/sub_commands/send.rs | 216 | ||||
| -rw-r--r-- | test_utils/src/lib.rs | 85 | ||||
| -rw-r--r-- | tests/list.rs | 3 | ||||
| -rw-r--r-- | tests/pull.rs | 3 | ||||
| -rw-r--r-- | tests/push.rs | 22 | ||||
| -rw-r--r-- | tests/send.rs | 380 |
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 | } |
| 18 | impl InteractorPrompt for Interactor { | 19 | impl 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)] | ||
| 158 | pub struct PromptMultiChoiceParms { | ||
| 159 | pub prompt: String, | ||
| 160 | pub choices: Vec<String>, | ||
| 161 | pub defaults: Option<Vec<bool>>, | ||
| 162 | pub report: bool, | ||
| 163 | } | ||
| 164 | |||
| 165 | impl 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 | } | ||
| @@ -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 @@ | |||
| 1 | use std::{str::FromStr, time::Duration}; | 1 | use std::{str::FromStr, time::Duration}; |
| 2 | 2 | ||
| 3 | use anyhow::{bail, Context, Result}; | 3 | use anyhow::{bail, Context, Result}; |
| 4 | use console::Style; | ||
| 4 | use futures::future::join_all; | 5 | use futures::future::join_all; |
| 5 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; | 6 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; |
| 6 | use nostr::{ | 7 | use nostr::{ |
| @@ -14,7 +15,9 @@ use crate::client::Client; | |||
| 14 | #[cfg(test)] | 15 | #[cfg(test)] |
| 15 | use crate::client::MockConnect; | 16 | use crate::client::MockConnect; |
| 16 | use crate::{ | 17 | use 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)] |
| 26 | pub struct SubCommandArgs { | 29 | pub 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 { | |||
| 46 | pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | 49 | pub 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 | ||
| 381 | fn 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 | |||
| 446 | fn 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 | |||
| 362 | mod tests_unique_and_duplicate { | 472 | mod 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 @@ | |||
| 1 | use std::{ffi::OsStr, path::PathBuf}; | 1 | use std::{ffi::OsStr, path::PathBuf}; |
| 2 | 2 | ||
| 3 | use anyhow::{ensure, Context, Result}; | 3 | use anyhow::{bail, ensure, Context, Result}; |
| 4 | use dialoguer::theme::{ColorfulTheme, Theme}; | 4 | use dialoguer::theme::{ColorfulTheme, Theme}; |
| 5 | use directories::ProjectDirs; | 5 | use directories::ProjectDirs; |
| 6 | use nostr::{self, prelude::FromSkStr, Kind, Tag}; | 6 | use 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 | ||
| 264 | pub struct CliTesterInputPrompt<'a> { | 278 | pub struct CliTesterInputPrompt<'a> { |
| @@ -448,6 +462,75 @@ impl CliTesterConfirmPrompt<'_> { | |||
| 448 | } | 462 | } |
| 449 | } | 463 | } |
| 450 | 464 | ||
| 465 | pub struct CliTesterMultiSelectPrompt<'a> { | ||
| 466 | tester: &'a mut CliTester, | ||
| 467 | prompt: String, | ||
| 468 | choices: Vec<String>, | ||
| 469 | } | ||
| 470 | |||
| 471 | impl 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 | |||
| 451 | pub struct CliTesterChoicePrompt<'a> { | 534 | pub 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 |
| 16 | fn 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 | |||
| 28 | mod when_commits_behind_ask_to_proceed { | 17 | mod 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] | ||
| 103 | fn 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 | |||
| 121 | fn is_cover_letter(event: &nostr::Event) -> bool { | 84 | fn 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 | ||
| 168 | fn expect_msgs_first(p: &mut CliTester, include_cover_letter: bool) -> Result<()> { | 132 | fn 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 | ||
| 250 | mod sends_cover_letter_and_2_patches_to_3_relays { | 216 | mod 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 | ||
| 940 | mod sends_2_patches_without_cover_letter { | 906 | mod 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 | ||
| 1121 | mod when_on_main_branch_defaults_to_last_commit { | 1087 | mod 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 | |||
| 1307 | mod 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 | ||
| 1562 | mod specify_in_reply_to { | 1275 | mod 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")?; |