diff options
| -rw-r--r-- | src/cli_interactor.rs | 9 | ||||
| -rw-r--r-- | src/client.rs | 12 | ||||
| -rw-r--r-- | src/git.rs | 967 | ||||
| -rw-r--r-- | src/sub_commands/prs/create.rs | 90 | ||||
| -rw-r--r-- | src/sub_commands/prs/list.rs | 225 | ||||
| -rw-r--r-- | src/sub_commands/prs/mod.rs | 3 | ||||
| -rw-r--r-- | test_utils/src/git.rs | 60 | ||||
| -rw-r--r-- | test_utils/src/lib.rs | 167 | ||||
| -rw-r--r-- | test_utils/src/relay.rs | 33 | ||||
| -rw-r--r-- | tests/prs_create.rs | 163 | ||||
| -rw-r--r-- | tests/prs_list.rs | 768 |
11 files changed, 2402 insertions, 95 deletions
diff --git a/src/cli_interactor.rs b/src/cli_interactor.rs index e52cefb..c6cd4e5 100644 --- a/src/cli_interactor.rs +++ b/src/cli_interactor.rs | |||
| @@ -40,6 +40,8 @@ impl InteractorPrompt for Interactor { | |||
| 40 | } | 40 | } |
| 41 | fn choice(&self, parms: PromptChoiceParms) -> Result<usize> { | 41 | fn choice(&self, parms: PromptChoiceParms) -> Result<usize> { |
| 42 | dialoguer::Select::with_theme(&self.theme) | 42 | dialoguer::Select::with_theme(&self.theme) |
| 43 | .with_prompt(parms.prompt) | ||
| 44 | .report(parms.report) | ||
| 43 | .items(&parms.choices) | 45 | .items(&parms.choices) |
| 44 | .interact() | 46 | .interact() |
| 45 | .context("failed to get choice") | 47 | .context("failed to get choice") |
| @@ -96,13 +98,20 @@ impl PromptConfirmParms { | |||
| 96 | pub struct PromptChoiceParms { | 98 | pub struct PromptChoiceParms { |
| 97 | pub prompt: String, | 99 | pub prompt: String, |
| 98 | pub choices: Vec<String>, | 100 | pub choices: Vec<String>, |
| 101 | pub report: bool, | ||
| 99 | } | 102 | } |
| 100 | 103 | ||
| 101 | impl PromptChoiceParms { | 104 | impl PromptChoiceParms { |
| 102 | pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self { | 105 | pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self { |
| 103 | self.prompt = prompt.into(); | 106 | self.prompt = prompt.into(); |
| 107 | self.report = true; | ||
| 104 | self | 108 | self |
| 105 | } | 109 | } |
| 110 | |||
| 111 | // pub fn dont_report(mut self) -> Self { | ||
| 112 | // self.report = false; | ||
| 113 | // self | ||
| 114 | // } | ||
| 106 | pub fn with_choices(mut self, choices: Vec<String>) -> Self { | 115 | pub fn with_choices(mut self, choices: Vec<String>) -> Self { |
| 107 | self.choices = choices; | 116 | self.choices = choices; |
| 108 | self | 117 | self |
diff --git a/src/client.rs b/src/client.rs index 1037b1b..860562c 100644 --- a/src/client.rs +++ b/src/client.rs | |||
| @@ -20,6 +20,7 @@ use nostr::Event; | |||
| 20 | pub struct Client { | 20 | pub struct Client { |
| 21 | client: nostr_sdk::Client, | 21 | client: nostr_sdk::Client, |
| 22 | fallback_relays: Vec<String>, | 22 | fallback_relays: Vec<String>, |
| 23 | more_fallback_relays: Vec<String>, | ||
| 23 | } | 24 | } |
| 24 | 25 | ||
| 25 | #[cfg_attr(test, automock)] | 26 | #[cfg_attr(test, automock)] |
| @@ -30,6 +31,7 @@ pub trait Connect { | |||
| 30 | async fn set_keys(&mut self, keys: &nostr::Keys); | 31 | async fn set_keys(&mut self, keys: &nostr::Keys); |
| 31 | async fn disconnect(&self) -> Result<()>; | 32 | async fn disconnect(&self) -> Result<()>; |
| 32 | fn get_fallback_relays(&self) -> &Vec<String>; | 33 | fn get_fallback_relays(&self) -> &Vec<String>; |
| 34 | fn get_more_fallback_relays(&self) -> &Vec<String>; | ||
| 33 | async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; | 35 | async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; |
| 34 | async fn get_events( | 36 | async fn get_events( |
| 35 | &self, | 37 | &self, |
| @@ -47,12 +49,17 @@ impl Connect for Client { | |||
| 47 | "ws://localhost:8051".to_string(), | 49 | "ws://localhost:8051".to_string(), |
| 48 | "ws://localhost:8052".to_string(), | 50 | "ws://localhost:8052".to_string(), |
| 49 | ], | 51 | ], |
| 52 | more_fallback_relays: vec![ | ||
| 53 | "ws://localhost:8055".to_string(), | ||
| 54 | "ws://localhost:8056".to_string(), | ||
| 55 | ], | ||
| 50 | } | 56 | } |
| 51 | } | 57 | } |
| 52 | fn new(opts: Params) -> Self { | 58 | fn new(opts: Params) -> Self { |
| 53 | Client { | 59 | Client { |
| 54 | client: nostr_sdk::Client::new(&opts.keys.unwrap_or(nostr::Keys::generate())), | 60 | client: nostr_sdk::Client::new(&opts.keys.unwrap_or(nostr::Keys::generate())), |
| 55 | fallback_relays: opts.fallback_relays, | 61 | fallback_relays: opts.fallback_relays, |
| 62 | more_fallback_relays: opts.more_fallback_relays, | ||
| 56 | } | 63 | } |
| 57 | } | 64 | } |
| 58 | 65 | ||
| @@ -69,6 +76,10 @@ impl Connect for Client { | |||
| 69 | &self.fallback_relays | 76 | &self.fallback_relays |
| 70 | } | 77 | } |
| 71 | 78 | ||
| 79 | fn get_more_fallback_relays(&self) -> &Vec<String> { | ||
| 80 | &self.more_fallback_relays | ||
| 81 | } | ||
| 82 | |||
| 72 | async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { | 83 | async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { |
| 73 | self.client.add_relay(url, None).await?; | 84 | self.client.add_relay(url, None).await?; |
| 74 | self.client.connect_relay(url).await?; | 85 | self.client.connect_relay(url).await?; |
| @@ -130,6 +141,7 @@ async fn get_events_of( | |||
| 130 | pub struct Params { | 141 | pub struct Params { |
| 131 | pub keys: Option<nostr::Keys>, | 142 | pub keys: Option<nostr::Keys>, |
| 132 | pub fallback_relays: Vec<String>, | 143 | pub fallback_relays: Vec<String>, |
| 144 | pub more_fallback_relays: Vec<String>, | ||
| 133 | } | 145 | } |
| 134 | 146 | ||
| 135 | fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> { | 147 | fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> { |
| @@ -6,6 +6,8 @@ use anyhow::{bail, Context, Result}; | |||
| 6 | use git2::{Oid, Revwalk}; | 6 | use git2::{Oid, Revwalk}; |
| 7 | use nostr::prelude::{sha1::Hash as Sha1Hash, Hash}; | 7 | use nostr::prelude::{sha1::Hash as Sha1Hash, Hash}; |
| 8 | 8 | ||
| 9 | use crate::sub_commands::prs::list::tag_value; | ||
| 10 | |||
| 9 | pub struct Repo { | 11 | pub struct Repo { |
| 10 | git_repo: git2::Repository, | 12 | git_repo: git2::Repository, |
| 11 | } | 13 | } |
| @@ -36,14 +38,26 @@ pub trait RepoActions { | |||
| 36 | fn does_commit_exist(&self, commit: &str) -> Result<bool>; | 38 | fn does_commit_exist(&self, commit: &str) -> Result<bool>; |
| 37 | fn get_head_commit(&self) -> Result<Sha1Hash>; | 39 | fn get_head_commit(&self) -> Result<Sha1Hash>; |
| 38 | fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>; | 40 | fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>; |
| 41 | fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String>; | ||
| 42 | /// returns vector ["name", "email", "unixtime"] | ||
| 43 | /// eg ["joe bloggs", "joe@pm.me", "12176,-300"] | ||
| 44 | fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>>; | ||
| 45 | /// returns vector ["name", "email", "unixtime"] | ||
| 46 | /// eg ["joe bloggs", "joe@pm.me", "12176,-300"] | ||
| 47 | fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>>; | ||
| 39 | fn get_commits_ahead_behind( | 48 | fn get_commits_ahead_behind( |
| 40 | &self, | 49 | &self, |
| 41 | base_commit: &Sha1Hash, | 50 | base_commit: &Sha1Hash, |
| 42 | latest_commit: &Sha1Hash, | 51 | latest_commit: &Sha1Hash, |
| 43 | ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>; | 52 | ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>; |
| 44 | fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String>; | 53 | fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String>; |
| 45 | fn checkout(&self, ref_name: &str) -> Result<()>; | 54 | fn checkout(&self, ref_name: &str) -> Result<Sha1Hash>; |
| 46 | fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>; | 55 | fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>; |
| 56 | fn apply_patch_chain( | ||
| 57 | &self, | ||
| 58 | branch_name: &str, | ||
| 59 | patch_and_ancestors: Vec<nostr::Event>, | ||
| 60 | ) -> Result<Vec<nostr::Event>>; | ||
| 47 | } | 61 | } |
| 48 | 62 | ||
| 49 | impl RepoActions for Repo { | 63 | impl RepoActions for Repo { |
| @@ -122,7 +136,7 @@ impl RepoActions for Repo { | |||
| 122 | } | 136 | } |
| 123 | 137 | ||
| 124 | fn does_commit_exist(&self, commit: &str) -> Result<bool> { | 138 | fn does_commit_exist(&self, commit: &str) -> Result<bool> { |
| 125 | if let Ok(c) = self.git_repo.find_commit(Oid::from_str(commit)?) { | 139 | if self.git_repo.find_commit(Oid::from_str(commit)?).is_ok() { |
| 126 | Ok(true) | 140 | Ok(true) |
| 127 | } else { | 141 | } else { |
| 128 | Ok(false) | 142 | Ok(false) |
| @@ -148,6 +162,34 @@ impl RepoActions for Repo { | |||
| 148 | Ok(oid_to_sha1(&parent_oid)) | 162 | Ok(oid_to_sha1(&parent_oid)) |
| 149 | } | 163 | } |
| 150 | 164 | ||
| 165 | fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String> { | ||
| 166 | Ok(self | ||
| 167 | .git_repo | ||
| 168 | .find_commit(sha1_to_oid(commit)?) | ||
| 169 | .context(format!("could not find commit {commit}"))? | ||
| 170 | .message_raw() | ||
| 171 | .context("commit message has unusual characters in (not valid utf-8)")? | ||
| 172 | .to_string()) | ||
| 173 | } | ||
| 174 | |||
| 175 | fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>> { | ||
| 176 | let commit = self | ||
| 177 | .git_repo | ||
| 178 | .find_commit(sha1_to_oid(commit)?) | ||
| 179 | .context(format!("could not find commit {commit}"))?; | ||
| 180 | let sig = commit.author(); | ||
| 181 | Ok(git_sig_to_tag_vec(&sig)) | ||
| 182 | } | ||
| 183 | |||
| 184 | fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>> { | ||
| 185 | let commit = self | ||
| 186 | .git_repo | ||
| 187 | .find_commit(sha1_to_oid(commit)?) | ||
| 188 | .context(format!("could not find commit {commit}"))?; | ||
| 189 | let sig = commit.committer(); | ||
| 190 | Ok(git_sig_to_tag_vec(&sig)) | ||
| 191 | } | ||
| 192 | |||
| 151 | fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String> { | 193 | fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String> { |
| 152 | let c = self | 194 | let c = self |
| 153 | .git_repo | 195 | .git_repo |
| @@ -225,7 +267,7 @@ impl RepoActions for Repo { | |||
| 225 | Ok((ahead, behind)) | 267 | Ok((ahead, behind)) |
| 226 | } | 268 | } |
| 227 | 269 | ||
| 228 | fn checkout(&self, ref_name: &str) -> Result<()> { | 270 | fn checkout(&self, ref_name: &str) -> Result<Sha1Hash> { |
| 229 | let (object, reference) = self.git_repo.revparse_ext(ref_name)?; | 271 | let (object, reference) = self.git_repo.revparse_ext(ref_name)?; |
| 230 | 272 | ||
| 231 | self.git_repo.checkout_tree(&object, None)?; | 273 | self.git_repo.checkout_tree(&object, None)?; |
| @@ -236,7 +278,9 @@ impl RepoActions for Repo { | |||
| 236 | // this is a commit, not a reference | 278 | // this is a commit, not a reference |
| 237 | None => self.git_repo.set_head_detached(object.id()), | 279 | None => self.git_repo.set_head_detached(object.id()), |
| 238 | }?; | 280 | }?; |
| 239 | Ok(()) | 281 | let oid = self.git_repo.head()?.peel_to_commit()?.id(); |
| 282 | |||
| 283 | Ok(oid_to_sha1(&oid)) | ||
| 240 | } | 284 | } |
| 241 | 285 | ||
| 242 | fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> { | 286 | fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> { |
| @@ -244,11 +288,66 @@ impl RepoActions for Repo { | |||
| 244 | .branch( | 288 | .branch( |
| 245 | branch_name, | 289 | branch_name, |
| 246 | &self.git_repo.find_commit(Oid::from_str(commit)?)?, | 290 | &self.git_repo.find_commit(Oid::from_str(commit)?)?, |
| 247 | false, | 291 | true, |
| 248 | ) | 292 | ) |
| 249 | .context("branch could not be created")?; | 293 | .context("branch could not be created")?; |
| 250 | Ok(()) | 294 | Ok(()) |
| 251 | } | 295 | } |
| 296 | /* returns patches applied */ | ||
| 297 | fn apply_patch_chain( | ||
| 298 | &self, | ||
| 299 | branch_name: &str, | ||
| 300 | patch_and_ancestors: Vec<nostr::Event>, | ||
| 301 | ) -> Result<Vec<nostr::Event>> { | ||
| 302 | // filter out existing ancestors | ||
| 303 | let mut patches_to_apply: Vec<nostr::Event> = patch_and_ancestors | ||
| 304 | .into_iter() | ||
| 305 | .filter(|e| { | ||
| 306 | !self | ||
| 307 | .does_commit_exist(&tag_value(e, "commit").unwrap()) | ||
| 308 | .unwrap() | ||
| 309 | }) | ||
| 310 | .collect(); | ||
| 311 | |||
| 312 | let parent_commit_id = tag_value( | ||
| 313 | if let Ok(last_patch) = patches_to_apply.last().context("no patches") { | ||
| 314 | last_patch | ||
| 315 | } else { | ||
| 316 | self.checkout(branch_name).context("latest commit in pr doesnt connect with an existing commit. Try a git pull first.")?; | ||
| 317 | return Ok(vec![]); | ||
| 318 | }, | ||
| 319 | "parent-commit", | ||
| 320 | )?; | ||
| 321 | |||
| 322 | // check patches can be applied | ||
| 323 | if !self.does_commit_exist(&parent_commit_id)? { | ||
| 324 | bail!("cannot find parent commit ({parent_commit_id}). run git pull and try again.") | ||
| 325 | } | ||
| 326 | |||
| 327 | // check for rebase or changes | ||
| 328 | if let Ok(current_tip) = self.get_tip_of_local_branch(branch_name) { | ||
| 329 | if !current_tip.to_string().eq(&parent_commit_id) { | ||
| 330 | // TODO: either changes have been made on the local branch or | ||
| 331 | // the latest commit in the pr has rebased onto a newer commit | ||
| 332 | // that you havn't pulled yet ask user whether | ||
| 333 | // they want to proceed | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | // checkout branch | ||
| 338 | if !self.get_checked_out_branch_name()?.eq(&branch_name) { | ||
| 339 | self.create_branch_at_commit(branch_name, &parent_commit_id)?; | ||
| 340 | } | ||
| 341 | self.checkout(branch_name)?; | ||
| 342 | |||
| 343 | // apply commits | ||
| 344 | patches_to_apply.reverse(); | ||
| 345 | |||
| 346 | for patch in &patches_to_apply { | ||
| 347 | apply_patch(self, patch)?; | ||
| 348 | } | ||
| 349 | Ok(patches_to_apply) | ||
| 350 | } | ||
| 252 | } | 351 | } |
| 253 | 352 | ||
| 254 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { | 353 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { |
| @@ -285,8 +384,142 @@ fn sha1_to_oid(hash: &Sha1Hash) -> Result<Oid> { | |||
| 285 | Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid") | 384 | Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid") |
| 286 | } | 385 | } |
| 287 | 386 | ||
| 387 | fn git_sig_to_tag_vec(sig: &git2::Signature) -> Vec<String> { | ||
| 388 | vec![ | ||
| 389 | sig.name().unwrap_or("").to_string(), | ||
| 390 | sig.email().unwrap_or("").to_string(), | ||
| 391 | format!("{},{}", sig.when().seconds(), sig.when().offset_minutes()), | ||
| 392 | ] | ||
| 393 | } | ||
| 394 | |||
| 395 | fn apply_patch(git_repo: &Repo, patch: &nostr::Event) -> Result<()> { | ||
| 396 | // check parent commit matches head | ||
| 397 | if !git_repo | ||
| 398 | .get_head_commit()? | ||
| 399 | .to_string() | ||
| 400 | .eq(&tag_value(patch, "parent-commit")?) | ||
| 401 | { | ||
| 402 | bail!( | ||
| 403 | "patch parent ({}) doesnt match current head ({})", | ||
| 404 | tag_value(patch, "parent-commit")?, | ||
| 405 | git_repo.get_head_commit()? | ||
| 406 | ); | ||
| 407 | } | ||
| 408 | |||
| 409 | let diff_from_patch = git2::Diff::from_buffer(patch.content.as_bytes()).unwrap(); | ||
| 410 | |||
| 411 | let mut apply_opts = git2::ApplyOptions::new(); | ||
| 412 | apply_opts.check(false); | ||
| 413 | |||
| 414 | git_repo.git_repo.apply( | ||
| 415 | &diff_from_patch, | ||
| 416 | git2::ApplyLocation::WorkDir, | ||
| 417 | Some(&mut apply_opts), | ||
| 418 | )?; | ||
| 419 | // stage and commit | ||
| 420 | let prev_oid = git_repo.git_repo.head().unwrap().peel_to_commit()?; | ||
| 421 | |||
| 422 | let mut index = git_repo.git_repo.index()?; | ||
| 423 | index.add_all(["."], git2::IndexAddOption::DEFAULT, None)?; | ||
| 424 | index.write()?; | ||
| 425 | |||
| 426 | git_repo.git_repo.commit( | ||
| 427 | Some("HEAD"), | ||
| 428 | &extract_sig_from_patch_tags(&patch.tags, "author")?, | ||
| 429 | &extract_sig_from_patch_tags(&patch.tags, "committer")?, | ||
| 430 | tag_value(patch, "description")?.as_str(), | ||
| 431 | &git_repo.git_repo.find_tree(index.write_tree()?)?, | ||
| 432 | &[&prev_oid], | ||
| 433 | )?; | ||
| 434 | // end of stage and commit | ||
| 435 | // check commit applied | ||
| 436 | if git_repo | ||
| 437 | .get_head_commit()? | ||
| 438 | .to_string() | ||
| 439 | .eq(&tag_value(patch, "parent-commit")?) | ||
| 440 | { | ||
| 441 | bail!("applying patch failed"); | ||
| 442 | } | ||
| 443 | |||
| 444 | let mut revwalk = git_repo.git_repo.revwalk().context("revwalk error")?; | ||
| 445 | revwalk.push_head().context("revwalk.push_head")?; | ||
| 446 | |||
| 447 | for (i, oid) in revwalk.enumerate() { | ||
| 448 | if i == 0 { | ||
| 449 | let old_commit = git_repo | ||
| 450 | .git_repo | ||
| 451 | .find_commit(oid.context("cannot get oid in revwalk")?) | ||
| 452 | .context("cannot find newly added commit oid")?; | ||
| 453 | // create commit using amend which relects the original commit id | ||
| 454 | let updated_commit_oid = old_commit | ||
| 455 | .amend( | ||
| 456 | None, | ||
| 457 | Some(&old_commit.author()), | ||
| 458 | Some(&old_commit.committer()), | ||
| 459 | None, | ||
| 460 | None, | ||
| 461 | None, | ||
| 462 | ) | ||
| 463 | .context("cannot ammend commit to produce new oid")?; | ||
| 464 | // replace the commit with the wrong oid with the newly created one with the | ||
| 465 | // correct oid | ||
| 466 | git_repo | ||
| 467 | .git_repo | ||
| 468 | .head() | ||
| 469 | .context("cannot get head of git_repo")? | ||
| 470 | .set_target(updated_commit_oid, "ref commit with fix committer details") | ||
| 471 | .context("cannot update branch with fixed commit")?; | ||
| 472 | |||
| 473 | if !updated_commit_oid | ||
| 474 | .to_string() | ||
| 475 | .eq(&tag_value(patch, "commit")?) | ||
| 476 | { | ||
| 477 | bail!( | ||
| 478 | "when applied the patch commit id ({}) doesn't match the one specified in the event tag ({})", | ||
| 479 | updated_commit_oid.to_string(), | ||
| 480 | tag_value(patch, "commit")?, | ||
| 481 | ) | ||
| 482 | } | ||
| 483 | } | ||
| 484 | } | ||
| 485 | Ok(()) | ||
| 486 | } | ||
| 487 | |||
| 488 | fn extract_sig_from_patch_tags<'a>( | ||
| 489 | tags: &'a [nostr::Tag], | ||
| 490 | tag_name: &str, | ||
| 491 | ) -> Result<git2::Signature<'a>> { | ||
| 492 | let v = tags | ||
| 493 | .iter() | ||
| 494 | .find(|t| t.as_vec()[0].eq(tag_name)) | ||
| 495 | .context(format!("tag '{tag_name}' not present in patch"))? | ||
| 496 | .as_vec(); | ||
| 497 | if v.len() != 4 { | ||
| 498 | bail!("tag '{tag_name}' is incorrectly formatted") | ||
| 499 | } | ||
| 500 | let git_time: Vec<&str> = v[3].split(',').collect(); | ||
| 501 | if git_time.len() != 2 { | ||
| 502 | bail!("tag '{tag_name}' time is incorrectly formatted") | ||
| 503 | } | ||
| 504 | git2::Signature::new( | ||
| 505 | v[1].as_str(), | ||
| 506 | v[2].as_str(), | ||
| 507 | &git2::Time::new( | ||
| 508 | git_time[0] | ||
| 509 | .parse() | ||
| 510 | .context("tag time is incorrectly formatted")?, | ||
| 511 | git_time[1] | ||
| 512 | .parse() | ||
| 513 | .context("tag time offset is incorrectly formatted")?, | ||
| 514 | ), | ||
| 515 | ) | ||
| 516 | .context("failed to create git signature") | ||
| 517 | } | ||
| 518 | |||
| 288 | #[cfg(test)] | 519 | #[cfg(test)] |
| 289 | mod tests { | 520 | mod tests { |
| 521 | use std::fs; | ||
| 522 | |||
| 290 | use test_utils::git::GitTestRepo; | 523 | use test_utils::git::GitTestRepo; |
| 291 | 524 | ||
| 292 | use super::*; | 525 | use super::*; |
| @@ -308,16 +541,177 @@ mod tests { | |||
| 308 | Ok(()) | 541 | Ok(()) |
| 309 | } | 542 | } |
| 310 | 543 | ||
| 544 | mod get_commit_message { | ||
| 545 | use super::*; | ||
| 546 | fn run(message: &str) -> Result<()> { | ||
| 547 | let test_repo = GitTestRepo::default(); | ||
| 548 | test_repo.populate()?; | ||
| 549 | std::fs::write(test_repo.dir.join("t100.md"), "some content")?; | ||
| 550 | let oid = test_repo.stage_and_commit(message)?; | ||
| 551 | |||
| 552 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 553 | |||
| 554 | assert_eq!(message, git_repo.get_commit_message(&oid_to_sha1(&oid))?,); | ||
| 555 | Ok(()) | ||
| 556 | } | ||
| 557 | #[test] | ||
| 558 | fn one_liner() -> Result<()> { | ||
| 559 | run("add t100.md") | ||
| 560 | } | ||
| 561 | |||
| 562 | #[test] | ||
| 563 | fn multiline() -> Result<()> { | ||
| 564 | run("add t100.md\r\nanother line\r\nthird line") | ||
| 565 | } | ||
| 566 | |||
| 567 | #[test] | ||
| 568 | fn trailing_newlines() -> Result<()> { | ||
| 569 | run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n") | ||
| 570 | } | ||
| 571 | |||
| 572 | #[test] | ||
| 573 | fn unicode_characters() -> Result<()> { | ||
| 574 | run("add t100.md ❤️") | ||
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | mod get_commit_author { | ||
| 579 | use super::*; | ||
| 580 | |||
| 581 | static NAME: &str = "carole"; | ||
| 582 | static EMAIL: &str = "carole@pm.me"; | ||
| 583 | |||
| 584 | fn prep(time: &git2::Time) -> Result<Vec<String>> { | ||
| 585 | let test_repo = GitTestRepo::default(); | ||
| 586 | test_repo.populate()?; | ||
| 587 | fs::write(test_repo.dir.join("x1.md"), "some content")?; | ||
| 588 | let oid = test_repo.stage_and_commit_custom_signature( | ||
| 589 | "add x1.md", | ||
| 590 | Some(&git2::Signature::new(NAME, EMAIL, time)?), | ||
| 591 | None, | ||
| 592 | )?; | ||
| 593 | |||
| 594 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 595 | git_repo.get_commit_author(&oid_to_sha1(&oid)) | ||
| 596 | } | ||
| 597 | |||
| 598 | #[test] | ||
| 599 | fn name() -> Result<()> { | ||
| 600 | let res = prep(&git2::Time::new(5000, 0))?; | ||
| 601 | assert_eq!(NAME, res[0]); | ||
| 602 | Ok(()) | ||
| 603 | } | ||
| 604 | |||
| 605 | #[test] | ||
| 606 | fn email() -> Result<()> { | ||
| 607 | let res = prep(&git2::Time::new(5000, 0))?; | ||
| 608 | assert_eq!(EMAIL, res[1]); | ||
| 609 | Ok(()) | ||
| 610 | } | ||
| 611 | |||
| 612 | mod time { | ||
| 613 | use super::*; | ||
| 614 | |||
| 615 | #[test] | ||
| 616 | fn no_offset() -> Result<()> { | ||
| 617 | let res = prep(&git2::Time::new(5000, 0))?; | ||
| 618 | assert_eq!("5000,0", res[2]); | ||
| 619 | Ok(()) | ||
| 620 | } | ||
| 621 | #[test] | ||
| 622 | fn positive_offset() -> Result<()> { | ||
| 623 | let res = prep(&git2::Time::new(5000, 300))?; | ||
| 624 | assert_eq!("5000,300", res[2]); | ||
| 625 | Ok(()) | ||
| 626 | } | ||
| 627 | #[test] | ||
| 628 | fn negative_offset() -> Result<()> { | ||
| 629 | let res = prep(&git2::Time::new(5000, -300))?; | ||
| 630 | assert_eq!("5000,-300", res[2]); | ||
| 631 | Ok(()) | ||
| 632 | } | ||
| 633 | } | ||
| 634 | |||
| 635 | mod extract_sig_from_patch_tags { | ||
| 636 | use super::*; | ||
| 637 | |||
| 638 | fn test(time: git2::Time) -> Result<()> { | ||
| 639 | assert_eq!( | ||
| 640 | extract_sig_from_patch_tags( | ||
| 641 | &[nostr::Tag::Generic( | ||
| 642 | nostr::TagKind::Custom("author".to_string()), | ||
| 643 | prep(&time)?, | ||
| 644 | )], | ||
| 645 | "author", | ||
| 646 | )? | ||
| 647 | .to_string(), | ||
| 648 | git2::Signature::new(NAME, EMAIL, &time)?.to_string(), | ||
| 649 | ); | ||
| 650 | Ok(()) | ||
| 651 | } | ||
| 652 | |||
| 653 | #[test] | ||
| 654 | fn no_offset() -> Result<()> { | ||
| 655 | test(git2::Time::new(5000, 0)) | ||
| 656 | } | ||
| 657 | |||
| 658 | #[test] | ||
| 659 | fn positive_offset() -> Result<()> { | ||
| 660 | test(git2::Time::new(5000, 300)) | ||
| 661 | } | ||
| 662 | |||
| 663 | #[test] | ||
| 664 | fn negative_offset() -> Result<()> { | ||
| 665 | test(git2::Time::new(5000, -300)) | ||
| 666 | } | ||
| 667 | } | ||
| 668 | } | ||
| 669 | |||
| 670 | mod get_commit_comitter { | ||
| 671 | use super::*; | ||
| 672 | |||
| 673 | static NAME: &str = "carole"; | ||
| 674 | static EMAIL: &str = "carole@pm.me"; | ||
| 675 | |||
| 676 | fn prep(time: &git2::Time) -> Result<Vec<String>> { | ||
| 677 | let test_repo = GitTestRepo::default(); | ||
| 678 | test_repo.populate()?; | ||
| 679 | fs::write(test_repo.dir.join("x1.md"), "some content")?; | ||
| 680 | let oid = test_repo.stage_and_commit_custom_signature( | ||
| 681 | "add x1.md", | ||
| 682 | None, | ||
| 683 | Some(&git2::Signature::new(NAME, EMAIL, time)?), | ||
| 684 | )?; | ||
| 685 | |||
| 686 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 687 | git_repo.get_commit_comitter(&oid_to_sha1(&oid)) | ||
| 688 | } | ||
| 689 | |||
| 690 | #[test] | ||
| 691 | fn name() -> Result<()> { | ||
| 692 | let res = prep(&git2::Time::new(5000, 0))?; | ||
| 693 | assert_eq!(NAME, res[0]); | ||
| 694 | Ok(()) | ||
| 695 | } | ||
| 696 | |||
| 697 | #[test] | ||
| 698 | fn email() -> Result<()> { | ||
| 699 | let res = prep(&git2::Time::new(5000, 0))?; | ||
| 700 | assert_eq!(EMAIL, res[1]); | ||
| 701 | Ok(()) | ||
| 702 | } | ||
| 703 | } | ||
| 704 | |||
| 311 | mod does_commit_exist { | 705 | mod does_commit_exist { |
| 312 | use super::*; | 706 | use super::*; |
| 313 | 707 | ||
| 314 | #[test] | 708 | #[test] |
| 315 | fn existing_commits_results_in_true() -> Result<()> { | 709 | fn existing_commits_results_in_true() -> Result<()> { |
| 316 | let test_repo = GitTestRepo::default(); | 710 | let test_repo = GitTestRepo::default(); |
| 317 | let oid = test_repo.populate()?; | 711 | test_repo.populate()?; |
| 318 | let git_repo = Repo::from_path(&test_repo.dir)?; | 712 | let git_repo = Repo::from_path(&test_repo.dir)?; |
| 319 | 713 | ||
| 320 | assert!(git_repo.does_commit_exist(&"431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?); | 714 | assert!(git_repo.does_commit_exist("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?); |
| 321 | Ok(()) | 715 | Ok(()) |
| 322 | } | 716 | } |
| 323 | 717 | ||
| @@ -325,10 +719,10 @@ mod tests { | |||
| 325 | fn correctly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_false() | 719 | fn correctly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_false() |
| 326 | -> Result<()> { | 720 | -> Result<()> { |
| 327 | let test_repo = GitTestRepo::default(); | 721 | let test_repo = GitTestRepo::default(); |
| 328 | let oid = test_repo.populate()?; | 722 | test_repo.populate()?; |
| 329 | let git_repo = Repo::from_path(&test_repo.dir)?; | 723 | let git_repo = Repo::from_path(&test_repo.dir)?; |
| 330 | 724 | ||
| 331 | assert!(!git_repo.does_commit_exist(&"000004edc0d2fa118d63faa3c2db9c73d630a5ae")?); | 725 | assert!(!git_repo.does_commit_exist("000004edc0d2fa118d63faa3c2db9c73d630a5ae")?); |
| 332 | Ok(()) | 726 | Ok(()) |
| 333 | } | 727 | } |
| 334 | 728 | ||
| @@ -336,10 +730,10 @@ mod tests { | |||
| 336 | fn incorrectly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_error() | 730 | fn incorrectly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_error() |
| 337 | -> Result<()> { | 731 | -> Result<()> { |
| 338 | let test_repo = GitTestRepo::default(); | 732 | let test_repo = GitTestRepo::default(); |
| 339 | let oid = test_repo.populate()?; | 733 | test_repo.populate()?; |
| 340 | let git_repo = Repo::from_path(&test_repo.dir)?; | 734 | let git_repo = Repo::from_path(&test_repo.dir)?; |
| 341 | 735 | ||
| 342 | assert!(!git_repo.does_commit_exist(&"00").is_err()); | 736 | assert!(git_repo.does_commit_exist("00").is_ok()); |
| 343 | Ok(()) | 737 | Ok(()) |
| 344 | } | 738 | } |
| 345 | } | 739 | } |
| @@ -633,7 +1027,7 @@ mod tests { | |||
| 633 | let branch_name = "test-name-1"; | 1027 | let branch_name = "test-name-1"; |
| 634 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; | 1028 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; |
| 635 | 1029 | ||
| 636 | assert!(test_repo.checkout(&branch_name).is_ok()); | 1030 | assert!(test_repo.checkout(branch_name).is_ok()); |
| 637 | Ok(()) | 1031 | Ok(()) |
| 638 | } | 1032 | } |
| 639 | 1033 | ||
| @@ -654,8 +1048,555 @@ mod tests { | |||
| 654 | let branch_name = "test-name-1"; | 1048 | let branch_name = "test-name-1"; |
| 655 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; | 1049 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; |
| 656 | 1050 | ||
| 657 | assert_eq!(test_repo.checkout(&branch_name)?, ahead_1_oid); | 1051 | assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid); |
| 1052 | Ok(()) | ||
| 1053 | } | ||
| 1054 | |||
| 1055 | mod when_branch_already_exists { | ||
| 1056 | use super::*; | ||
| 1057 | |||
| 1058 | #[test] | ||
| 1059 | fn when_new_tip_specified_it_is_updated() -> Result<()> { | ||
| 1060 | let test_repo = GitTestRepo::default(); | ||
| 1061 | test_repo.populate()?; | ||
| 1062 | // create feature branch and add 2 commits | ||
| 1063 | test_repo.create_branch("feature")?; | ||
| 1064 | test_repo.checkout("feature")?; | ||
| 1065 | std::fs::write(test_repo.dir.join("t3.md"), "some content")?; | ||
| 1066 | let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; | ||
| 1067 | std::fs::write(test_repo.dir.join("t4.md"), "some content")?; | ||
| 1068 | let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; | ||
| 1069 | |||
| 1070 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1071 | |||
| 1072 | let branch_name = "test-name-1"; | ||
| 1073 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; | ||
| 1074 | |||
| 1075 | git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?; | ||
| 1076 | assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid); | ||
| 1077 | Ok(()) | ||
| 1078 | } | ||
| 1079 | |||
| 1080 | #[test] | ||
| 1081 | fn when_same_tip_is_specified_it_doesnt_error() -> Result<()> { | ||
| 1082 | let test_repo = GitTestRepo::default(); | ||
| 1083 | test_repo.populate()?; | ||
| 1084 | // create feature branch and add 2 commits | ||
| 1085 | test_repo.create_branch("feature")?; | ||
| 1086 | test_repo.checkout("feature")?; | ||
| 1087 | std::fs::write(test_repo.dir.join("t3.md"), "some content")?; | ||
| 1088 | let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; | ||
| 1089 | std::fs::write(test_repo.dir.join("t4.md"), "some content")?; | ||
| 1090 | test_repo.stage_and_commit("add t4.md")?; | ||
| 1091 | |||
| 1092 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1093 | |||
| 1094 | let branch_name = "test-name-1"; | ||
| 1095 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; | ||
| 1096 | |||
| 1097 | git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; | ||
| 1098 | assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid); | ||
| 1099 | Ok(()) | ||
| 1100 | } | ||
| 1101 | } | ||
| 1102 | } | ||
| 1103 | |||
| 1104 | mod apply_patch { | ||
| 1105 | use test_utils::TEST_KEY_1_KEYS; | ||
| 1106 | |||
| 1107 | use super::*; | ||
| 1108 | use crate::sub_commands::prs::create::generate_patch_event; | ||
| 1109 | |||
| 1110 | fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> { | ||
| 1111 | let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); | ||
| 1112 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1113 | generate_patch_event( | ||
| 1114 | &git_repo, | ||
| 1115 | &git_repo.get_root_commit("main")?, | ||
| 1116 | &oid_to_sha1(&original_oid), | ||
| 1117 | nostr::EventId::all_zeros(), | ||
| 1118 | &TEST_KEY_1_KEYS, | ||
| 1119 | ) | ||
| 1120 | } | ||
| 1121 | fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { | ||
| 1122 | let test_repo = GitTestRepo::default(); | ||
| 1123 | test_repo.populate()?; | ||
| 1124 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1125 | println!("{:?}", &patch_event); | ||
| 1126 | apply_patch(&git_repo, &patch_event)?; | ||
| 1127 | let commit_id = tag_value(&patch_event, "commit")?; | ||
| 1128 | // does commit with id exist? | ||
| 1129 | assert!(git_repo.does_commit_exist(&commit_id)?); | ||
| 1130 | // is commit head | ||
| 1131 | assert_eq!( | ||
| 1132 | test_repo | ||
| 1133 | .git_repo | ||
| 1134 | .head()? | ||
| 1135 | .peel_to_commit()? | ||
| 1136 | .id() | ||
| 1137 | .to_string(), | ||
| 1138 | commit_id, | ||
| 1139 | ); | ||
| 1140 | // applied to current checked branch (head hasn't moved to specific commit) | ||
| 1141 | assert_eq!( | ||
| 1142 | test_repo | ||
| 1143 | .git_repo | ||
| 1144 | .head()? | ||
| 1145 | .shorthand() | ||
| 1146 | .context("an object without a shorthand is checked out")? | ||
| 1147 | .to_string(), | ||
| 1148 | "main", | ||
| 1149 | ); | ||
| 1150 | |||
| 658 | Ok(()) | 1151 | Ok(()) |
| 659 | } | 1152 | } |
| 1153 | |||
| 1154 | mod patch_created_as_commit_with_matching_id { | ||
| 1155 | use test_utils::git::joe_signature; | ||
| 1156 | |||
| 1157 | use super::*; | ||
| 1158 | |||
| 1159 | #[test] | ||
| 1160 | fn simple_signature_author_committer_same_as_git_user_0_unixtime_no_pgp_signature() | ||
| 1161 | -> Result<()> { | ||
| 1162 | let source_repo = GitTestRepo::default(); | ||
| 1163 | source_repo.populate()?; | ||
| 1164 | fs::write(source_repo.dir.join("x1.md"), "some content")?; | ||
| 1165 | source_repo.stage_and_commit("add x1.md")?; | ||
| 1166 | |||
| 1167 | test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?) | ||
| 1168 | } | ||
| 1169 | |||
| 1170 | #[test] | ||
| 1171 | fn signature_with_specific_author_time() -> Result<()> { | ||
| 1172 | let source_repo = GitTestRepo::default(); | ||
| 1173 | source_repo.populate()?; | ||
| 1174 | fs::write(source_repo.dir.join("x1.md"), "some content")?; | ||
| 1175 | source_repo.stage_and_commit_custom_signature( | ||
| 1176 | "add x1.md", | ||
| 1177 | Some(&git2::Signature::new( | ||
| 1178 | joe_signature().name().unwrap(), | ||
| 1179 | joe_signature().email().unwrap(), | ||
| 1180 | &git2::Time::new(5000, 0), | ||
| 1181 | )?), | ||
| 1182 | None, | ||
| 1183 | )?; | ||
| 1184 | |||
| 1185 | test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?) | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | #[test] | ||
| 1189 | fn author_name_and_email_not_current_git_user() -> Result<()> { | ||
| 1190 | let source_repo = GitTestRepo::default(); | ||
| 1191 | source_repo.populate()?; | ||
| 1192 | fs::write(source_repo.dir.join("x1.md"), "some content")?; | ||
| 1193 | source_repo.stage_and_commit_custom_signature( | ||
| 1194 | "add x1.md", | ||
| 1195 | Some(&git2::Signature::new( | ||
| 1196 | "carole", | ||
| 1197 | "carole@pm.me", | ||
| 1198 | &git2::Time::new(0, 0), | ||
| 1199 | )?), | ||
| 1200 | None, | ||
| 1201 | )?; | ||
| 1202 | |||
| 1203 | test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?) | ||
| 1204 | } | ||
| 1205 | |||
| 1206 | #[test] | ||
| 1207 | fn comiiter_name_and_email_not_current_git_user_or_author() -> Result<()> { | ||
| 1208 | let source_repo = GitTestRepo::default(); | ||
| 1209 | source_repo.populate()?; | ||
| 1210 | fs::write(source_repo.dir.join("x1.md"), "some content")?; | ||
| 1211 | source_repo.stage_and_commit_custom_signature( | ||
| 1212 | "add x1.md", | ||
| 1213 | Some(&git2::Signature::new( | ||
| 1214 | "carole", | ||
| 1215 | "carole@pm.me", | ||
| 1216 | &git2::Time::new(0, 0), | ||
| 1217 | )?), | ||
| 1218 | Some(&git2::Signature::new( | ||
| 1219 | "bob", | ||
| 1220 | "bob@pm.me", | ||
| 1221 | &git2::Time::new(0, 0), | ||
| 1222 | )?), | ||
| 1223 | )?; | ||
| 1224 | |||
| 1225 | test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?) | ||
| 1226 | } | ||
| 1227 | |||
| 1228 | // TODO: pgp signature | ||
| 1229 | |||
| 1230 | #[test] | ||
| 1231 | fn unique_author_and_commiter_details() -> Result<()> { | ||
| 1232 | let source_repo = GitTestRepo::default(); | ||
| 1233 | source_repo.populate()?; | ||
| 1234 | fs::write(source_repo.dir.join("x1.md"), "some content")?; | ||
| 1235 | source_repo.stage_and_commit_custom_signature( | ||
| 1236 | "add x1.md", | ||
| 1237 | Some(&git2::Signature::new( | ||
| 1238 | "carole", | ||
| 1239 | "carole@pm.me", | ||
| 1240 | &git2::Time::new(5000, 0), | ||
| 1241 | )?), | ||
| 1242 | Some(&git2::Signature::new( | ||
| 1243 | "bob", | ||
| 1244 | "bob@pm.me", | ||
| 1245 | &git2::Time::new(1000, 0), | ||
| 1246 | )?), | ||
| 1247 | )?; | ||
| 1248 | |||
| 1249 | test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?) | ||
| 1250 | } | ||
| 1251 | } | ||
| 1252 | } | ||
| 1253 | |||
| 1254 | mod apply_patch_chain { | ||
| 1255 | use test_utils::TEST_KEY_1_KEYS; | ||
| 1256 | |||
| 1257 | use super::*; | ||
| 1258 | use crate::sub_commands::prs::create::generate_pr_and_patch_events; | ||
| 1259 | |||
| 1260 | static BRANCH_NAME: &str = "add-example-feature"; | ||
| 1261 | // returns original_repo, pr_event, patch_events | ||
| 1262 | fn generate_test_repo_and_events() -> Result<(GitTestRepo, nostr::Event, Vec<nostr::Event>)> | ||
| 1263 | { | ||
| 1264 | let original_repo = GitTestRepo::default(); | ||
| 1265 | let oid3 = original_repo.populate_with_test_branch()?; | ||
| 1266 | let oid2 = original_repo.git_repo.find_commit(oid3)?.parent_id(0)?; | ||
| 1267 | let oid1 = original_repo.git_repo.find_commit(oid2)?.parent_id(0)?; | ||
| 1268 | // TODO: generate pr and patch events | ||
| 1269 | let git_repo = Repo::from_path(&original_repo.dir)?; | ||
| 1270 | |||
| 1271 | let mut events = generate_pr_and_patch_events( | ||
| 1272 | "title", | ||
| 1273 | "description", | ||
| 1274 | BRANCH_NAME, | ||
| 1275 | &git_repo, | ||
| 1276 | &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)], | ||
| 1277 | &TEST_KEY_1_KEYS, | ||
| 1278 | )?; | ||
| 1279 | |||
| 1280 | events.reverse(); | ||
| 1281 | |||
| 1282 | Ok((original_repo, events.pop().unwrap(), events)) | ||
| 1283 | } | ||
| 1284 | |||
| 1285 | mod when_branch_and_commits_dont_exist { | ||
| 1286 | use super::*; | ||
| 1287 | |||
| 1288 | mod when_branch_root_is_tip_of_main { | ||
| 1289 | use super::*; | ||
| 1290 | |||
| 1291 | #[test] | ||
| 1292 | fn branch_gets_created_with_name_specified_in_pr() -> Result<()> { | ||
| 1293 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1294 | let test_repo = GitTestRepo::default(); | ||
| 1295 | test_repo.populate()?; | ||
| 1296 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1297 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1298 | assert!( | ||
| 1299 | git_repo | ||
| 1300 | .get_local_branch_names()? | ||
| 1301 | .contains(&BRANCH_NAME.to_string()) | ||
| 1302 | ); | ||
| 1303 | Ok(()) | ||
| 1304 | } | ||
| 1305 | |||
| 1306 | #[test] | ||
| 1307 | fn branch_checked_out() -> Result<()> { | ||
| 1308 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1309 | let test_repo = GitTestRepo::default(); | ||
| 1310 | test_repo.populate()?; | ||
| 1311 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1312 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1313 | assert_eq!( | ||
| 1314 | git_repo.get_checked_out_branch_name()?, | ||
| 1315 | BRANCH_NAME.to_string(), | ||
| 1316 | ); | ||
| 1317 | Ok(()) | ||
| 1318 | } | ||
| 1319 | |||
| 1320 | #[test] | ||
| 1321 | fn patches_get_created_as_commits() -> Result<()> { | ||
| 1322 | let (original_repo, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1323 | let test_repo = GitTestRepo::default(); | ||
| 1324 | test_repo.populate()?; | ||
| 1325 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1326 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1327 | assert_eq!( | ||
| 1328 | test_repo.git_repo.head()?.peel_to_commit()?.id(), | ||
| 1329 | original_repo.git_repo.head()?.peel_to_commit()?.id(), | ||
| 1330 | ); | ||
| 1331 | Ok(()) | ||
| 1332 | } | ||
| 1333 | |||
| 1334 | #[test] | ||
| 1335 | fn branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 1336 | let (original_repo, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1337 | let test_repo = GitTestRepo::default(); | ||
| 1338 | test_repo.populate()?; | ||
| 1339 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1340 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1341 | assert_eq!( | ||
| 1342 | git_repo.get_tip_of_local_branch(BRANCH_NAME)?, | ||
| 1343 | oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), | ||
| 1344 | ); | ||
| 1345 | Ok(()) | ||
| 1346 | } | ||
| 1347 | |||
| 1348 | #[test] | ||
| 1349 | fn previously_checked_out_branch_tip_does_not_change() -> Result<()> { | ||
| 1350 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1351 | let test_repo = GitTestRepo::default(); | ||
| 1352 | test_repo.populate()?; | ||
| 1353 | let existing_branch = test_repo.get_checked_out_branch_name()?; | ||
| 1354 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1355 | let previous_tip_of_existing_branch = | ||
| 1356 | git_repo.get_tip_of_local_branch(existing_branch.as_str())?; | ||
| 1357 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1358 | assert_eq!( | ||
| 1359 | previous_tip_of_existing_branch, | ||
| 1360 | git_repo.get_tip_of_local_branch(existing_branch.as_str())?, | ||
| 1361 | ); | ||
| 1362 | Ok(()) | ||
| 1363 | } | ||
| 1364 | |||
| 1365 | #[test] | ||
| 1366 | fn returns_all_patches_applied() -> Result<()> { | ||
| 1367 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1368 | let test_repo = GitTestRepo::default(); | ||
| 1369 | test_repo.populate()?; | ||
| 1370 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1371 | let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1372 | assert_eq!(res.len(), 3); | ||
| 1373 | Ok(()) | ||
| 1374 | } | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | mod when_branch_root_is_tip_behind_main { | ||
| 1378 | use super::*; | ||
| 1379 | |||
| 1380 | #[test] | ||
| 1381 | fn branch_gets_created_with_name_specified_in_pr() -> Result<()> { | ||
| 1382 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1383 | let test_repo = GitTestRepo::default(); | ||
| 1384 | test_repo.populate()?; | ||
| 1385 | std::fs::write(test_repo.dir.join("m3.md"), "some content")?; | ||
| 1386 | test_repo.stage_and_commit("add m3.md")?; | ||
| 1387 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1388 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1389 | assert!( | ||
| 1390 | git_repo | ||
| 1391 | .get_local_branch_names()? | ||
| 1392 | .contains(&BRANCH_NAME.to_string()) | ||
| 1393 | ); | ||
| 1394 | Ok(()) | ||
| 1395 | } | ||
| 1396 | |||
| 1397 | #[test] | ||
| 1398 | fn branch_checked_out() -> Result<()> { | ||
| 1399 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1400 | let test_repo = GitTestRepo::default(); | ||
| 1401 | test_repo.populate()?; | ||
| 1402 | std::fs::write(test_repo.dir.join("m3.md"), "some content")?; | ||
| 1403 | test_repo.stage_and_commit("add m3.md")?; | ||
| 1404 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1405 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1406 | assert_eq!( | ||
| 1407 | git_repo.get_checked_out_branch_name()?, | ||
| 1408 | BRANCH_NAME.to_string(), | ||
| 1409 | ); | ||
| 1410 | Ok(()) | ||
| 1411 | } | ||
| 1412 | |||
| 1413 | #[test] | ||
| 1414 | fn branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 1415 | let (original_repo, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1416 | let test_repo = GitTestRepo::default(); | ||
| 1417 | test_repo.populate()?; | ||
| 1418 | std::fs::write(test_repo.dir.join("m3.md"), "some content")?; | ||
| 1419 | test_repo.stage_and_commit("add m3.md")?; | ||
| 1420 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1421 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1422 | assert_eq!( | ||
| 1423 | git_repo.get_tip_of_local_branch(BRANCH_NAME)?, | ||
| 1424 | oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), | ||
| 1425 | ); | ||
| 1426 | Ok(()) | ||
| 1427 | } | ||
| 1428 | |||
| 1429 | #[test] | ||
| 1430 | fn previously_checked_out_branch_tip_does_not_change() -> Result<()> { | ||
| 1431 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1432 | let test_repo = GitTestRepo::default(); | ||
| 1433 | test_repo.populate()?; | ||
| 1434 | std::fs::write(test_repo.dir.join("m3.md"), "some content")?; | ||
| 1435 | test_repo.stage_and_commit("add m3.md")?; | ||
| 1436 | let existing_branch = test_repo.get_checked_out_branch_name()?; | ||
| 1437 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1438 | let previous_tip_of_existing_branch = | ||
| 1439 | git_repo.get_tip_of_local_branch(existing_branch.as_str())?; | ||
| 1440 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1441 | assert_eq!( | ||
| 1442 | previous_tip_of_existing_branch, | ||
| 1443 | git_repo.get_tip_of_local_branch(existing_branch.as_str())?, | ||
| 1444 | ); | ||
| 1445 | Ok(()) | ||
| 1446 | } | ||
| 1447 | |||
| 1448 | #[test] | ||
| 1449 | fn returns_all_patches_applied() -> Result<()> { | ||
| 1450 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1451 | let test_repo = GitTestRepo::default(); | ||
| 1452 | test_repo.populate()?; | ||
| 1453 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1454 | let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1455 | assert_eq!(res.len(), 3); | ||
| 1456 | Ok(()) | ||
| 1457 | } | ||
| 1458 | } | ||
| 1459 | |||
| 1460 | // TODO when_pr_root_is_tip_ahead_of_main_and_doesnt_exist | ||
| 1461 | } | ||
| 1462 | |||
| 1463 | mod when_branch_and_first_commits_exists { | ||
| 1464 | use super::*; | ||
| 1465 | |||
| 1466 | mod when_branch_already_checked_out { | ||
| 1467 | use super::*; | ||
| 1468 | |||
| 1469 | #[test] | ||
| 1470 | fn branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 1471 | let (original_repo, _, mut patch_events) = generate_test_repo_and_events()?; | ||
| 1472 | let test_repo = GitTestRepo::default(); | ||
| 1473 | test_repo.populate()?; | ||
| 1474 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1475 | git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; | ||
| 1476 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1477 | |||
| 1478 | assert_eq!( | ||
| 1479 | git_repo.get_tip_of_local_branch(BRANCH_NAME)?, | ||
| 1480 | oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), | ||
| 1481 | ); | ||
| 1482 | Ok(()) | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | #[test] | ||
| 1486 | fn returns_all_patches_applied() -> Result<()> { | ||
| 1487 | let (_, _, mut patch_events) = generate_test_repo_and_events()?; | ||
| 1488 | let test_repo = GitTestRepo::default(); | ||
| 1489 | test_repo.populate()?; | ||
| 1490 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1491 | git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; | ||
| 1492 | let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1493 | assert_eq!(res.len(), 2); | ||
| 1494 | Ok(()) | ||
| 1495 | } | ||
| 1496 | } | ||
| 1497 | mod when_branch_not_checked_out { | ||
| 1498 | use super::*; | ||
| 1499 | |||
| 1500 | #[test] | ||
| 1501 | fn branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 1502 | let (original_repo, _, mut patch_events) = generate_test_repo_and_events()?; | ||
| 1503 | let test_repo = GitTestRepo::default(); | ||
| 1504 | test_repo.populate()?; | ||
| 1505 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1506 | git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; | ||
| 1507 | git_repo.checkout("main")?; | ||
| 1508 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1509 | |||
| 1510 | assert_eq!( | ||
| 1511 | git_repo.get_tip_of_local_branch(BRANCH_NAME)?, | ||
| 1512 | oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), | ||
| 1513 | ); | ||
| 1514 | Ok(()) | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | #[test] | ||
| 1518 | fn branch_checked_out() -> Result<()> { | ||
| 1519 | let (_, _, mut patch_events) = generate_test_repo_and_events()?; | ||
| 1520 | let test_repo = GitTestRepo::default(); | ||
| 1521 | test_repo.populate()?; | ||
| 1522 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1523 | git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; | ||
| 1524 | git_repo.checkout("main")?; | ||
| 1525 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1526 | |||
| 1527 | assert_eq!( | ||
| 1528 | git_repo.get_checked_out_branch_name()?, | ||
| 1529 | BRANCH_NAME.to_string(), | ||
| 1530 | ); | ||
| 1531 | Ok(()) | ||
| 1532 | } | ||
| 1533 | |||
| 1534 | #[test] | ||
| 1535 | fn returns_all_patches_applied() -> Result<()> { | ||
| 1536 | let (_, _, mut patch_events) = generate_test_repo_and_events()?; | ||
| 1537 | let test_repo = GitTestRepo::default(); | ||
| 1538 | test_repo.populate()?; | ||
| 1539 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1540 | git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; | ||
| 1541 | git_repo.checkout("main")?; | ||
| 1542 | let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1543 | assert_eq!(res.len(), 2); | ||
| 1544 | Ok(()) | ||
| 1545 | } | ||
| 1546 | } | ||
| 1547 | // TODO when branch ahead (rebased or user commits) | ||
| 1548 | } | ||
| 1549 | mod when_branch_exists_and_is_up_to_date { | ||
| 1550 | use super::*; | ||
| 1551 | |||
| 1552 | mod when_branch_already_checked_out { | ||
| 1553 | use super::*; | ||
| 1554 | |||
| 1555 | #[test] | ||
| 1556 | fn returns_all_patches_applied_0() -> Result<()> { | ||
| 1557 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1558 | let test_repo = GitTestRepo::default(); | ||
| 1559 | test_repo.populate()?; | ||
| 1560 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1561 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?; | ||
| 1562 | let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1563 | assert_eq!(res.len(), 0); | ||
| 1564 | Ok(()) | ||
| 1565 | } | ||
| 1566 | } | ||
| 1567 | mod when_branch_not_checked_out { | ||
| 1568 | use super::*; | ||
| 1569 | |||
| 1570 | #[test] | ||
| 1571 | fn branch_checked_out() -> Result<()> { | ||
| 1572 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1573 | let test_repo = GitTestRepo::default(); | ||
| 1574 | test_repo.populate()?; | ||
| 1575 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1576 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?; | ||
| 1577 | git_repo.checkout("main")?; | ||
| 1578 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1579 | |||
| 1580 | assert_eq!( | ||
| 1581 | git_repo.get_checked_out_branch_name()?, | ||
| 1582 | BRANCH_NAME.to_string(), | ||
| 1583 | ); | ||
| 1584 | Ok(()) | ||
| 1585 | } | ||
| 1586 | |||
| 1587 | #[test] | ||
| 1588 | fn returns_all_patches_applied_0() -> Result<()> { | ||
| 1589 | let (_, _, patch_events) = generate_test_repo_and_events()?; | ||
| 1590 | let test_repo = GitTestRepo::default(); | ||
| 1591 | test_repo.populate()?; | ||
| 1592 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 1593 | git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?; | ||
| 1594 | git_repo.checkout("main")?; | ||
| 1595 | let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; | ||
| 1596 | assert_eq!(res.len(), 0); | ||
| 1597 | Ok(()) | ||
| 1598 | } | ||
| 1599 | } | ||
| 1600 | } | ||
| 660 | } | 1601 | } |
| 661 | } | 1602 | } |
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs index ce30c12..5c4e578 100644 --- a/src/sub_commands/prs/create.rs +++ b/src/sub_commands/prs/create.rs | |||
| @@ -301,9 +301,9 @@ mod tests_unique_and_duplicate { | |||
| 301 | pub static PR_KIND: u64 = 318; | 301 | pub static PR_KIND: u64 = 318; |
| 302 | pub static PATCH_KIND: u64 = 317; | 302 | pub static PATCH_KIND: u64 = 317; |
| 303 | 303 | ||
| 304 | fn generate_pr_and_patch_events( | 304 | pub fn generate_pr_and_patch_events( |
| 305 | title: &String, | 305 | title: &str, |
| 306 | description: &String, | 306 | description: &str, |
| 307 | to_branch: &str, | 307 | to_branch: &str, |
| 308 | git_repo: &Repo, | 308 | git_repo: &Repo, |
| 309 | commits: &Vec<Sha1Hash>, | 309 | commits: &Vec<Sha1Hash>, |
| @@ -314,6 +314,7 @@ fn generate_pr_and_patch_events( | |||
| 314 | .context("failed to get root commit of the repository")?; | 314 | .context("failed to get root commit of the repository")?; |
| 315 | 315 | ||
| 316 | let mut pr_tags = vec![ | 316 | let mut pr_tags = vec![ |
| 317 | Tag::Identifier(root_commit.to_string()), | ||
| 317 | Tag::Reference(format!("r-{root_commit}")), | 318 | Tag::Reference(format!("r-{root_commit}")), |
| 318 | Tag::Name(title.to_string()), | 319 | Tag::Name(title.to_string()), |
| 319 | Tag::Description(description.to_string()), | 320 | Tag::Description(description.to_string()), |
| @@ -341,42 +342,63 @@ fn generate_pr_and_patch_events( | |||
| 341 | 342 | ||
| 342 | let mut events = vec![pr_event]; | 343 | let mut events = vec![pr_event]; |
| 343 | for commit in commits { | 344 | for commit in commits { |
| 344 | let commit_parent = git_repo | ||
| 345 | .get_commit_parent(commit) | ||
| 346 | .context("failed to create patch event")?; | ||
| 347 | events.push( | 345 | events.push( |
| 348 | EventBuilder::new( | 346 | generate_patch_event(git_repo, &root_commit, commit, pr_event_id, keys) |
| 349 | nostr::event::Kind::Custom(PATCH_KIND), | 347 | .context("failed to generate patch event")?, |
| 350 | git_repo | ||
| 351 | .make_patch_from_commit(commit) | ||
| 352 | .context(format!("cannot make patch for commit {commit}"))?, | ||
| 353 | &[ | ||
| 354 | Tag::Reference(format!("r-{root_commit}")), | ||
| 355 | Tag::Reference(commit.to_string()), | ||
| 356 | Tag::Reference(commit_parent.to_string()), | ||
| 357 | Tag::Event( | ||
| 358 | pr_event_id, | ||
| 359 | None, // TODO: add relay | ||
| 360 | Some(Marker::Root), | ||
| 361 | ), | ||
| 362 | Tag::Generic( | ||
| 363 | TagKind::Custom("commit".to_string()), | ||
| 364 | vec![commit.to_string()], | ||
| 365 | ), | ||
| 366 | Tag::Generic( | ||
| 367 | TagKind::Custom("parent-commit".to_string()), | ||
| 368 | vec![commit_parent.to_string()], | ||
| 369 | ), | ||
| 370 | // TODO: add Repo event tags | ||
| 371 | // TODO: people tag maintainers | ||
| 372 | // TODO: add relay tags | ||
| 373 | ], | ||
| 374 | ) | ||
| 375 | .to_event(keys)?, | ||
| 376 | ); | 348 | ); |
| 377 | } | 349 | } |
| 378 | Ok(events) | 350 | Ok(events) |
| 379 | } | 351 | } |
| 352 | |||
| 353 | pub fn generate_patch_event( | ||
| 354 | git_repo: &Repo, | ||
| 355 | root_commit: &Sha1Hash, | ||
| 356 | commit: &Sha1Hash, | ||
| 357 | pr_event_id: nostr::EventId, | ||
| 358 | keys: &nostr::Keys, | ||
| 359 | ) -> Result<nostr::Event> { | ||
| 360 | let commit_parent = git_repo | ||
| 361 | .get_commit_parent(commit) | ||
| 362 | .context("failed to get parent commit")?; | ||
| 363 | EventBuilder::new( | ||
| 364 | nostr::event::Kind::Custom(PATCH_KIND), | ||
| 365 | git_repo | ||
| 366 | .make_patch_from_commit(commit) | ||
| 367 | .context(format!("cannot make patch for commit {commit}"))?, | ||
| 368 | &[ | ||
| 369 | Tag::Reference(format!("r-{root_commit}")), | ||
| 370 | Tag::Reference(commit.to_string()), | ||
| 371 | Tag::Reference(commit_parent.to_string()), | ||
| 372 | Tag::Event( | ||
| 373 | pr_event_id, | ||
| 374 | None, // TODO: add relay | ||
| 375 | Some(Marker::Root), | ||
| 376 | ), | ||
| 377 | Tag::Generic( | ||
| 378 | TagKind::Custom("commit".to_string()), | ||
| 379 | vec![commit.to_string()], | ||
| 380 | ), | ||
| 381 | Tag::Generic( | ||
| 382 | TagKind::Custom("parent-commit".to_string()), | ||
| 383 | vec![commit_parent.to_string()], | ||
| 384 | ), | ||
| 385 | Tag::Description(git_repo.get_commit_message(commit)?.to_string()), | ||
| 386 | Tag::Generic( | ||
| 387 | TagKind::Custom("author".to_string()), | ||
| 388 | git_repo.get_commit_author(commit)?, | ||
| 389 | ), | ||
| 390 | Tag::Generic( | ||
| 391 | TagKind::Custom("committer".to_string()), | ||
| 392 | git_repo.get_commit_comitter(commit)?, | ||
| 393 | ), | ||
| 394 | // TODO: add Repo event tags | ||
| 395 | // TODO: people tag maintainers | ||
| 396 | // TODO: add relay tags | ||
| 397 | ], | ||
| 398 | ) | ||
| 399 | .to_event(keys) | ||
| 400 | .context("failed to sign event") | ||
| 401 | } | ||
| 380 | // TODO | 402 | // TODO |
| 381 | // - find profile | 403 | // - find profile |
| 382 | // - file relays | 404 | // - file relays |
diff --git a/src/sub_commands/prs/list.rs b/src/sub_commands/prs/list.rs new file mode 100644 index 0000000..13f29fd --- /dev/null +++ b/src/sub_commands/prs/list.rs | |||
| @@ -0,0 +1,225 @@ | |||
| 1 | use anyhow::{Context, Result}; | ||
| 2 | |||
| 3 | #[cfg(not(test))] | ||
| 4 | use crate::client::Client; | ||
| 5 | #[cfg(test)] | ||
| 6 | use crate::client::MockConnect; | ||
| 7 | use crate::{ | ||
| 8 | cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms}, | ||
| 9 | client::Connect, | ||
| 10 | git::{Repo, RepoActions}, | ||
| 11 | repo_ref, | ||
| 12 | sub_commands::prs::create::{PATCH_KIND, PR_KIND}, | ||
| 13 | Cli, | ||
| 14 | }; | ||
| 15 | |||
| 16 | #[derive(Debug, clap::Args)] | ||
| 17 | pub struct SubCommandArgs { | ||
| 18 | /// TODO ignore merged, and closed | ||
| 19 | #[arg(long, action)] | ||
| 20 | open_only: bool, | ||
| 21 | } | ||
| 22 | |||
| 23 | pub async fn launch( | ||
| 24 | _cli_args: &Cli, | ||
| 25 | _pr_args: &super::SubCommandArgs, | ||
| 26 | _args: &SubCommandArgs, | ||
| 27 | ) -> Result<()> { | ||
| 28 | let git_repo = Repo::discover().context("cannot find a git repository")?; | ||
| 29 | |||
| 30 | let (main_or_master_branch_name, _) = git_repo | ||
| 31 | .get_main_or_master_branch() | ||
| 32 | .context("no main or master branch")?; | ||
| 33 | |||
| 34 | let root_commit = git_repo | ||
| 35 | .get_root_commit(main_or_master_branch_name) | ||
| 36 | .context("failed to get root commit of the repository")?; | ||
| 37 | |||
| 38 | // TODO: check for empty repo | ||
| 39 | // TODO: check for existing maintaiers file | ||
| 40 | // TODO: check for other claims | ||
| 41 | |||
| 42 | #[cfg(not(test))] | ||
| 43 | let client = Client::default(); | ||
| 44 | #[cfg(test)] | ||
| 45 | let client = <MockConnect as std::default::Default>::default(); | ||
| 46 | |||
| 47 | let repo_ref = repo_ref::fetch( | ||
| 48 | root_commit.to_string(), | ||
| 49 | &client, | ||
| 50 | client.get_more_fallback_relays().clone(), | ||
| 51 | ) | ||
| 52 | .await?; | ||
| 53 | |||
| 54 | println!("finding PRs..."); | ||
| 55 | |||
| 56 | let pr_events: Vec<nostr::Event> = client | ||
| 57 | .get_events( | ||
| 58 | repo_ref.relays.clone(), | ||
| 59 | vec![ | ||
| 60 | nostr::Filter::default() | ||
| 61 | .kind(nostr::Kind::Custom(PR_KIND)) | ||
| 62 | .reference(format!("r-{root_commit}")), | ||
| 63 | ], | ||
| 64 | ) | ||
| 65 | .await? | ||
| 66 | .iter() | ||
| 67 | .filter(|e| { | ||
| 68 | e.kind.as_u64() == PR_KIND | ||
| 69 | && e.tags | ||
| 70 | .iter() | ||
| 71 | .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("r-{root_commit}"))) | ||
| 72 | }) | ||
| 73 | .map(std::borrow::ToOwned::to_owned) | ||
| 74 | .collect(); | ||
| 75 | |||
| 76 | // let pr_branch_names: Vec<String> = pr_events | ||
| 77 | // .iter() | ||
| 78 | // .map(|e| { | ||
| 79 | // format!( | ||
| 80 | // "{}-{}", | ||
| 81 | // &e.id.to_string()[..5], | ||
| 82 | // if let Some(t) = e.tags.iter().find(|t| t.as_vec()[0] == | ||
| 83 | // "branch-name") { t.as_vec()[1].to_string() | ||
| 84 | // } else { | ||
| 85 | // "".to_string() | ||
| 86 | // } // git_repo.get_checked_out_branch_name(), | ||
| 87 | // ) | ||
| 88 | // }) | ||
| 89 | // .collect(); | ||
| 90 | |||
| 91 | let selected_index = Interactor::default().choice( | ||
| 92 | PromptChoiceParms::default() | ||
| 93 | .with_prompt("All PRs") | ||
| 94 | .with_choices( | ||
| 95 | pr_events | ||
| 96 | .iter() | ||
| 97 | .map(|e| { | ||
| 98 | if let Ok(name) = tag_value(e, "name") { | ||
| 99 | name | ||
| 100 | } else { | ||
| 101 | e.id.to_string() | ||
| 102 | } | ||
| 103 | }) | ||
| 104 | .collect(), | ||
| 105 | ), | ||
| 106 | )?; | ||
| 107 | // println!("prs:{:?}", &pr_events); | ||
| 108 | |||
| 109 | println!("finding commits..."); | ||
| 110 | |||
| 111 | let commits_events: Vec<nostr::Event> = client | ||
| 112 | .get_events( | ||
| 113 | repo_ref.relays.clone(), | ||
| 114 | vec![ | ||
| 115 | nostr::Filter::default() | ||
| 116 | .kind(nostr::Kind::Custom(PATCH_KIND)) | ||
| 117 | .event(pr_events[selected_index].id) | ||
| 118 | .reference(format!("r-{root_commit}")), | ||
| 119 | ], | ||
| 120 | ) | ||
| 121 | .await? | ||
| 122 | .iter() | ||
| 123 | .filter(|e| { | ||
| 124 | e.kind.as_u64() == PATCH_KIND | ||
| 125 | && e.tags.iter().any(|t| { | ||
| 126 | t.as_vec().len() > 2 | ||
| 127 | && t.as_vec()[1].eq(&pr_events[selected_index].id.to_string()) | ||
| 128 | }) | ||
| 129 | && e.tags | ||
| 130 | .iter() | ||
| 131 | .any(|t| t.as_vec().len() > 1 && t.as_vec()[1].eq(&format!("r-{root_commit}"))) | ||
| 132 | }) | ||
| 133 | .map(std::borrow::ToOwned::to_owned) | ||
| 134 | .collect(); | ||
| 135 | |||
| 136 | // TODO: are there outstanding changes to prevent checking out a new branch? | ||
| 137 | |||
| 138 | let most_recent_pr_patch_chain = get_most_recent_patch_with_ancestors(commits_events) | ||
| 139 | .context("cannot get most recent patch for PR")?; | ||
| 140 | |||
| 141 | let branch_name = tag_value(&pr_events[selected_index], "branch-name")?; | ||
| 142 | |||
| 143 | let applied = git_repo | ||
| 144 | .apply_patch_chain(&branch_name, most_recent_pr_patch_chain) | ||
| 145 | .context("cannot apply patch chain")?; | ||
| 146 | |||
| 147 | if applied.is_empty() { | ||
| 148 | println!("checked out PR branch. no new commits to pull"); | ||
| 149 | } else { | ||
| 150 | println!( | ||
| 151 | "checked out PR branch. pulled {} new commits", | ||
| 152 | applied.len(), | ||
| 153 | ); | ||
| 154 | } | ||
| 155 | |||
| 156 | // // TODO: look for mapping of existing branch | ||
| 157 | |||
| 158 | // // if latest_commit_id exists locally | ||
| 159 | // if local_branch_base == latest_commit_id { | ||
| 160 | // // TODO: check if its in the main / master branch (already merged) | ||
| 161 | // // TODO: check if it has any decendants and warn. maybe the user has | ||
| 162 | // // been working on a updates to be pushed? Suggest checking | ||
| 163 | // // out that branch. | ||
| 164 | // // we could search nostr for decendants of the commit as well? | ||
| 165 | // // perhaps this is overkill | ||
| 166 | // // TODO: check out the branch which it is the tip of. if the name of the | ||
| 167 | // // branch is different then ask the user if they would like to | ||
| 168 | // // use the existing branch or create one with the name of the PR. | ||
| 169 | // // TODO: if there are no decendants and its not the tip then | ||
| 170 | // // its an ophan commit so just make a branch from this commit. | ||
| 171 | // } | ||
| 172 | // // if commits ahead exist in a branch other than main or master | ||
| 173 | // // TODO: Identify probable existing branches - check remote tracker? | ||
| 174 | // // TODO: beind head | ||
| 175 | // else { | ||
| 176 | // // TODO: look for existing branch with same name | ||
| 177 | // // TODO: create remote tracker | ||
| 178 | // git_repo.create_branch_at_commit(&branch_name, &local_branch_base); | ||
| 179 | // git_repo.checkout(&branch_name)?; | ||
| 180 | // ahead.reverse(); | ||
| 181 | // for event in ahead { | ||
| 182 | // git_repo.apply_patch(event, branch_name)?; | ||
| 183 | // } | ||
| 184 | // println!("applied!") | ||
| 185 | // } | ||
| 186 | // // TODO: check if commits in pr exist, if so look for branches with they are | ||
| 187 | // in // could we suggest pulling updates into that branch? | ||
| 188 | // // | ||
| 189 | |||
| 190 | // TODO: checkout PR branch | ||
| 191 | Ok(()) | ||
| 192 | } | ||
| 193 | |||
| 194 | pub fn tag_value(event: &nostr::Event, tag_name: &str) -> Result<String> { | ||
| 195 | Ok(event | ||
| 196 | .tags | ||
| 197 | .iter() | ||
| 198 | .find(|t| t.as_vec()[0].eq(tag_name)) | ||
| 199 | .context(format!("tag '{tag_name}'not present"))? | ||
| 200 | .as_vec()[1] | ||
| 201 | .clone()) | ||
| 202 | } | ||
| 203 | |||
| 204 | pub fn get_most_recent_patch_with_ancestors( | ||
| 205 | mut patches: Vec<nostr::Event>, | ||
| 206 | ) -> Result<Vec<nostr::Event>> { | ||
| 207 | patches.sort_by_key(|e| e.created_at); | ||
| 208 | |||
| 209 | let mut res = vec![]; | ||
| 210 | |||
| 211 | let latest_commit_id = tag_value(patches.first().context("no patches found")?, "commit")?; | ||
| 212 | |||
| 213 | let mut commit_id_to_search = latest_commit_id; | ||
| 214 | |||
| 215 | while let Some(event) = patches.iter().find(|e| { | ||
| 216 | tag_value(e, "commit") | ||
| 217 | .context("patch event doesnt contain commit tag") | ||
| 218 | .unwrap() | ||
| 219 | .eq(&commit_id_to_search) | ||
| 220 | }) { | ||
| 221 | res.push(event.clone()); | ||
| 222 | commit_id_to_search = tag_value(event, "parent-commit")?; | ||
| 223 | } | ||
| 224 | Ok(res) | ||
| 225 | } | ||
diff --git a/src/sub_commands/prs/mod.rs b/src/sub_commands/prs/mod.rs index 982e866..a41c495 100644 --- a/src/sub_commands/prs/mod.rs +++ b/src/sub_commands/prs/mod.rs | |||
| @@ -3,6 +3,7 @@ use clap::Subcommand; | |||
| 3 | 3 | ||
| 4 | use crate::Cli; | 4 | use crate::Cli; |
| 5 | pub mod create; | 5 | pub mod create; |
| 6 | pub mod list; | ||
| 6 | 7 | ||
| 7 | #[derive(clap::Parser)] | 8 | #[derive(clap::Parser)] |
| 8 | pub struct SubCommandArgs { | 9 | pub struct SubCommandArgs { |
| @@ -13,10 +14,12 @@ pub struct SubCommandArgs { | |||
| 13 | #[derive(Debug, Subcommand)] | 14 | #[derive(Debug, Subcommand)] |
| 14 | pub enum Commands { | 15 | pub enum Commands { |
| 15 | Create(create::SubCommandArgs), | 16 | Create(create::SubCommandArgs), |
| 17 | List(list::SubCommandArgs), | ||
| 16 | } | 18 | } |
| 17 | 19 | ||
| 18 | pub async fn launch(cli_args: &Cli, pr_args: &SubCommandArgs) -> Result<()> { | 20 | pub async fn launch(cli_args: &Cli, pr_args: &SubCommandArgs) -> Result<()> { |
| 19 | match &pr_args.prs_command { | 21 | match &pr_args.prs_command { |
| 20 | Commands::Create(args) => create::launch(cli_args, pr_args, args).await, | 22 | Commands::Create(args) => create::launch(cli_args, pr_args, args).await, |
| 23 | Commands::List(args) => list::launch(cli_args, pr_args, args).await, | ||
| 21 | } | 24 | } |
| 22 | } | 25 | } |
diff --git a/test_utils/src/git.rs b/test_utils/src/git.rs index 166693d..af87a3a 100644 --- a/test_utils/src/git.rs +++ b/test_utils/src/git.rs | |||
| @@ -3,7 +3,7 @@ | |||
| 3 | // implement drop? | 3 | // implement drop? |
| 4 | use std::{env::current_dir, fs, path::PathBuf}; | 4 | use std::{env::current_dir, fs, path::PathBuf}; |
| 5 | 5 | ||
| 6 | use anyhow::Result; | 6 | use anyhow::{Context, Result}; |
| 7 | use git2::{Oid, RepositoryInitOptions, Signature, Time}; | 7 | use git2::{Oid, RepositoryInitOptions, Signature, Time}; |
| 8 | 8 | ||
| 9 | pub struct GitTestRepo { | 9 | pub struct GitTestRepo { |
| @@ -53,7 +53,27 @@ impl GitTestRepo { | |||
| 53 | self.stage_and_commit("add t2.md") | 53 | self.stage_and_commit("add t2.md") |
| 54 | } | 54 | } |
| 55 | 55 | ||
| 56 | pub fn populate_with_test_branch(&self) -> Result<Oid> { | ||
| 57 | self.populate()?; | ||
| 58 | self.create_branch("add-example-feature")?; | ||
| 59 | fs::write(self.dir.join("f1.md"), "some content")?; | ||
| 60 | self.stage_and_commit("add f1.md")?; | ||
| 61 | fs::write(self.dir.join("f2.md"), "some content")?; | ||
| 62 | self.stage_and_commit("add f2.md")?; | ||
| 63 | fs::write(self.dir.join("f3.md"), "some content1")?; | ||
| 64 | self.stage_and_commit("add f3.md") | ||
| 65 | } | ||
| 66 | |||
| 56 | pub fn stage_and_commit(&self, message: &str) -> Result<Oid> { | 67 | pub fn stage_and_commit(&self, message: &str) -> Result<Oid> { |
| 68 | self.stage_and_commit_custom_signature(message, None, None) | ||
| 69 | } | ||
| 70 | |||
| 71 | pub fn stage_and_commit_custom_signature( | ||
| 72 | &self, | ||
| 73 | message: &str, | ||
| 74 | author: Option<&git2::Signature>, | ||
| 75 | commiter: Option<&git2::Signature>, | ||
| 76 | ) -> Result<Oid> { | ||
| 57 | let prev_oid = self.git_repo.head().unwrap().peel_to_commit()?; | 77 | let prev_oid = self.git_repo.head().unwrap().peel_to_commit()?; |
| 58 | 78 | ||
| 59 | let mut index = self.git_repo.index()?; | 79 | let mut index = self.git_repo.index()?; |
| @@ -62,8 +82,8 @@ impl GitTestRepo { | |||
| 62 | 82 | ||
| 63 | let oid = self.git_repo.commit( | 83 | let oid = self.git_repo.commit( |
| 64 | Some("HEAD"), | 84 | Some("HEAD"), |
| 65 | &joe_signature(), | 85 | author.unwrap_or(&joe_signature()), |
| 66 | &joe_signature(), | 86 | commiter.unwrap_or(&joe_signature()), |
| 67 | message, | 87 | message, |
| 68 | &self.git_repo.find_tree(index.write_tree()?)?, | 88 | &self.git_repo.find_tree(index.write_tree()?)?, |
| 69 | &[&prev_oid], | 89 | &[&prev_oid], |
| @@ -92,6 +112,40 @@ impl GitTestRepo { | |||
| 92 | let oid = self.git_repo.head()?.peel_to_commit()?.id(); | 112 | let oid = self.git_repo.head()?.peel_to_commit()?.id(); |
| 93 | Ok(oid) | 113 | Ok(oid) |
| 94 | } | 114 | } |
| 115 | |||
| 116 | pub fn get_local_branch_names(&self) -> Result<Vec<String>> { | ||
| 117 | let local_branches = self | ||
| 118 | .git_repo | ||
| 119 | .branches(Some(git2::BranchType::Local)) | ||
| 120 | .context("getting GitRepo branches should not error even for a blank repository")?; | ||
| 121 | |||
| 122 | let mut branch_names = vec![]; | ||
| 123 | |||
| 124 | for iter in local_branches { | ||
| 125 | let branch = iter?.0; | ||
| 126 | if let Some(name) = branch.name()? { | ||
| 127 | branch_names.push(name.to_string()); | ||
| 128 | } | ||
| 129 | } | ||
| 130 | Ok(branch_names) | ||
| 131 | } | ||
| 132 | |||
| 133 | pub fn get_checked_out_branch_name(&self) -> Result<String> { | ||
| 134 | Ok(self | ||
| 135 | .git_repo | ||
| 136 | .head()? | ||
| 137 | .shorthand() | ||
| 138 | .context("an object without a shorthand is checked out")? | ||
| 139 | .to_string()) | ||
| 140 | } | ||
| 141 | |||
| 142 | pub fn get_tip_of_local_branch(&self, branch_name: &str) -> Result<Oid> { | ||
| 143 | let branch = self | ||
| 144 | .git_repo | ||
| 145 | .find_branch(branch_name, git2::BranchType::Local) | ||
| 146 | .context(format!("cannot find branch {branch_name}"))?; | ||
| 147 | Ok(branch.into_reference().peel_to_commit()?.id()) | ||
| 148 | } | ||
| 95 | } | 149 | } |
| 96 | 150 | ||
| 97 | impl Drop for GitTestRepo { | 151 | impl Drop for GitTestRepo { |
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index e867a7b..fa80f1f 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs | |||
| @@ -11,6 +11,10 @@ use strip_ansi_escapes::strip_str; | |||
| 11 | pub mod git; | 11 | pub mod git; |
| 12 | pub mod relay; | 12 | pub mod relay; |
| 13 | 13 | ||
| 14 | pub static PR_KIND: u64 = 318; | ||
| 15 | pub static PATCH_KIND: u64 = 317; | ||
| 16 | pub static REPOSITORY_KIND: u64 = 300317; | ||
| 17 | |||
| 14 | pub static TEST_KEY_1_NSEC: &str = | 18 | pub static TEST_KEY_1_NSEC: &str = |
| 15 | "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq"; | 19 | "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq"; |
| 16 | pub static TEST_KEY_1_SK_HEX: &str = | 20 | pub static TEST_KEY_1_SK_HEX: &str = |
| @@ -120,8 +124,6 @@ pub fn make_event_old_or_change_user( | |||
| 120 | unsigned.sign(keys).unwrap() | 124 | unsigned.sign(keys).unwrap() |
| 121 | } | 125 | } |
| 122 | 126 | ||
| 123 | pub static REPOSITORY_KIND: u64 = 300317; | ||
| 124 | |||
| 125 | pub fn generate_repo_ref_event() -> nostr::Event { | 127 | pub fn generate_repo_ref_event() -> nostr::Event { |
| 126 | // taken from test git_repo | 128 | // taken from test git_repo |
| 127 | let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d"; | 129 | let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d"; |
| @@ -208,6 +210,20 @@ impl CliTester { | |||
| 208 | i.prompt(true, default).context("initial confirm prompt")?; | 210 | i.prompt(true, default).context("initial confirm prompt")?; |
| 209 | Ok(i) | 211 | Ok(i) |
| 210 | } | 212 | } |
| 213 | |||
| 214 | pub fn expect_choice( | ||
| 215 | &mut self, | ||
| 216 | prompt: &str, | ||
| 217 | choices: Vec<String>, | ||
| 218 | ) -> Result<CliTesterChoicePrompt> { | ||
| 219 | let mut i = CliTesterChoicePrompt { | ||
| 220 | tester: self, | ||
| 221 | prompt: prompt.to_string(), | ||
| 222 | choices, | ||
| 223 | }; | ||
| 224 | i.prompt(false).context("initial confirm prompt")?; | ||
| 225 | Ok(i) | ||
| 226 | } | ||
| 211 | } | 227 | } |
| 212 | 228 | ||
| 213 | pub struct CliTesterInputPrompt<'a> { | 229 | pub struct CliTesterInputPrompt<'a> { |
| @@ -397,6 +413,137 @@ impl CliTesterConfirmPrompt<'_> { | |||
| 397 | } | 413 | } |
| 398 | } | 414 | } |
| 399 | 415 | ||
| 416 | pub struct CliTesterChoicePrompt<'a> { | ||
| 417 | tester: &'a mut CliTester, | ||
| 418 | prompt: String, | ||
| 419 | choices: Vec<String>, | ||
| 420 | } | ||
| 421 | |||
| 422 | impl CliTesterChoicePrompt<'_> { | ||
| 423 | fn prompt(&mut self, eventually: bool) -> Result<&mut Self> { | ||
| 424 | let mut s = String::new(); | ||
| 425 | self.tester | ||
| 426 | .formatter | ||
| 427 | .format_select_prompt(&mut s, self.prompt.as_str()) | ||
| 428 | .expect("diagluer theme formatter should succeed"); | ||
| 429 | ensure!( | ||
| 430 | s.contains(self.prompt.as_str()), | ||
| 431 | "dialoguer must be broken as formatted prompt success doesnt contain prompt" | ||
| 432 | ); | ||
| 433 | |||
| 434 | if eventually { | ||
| 435 | self.tester | ||
| 436 | .expect_eventually(sanatize(s).as_str()) | ||
| 437 | .context("expect input prompt eventually")?; | ||
| 438 | } else { | ||
| 439 | self.tester | ||
| 440 | .expect(sanatize(s).as_str()) | ||
| 441 | .context("expect confirm prompt")?; | ||
| 442 | } | ||
| 443 | |||
| 444 | Ok(self) | ||
| 445 | } | ||
| 446 | |||
| 447 | pub fn succeeds_with(&mut self, chosen_index: u64, report: bool) -> Result<&mut Self> { | ||
| 448 | fn show_options( | ||
| 449 | tester: &mut CliTester, | ||
| 450 | choices: &Vec<String>, | ||
| 451 | selected_index: Option<usize>, | ||
| 452 | ) -> Result<()> { | ||
| 453 | if selected_index.is_some() { | ||
| 454 | for _ in 0..choices.len() { | ||
| 455 | tester.expect("\r").context("expect new line per choice")?; | ||
| 456 | } | ||
| 457 | } else { | ||
| 458 | tester | ||
| 459 | .expect("\r\n") | ||
| 460 | .context("expect new line before choices")?; | ||
| 461 | } | ||
| 462 | |||
| 463 | for (index, item) in choices.iter().enumerate() { | ||
| 464 | let mut s = String::new(); | ||
| 465 | tester | ||
| 466 | .formatter | ||
| 467 | .format_select_prompt_item( | ||
| 468 | &mut s, | ||
| 469 | item.as_str(), | ||
| 470 | if let Some(i) = selected_index { | ||
| 471 | index == i | ||
| 472 | } else { | ||
| 473 | false | ||
| 474 | }, | ||
| 475 | ) | ||
| 476 | .expect("diagluer theme formatter should succeed"); | ||
| 477 | ensure!( | ||
| 478 | s.contains(item.as_str()), | ||
| 479 | "dialoguer must be broken as formatted prompt success doesnt contain prompt" | ||
| 480 | ); | ||
| 481 | tester.expect(sanatize(s)).context("expect choice item")?; | ||
| 482 | |||
| 483 | tester | ||
| 484 | .expect(if choices.len() == index { | ||
| 485 | "\r\r" | ||
| 486 | } else { | ||
| 487 | "\r\n" | ||
| 488 | }) | ||
| 489 | .context("expect new line after choice item")?; | ||
| 490 | } | ||
| 491 | Ok(()) | ||
| 492 | } | ||
| 493 | fn show_selected( | ||
| 494 | tester: &mut CliTester, | ||
| 495 | prompt: &str, | ||
| 496 | choices: &[String], | ||
| 497 | selected_index: u64, | ||
| 498 | ) -> Result<()> { | ||
| 499 | let mut s = String::new(); | ||
| 500 | |||
| 501 | let selected = choices[usize::try_from(selected_index)?].clone(); | ||
| 502 | tester | ||
| 503 | .formatter | ||
| 504 | .format_select_prompt_selection(&mut s, prompt, selected.as_str()) | ||
| 505 | .expect("diagluer theme formatter should succeed"); | ||
| 506 | ensure!( | ||
| 507 | s.contains(selected.as_str()), | ||
| 508 | "dialoguer must be broken as formatted prompt success doesnt contain prompt" | ||
| 509 | ); | ||
| 510 | tester.expect(sanatize(s)).context("expect choice item")?; | ||
| 511 | Ok(()) | ||
| 512 | } | ||
| 513 | |||
| 514 | show_options(self.tester, &self.choices, None)?; | ||
| 515 | |||
| 516 | for _ in 0..(chosen_index + 1) { | ||
| 517 | self.tester.send("j")?; | ||
| 518 | } | ||
| 519 | |||
| 520 | self.tester.send(" ")?; | ||
| 521 | |||
| 522 | for index in 0..(chosen_index + 1) { | ||
| 523 | show_options(self.tester, &self.choices, Some(usize::try_from(index)?))?; | ||
| 524 | } | ||
| 525 | |||
| 526 | for _ in 0..self.choices.len() { | ||
| 527 | self.tester | ||
| 528 | .expect("\r") | ||
| 529 | .context("expect new line per option")?; | ||
| 530 | } | ||
| 531 | |||
| 532 | self.tester | ||
| 533 | .expect("\r") | ||
| 534 | .context("expect new line after options")?; | ||
| 535 | |||
| 536 | if report { | ||
| 537 | show_selected(self.tester, &self.prompt, &self.choices, chosen_index)?; | ||
| 538 | self.tester | ||
| 539 | .expect("\r\n") | ||
| 540 | .context("expect new line at end")?; | ||
| 541 | } | ||
| 542 | |||
| 543 | Ok(self) | ||
| 544 | } | ||
| 545 | } | ||
| 546 | |||
| 400 | impl CliTester { | 547 | impl CliTester { |
| 401 | pub fn new<I, S>(args: I) -> Self | 548 | pub fn new<I, S>(args: I) -> Self |
| 402 | where | 549 | where |
| @@ -525,6 +672,16 @@ impl CliTester { | |||
| 525 | Ok(()) | 672 | Ok(()) |
| 526 | } | 673 | } |
| 527 | 674 | ||
| 675 | pub fn expect_end_eventually_and_print(&mut self) -> Result<()> { | ||
| 676 | let before = self | ||
| 677 | .rexpect_session | ||
| 678 | .exp_eof() | ||
| 679 | .context("expected immediate end but got timed out")?; | ||
| 680 | println!("ended eventually with:"); | ||
| 681 | println!("{}", &before); | ||
| 682 | Ok(()) | ||
| 683 | } | ||
| 684 | |||
| 528 | pub fn expect_end_with_whitespace(&mut self) -> Result<()> { | 685 | pub fn expect_end_with_whitespace(&mut self) -> Result<()> { |
| 529 | let before = self | 686 | let before = self |
| 530 | .rexpect_session | 687 | .rexpect_session |
| @@ -551,6 +708,12 @@ impl CliTester { | |||
| 551 | .context("send_line failed")?; | 708 | .context("send_line failed")?; |
| 552 | Ok(()) | 709 | Ok(()) |
| 553 | } | 710 | } |
| 711 | |||
| 712 | fn send(&mut self, s: &str) -> Result<()> { | ||
| 713 | self.rexpect_session.send(s).context("send failed")?; | ||
| 714 | self.rexpect_session.flush()?; | ||
| 715 | Ok(()) | ||
| 716 | } | ||
| 554 | } | 717 | } |
| 555 | 718 | ||
| 556 | /// sanatize unicode string for rexpect | 719 | /// sanatize unicode string for rexpect |
diff --git a/test_utils/src/relay.rs b/test_utils/src/relay.rs index 4ef34e6..50f6337 100644 --- a/test_utils/src/relay.rs +++ b/test_utils/src/relay.rs | |||
| @@ -91,6 +91,27 @@ impl<'a> Relay<'a> { | |||
| 91 | self.respond_eose(client_id, subscription_id.clone()) | 91 | self.respond_eose(client_id, subscription_id.clone()) |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | /// send collected events, filtered by filters, and eose | ||
| 95 | pub fn respond_standard_req( | ||
| 96 | &self, | ||
| 97 | client_id: u64, | ||
| 98 | subscription_id: &nostr::SubscriptionId, | ||
| 99 | filters: &[nostr::Filter], | ||
| 100 | ) -> Result<bool> { | ||
| 101 | // let t: Vec<nostr::Kind> = self.events.iter().map(|e| e.kind).collect(); | ||
| 102 | // .filter(|e| filters.iter().any(|filter| filter.match_event(e))) | ||
| 103 | // println!("letsgo{:?}", t); | ||
| 104 | self.respond_events( | ||
| 105 | client_id, | ||
| 106 | subscription_id, | ||
| 107 | &self | ||
| 108 | .events | ||
| 109 | .iter() | ||
| 110 | .filter(|e| filters.iter().any(|filter| filter.match_event(e))) | ||
| 111 | .cloned() | ||
| 112 | .collect(), | ||
| 113 | ) | ||
| 114 | } | ||
| 94 | /// listen, collect events and responds with event_listener to events or | 115 | /// listen, collect events and responds with event_listener to events or |
| 95 | /// Ok(eventid) if event_listner is None | 116 | /// Ok(eventid) if event_listner is None |
| 96 | pub async fn listen_until_close(&mut self) -> Result<()> { | 117 | pub async fn listen_until_close(&mut self) -> Result<()> { |
| @@ -108,6 +129,8 @@ impl<'a> Relay<'a> { | |||
| 108 | // break; | 129 | // break; |
| 109 | } | 130 | } |
| 110 | simple_websockets::Event::Message(client_id, message) => { | 131 | simple_websockets::Event::Message(client_id, message) => { |
| 132 | // println!("bla{:?}", &message); | ||
| 133 | |||
| 111 | println!( | 134 | println!( |
| 112 | "{} Received a message from client #{}: {:?}", | 135 | "{} Received a message from client #{}: {:?}", |
| 113 | self.port, client_id, message | 136 | self.port, client_id, message |
| @@ -118,8 +141,15 @@ impl<'a> Relay<'a> { | |||
| 118 | break; | 141 | break; |
| 119 | } | 142 | } |
| 120 | } | 143 | } |
| 144 | // println!("{:?}", &message); | ||
| 121 | if let Ok(event) = get_nevent(&message) { | 145 | if let Ok(event) = get_nevent(&message) { |
| 146 | // println!("{:?}", &event); | ||
| 147 | // let t: Vec<nostr::Kind> = self.events.iter().map(|e| e.kind).collect(); | ||
| 148 | // println!("before{:?}", t); | ||
| 122 | self.events.push(event.clone()); | 149 | self.events.push(event.clone()); |
| 150 | // let t: Vec<nostr::Kind> = self.events.iter().map(|e| e.kind).collect(); | ||
| 151 | // println!("after{:?}", t); | ||
| 152 | |||
| 123 | if let Some(listner) = self.event_listener { | 153 | if let Some(listner) = self.event_listener { |
| 124 | listner(self, client_id, event)?; | 154 | listner(self, client_id, event)?; |
| 125 | } else { | 155 | } else { |
| @@ -132,7 +162,8 @@ impl<'a> Relay<'a> { | |||
| 132 | if let Some(listner) = self.req_listener { | 162 | if let Some(listner) = self.req_listener { |
| 133 | listner(self, client_id, subscription_id, filters)?; | 163 | listner(self, client_id, subscription_id, filters)?; |
| 134 | } else { | 164 | } else { |
| 135 | self.respond_eose(client_id, subscription_id)?; | 165 | self.respond_standard_req(client_id, &subscription_id, &filters)?; |
| 166 | // self.respond_eose(client_id, subscription_id)?; | ||
| 136 | } | 167 | } |
| 137 | // respond with events | 168 | // respond with events |
| 138 | // respond with EOSE | 169 | // respond with EOSE |
diff --git a/tests/prs_create.rs b/tests/prs_create.rs index dc6eec0..d00bb9f 100644 --- a/tests/prs_create.rs +++ b/tests/prs_create.rs | |||
| @@ -147,9 +147,6 @@ mod sends_pr_and_2_patches_to_3_relays { | |||
| 147 | 147 | ||
| 148 | use super::*; | 148 | use super::*; |
| 149 | 149 | ||
| 150 | static PR_KIND: u64 = 318; | ||
| 151 | static PATCH_KIND: u64 = 317; | ||
| 152 | |||
| 153 | fn prep_git_repo() -> Result<GitTestRepo> { | 150 | fn prep_git_repo() -> Result<GitTestRepo> { |
| 154 | let test_repo = GitTestRepo::default(); | 151 | let test_repo = GitTestRepo::default(); |
| 155 | test_repo.populate()?; | 152 | test_repo.populate()?; |
| @@ -411,8 +408,31 @@ mod sends_pr_and_2_patches_to_3_relays { | |||
| 411 | use super::*; | 408 | use super::*; |
| 412 | #[test] | 409 | #[test] |
| 413 | #[serial] | 410 | #[serial] |
| 411 | fn pr_tags_repo_commit_as_identifier() -> Result<()> { | ||
| 412 | let (_, _, r53, r55, r56) = futures::executor::block_on(prep_run_create_pr())?; | ||
| 413 | let root_commit = GitTestRepo::default().initial_commit()?; | ||
| 414 | |||
| 415 | for relay in [&r53, &r55, &r56] { | ||
| 416 | let pr_event: &nostr::Event = relay | ||
| 417 | .events | ||
| 418 | .iter() | ||
| 419 | .find(|e| e.kind.as_u64().eq(&PR_KIND)) | ||
| 420 | .unwrap(); | ||
| 421 | |||
| 422 | // root commit identifier tag | ||
| 423 | assert!(pr_event.tags.iter().any( | ||
| 424 | |t| t.as_vec()[0].eq("d") && t.as_vec()[1].eq(&format!("{}", root_commit)) | ||
| 425 | )); | ||
| 426 | } | ||
| 427 | Ok(()) | ||
| 428 | } | ||
| 429 | |||
| 430 | #[test] | ||
| 431 | #[serial] | ||
| 414 | fn pr_tags_repo_commit() -> Result<()> { | 432 | fn pr_tags_repo_commit() -> Result<()> { |
| 415 | let (_, _, r53, r55, r56) = futures::executor::block_on(prep_run_create_pr())?; | 433 | let (_, _, r53, r55, r56) = futures::executor::block_on(prep_run_create_pr())?; |
| 434 | let root_commit = GitTestRepo::default().initial_commit()?; | ||
| 435 | |||
| 416 | for relay in [&r53, &r55, &r56] { | 436 | for relay in [&r53, &r55, &r56] { |
| 417 | let pr_event: &nostr::Event = relay | 437 | let pr_event: &nostr::Event = relay |
| 418 | .events | 438 | .events |
| @@ -421,8 +441,10 @@ mod sends_pr_and_2_patches_to_3_relays { | |||
| 421 | .unwrap(); | 441 | .unwrap(); |
| 422 | 442 | ||
| 423 | // root commit 'r' tag | 443 | // root commit 'r' tag |
| 424 | assert!(pr_event.tags.iter().any(|t| t.as_vec()[0].eq("r") | 444 | assert!( |
| 425 | && t.as_vec()[1].eq("r-9ee507fc4357d7ee16a5d8901bedcd103f23c17d"))); | 445 | pr_event.tags.iter().any(|t| t.as_vec()[0].eq("r") |
| 446 | && t.as_vec()[1].eq(&format!("r-{}", root_commit))) | ||
| 447 | ); | ||
| 426 | } | 448 | } |
| 427 | Ok(()) | 449 | Ok(()) |
| 428 | } | 450 | } |
| @@ -503,50 +525,107 @@ mod sends_pr_and_2_patches_to_3_relays { | |||
| 503 | 525 | ||
| 504 | mod patch_tags { | 526 | mod patch_tags { |
| 505 | use super::*; | 527 | use super::*; |
| 528 | |||
| 529 | fn prep() -> Result<nostr::Event> { | ||
| 530 | let (_, _, r53, _, _) = futures::executor::block_on(prep_run_create_pr())?; | ||
| 531 | Ok(r53 | ||
| 532 | .events | ||
| 533 | .iter() | ||
| 534 | .find(|e| e.kind.as_u64().eq(&PATCH_KIND)) | ||
| 535 | .unwrap() | ||
| 536 | .clone()) | ||
| 537 | } | ||
| 538 | |||
| 506 | #[test] | 539 | #[test] |
| 507 | #[serial] | 540 | #[serial] |
| 508 | fn patch_tags_correctly_formatted() -> Result<()> { | 541 | fn commit_and_commit_r() -> Result<()> { |
| 509 | let (_, _, r53, r55, r56) = futures::executor::block_on(prep_run_create_pr())?; | 542 | static COMMIT_ID: &str = "fe973a840fba2a8ab37dd505c154854a69a6505c"; |
| 510 | for relay in [&r53, &r55, &r56] { | 543 | let most_recent_patch = prep()?; |
| 511 | let patch_events: Vec<&nostr::Event> = relay | 544 | assert!( |
| 512 | .events | 545 | most_recent_patch |
| 546 | .tags | ||
| 513 | .iter() | 547 | .iter() |
| 514 | .filter(|e| e.kind.as_u64().eq(&PATCH_KIND)) | 548 | .any(|t| t.as_vec()[0].eq("r") && t.as_vec()[1].eq(COMMIT_ID)) |
| 515 | .collect(); | 549 | ); |
| 550 | assert!( | ||
| 551 | most_recent_patch | ||
| 552 | .tags | ||
| 553 | .iter() | ||
| 554 | .any(|t| t.as_vec()[0].eq("commit") && t.as_vec()[1].eq(COMMIT_ID)) | ||
| 555 | ); | ||
| 556 | Ok(()) | ||
| 557 | } | ||
| 516 | 558 | ||
| 517 | static COMMIT_ID: &str = "fe973a840fba2a8ab37dd505c154854a69a6505c"; | 559 | #[test] |
| 518 | let most_recent_patch = patch_events[0]; | 560 | #[serial] |
| 561 | fn parent_commit_and_parent_commit_r() -> Result<()> { | ||
| 562 | // commit parent 'r' and 'parent-commit' tag | ||
| 563 | static COMMIT_PARENT_ID: &str = "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"; | ||
| 564 | let most_recent_patch = prep()?; | ||
| 565 | assert!( | ||
| 566 | most_recent_patch | ||
| 567 | .tags | ||
| 568 | .iter() | ||
| 569 | .any(|t| t.as_vec()[0].eq("r") && t.as_vec()[1].eq(COMMIT_PARENT_ID)) | ||
| 570 | ); | ||
| 571 | assert!( | ||
| 572 | most_recent_patch.tags.iter().any( | ||
| 573 | |t| t.as_vec()[0].eq("parent-commit") && t.as_vec()[1].eq(COMMIT_PARENT_ID) | ||
| 574 | ) | ||
| 575 | ); | ||
| 576 | Ok(()) | ||
| 577 | } | ||
| 519 | 578 | ||
| 520 | // commit 'r' and 'commit' tag | 579 | #[test] |
| 521 | assert!( | 580 | #[serial] |
| 522 | most_recent_patch | 581 | fn root_commit_as_r_with_r_hypen_prefix() -> Result<()> { |
| 523 | .tags | 582 | assert!(prep()?.tags.iter().any(|t| t.as_vec()[0].eq("r") |
| 524 | .iter() | 583 | && t.as_vec()[1].eq("r-9ee507fc4357d7ee16a5d8901bedcd103f23c17d"))); |
| 525 | .any(|t| t.as_vec()[0].eq("r") && t.as_vec()[1].eq(COMMIT_ID)) | 584 | Ok(()) |
| 526 | ); | 585 | } |
| 527 | assert!( | ||
| 528 | most_recent_patch | ||
| 529 | .tags | ||
| 530 | .iter() | ||
| 531 | .any(|t| t.as_vec()[0].eq("commit") && t.as_vec()[1].eq(COMMIT_ID)) | ||
| 532 | ); | ||
| 533 | 586 | ||
| 534 | // commit parent 't' and 'parent-commit' tag | 587 | #[test] |
| 535 | static COMMIT_PARENT_ID: &str = "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"; | 588 | #[serial] |
| 536 | assert!( | 589 | fn description_with_commit_message() -> Result<()> { |
| 537 | most_recent_patch | 590 | assert_eq!( |
| 538 | .tags | 591 | prep()? |
| 539 | .iter() | 592 | .tags |
| 540 | .any(|t| t.as_vec()[0].eq("r") && t.as_vec()[1].eq(COMMIT_PARENT_ID)) | 593 | .iter() |
| 541 | ); | 594 | .find(|t| t.as_vec()[0].eq("description")) |
| 542 | assert!(most_recent_patch.tags.iter().any( | 595 | .unwrap() |
| 543 | |t| t.as_vec()[0].eq("parent-commit") && t.as_vec()[1].eq(COMMIT_PARENT_ID) | 596 | .as_vec()[1], |
| 544 | )); | 597 | "add t4.md" |
| 598 | ); | ||
| 599 | Ok(()) | ||
| 600 | } | ||
| 545 | 601 | ||
| 546 | // root commit 't' tag | 602 | #[test] |
| 547 | assert!(most_recent_patch.tags.iter().any(|t| t.as_vec()[0].eq("r") | 603 | #[serial] |
| 548 | && t.as_vec()[1].eq("r-9ee507fc4357d7ee16a5d8901bedcd103f23c17d"))); | 604 | fn commit_author() -> Result<()> { |
| 549 | } | 605 | assert_eq!( |
| 606 | prep()? | ||
| 607 | .tags | ||
| 608 | .iter() | ||
| 609 | .find(|t| t.as_vec()[0].eq("author")) | ||
| 610 | .unwrap() | ||
| 611 | .as_vec(), | ||
| 612 | vec!["author", "Joe Bloggs", "joe.bloggs@pm.me", "0,0"], | ||
| 613 | ); | ||
| 614 | Ok(()) | ||
| 615 | } | ||
| 616 | |||
| 617 | #[test] | ||
| 618 | #[serial] | ||
| 619 | fn commit_committer() -> Result<()> { | ||
| 620 | assert_eq!( | ||
| 621 | prep()? | ||
| 622 | .tags | ||
| 623 | .iter() | ||
| 624 | .find(|t| t.as_vec()[0].eq("committer")) | ||
| 625 | .unwrap() | ||
| 626 | .as_vec(), | ||
| 627 | vec!["committer", "Joe Bloggs", "joe.bloggs@pm.me", "0,0"], | ||
| 628 | ); | ||
| 550 | Ok(()) | 629 | Ok(()) |
| 551 | } | 630 | } |
| 552 | 631 | ||
diff --git a/tests/prs_list.rs b/tests/prs_list.rs new file mode 100644 index 0000000..7bc3935 --- /dev/null +++ b/tests/prs_list.rs | |||
| @@ -0,0 +1,768 @@ | |||
| 1 | use anyhow::Result; | ||
| 2 | use futures::join; | ||
| 3 | use serial_test::serial; | ||
| 4 | use test_utils::{git::GitTestRepo, relay::Relay, *}; | ||
| 5 | |||
| 6 | static FEATURE_BRANCH_NAME_1: &str = "feature-example-t"; | ||
| 7 | static FEATURE_BRANCH_NAME_2: &str = "feature-example-f"; | ||
| 8 | static FEATURE_BRANCH_NAME_3: &str = "feature-example-c"; | ||
| 9 | |||
| 10 | static PR_TITLE_1: &str = "pr a"; | ||
| 11 | static PR_TITLE_2: &str = "pr b"; | ||
| 12 | static PR_TITLE_3: &str = "pr c"; | ||
| 13 | |||
| 14 | fn cli_tester_create_prs() -> Result<GitTestRepo> { | ||
| 15 | let git_repo = GitTestRepo::default(); | ||
| 16 | git_repo.populate()?; | ||
| 17 | cli_tester_create_pr( | ||
| 18 | &git_repo, | ||
| 19 | FEATURE_BRANCH_NAME_1, | ||
| 20 | "a", | ||
| 21 | PR_TITLE_1, | ||
| 22 | "pr a description", | ||
| 23 | )?; | ||
| 24 | cli_tester_create_pr( | ||
| 25 | &git_repo, | ||
| 26 | FEATURE_BRANCH_NAME_2, | ||
| 27 | "b", | ||
| 28 | PR_TITLE_2, | ||
| 29 | "pr b description", | ||
| 30 | )?; | ||
| 31 | cli_tester_create_pr( | ||
| 32 | &git_repo, | ||
| 33 | FEATURE_BRANCH_NAME_3, | ||
| 34 | "c", | ||
| 35 | PR_TITLE_3, | ||
| 36 | "pr c description", | ||
| 37 | )?; | ||
| 38 | Ok(git_repo) | ||
| 39 | } | ||
| 40 | |||
| 41 | fn create_and_populate_branch( | ||
| 42 | test_repo: &GitTestRepo, | ||
| 43 | branch_name: &str, | ||
| 44 | prefix: &str, | ||
| 45 | only_one_commit: bool, | ||
| 46 | ) -> Result<()> { | ||
| 47 | test_repo.checkout("main")?; | ||
| 48 | test_repo.create_branch(branch_name)?; | ||
| 49 | test_repo.checkout(branch_name)?; | ||
| 50 | std::fs::write( | ||
| 51 | test_repo.dir.join(format!("{}3.md", prefix)), | ||
| 52 | "some content", | ||
| 53 | )?; | ||
| 54 | test_repo.stage_and_commit(format!("add {}3.md", prefix).as_str())?; | ||
| 55 | if !only_one_commit { | ||
| 56 | std::fs::write( | ||
| 57 | test_repo.dir.join(format!("{}4.md", prefix)), | ||
| 58 | "some content", | ||
| 59 | )?; | ||
| 60 | test_repo.stage_and_commit(format!("add {}4.md", prefix).as_str())?; | ||
| 61 | } | ||
| 62 | Ok(()) | ||
| 63 | } | ||
| 64 | |||
| 65 | fn cli_tester_create_pr( | ||
| 66 | test_repo: &GitTestRepo, | ||
| 67 | branch_name: &str, | ||
| 68 | prefix: &str, | ||
| 69 | title: &str, | ||
| 70 | description: &str, | ||
| 71 | ) -> Result<()> { | ||
| 72 | create_and_populate_branch(test_repo, branch_name, prefix, false)?; | ||
| 73 | |||
| 74 | let mut p = CliTester::new_from_dir( | ||
| 75 | &test_repo.dir, | ||
| 76 | [ | ||
| 77 | "--nsec", | ||
| 78 | TEST_KEY_1_NSEC, | ||
| 79 | "--password", | ||
| 80 | TEST_PASSWORD, | ||
| 81 | "--disable-cli-spinners", | ||
| 82 | "prs", | ||
| 83 | "create", | ||
| 84 | "--title", | ||
| 85 | format!("\"{title}\"").as_str(), | ||
| 86 | "--description", | ||
| 87 | format!("\"{description}\"").as_str(), | ||
| 88 | ], | ||
| 89 | ); | ||
| 90 | p.expect_end_eventually()?; | ||
| 91 | Ok(()) | ||
| 92 | } | ||
| 93 | |||
| 94 | mod when_main_branch_is_uptodate { | ||
| 95 | use super::*; | ||
| 96 | |||
| 97 | mod when_pr_branch_doesnt_exist { | ||
| 98 | use super::*; | ||
| 99 | |||
| 100 | mod when_main_is_checked_out { | ||
| 101 | use super::*; | ||
| 102 | |||
| 103 | mod when_first_pr_selected { | ||
| 104 | use super::*; | ||
| 105 | |||
| 106 | // TODO: test when other prs with the same name but from other repositories are | ||
| 107 | // present on relays | ||
| 108 | async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 109 | // fallback (51,52) user write (53, 55) repo (55, 56) | ||
| 110 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 111 | Relay::new(8051, None, None), | ||
| 112 | Relay::new(8052, None, None), | ||
| 113 | Relay::new(8053, None, None), | ||
| 114 | Relay::new(8055, None, None), | ||
| 115 | Relay::new(8056, None, None), | ||
| 116 | ); | ||
| 117 | |||
| 118 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 119 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 120 | r51.events.push(generate_repo_ref_event()); | ||
| 121 | |||
| 122 | r55.events.push(generate_repo_ref_event()); | ||
| 123 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 124 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 125 | |||
| 126 | let cli_tester_handle = | ||
| 127 | std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 128 | let originating_repo = cli_tester_create_prs()?; | ||
| 129 | |||
| 130 | let test_repo = GitTestRepo::default(); | ||
| 131 | test_repo.populate()?; | ||
| 132 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 133 | |||
| 134 | p.expect("finding PRs...\r\n")?; | ||
| 135 | let mut c = p.expect_choice( | ||
| 136 | "All PRs", | ||
| 137 | vec![ | ||
| 138 | format!("\"{PR_TITLE_1}\""), | ||
| 139 | format!("\"{PR_TITLE_2}\""), | ||
| 140 | format!("\"{PR_TITLE_3}\""), | ||
| 141 | ], | ||
| 142 | )?; | ||
| 143 | c.succeeds_with(0, true)?; | ||
| 144 | |||
| 145 | p.expect_end_eventually_and_print()?; | ||
| 146 | |||
| 147 | for p in [51, 52, 53, 55, 56] { | ||
| 148 | relay::shutdown_relay(8000 + p)?; | ||
| 149 | } | ||
| 150 | Ok((originating_repo, test_repo)) | ||
| 151 | }); | ||
| 152 | |||
| 153 | // launch relay | ||
| 154 | let _ = join!( | ||
| 155 | r51.listen_until_close(), | ||
| 156 | r52.listen_until_close(), | ||
| 157 | r53.listen_until_close(), | ||
| 158 | r55.listen_until_close(), | ||
| 159 | r56.listen_until_close(), | ||
| 160 | ); | ||
| 161 | let res = cli_tester_handle.join().unwrap()?; | ||
| 162 | |||
| 163 | Ok(res) | ||
| 164 | } | ||
| 165 | |||
| 166 | mod cli_prompts { | ||
| 167 | use super::*; | ||
| 168 | async fn run_async_prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 169 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 170 | Relay::new(8051, None, None), | ||
| 171 | Relay::new(8052, None, None), | ||
| 172 | Relay::new(8053, None, None), | ||
| 173 | Relay::new(8055, None, None), | ||
| 174 | Relay::new(8056, None, None), | ||
| 175 | ); | ||
| 176 | |||
| 177 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 178 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 179 | r51.events.push(generate_repo_ref_event()); | ||
| 180 | |||
| 181 | r55.events.push(generate_repo_ref_event()); | ||
| 182 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 183 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 184 | |||
| 185 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 186 | cli_tester_create_prs()?; | ||
| 187 | |||
| 188 | let test_repo = GitTestRepo::default(); | ||
| 189 | test_repo.populate()?; | ||
| 190 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 191 | |||
| 192 | p.expect("finding PRs...\r\n")?; | ||
| 193 | let mut c = p.expect_choice( | ||
| 194 | "All PRs", | ||
| 195 | vec![ | ||
| 196 | format!("\"{PR_TITLE_1}\""), | ||
| 197 | format!("\"{PR_TITLE_2}\""), | ||
| 198 | format!("\"{PR_TITLE_3}\""), | ||
| 199 | ], | ||
| 200 | )?; | ||
| 201 | c.succeeds_with(0, true)?; | ||
| 202 | p.expect("finding commits...\r\n")?; | ||
| 203 | p.expect("checked out PR branch. pulled 2 new commits\r\n")?; | ||
| 204 | p.expect_end()?; | ||
| 205 | |||
| 206 | for p in [51, 52, 53, 55, 56] { | ||
| 207 | relay::shutdown_relay(8000 + p)?; | ||
| 208 | } | ||
| 209 | Ok(()) | ||
| 210 | }); | ||
| 211 | |||
| 212 | // launch relay | ||
| 213 | let _ = join!( | ||
| 214 | r51.listen_until_close(), | ||
| 215 | r52.listen_until_close(), | ||
| 216 | r53.listen_until_close(), | ||
| 217 | r55.listen_until_close(), | ||
| 218 | r56.listen_until_close(), | ||
| 219 | ); | ||
| 220 | cli_tester_handle.join().unwrap()?; | ||
| 221 | println!("{:?}", r55.events); | ||
| 222 | Ok(()) | ||
| 223 | } | ||
| 224 | |||
| 225 | #[test] | ||
| 226 | #[serial] | ||
| 227 | fn prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 228 | futures::executor::block_on(run_async_prompts_to_choose_from_pr_titles()) | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | #[test] | ||
| 233 | #[serial] | ||
| 234 | fn pr_branch_created_with_correct_name() -> Result<()> { | ||
| 235 | let (_, test_repo) = futures::executor::block_on(prep_and_run())?; | ||
| 236 | assert_eq!( | ||
| 237 | vec![FEATURE_BRANCH_NAME_1, "main"], | ||
| 238 | test_repo.get_local_branch_names()? | ||
| 239 | ); | ||
| 240 | Ok(()) | ||
| 241 | } | ||
| 242 | |||
| 243 | #[test] | ||
| 244 | #[serial] | ||
| 245 | fn pr_branch_checked_out() -> Result<()> { | ||
| 246 | let (_, test_repo) = futures::executor::block_on(prep_and_run())?; | ||
| 247 | assert_eq!( | ||
| 248 | FEATURE_BRANCH_NAME_1, | ||
| 249 | test_repo.get_checked_out_branch_name()?, | ||
| 250 | ); | ||
| 251 | Ok(()) | ||
| 252 | } | ||
| 253 | |||
| 254 | #[test] | ||
| 255 | #[serial] | ||
| 256 | fn pr_branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 257 | let (originating_repo, test_repo) = | ||
| 258 | futures::executor::block_on(prep_and_run())?; | ||
| 259 | assert_eq!( | ||
| 260 | originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, | ||
| 261 | test_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, | ||
| 262 | ); | ||
| 263 | Ok(()) | ||
| 264 | } | ||
| 265 | } | ||
| 266 | mod when_third_pr_selected { | ||
| 267 | use super::*; | ||
| 268 | |||
| 269 | async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 270 | // fallback (51,52) user write (53, 55) repo (55, 56) | ||
| 271 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 272 | Relay::new(8051, None, None), | ||
| 273 | Relay::new(8052, None, None), | ||
| 274 | Relay::new(8053, None, None), | ||
| 275 | Relay::new(8055, None, None), | ||
| 276 | Relay::new(8056, None, None), | ||
| 277 | ); | ||
| 278 | |||
| 279 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 280 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 281 | r51.events.push(generate_repo_ref_event()); | ||
| 282 | |||
| 283 | r55.events.push(generate_repo_ref_event()); | ||
| 284 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 285 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 286 | |||
| 287 | let cli_tester_handle = | ||
| 288 | std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 289 | let originating_repo = cli_tester_create_prs()?; | ||
| 290 | |||
| 291 | let test_repo = GitTestRepo::default(); | ||
| 292 | test_repo.populate()?; | ||
| 293 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 294 | |||
| 295 | p.expect("finding PRs...\r\n")?; | ||
| 296 | let mut c = p.expect_choice( | ||
| 297 | "All PRs", | ||
| 298 | vec![ | ||
| 299 | format!("\"{PR_TITLE_1}\""), | ||
| 300 | format!("\"{PR_TITLE_2}\""), | ||
| 301 | format!("\"{PR_TITLE_3}\""), | ||
| 302 | ], | ||
| 303 | )?; | ||
| 304 | c.succeeds_with(2, true)?; | ||
| 305 | |||
| 306 | p.expect_end_eventually_and_print()?; | ||
| 307 | |||
| 308 | for p in [51, 52, 53, 55, 56] { | ||
| 309 | relay::shutdown_relay(8000 + p)?; | ||
| 310 | } | ||
| 311 | Ok((originating_repo, test_repo)) | ||
| 312 | }); | ||
| 313 | |||
| 314 | // launch relay | ||
| 315 | let _ = join!( | ||
| 316 | r51.listen_until_close(), | ||
| 317 | r52.listen_until_close(), | ||
| 318 | r53.listen_until_close(), | ||
| 319 | r55.listen_until_close(), | ||
| 320 | r56.listen_until_close(), | ||
| 321 | ); | ||
| 322 | let res = cli_tester_handle.join().unwrap()?; | ||
| 323 | |||
| 324 | Ok(res) | ||
| 325 | } | ||
| 326 | |||
| 327 | mod cli_prompts { | ||
| 328 | use super::*; | ||
| 329 | async fn run_async_prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 330 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 331 | Relay::new(8051, None, None), | ||
| 332 | Relay::new(8052, None, None), | ||
| 333 | Relay::new(8053, None, None), | ||
| 334 | Relay::new(8055, None, None), | ||
| 335 | Relay::new(8056, None, None), | ||
| 336 | ); | ||
| 337 | |||
| 338 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 339 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 340 | r51.events.push(generate_repo_ref_event()); | ||
| 341 | |||
| 342 | r55.events.push(generate_repo_ref_event()); | ||
| 343 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 344 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 345 | |||
| 346 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 347 | cli_tester_create_prs()?; | ||
| 348 | |||
| 349 | let test_repo = GitTestRepo::default(); | ||
| 350 | test_repo.populate()?; | ||
| 351 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 352 | |||
| 353 | p.expect("finding PRs...\r\n")?; | ||
| 354 | let mut c = p.expect_choice( | ||
| 355 | "All PRs", | ||
| 356 | vec![ | ||
| 357 | format!("\"{PR_TITLE_1}\""), | ||
| 358 | format!("\"{PR_TITLE_2}\""), | ||
| 359 | format!("\"{PR_TITLE_3}\""), | ||
| 360 | ], | ||
| 361 | )?; | ||
| 362 | c.succeeds_with(2, true)?; | ||
| 363 | p.expect("finding commits...\r\n")?; | ||
| 364 | p.expect("checked out PR branch. pulled 2 new commits\r\n")?; | ||
| 365 | p.expect_end()?; | ||
| 366 | |||
| 367 | for p in [51, 52, 53, 55, 56] { | ||
| 368 | relay::shutdown_relay(8000 + p)?; | ||
| 369 | } | ||
| 370 | Ok(()) | ||
| 371 | }); | ||
| 372 | |||
| 373 | // launch relay | ||
| 374 | let _ = join!( | ||
| 375 | r51.listen_until_close(), | ||
| 376 | r52.listen_until_close(), | ||
| 377 | r53.listen_until_close(), | ||
| 378 | r55.listen_until_close(), | ||
| 379 | r56.listen_until_close(), | ||
| 380 | ); | ||
| 381 | cli_tester_handle.join().unwrap()?; | ||
| 382 | println!("{:?}", r55.events); | ||
| 383 | Ok(()) | ||
| 384 | } | ||
| 385 | |||
| 386 | #[test] | ||
| 387 | #[serial] | ||
| 388 | fn prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 389 | futures::executor::block_on(run_async_prompts_to_choose_from_pr_titles()) | ||
| 390 | } | ||
| 391 | } | ||
| 392 | |||
| 393 | #[test] | ||
| 394 | #[serial] | ||
| 395 | fn pr_branch_created_with_correct_name() -> Result<()> { | ||
| 396 | let (_, test_repo) = futures::executor::block_on(prep_and_run())?; | ||
| 397 | assert_eq!( | ||
| 398 | vec![FEATURE_BRANCH_NAME_3, "main"], | ||
| 399 | test_repo.get_local_branch_names()? | ||
| 400 | ); | ||
| 401 | Ok(()) | ||
| 402 | } | ||
| 403 | |||
| 404 | #[test] | ||
| 405 | #[serial] | ||
| 406 | fn pr_branch_checked_out() -> Result<()> { | ||
| 407 | let (_, test_repo) = futures::executor::block_on(prep_and_run())?; | ||
| 408 | assert_eq!( | ||
| 409 | FEATURE_BRANCH_NAME_3, | ||
| 410 | test_repo.get_checked_out_branch_name()?, | ||
| 411 | ); | ||
| 412 | Ok(()) | ||
| 413 | } | ||
| 414 | |||
| 415 | #[test] | ||
| 416 | #[serial] | ||
| 417 | fn pr_branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 418 | let (originating_repo, test_repo) = | ||
| 419 | futures::executor::block_on(prep_and_run())?; | ||
| 420 | assert_eq!( | ||
| 421 | originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_3)?, | ||
| 422 | test_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_3)?, | ||
| 423 | ); | ||
| 424 | Ok(()) | ||
| 425 | } | ||
| 426 | } | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | mod when_pr_branch_exists { | ||
| 431 | use super::*; | ||
| 432 | |||
| 433 | mod when_main_is_checked_out { | ||
| 434 | use super::*; | ||
| 435 | |||
| 436 | mod when_branch_is_up_to_date { | ||
| 437 | use super::*; | ||
| 438 | async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 439 | // fallback (51,52) user write (53, 55) repo (55, 56) | ||
| 440 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 441 | Relay::new(8051, None, None), | ||
| 442 | Relay::new(8052, None, None), | ||
| 443 | Relay::new(8053, None, None), | ||
| 444 | Relay::new(8055, None, None), | ||
| 445 | Relay::new(8056, None, None), | ||
| 446 | ); | ||
| 447 | |||
| 448 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 449 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 450 | r51.events.push(generate_repo_ref_event()); | ||
| 451 | |||
| 452 | r55.events.push(generate_repo_ref_event()); | ||
| 453 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 454 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 455 | |||
| 456 | let cli_tester_handle = | ||
| 457 | std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 458 | let originating_repo = cli_tester_create_prs()?; | ||
| 459 | |||
| 460 | let test_repo = GitTestRepo::default(); | ||
| 461 | test_repo.populate()?; | ||
| 462 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 463 | |||
| 464 | create_and_populate_branch( | ||
| 465 | &test_repo, | ||
| 466 | FEATURE_BRANCH_NAME_1, | ||
| 467 | "a", | ||
| 468 | false, | ||
| 469 | )?; | ||
| 470 | test_repo.checkout("main")?; | ||
| 471 | p.expect("finding PRs...\r\n")?; | ||
| 472 | let mut c = p.expect_choice( | ||
| 473 | "All PRs", | ||
| 474 | vec![ | ||
| 475 | format!("\"{PR_TITLE_1}\""), | ||
| 476 | format!("\"{PR_TITLE_2}\""), | ||
| 477 | format!("\"{PR_TITLE_3}\""), | ||
| 478 | ], | ||
| 479 | )?; | ||
| 480 | c.succeeds_with(0, true)?; | ||
| 481 | p.expect_end_eventually_and_print()?; | ||
| 482 | |||
| 483 | for p in [51, 52, 53, 55, 56] { | ||
| 484 | relay::shutdown_relay(8000 + p)?; | ||
| 485 | } | ||
| 486 | Ok((originating_repo, test_repo)) | ||
| 487 | }); | ||
| 488 | |||
| 489 | // launch relay | ||
| 490 | let _ = join!( | ||
| 491 | r51.listen_until_close(), | ||
| 492 | r52.listen_until_close(), | ||
| 493 | r53.listen_until_close(), | ||
| 494 | r55.listen_until_close(), | ||
| 495 | r56.listen_until_close(), | ||
| 496 | ); | ||
| 497 | let res = cli_tester_handle.join().unwrap()?; | ||
| 498 | |||
| 499 | Ok(res) | ||
| 500 | } | ||
| 501 | |||
| 502 | mod cli_prompts { | ||
| 503 | use super::*; | ||
| 504 | async fn run_async_prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 505 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 506 | Relay::new(8051, None, None), | ||
| 507 | Relay::new(8052, None, None), | ||
| 508 | Relay::new(8053, None, None), | ||
| 509 | Relay::new(8055, None, None), | ||
| 510 | Relay::new(8056, None, None), | ||
| 511 | ); | ||
| 512 | |||
| 513 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 514 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 515 | r51.events.push(generate_repo_ref_event()); | ||
| 516 | |||
| 517 | r55.events.push(generate_repo_ref_event()); | ||
| 518 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 519 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 520 | |||
| 521 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 522 | cli_tester_create_prs()?; | ||
| 523 | |||
| 524 | let test_repo = GitTestRepo::default(); | ||
| 525 | test_repo.populate()?; | ||
| 526 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 527 | |||
| 528 | create_and_populate_branch( | ||
| 529 | &test_repo, | ||
| 530 | FEATURE_BRANCH_NAME_1, | ||
| 531 | "a", | ||
| 532 | false, | ||
| 533 | )?; | ||
| 534 | test_repo.checkout("main")?; | ||
| 535 | |||
| 536 | p.expect("finding PRs...\r\n")?; | ||
| 537 | let mut c = p.expect_choice( | ||
| 538 | "All PRs", | ||
| 539 | vec![ | ||
| 540 | format!("\"{PR_TITLE_1}\""), | ||
| 541 | format!("\"{PR_TITLE_2}\""), | ||
| 542 | format!("\"{PR_TITLE_3}\""), | ||
| 543 | ], | ||
| 544 | )?; | ||
| 545 | c.succeeds_with(0, true)?; | ||
| 546 | p.expect("finding commits...\r\n")?; | ||
| 547 | p.expect("checked out PR branch. no new commits to pull\r\n")?; | ||
| 548 | p.expect_end()?; | ||
| 549 | |||
| 550 | for p in [51, 52, 53, 55, 56] { | ||
| 551 | relay::shutdown_relay(8000 + p)?; | ||
| 552 | } | ||
| 553 | Ok(()) | ||
| 554 | }); | ||
| 555 | |||
| 556 | // launch relay | ||
| 557 | let _ = join!( | ||
| 558 | r51.listen_until_close(), | ||
| 559 | r52.listen_until_close(), | ||
| 560 | r53.listen_until_close(), | ||
| 561 | r55.listen_until_close(), | ||
| 562 | r56.listen_until_close(), | ||
| 563 | ); | ||
| 564 | cli_tester_handle.join().unwrap()?; | ||
| 565 | println!("{:?}", r55.events); | ||
| 566 | Ok(()) | ||
| 567 | } | ||
| 568 | |||
| 569 | #[test] | ||
| 570 | #[serial] | ||
| 571 | fn prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 572 | futures::executor::block_on(run_async_prompts_to_choose_from_pr_titles()) | ||
| 573 | } | ||
| 574 | } | ||
| 575 | |||
| 576 | #[test] | ||
| 577 | #[serial] | ||
| 578 | fn pr_branch_checked_out() -> Result<()> { | ||
| 579 | let (_, test_repo) = futures::executor::block_on(prep_and_run())?; | ||
| 580 | assert_eq!( | ||
| 581 | FEATURE_BRANCH_NAME_1, | ||
| 582 | test_repo.get_checked_out_branch_name()?, | ||
| 583 | ); | ||
| 584 | Ok(()) | ||
| 585 | } | ||
| 586 | } | ||
| 587 | |||
| 588 | mod when_branch_is_behind { | ||
| 589 | use super::*; | ||
| 590 | |||
| 591 | async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 592 | // fallback (51,52) user write (53, 55) repo (55, 56) | ||
| 593 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 594 | Relay::new(8051, None, None), | ||
| 595 | Relay::new(8052, None, None), | ||
| 596 | Relay::new(8053, None, None), | ||
| 597 | Relay::new(8055, None, None), | ||
| 598 | Relay::new(8056, None, None), | ||
| 599 | ); | ||
| 600 | |||
| 601 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 602 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 603 | r51.events.push(generate_repo_ref_event()); | ||
| 604 | |||
| 605 | r55.events.push(generate_repo_ref_event()); | ||
| 606 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 607 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 608 | |||
| 609 | let cli_tester_handle = | ||
| 610 | std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { | ||
| 611 | let originating_repo = cli_tester_create_prs()?; | ||
| 612 | |||
| 613 | let test_repo = GitTestRepo::default(); | ||
| 614 | test_repo.populate()?; | ||
| 615 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 616 | |||
| 617 | create_and_populate_branch( | ||
| 618 | &test_repo, | ||
| 619 | FEATURE_BRANCH_NAME_1, | ||
| 620 | "a", | ||
| 621 | true, | ||
| 622 | )?; | ||
| 623 | test_repo.checkout("main")?; | ||
| 624 | |||
| 625 | p.expect("finding PRs...\r\n")?; | ||
| 626 | let mut c = p.expect_choice( | ||
| 627 | "All PRs", | ||
| 628 | vec![ | ||
| 629 | format!("\"{PR_TITLE_1}\""), | ||
| 630 | format!("\"{PR_TITLE_2}\""), | ||
| 631 | format!("\"{PR_TITLE_3}\""), | ||
| 632 | ], | ||
| 633 | )?; | ||
| 634 | c.succeeds_with(0, true)?; | ||
| 635 | |||
| 636 | p.expect_end_eventually_and_print()?; | ||
| 637 | |||
| 638 | for p in [51, 52, 53, 55, 56] { | ||
| 639 | relay::shutdown_relay(8000 + p)?; | ||
| 640 | } | ||
| 641 | Ok((originating_repo, test_repo)) | ||
| 642 | }); | ||
| 643 | |||
| 644 | // launch relay | ||
| 645 | let _ = join!( | ||
| 646 | r51.listen_until_close(), | ||
| 647 | r52.listen_until_close(), | ||
| 648 | r53.listen_until_close(), | ||
| 649 | r55.listen_until_close(), | ||
| 650 | r56.listen_until_close(), | ||
| 651 | ); | ||
| 652 | let res = cli_tester_handle.join().unwrap()?; | ||
| 653 | |||
| 654 | Ok(res) | ||
| 655 | } | ||
| 656 | |||
| 657 | mod cli_prompts { | ||
| 658 | use super::*; | ||
| 659 | async fn run_async_prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 660 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 661 | Relay::new(8051, None, None), | ||
| 662 | Relay::new(8052, None, None), | ||
| 663 | Relay::new(8053, None, None), | ||
| 664 | Relay::new(8055, None, None), | ||
| 665 | Relay::new(8056, None, None), | ||
| 666 | ); | ||
| 667 | |||
| 668 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 669 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 670 | r51.events.push(generate_repo_ref_event()); | ||
| 671 | |||
| 672 | r55.events.push(generate_repo_ref_event()); | ||
| 673 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 674 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 675 | |||
| 676 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 677 | cli_tester_create_prs()?; | ||
| 678 | |||
| 679 | let test_repo = GitTestRepo::default(); | ||
| 680 | test_repo.populate()?; | ||
| 681 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["prs", "list"]); | ||
| 682 | |||
| 683 | create_and_populate_branch( | ||
| 684 | &test_repo, | ||
| 685 | FEATURE_BRANCH_NAME_1, | ||
| 686 | "a", | ||
| 687 | true, | ||
| 688 | )?; | ||
| 689 | test_repo.checkout("main")?; | ||
| 690 | |||
| 691 | p.expect("finding PRs...\r\n")?; | ||
| 692 | let mut c = p.expect_choice( | ||
| 693 | "All PRs", | ||
| 694 | vec![ | ||
| 695 | format!("\"{PR_TITLE_1}\""), | ||
| 696 | format!("\"{PR_TITLE_2}\""), | ||
| 697 | format!("\"{PR_TITLE_3}\""), | ||
| 698 | ], | ||
| 699 | )?; | ||
| 700 | c.succeeds_with(0, true)?; | ||
| 701 | p.expect("finding commits...\r\n")?; | ||
| 702 | p.expect("checked out PR branch. pulled 1 new commits\r\n")?; | ||
| 703 | p.expect_end()?; | ||
| 704 | |||
| 705 | for p in [51, 52, 53, 55, 56] { | ||
| 706 | relay::shutdown_relay(8000 + p)?; | ||
| 707 | } | ||
| 708 | Ok(()) | ||
| 709 | }); | ||
| 710 | |||
| 711 | // launch relay | ||
| 712 | let _ = join!( | ||
| 713 | r51.listen_until_close(), | ||
| 714 | r52.listen_until_close(), | ||
| 715 | r53.listen_until_close(), | ||
| 716 | r55.listen_until_close(), | ||
| 717 | r56.listen_until_close(), | ||
| 718 | ); | ||
| 719 | cli_tester_handle.join().unwrap()?; | ||
| 720 | println!("{:?}", r55.events); | ||
| 721 | Ok(()) | ||
| 722 | } | ||
| 723 | |||
| 724 | #[test] | ||
| 725 | #[serial] | ||
| 726 | fn prompts_to_choose_from_pr_titles() -> Result<()> { | ||
| 727 | futures::executor::block_on(run_async_prompts_to_choose_from_pr_titles()) | ||
| 728 | } | ||
| 729 | } | ||
| 730 | |||
| 731 | #[test] | ||
| 732 | #[serial] | ||
| 733 | fn pr_branch_checked_out() -> Result<()> { | ||
| 734 | let (_, test_repo) = futures::executor::block_on(prep_and_run())?; | ||
| 735 | assert_eq!( | ||
| 736 | FEATURE_BRANCH_NAME_1, | ||
| 737 | test_repo.get_checked_out_branch_name()?, | ||
| 738 | ); | ||
| 739 | Ok(()) | ||
| 740 | } | ||
| 741 | |||
| 742 | #[test] | ||
| 743 | #[serial] | ||
| 744 | fn pr_branch_tip_is_most_recent_patch() -> Result<()> { | ||
| 745 | let (originating_repo, test_repo) = | ||
| 746 | futures::executor::block_on(prep_and_run())?; | ||
| 747 | assert_eq!( | ||
| 748 | originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, | ||
| 749 | test_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, | ||
| 750 | ); | ||
| 751 | Ok(()) | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | mod when_branch_is_ahead { | ||
| 756 | // use super::*; | ||
| 757 | // TODO latest commit in pr builds off an older commit in pr | ||
| 758 | // instead of previous. | ||
| 759 | // TODO current git user created commit on branch | ||
| 760 | } | ||
| 761 | |||
| 762 | mod when_latest_event_rebases_branch { | ||
| 763 | // use super::*; | ||
| 764 | // TODO | ||
| 765 | } | ||
| 766 | } | ||
| 767 | } | ||
| 768 | } | ||