upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cli_interactor.rs9
-rw-r--r--src/client.rs12
-rw-r--r--src/git.rs967
-rw-r--r--src/sub_commands/prs/create.rs90
-rw-r--r--src/sub_commands/prs/list.rs225
-rw-r--r--src/sub_commands/prs/mod.rs3
-rw-r--r--test_utils/src/git.rs60
-rw-r--r--test_utils/src/lib.rs167
-rw-r--r--test_utils/src/relay.rs33
-rw-r--r--tests/prs_create.rs163
-rw-r--r--tests/prs_list.rs768
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 {
96pub struct PromptChoiceParms { 98pub 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
101impl PromptChoiceParms { 104impl 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;
20pub struct Client { 20pub 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(
130pub struct Params { 141pub 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
135fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> { 147fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> {
diff --git a/src/git.rs b/src/git.rs
index 281f00c..f4f73a5 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -6,6 +6,8 @@ use anyhow::{bail, Context, Result};
6use git2::{Oid, Revwalk}; 6use git2::{Oid, Revwalk};
7use nostr::prelude::{sha1::Hash as Sha1Hash, Hash}; 7use nostr::prelude::{sha1::Hash as Sha1Hash, Hash};
8 8
9use crate::sub_commands::prs::list::tag_value;
10
9pub struct Repo { 11pub 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
49impl RepoActions for Repo { 63impl 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
254fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { 353fn 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
387fn 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
395fn 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
488fn 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)]
289mod tests { 520mod 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 {
301pub static PR_KIND: u64 = 318; 301pub static PR_KIND: u64 = 318;
302pub static PATCH_KIND: u64 = 317; 302pub static PATCH_KIND: u64 = 317;
303 303
304fn generate_pr_and_patch_events( 304pub 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
353pub 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 @@
1use anyhow::{Context, Result};
2
3#[cfg(not(test))]
4use crate::client::Client;
5#[cfg(test)]
6use crate::client::MockConnect;
7use 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)]
17pub struct SubCommandArgs {
18 /// TODO ignore merged, and closed
19 #[arg(long, action)]
20 open_only: bool,
21}
22
23pub 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
194pub 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
204pub 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
4use crate::Cli; 4use crate::Cli;
5pub mod create; 5pub mod create;
6pub mod list;
6 7
7#[derive(clap::Parser)] 8#[derive(clap::Parser)]
8pub struct SubCommandArgs { 9pub struct SubCommandArgs {
@@ -13,10 +14,12 @@ pub struct SubCommandArgs {
13#[derive(Debug, Subcommand)] 14#[derive(Debug, Subcommand)]
14pub enum Commands { 15pub enum Commands {
15 Create(create::SubCommandArgs), 16 Create(create::SubCommandArgs),
17 List(list::SubCommandArgs),
16} 18}
17 19
18pub async fn launch(cli_args: &Cli, pr_args: &SubCommandArgs) -> Result<()> { 20pub 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?
4use std::{env::current_dir, fs, path::PathBuf}; 4use std::{env::current_dir, fs, path::PathBuf};
5 5
6use anyhow::Result; 6use anyhow::{Context, Result};
7use git2::{Oid, RepositoryInitOptions, Signature, Time}; 7use git2::{Oid, RepositoryInitOptions, Signature, Time};
8 8
9pub struct GitTestRepo { 9pub 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
97impl Drop for GitTestRepo { 151impl 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;
11pub mod git; 11pub mod git;
12pub mod relay; 12pub mod relay;
13 13
14pub static PR_KIND: u64 = 318;
15pub static PATCH_KIND: u64 = 317;
16pub static REPOSITORY_KIND: u64 = 300317;
17
14pub static TEST_KEY_1_NSEC: &str = 18pub static TEST_KEY_1_NSEC: &str =
15 "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq"; 19 "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq";
16pub static TEST_KEY_1_SK_HEX: &str = 20pub 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
123pub static REPOSITORY_KIND: u64 = 300317;
124
125pub fn generate_repo_ref_event() -> nostr::Event { 127pub 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
213pub struct CliTesterInputPrompt<'a> { 229pub struct CliTesterInputPrompt<'a> {
@@ -397,6 +413,137 @@ impl CliTesterConfirmPrompt<'_> {
397 } 413 }
398} 414}
399 415
416pub struct CliTesterChoicePrompt<'a> {
417 tester: &'a mut CliTester,
418 prompt: String,
419 choices: Vec<String>,
420}
421
422impl 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
400impl CliTester { 547impl 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 @@
1use anyhow::Result;
2use futures::join;
3use serial_test::serial;
4use test_utils::{git::GitTestRepo, relay::Relay, *};
5
6static FEATURE_BRANCH_NAME_1: &str = "feature-example-t";
7static FEATURE_BRANCH_NAME_2: &str = "feature-example-f";
8static FEATURE_BRANCH_NAME_3: &str = "feature-example-c";
9
10static PR_TITLE_1: &str = "pr a";
11static PR_TITLE_2: &str = "pr b";
12static PR_TITLE_3: &str = "pr c";
13
14fn 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
41fn 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
65fn 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
94mod 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}