upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-08-05 14:15:29 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-08-05 14:15:29 +0100
commitf238fc8c0a122487f4fb71bb78a2e365e147d747 (patch)
tree5e8760c192f62fef67189bc82dcae4eef3ce883a
parente5750b5b3dfe2c0072902c2523fdf32986aa74b8 (diff)
feat(remote): `push` handle out-of-sync servers
1. don't attempt to push to a remote which is already up-to-date 2. don't attempt to delete branch on remote if it is already deleted 3. only push when out of sync if remote tip is ancestor of pushed commit 4. force push to remote if user force pushed and remote is in sync with nostr
-rw-r--r--src/git.rs16
-rw-r--r--src/git_remote_helper.rs339
-rw-r--r--tests/git_remote_helper.rs265
3 files changed, 447 insertions, 173 deletions
diff --git a/src/git.rs b/src/git.rs
index eaea512..0794788 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -39,6 +39,7 @@ pub trait RepoActions {
39 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>; 39 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>;
40 fn get_checked_out_branch_name(&self) -> Result<String>; 40 fn get_checked_out_branch_name(&self) -> Result<String>;
41 fn get_tip_of_branch(&self, branch_name: &str) -> Result<Sha1Hash>; 41 fn get_tip_of_branch(&self, branch_name: &str) -> Result<Sha1Hash>;
42 fn get_commit_or_tip_of_reference(&self, reference: &str) -> Result<Sha1Hash>;
42 fn get_root_commit(&self) -> Result<Sha1Hash>; 43 fn get_root_commit(&self) -> Result<Sha1Hash>;
43 fn does_commit_exist(&self, commit: &str) -> Result<bool>; 44 fn does_commit_exist(&self, commit: &str) -> Result<bool>;
44 fn get_head_commit(&self) -> Result<Sha1Hash>; 45 fn get_head_commit(&self) -> Result<Sha1Hash>;
@@ -215,6 +216,21 @@ impl RepoActions for Repo {
215 Ok(oid_to_sha1(&branch.into_reference().peel_to_commit()?.id())) 216 Ok(oid_to_sha1(&branch.into_reference().peel_to_commit()?.id()))
216 } 217 }
217 218
219 fn get_commit_or_tip_of_reference(&self, sha1_or_reference: &str) -> Result<Sha1Hash> {
220 let oid = {
221 if let Ok(oid) = Oid::from_str(sha1_or_reference) {
222 self.git_repo.find_commit(oid)?;
223 oid
224 } else {
225 self.git_repo
226 .find_reference(sha1_or_reference)?
227 .peel_to_commit()?
228 .id()
229 }
230 };
231 Ok(oid_to_sha1(&oid))
232 }
233
218 fn get_root_commit(&self) -> Result<Sha1Hash> { 234 fn get_root_commit(&self) -> Result<Sha1Hash> {
219 let mut revwalk = self 235 let mut revwalk = self
220 .git_repo 236 .git_repo
diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs
index 13d6c03..6f645da 100644
--- a/src/git_remote_helper.rs
+++ b/src/git_remote_helper.rs
@@ -78,6 +78,7 @@ async fn main() -> Result<()> {
78 let stdin = io::stdin(); 78 let stdin = io::stdin();
79 let mut line = String::new(); 79 let mut line = String::new();
80 80
81 let mut list_outputs = None;
81 loop { 82 loop {
82 let tokens = read_line(&stdin, &mut line)?; 83 let tokens = read_line(&stdin, &mut line)?;
83 84
@@ -105,14 +106,15 @@ async fn main() -> Result<()> {
105 &stdin, 106 &stdin,
106 refspec, 107 refspec,
107 &client, 108 &client,
109 list_outputs.clone(),
108 ) 110 )
109 .await?; 111 .await?;
110 } 112 }
111 ["list"] => { 113 ["list"] => {
112 list(&git_repo, &repo_ref, false).await?; 114 list_outputs = Some(list(&git_repo, &repo_ref, false).await?);
113 } 115 }
114 ["list", "for-push"] => { 116 ["list", "for-push"] => {
115 list(&git_repo, &repo_ref, true).await?; 117 list_outputs = Some(list(&git_repo, &repo_ref, true).await?);
116 } 118 }
117 [] => { 119 [] => {
118 return Ok(()); 120 return Ok(());
@@ -351,6 +353,7 @@ fn fetch_from_git_server(
351 Ok(()) 353 Ok(())
352} 354}
353 355
356#[allow(clippy::too_many_lines)]
354async fn push( 357async fn push(
355 git_repo: &Repo, 358 git_repo: &Repo,
356 repo_ref: &RepoRef, 359 repo_ref: &RepoRef,
@@ -359,56 +362,63 @@ async fn push(
359 initial_refspec: &str, 362 initial_refspec: &str,
360 #[cfg(test)] client: &crate::client::MockConnect, 363 #[cfg(test)] client: &crate::client::MockConnect,
361 #[cfg(not(test))] client: &Client, 364 #[cfg(not(test))] client: &Client,
365 list_outputs: Option<HashMap<String, HashMap<String, String>>>,
362) -> Result<()> { 366) -> Result<()> {
363 // TODO check 367 let mut refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?;
364 // bail!( 368
365 // "git server {} tip for branch {} conflicts with nostr and local branch. 369 let term = console::Term::stderr();
366 // to resolve either:\r\n 1. pull from that git server and resolve\r\n 2. 370
367 // force push your branch to the git server before pushing to nostr remote" 371 let list_outputs = match list_outputs {
368 // )?; 372 Some(outputs) => outputs,
369 373 _ => list_from_remotes(&term, git_repo, &repo_ref.git_server)?,
370 // if no state events - create from first git server listed 374 };
371 let refspecs = get_refspecs_from_push_batch(stdin, initial_refspec)?; 375
372 let git_server_url = repo_ref 376 let nostr_state = get_state_from_cache(git_repo.get_path()?, repo_ref).await;
373 .git_server 377
374 .first() 378 let existing_state = {
375 .context("no git server listed in nostr repository announcement")?; 379 // if no state events - create from first git server listed
376 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; 380 if let Ok(nostr_state) = &nostr_state {
377 381 nostr_state.state.clone()
378 let auth = GitAuthenticator::default(); 382 } else if let Some(url) = repo_ref
379 let git_config = git_repo.git_repo.config()?; 383 .git_server
380 let mut push_options = git2::PushOptions::new(); 384 .iter()
381 let mut remote_callbacks = git2::RemoteCallbacks::new(); 385 .find(|&url| list_outputs.contains_key(url))
382 remote_callbacks.credentials(auth.credentials(&git_config)); 386 {
383 remote_callbacks.push_update_reference(|name, error| { 387 list_outputs.get(url).unwrap().to_owned()
384 if let Some(error) = error {
385 println!("error {name} {error}");
386 } else { 388 } else {
387 if let Some(refspec) = refspecs 389 bail!(
388 .iter() 390 "cannot connect to git servers: {}",
389 .find(|r| r.contains(format!(":{name}").as_str())) 391 repo_ref.git_server.join(" ")
390 { 392 );
391 if let Err(e) = 393 }
392 update_remote_refs_pushed(&git_repo.git_repo, refspec, nostr_remote_url) 394 };
393 .context("could not update remote_ref locally") 395
394 { 396 let (rejected_refspecs, remote_refspecs) = create_rejected_refspecs_and_remotes_refspecs(
395 return Err(git2::Error::from_str(e.to_string().as_str())); 397 &term,
396 } 398 git_repo,
397 } 399 &refspecs,
398 println!("ok {name}",); 400 &existing_state,
401 &list_outputs,
402 )?;
403
404 refspecs.retain(|refspec| {
405 if let Some(rejected) = rejected_refspecs.get(&refspec.to_string()) {
406 let (_, to) = refspec_to_from_to(refspec).unwrap();
407 println!("error {to} {} out of sync with nostr", rejected.join(" "));
408 false
409 } else {
410 true
399 } 411 }
400 Ok(())
401 }); 412 });
402 push_options.remote_callbacks(remote_callbacks);
403 git_server_remote.push(&refspecs, Some(&mut push_options))?;
404 git_server_remote.disconnect()?;
405 413
406 // TODO check whether push was succesful before proceeding - geting outcome from 414 if refspecs.is_empty() {
407 // callback isn't straightforward 415 // all refspecs rejected
416 println!();
417 return Ok(());
418 }
408 419
409 let new_state = generate_updated_state(git_repo, repo_ref, &refspecs).await?; 420 let new_state = generate_updated_state(git_repo, &existing_state, &refspecs)?;
410 421
411 // TODO enable interactive login
412 let (signer, user_ref) = login::launch( 422 let (signer, user_ref) = login::launch(
413 git_repo, 423 git_repo,
414 &None, 424 &None,
@@ -420,8 +430,12 @@ async fn push(
420 true, 430 true,
421 ) 431 )
422 .await?; 432 .await?;
433
423 let new_repo_state = RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?; 434 let new_repo_state = RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?;
424 435
436 // TODO check whether tip of each branch pushed is on at least one git server
437 // before broadcasting the nostr state
438
425 send_events( 439 send_events(
426 client, 440 client,
427 git_repo.get_path()?, 441 git_repo.get_path()?,
@@ -433,71 +447,218 @@ async fn push(
433 ) 447 )
434 .await?; 448 .await?;
435 449
436 // silently push to any other git servers 450 for refspec in &refspecs {
437 for (i, git_server_url) in repo_ref.git_server.iter().enumerate() { 451 let (_, to) = refspec_to_from_to(refspec)?;
438 // we have already pushed to the first one 452 println!("ok {to}");
439 if i.gt(&0) { 453 update_remote_refs_pushed(&git_repo.git_repo, refspec, nostr_remote_url)
440 if let Ok(mut git_server_remote) = git_repo.git_repo.remote_anonymous(git_server_url) { 454 .context("could not update remote_ref locally")?;
455 }
456
457 // TODO make async - check gitlib2 callbacks work async
458 let git_config = git_repo.git_repo.config()?;
459 for (git_server_url, refspecs) in remote_refspecs {
460 if !refspecs.is_empty() {
461 if let Ok(mut git_server_remote) = git_repo.git_repo.remote_anonymous(&git_server_url) {
441 let auth = GitAuthenticator::default(); 462 let auth = GitAuthenticator::default();
442 let git_config = git_repo.git_repo.config()?;
443 let mut push_options = git2::PushOptions::new(); 463 let mut push_options = git2::PushOptions::new();
444 let mut remote_callbacks = git2::RemoteCallbacks::new(); 464 let mut remote_callbacks = git2::RemoteCallbacks::new();
445 remote_callbacks.credentials(auth.credentials(&git_config)); 465 remote_callbacks.credentials(auth.credentials(&git_config));
466 remote_callbacks.push_update_reference(|name, error| {
467 if let Some(error) = error {
468 term.write_line(
469 format!("WARNING: error pushing {name} to {git_server_url} {error}")
470 .as_str(),
471 )
472 .unwrap();
473 }
474 Ok(())
475 });
446 push_options.remote_callbacks(remote_callbacks); 476 push_options.remote_callbacks(remote_callbacks);
447 let _ = git_server_remote.push(&refspecs, Some(&mut push_options)); 477 let _ = git_server_remote.push(&refspecs, Some(&mut push_options));
448 let _ = git_server_remote.disconnect(); 478 let _ = git_server_remote.disconnect();
449 } 479 }
450 } 480 }
451 } 481 }
452 // todo report on errors
453
454 println!(); 482 println!();
455 Ok(()) 483 Ok(())
456} 484}
457 485
458async fn generate_updated_state( 486type HashMapUrlRefspecs = HashMap<String, Vec<String>>;
487
488#[allow(clippy::too_many_lines)]
489fn create_rejected_refspecs_and_remotes_refspecs(
490 term: &console::Term,
459 git_repo: &Repo, 491 git_repo: &Repo,
460 repo_ref: &RepoRef,
461 refspecs: &Vec<String>, 492 refspecs: &Vec<String>,
462) -> Result<HashMap<String, String>> { 493 nostr_state: &HashMap<String, String>,
463 let new_state = { 494 list_outputs: &HashMap<String, HashMap<String, String>>,
464 if let Ok(mut repo_state) = get_state_from_cache(git_repo.get_path()?, repo_ref).await { 495) -> Result<(HashMapUrlRefspecs, HashMapUrlRefspecs)> {
465 for refspec in refspecs { 496 let mut refspecs_for_remotes = HashMap::new();
466 let (from, to) = refspec_to_from_to(refspec)?; 497
467 if from.is_empty() { 498 let mut rejected_refspecs: HashMapUrlRefspecs = HashMap::new();
468 // delete 499
469 repo_state.state.remove(to); 500 for (url, remote_state) in list_outputs {
470 } else { 501 let mut refspecs_for_remote = vec![];
471 // add or update 502 for refspec in refspecs {
472 repo_state.state.insert( 503 let (from, to) = refspec_to_from_to(refspec)?;
473 to.to_string(), 504 let nostr_value = nostr_state.get(to);
474 reference_to_ref_value(&git_repo.git_repo, to).unwrap(), 505 let remote_value = remote_state.get(to);
475 ); 506 if from.is_empty() {
507 if remote_value.is_some() {
508 // delete remote branch
509 refspecs_for_remote.push(refspec.clone());
476 } 510 }
511 continue;
477 } 512 }
478 repo_state.state 513 let from_tip = git_repo.get_commit_or_tip_of_reference(from)?;
479 } else { 514 if let Some(nostr_value) = nostr_value {
480 let mut state = HashMap::new(); 515 if let Some(remote_value) = remote_value {
481 let git_server_url = repo_ref 516 if nostr_value.eq(remote_value) {
482 .git_server 517 // in sync - existing branch at same state
483 .first() 518 let is_remote_tip_ancestor_of_commit = if let Ok(remote_value_tip) =
484 .context("no git server listed in nostr repository announcement")?; 519 git_repo.get_commit_or_tip_of_reference(remote_value)
485 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; 520 {
486 git_server_remote.connect(git2::Direction::Fetch)?; 521 if let Ok((_, behind)) =
487 for head in git_server_remote.list()? { 522 git_repo.get_commits_ahead_behind(&remote_value_tip, &from_tip)
488 state.insert( 523 {
489 head.name().to_string(), 524 behind.is_empty()
490 if let Some(symbolic_ref) = head.symref_target() { 525 } else {
491 format!("ref: {symbolic_ref}") 526 false
527 }
528 } else {
529 false
530 };
531 if is_remote_tip_ancestor_of_commit {
532 refspecs_for_remote.push(refspec.clone());
533 } else {
534 // this is a force push so we need to force push to git server too
535 if refspec.starts_with('+') {
536 refspecs_for_remote.push(refspec.clone());
537 } else {
538 refspecs_for_remote.push(format!("+{refspec}"));
539 }
540 }
541 // TODO do we need to force push to this remote?
542 } else if let Ok(remote_value_tip) =
543 git_repo.get_commit_or_tip_of_reference(remote_value)
544 {
545 if from_tip.eq(&remote_value_tip) {
546 // remote already at correct state
547 term.write_line(
548 format!("{to} already at pushed commit state on {url}").as_str(),
549 )?;
550 }
551 let (_, behind) =
552 git_repo.get_commits_ahead_behind(&remote_value_tip, &from_tip)?;
553 if behind.is_empty() {
554 // can soft push
555 refspecs_for_remote.push(refspec.clone());
556 } else {
557 // cant soft push
558 rejected_refspecs
559 .entry(refspec.to_string())
560 .and_modify(|a| a.push(url.to_string()))
561 .or_insert(vec![url.to_string()]);
562 term.write_line(
563 format!("ERROR: {to} on {url} conflicts with nostr and is {} behind local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote", behind.len()).as_str(),
564 )?;
565 };
492 } else { 566 } else {
493 head.oid().to_string() 567 // remote_value oid is not present locally
494 }, 568 // TODO can we download the remote reference?
495 ); 569
570 // cant soft push
571 rejected_refspecs
572 .entry(refspec.to_string())
573 .and_modify(|a| a.push(url.to_string()))
574 .or_insert(vec![url.to_string()]);
575 term.write_line(
576 format!("ERROR: {to} on {url} conflicts with nostr and is not an ancestor of local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote").as_str(),
577 )?;
578 }
579 } else {
580 // existing nostr branch not on remote
581 // report - creating new branch
582 term.write_line(format!("pushing {to} as new branch on {url}").as_str())?;
583 refspecs_for_remote.push(refspec.clone());
584 }
585 } else if let Some(remote_value) = remote_value {
586 // new to nostr but on remote
587 if let Ok(remote_value_tip) = git_repo.get_commit_or_tip_of_reference(remote_value)
588 {
589 let (_, behind) =
590 git_repo.get_commits_ahead_behind(&remote_value_tip, &from_tip)?;
591 if behind.is_empty() {
592 // can soft push
593 refspecs_for_remote.push(refspec.clone());
594 } else {
595 // cant soft push
596 rejected_refspecs
597 .entry(refspec.to_string())
598 .and_modify(|a| a.push(url.to_string()))
599 .or_insert(vec![url.to_string()]);
600 term.write_line(
601 format!("ERROR: {to} not on nostr but on {url} is {} behind local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote", behind.len()).as_str(),
602 )?;
603 }
604 } else {
605 // havn't fetched oid from remote
606 // TODO fetch oid from remote
607 // cant soft push
608 rejected_refspecs
609 .entry(refspec.to_string())
610 .and_modify(|a| a.push(url.to_string()))
611 .or_insert(vec![url.to_string()]);
612 term.write_line(
613 format!("ERROR: {to} not on nostr but on {url} is not an ancestor of local branch. either:\r\n 1. pull from that git server and resolve\r\n 2. force push your branch to the git server before pushing to nostr remote").as_str(),
614 )?;
615 }
616 } else {
617 // in sync - new branch
618 refspecs_for_remote.push(refspec.clone());
496 } 619 }
497 git_server_remote.disconnect()?;
498 state
499 } 620 }
500 }; 621 refspecs_for_remotes.insert(url.to_string(), refspecs_for_remote);
622 }
623
624 // remove rejected refspecs so they dont get pushed to some remotes
625 let mut remotes_refspecs_without_rejected = HashMap::new();
626 for (url, value) in &refspecs_for_remotes {
627 remotes_refspecs_without_rejected.insert(
628 url.to_string(),
629 value
630 .iter()
631 .filter(|refspec| !rejected_refspecs.contains_key(*refspec))
632 .cloned()
633 .collect(),
634 );
635 }
636 Ok((rejected_refspecs, remotes_refspecs_without_rejected))
637}
638
639fn generate_updated_state(
640 git_repo: &Repo,
641 existing_state: &HashMap<String, String>,
642 refspecs: &Vec<String>,
643) -> Result<HashMap<String, String>> {
644 let mut new_state = existing_state.clone();
645
646 for refspec in refspecs {
647 let (from, to) = refspec_to_from_to(refspec)?;
648 if from.is_empty() {
649 // delete
650 new_state.remove(to);
651 } else {
652 // add or update
653 new_state.insert(
654 to.to_string(),
655 git_repo
656 .get_commit_or_tip_of_reference(from)
657 .unwrap()
658 .to_string(),
659 );
660 }
661 }
501 Ok(new_state) 662 Ok(new_state)
502} 663}
503 664
diff --git a/tests/git_remote_helper.rs b/tests/git_remote_helper.rs
index 5a6c5f5..f2fa95f 100644
--- a/tests/git_remote_helper.rs
+++ b/tests/git_remote_helper.rs
@@ -64,6 +64,19 @@ fn cli_tester_after_fetch(git_repo: &GitTestRepo) -> Result<CliTester> {
64 Ok(p) 64 Ok(p)
65} 65}
66 66
67/// git runs `list for-push` before `push`. in `push` we use the git server
68/// remote refs downloaded by `list` to assess how to push to git servers.
69/// we are therefore running it this way in our tests
70fn cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(
71 git_repo: &GitTestRepo,
72) -> Result<CliTester> {
73 let mut p = cli_tester_after_fetch(git_repo)?;
74
75 p.send_line("list for-push")?;
76 p.expect_eventually_and_print("\r\n\r\n")?;
77 Ok(p)
78}
79
67async fn generate_repo_with_state_event() -> Result<(nostr::Event, GitTestRepo)> { 80async fn generate_repo_with_state_event() -> Result<(nostr::Event, GitTestRepo)> {
68 let git_repo = prep_git_repo()?; 81 let git_repo = prep_git_repo()?;
69 git_repo.create_branch("example-branch")?; 82 git_repo.create_branch("example-branch")?;
@@ -93,12 +106,11 @@ async fn generate_repo_with_state_event() -> Result<(nostr::Event, GitTestRepo)>
93 r55.events = events; 106 r55.events = events;
94 107
95 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 108 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
96 let mut p = cli_tester_after_fetch(&git_repo)?; 109 let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
97 p.send_line("push refs/heads/main:refs/heads/main")?; 110 p.send_line("push refs/heads/main:refs/heads/main")?;
98 p.send_line("")?; 111 p.send_line("")?;
99 p.expect("ok refs/heads/main\r\n")?; 112 // p.expect("ok refs/heads/main\r\n")?;
100 p.expect("\r\n")?; 113 p.expect_eventually_and_print("\r\n\r\n")?;
101
102 p.exit()?; 114 p.exit()?;
103 for p in [51, 52, 53, 55, 56, 57] { 115 for p in [51, 52, 53, 55, 56, 57] {
104 relay::shutdown_relay(8000 + p)?; 116 relay::shutdown_relay(8000 + p)?;
@@ -574,19 +586,12 @@ mod push {
574 586
575 use super::*; 587 use super::*;
576 588
577 /// git runs `list for-push` before `push`. in `push` we use the git server 589 #[tokio::test]
578 /// remote refs downloaded by `list` to assess how to push to git servers. 590 #[serial]
579 /// we are therefore running it this way in our tests 591 async fn new_branch_when_no_state_event_exists() -> Result<()> {
580 fn cli_tester_after_nostr_fetch_and_sent_list_for_push_responds( 592 generate_repo_with_state_event().await?;
581 git_repo: &GitTestRepo, 593 Ok(())
582 ) -> Result<CliTester> {
583 let mut p = cli_tester_after_fetch(git_repo)?;
584
585 p.send_line("list for-push")?;
586 p.expect_eventually_and_print("\r\n\r\n")?;
587 Ok(p)
588 } 594 }
589
590 mod two_branches_in_batch_one_added_one_updated { 595 mod two_branches_in_batch_one_added_one_updated {
591 596
592 use super::*; 597 use super::*;
@@ -861,7 +866,7 @@ mod push {
861 p.send_line("push refs/heads/main:refs/heads/main")?; 866 p.send_line("push refs/heads/main:refs/heads/main")?;
862 p.send_line("push refs/heads/vnext:refs/heads/vnext")?; 867 p.send_line("push refs/heads/vnext:refs/heads/vnext")?;
863 p.send_line("")?; 868 p.send_line("")?;
864 p.expect_eventually("\r\n\r\n")?; 869 p.expect_eventually_and_print("\r\n\r\n")?;
865 p.exit()?; 870 p.exit()?;
866 for p in [51, 52, 53, 55, 56, 57] { 871 for p in [51, 52, 53, 55, 56, 57] {
867 relay::shutdown_relay(8000 + p)?; 872 relay::shutdown_relay(8000 + p)?;
@@ -1073,7 +1078,7 @@ mod push {
1073 cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?; 1078 cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
1074 p.send_line("push :refs/heads/vnext")?; 1079 p.send_line("push :refs/heads/vnext")?;
1075 p.send_line("")?; 1080 p.send_line("")?;
1076 p.expect_eventually("\r\n\r\n")?; 1081 p.expect_eventually_and_print("\r\n\r\n")?;
1077 p.exit()?; 1082 p.exit()?;
1078 for p in [51, 52, 53, 55, 56, 57] { 1083 for p in [51, 52, 53, 55, 56, 57] {
1079 relay::shutdown_relay(8000 + p)?; 1084 relay::shutdown_relay(8000 + p)?;
@@ -1238,80 +1243,172 @@ mod push {
1238 Ok(()) 1243 Ok(())
1239 } 1244 }
1240 1245
1241 #[tokio::test] 1246 mod when_existing_state_event {
1242 #[serial] 1247 use super::*;
1243 async fn existing_state_event_updated_with_branch_deleted_and_ok_printed() -> Result<()> {
1244 let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
1245 1248
1246 let git_repo = prep_git_repo()?; 1249 #[tokio::test]
1247 let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); // same as example 1250 #[serial]
1251 async fn state_event_updated_and_branch_deleted_and_ok_printed() -> Result<()> {
1252 let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
1248 1253
1249 let events = vec![ 1254 let git_repo = prep_git_repo()?;
1250 generate_test_key_1_metadata_event("fred"), 1255 let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); // same as example
1251 generate_test_key_1_relay_list_event(),
1252 generate_repo_ref_event_with_git_server(vec![
1253 source_git_repo.dir.to_str().unwrap().to_string(),
1254 ]),
1255 state_event.clone(),
1256 ];
1257 1256
1258 // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) 1257 let events = vec![
1259 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( 1258 generate_test_key_1_metadata_event("fred"),
1260 Relay::new(8051, None, None), 1259 generate_test_key_1_relay_list_event(),
1261 Relay::new(8052, None, None), 1260 generate_repo_ref_event_with_git_server(vec![
1262 Relay::new(8053, None, None), 1261 source_git_repo.dir.to_str().unwrap().to_string(),
1263 Relay::new(8055, None, None), 1262 ]),
1264 Relay::new(8056, None, None), 1263 state_event.clone(),
1265 Relay::new(8057, None, None), 1264 ];
1266 );
1267 r51.events = events.clone();
1268 r55.events = events;
1269 1265
1270 let cli_tester_handle = std::thread::spawn(move || -> Result<()> { 1266 // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57)
1271 let mut p = 1267 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
1272 cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?; 1268 Relay::new(8051, None, None),
1273 p.send_line("push :refs/heads/example-branch")?; 1269 Relay::new(8052, None, None),
1274 p.send_line("")?; 1270 Relay::new(8053, None, None),
1275 p.expect("ok refs/heads/example-branch\r\n")?; 1271 Relay::new(8055, None, None),
1276 p.expect("\r\n")?; 1272 Relay::new(8056, None, None),
1277 p.exit()?; 1273 Relay::new(8057, None, None),
1278 for p in [51, 52, 53, 55, 56, 57] { 1274 );
1279 relay::shutdown_relay(8000 + p)?; 1275 r51.events = events.clone();
1280 } 1276 r55.events = events;
1281 Ok(())
1282 });
1283 // launch relays
1284 let _ = join!(
1285 r51.listen_until_close(),
1286 r52.listen_until_close(),
1287 r53.listen_until_close(),
1288 r55.listen_until_close(),
1289 r56.listen_until_close(),
1290 r57.listen_until_close(),
1291 );
1292 1277
1293 cli_tester_handle.join().unwrap()?; 1278 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1279 let mut p =
1280 cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(&git_repo)?;
1281 p.send_line("push :refs/heads/example-branch")?;
1282 p.send_line("")?;
1283 p.expect("ok refs/heads/example-branch\r\n")?;
1284 p.expect("\r\n")?;
1285 p.exit()?;
1286 for p in [51, 52, 53, 55, 56, 57] {
1287 relay::shutdown_relay(8000 + p)?;
1288 }
1289 Ok(())
1290 });
1291 // launch relays
1292 let _ = join!(
1293 r51.listen_until_close(),
1294 r52.listen_until_close(),
1295 r53.listen_until_close(),
1296 r55.listen_until_close(),
1297 r56.listen_until_close(),
1298 r57.listen_until_close(),
1299 );
1294 1300
1295 let state_event = r56 1301 cli_tester_handle.join().unwrap()?;
1296 .events
1297 .iter()
1298 .find(|e| e.kind().eq(&STATE_KIND))
1299 .context("state event not created")?;
1300 1302
1301 // println!("{:#?}", state_event); 1303 let state_event = r56
1302 assert_eq!( 1304 .events
1303 state_event
1304 .tags
1305 .iter() 1305 .iter()
1306 .filter(|t| t.kind().to_string().as_str().ne("d")) 1306 .find(|e| e.kind().eq(&STATE_KIND))
1307 .map(|t| t.as_vec().to_vec()) 1307 .context("state event not created")?;
1308 .collect::<HashSet<Vec<String>>>(), 1308
1309 HashSet::from([ 1309 // println!("{:#?}", state_event);
1310 vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()], 1310 assert_eq!(
1311 vec!["refs/heads/main".to_string(), main_commit_id.to_string()], 1311 state_event
1312 ]), 1312 .tags
1313 ); 1313 .iter()
1314 Ok(()) 1314 .filter(|t| t.kind().to_string().as_str().ne("d"))
1315 .map(|t| t.as_vec().to_vec())
1316 .collect::<HashSet<Vec<String>>>(),
1317 HashSet::from([
1318 vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
1319 vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
1320 ]),
1321 );
1322 Ok(())
1323 }
1324
1325 mod already_deleted_on_git_server {
1326 use super::*;
1327
1328 #[tokio::test]
1329 #[serial]
1330 async fn existing_state_event_updated_and_ok_printed() -> Result<()> {
1331 let (state_event, source_git_repo) = generate_repo_with_state_event().await?;
1332
1333 {
1334 // delete branch on git server
1335 let tmp_repo = GitTestRepo::clone_repo(&source_git_repo)?;
1336 let mut remote = tmp_repo.git_repo.find_remote("origin")?;
1337 remote.push(&[":refs/heads/example-branch"], None)?;
1338 }
1339
1340 let git_repo = prep_git_repo()?;
1341 let main_commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); // same as example
1342
1343 let events = vec![
1344 generate_test_key_1_metadata_event("fred"),
1345 generate_test_key_1_relay_list_event(),
1346 generate_repo_ref_event_with_git_server(vec![
1347 source_git_repo.dir.to_str().unwrap().to_string(),
1348 ]),
1349 state_event.clone(),
1350 ];
1351
1352 // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57)
1353 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
1354 Relay::new(8051, None, None),
1355 Relay::new(8052, None, None),
1356 Relay::new(8053, None, None),
1357 Relay::new(8055, None, None),
1358 Relay::new(8056, None, None),
1359 Relay::new(8057, None, None),
1360 );
1361 r51.events = events.clone();
1362 r55.events = events;
1363
1364 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1365 let mut p = cli_tester_after_nostr_fetch_and_sent_list_for_push_responds(
1366 &git_repo,
1367 )?;
1368 p.send_line("push :refs/heads/example-branch")?;
1369 p.send_line("")?;
1370 p.expect("ok refs/heads/example-branch\r\n")?;
1371 p.expect("\r\n")?;
1372 p.exit()?;
1373 for p in [51, 52, 53, 55, 56, 57] {
1374 relay::shutdown_relay(8000 + p)?;
1375 }
1376 Ok(())
1377 });
1378 // launch relays
1379 let _ = join!(
1380 r51.listen_until_close(),
1381 r52.listen_until_close(),
1382 r53.listen_until_close(),
1383 r55.listen_until_close(),
1384 r56.listen_until_close(),
1385 r57.listen_until_close(),
1386 );
1387
1388 cli_tester_handle.join().unwrap()?;
1389
1390 let state_event = r56
1391 .events
1392 .iter()
1393 .find(|e| e.kind().eq(&STATE_KIND))
1394 .context("state event not created")?;
1395
1396 // println!("{:#?}", state_event);
1397 assert_eq!(
1398 state_event
1399 .tags
1400 .iter()
1401 .filter(|t| t.kind().to_string().as_str().ne("d"))
1402 .map(|t| t.as_vec().to_vec())
1403 .collect::<HashSet<Vec<String>>>(),
1404 HashSet::from([
1405 vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()],
1406 vec!["refs/heads/main".to_string(), main_commit_id.to_string()],
1407 ]),
1408 );
1409 Ok(())
1410 }
1411 }
1315 } 1412 }
1316 } 1413 }
1317 1414