diff options
| -rw-r--r-- | src/git_remote_helper.rs | 40 | ||||
| -rw-r--r-- | src/repo_state.rs | 10 | ||||
| -rw-r--r-- | tests/git_remote_helper.rs | 169 |
3 files changed, 186 insertions, 33 deletions
diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs index 68aa681..08a59c1 100644 --- a/src/git_remote_helper.rs +++ b/src/git_remote_helper.rs | |||
| @@ -6,7 +6,7 @@ | |||
| 6 | 6 | ||
| 7 | use core::str; | 7 | use core::str; |
| 8 | use std::{ | 8 | use std::{ |
| 9 | collections::HashSet, | 9 | collections::{HashMap, HashSet}, |
| 10 | env, | 10 | env, |
| 11 | io::{self, Stdin}, | 11 | io::{self, Stdin}, |
| 12 | path::PathBuf, | 12 | path::PathBuf, |
| @@ -261,41 +261,25 @@ async fn generate_updated_state( | |||
| 261 | git_repo: &Repo, | 261 | git_repo: &Repo, |
| 262 | repo_ref: &RepoRef, | 262 | repo_ref: &RepoRef, |
| 263 | refspecs: &Vec<String>, | 263 | refspecs: &Vec<String>, |
| 264 | ) -> Result<Vec<(String, String)>> { | 264 | ) -> Result<HashMap<String, String>> { |
| 265 | let new_state = { | 265 | let new_state = { |
| 266 | if let Ok(mut repo_state) = get_state_from_cache(git_repo.get_path()?, repo_ref).await { | 266 | if let Ok(mut repo_state) = get_state_from_cache(git_repo.get_path()?, repo_ref).await { |
| 267 | for refspec in refspecs { | 267 | for refspec in refspecs { |
| 268 | let (from, to) = refspec_to_from_to(refspec)?; | 268 | let (from, to) = refspec_to_from_to(refspec)?; |
| 269 | if to.is_empty() { | 269 | if from.is_empty() { |
| 270 | // delete | 270 | // delete |
| 271 | repo_state.state.retain(|(name, _)| !name.eq(to)); | 271 | repo_state.state.remove(to); |
| 272 | } else if repo_state.state.iter().any(|(name, _)| name.eq(from)) { | ||
| 273 | // update | ||
| 274 | repo_state.state = repo_state | ||
| 275 | .state | ||
| 276 | .iter() | ||
| 277 | .map(|(name, value)| { | ||
| 278 | ( | ||
| 279 | name.clone(), | ||
| 280 | if name.eq(to) { | ||
| 281 | reference_to_ref_value(&git_repo.git_repo, to).unwrap() | ||
| 282 | } else { | ||
| 283 | value.to_string() | ||
| 284 | }, | ||
| 285 | ) | ||
| 286 | }) | ||
| 287 | .collect(); | ||
| 288 | } else { | 272 | } else { |
| 289 | // add | 273 | // add or update |
| 290 | repo_state.state.push(( | 274 | repo_state.state.insert( |
| 291 | to.to_string(), | 275 | to.to_string(), |
| 292 | reference_to_ref_value(&git_repo.git_repo, to).unwrap(), | 276 | reference_to_ref_value(&git_repo.git_repo, to).unwrap(), |
| 293 | )); | 277 | ); |
| 294 | } | 278 | } |
| 295 | } | 279 | } |
| 296 | repo_state.state | 280 | repo_state.state |
| 297 | } else { | 281 | } else { |
| 298 | let mut state = vec![]; | 282 | let mut state = HashMap::new(); |
| 299 | let git_server_url = repo_ref | 283 | let git_server_url = repo_ref |
| 300 | .git_server | 284 | .git_server |
| 301 | .first() | 285 | .first() |
| @@ -303,14 +287,14 @@ async fn generate_updated_state( | |||
| 303 | let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; | 287 | let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; |
| 304 | git_server_remote.connect(git2::Direction::Fetch)?; | 288 | git_server_remote.connect(git2::Direction::Fetch)?; |
| 305 | for head in git_server_remote.list()? { | 289 | for head in git_server_remote.list()? { |
| 306 | state.push(( | 290 | state.insert( |
| 307 | head.name().to_string(), | 291 | head.name().to_string(), |
| 308 | if let Some(symbolic_ref) = head.symref_target() { | 292 | if let Some(symbolic_ref) = head.symref_target() { |
| 309 | format!("ref: {}", symbolic_ref) | 293 | format!("ref: {symbolic_ref}") |
| 310 | } else { | 294 | } else { |
| 311 | head.oid().to_string() | 295 | head.oid().to_string() |
| 312 | }, | 296 | }, |
| 313 | )); | 297 | ); |
| 314 | } | 298 | } |
| 315 | git_server_remote.disconnect()?; | 299 | git_server_remote.disconnect()?; |
| 316 | state | 300 | state |
| @@ -460,7 +444,7 @@ fn get_refspecs_from_push_batch(stdin: &Stdin, initial_refspec: &str) -> Result< | |||
| 460 | impl RepoState { | 444 | impl RepoState { |
| 461 | pub async fn build( | 445 | pub async fn build( |
| 462 | identifier: String, | 446 | identifier: String, |
| 463 | state: Vec<(String, String)>, | 447 | state: HashMap<String, String>, |
| 464 | signer: &NostrSigner, | 448 | signer: &NostrSigner, |
| 465 | ) -> Result<RepoState> { | 449 | ) -> Result<RepoState> { |
| 466 | let mut tags = vec![Tag::identifier(identifier.clone())]; | 450 | let mut tags = vec![Tag::identifier(identifier.clone())]; |
diff --git a/src/repo_state.rs b/src/repo_state.rs index 0c1aa30..a5cebab 100644 --- a/src/repo_state.rs +++ b/src/repo_state.rs | |||
| @@ -1,9 +1,11 @@ | |||
| 1 | use std::collections::HashMap; | ||
| 2 | |||
| 1 | use anyhow::{Context, Result}; | 3 | use anyhow::{Context, Result}; |
| 2 | use git2::Oid; | 4 | use git2::Oid; |
| 3 | 5 | ||
| 4 | pub struct RepoState { | 6 | pub struct RepoState { |
| 5 | pub identifier: String, | 7 | pub identifier: String, |
| 6 | pub state: Vec<(String, String)>, | 8 | pub state: HashMap<String, String>, |
| 7 | pub event: nostr::Event, | 9 | pub event: nostr::Event, |
| 8 | } | 10 | } |
| 9 | 11 | ||
| @@ -11,7 +13,7 @@ impl RepoState { | |||
| 11 | pub fn try_from(mut state_events: Vec<nostr::Event>) -> Result<Self> { | 13 | pub fn try_from(mut state_events: Vec<nostr::Event>) -> Result<Self> { |
| 12 | state_events.sort_by_key(|e| e.created_at); | 14 | state_events.sort_by_key(|e| e.created_at); |
| 13 | let event = state_events.first().context("no state events")?; | 15 | let event = state_events.first().context("no state events")?; |
| 14 | let mut state = vec![]; | 16 | let mut state = HashMap::new(); |
| 15 | for tag in &event.tags { | 17 | for tag in &event.tags { |
| 16 | if let Some(name) = tag.as_vec().first() { | 18 | if let Some(name) = tag.as_vec().first() { |
| 17 | if ["refs/heads/", "refs/tags", "HEAD"] | 19 | if ["refs/heads/", "refs/tags", "HEAD"] |
| @@ -19,8 +21,8 @@ impl RepoState { | |||
| 19 | .any(|s| name.starts_with(*s)) | 21 | .any(|s| name.starts_with(*s)) |
| 20 | { | 22 | { |
| 21 | if let Some(value) = tag.as_vec().get(1) { | 23 | if let Some(value) = tag.as_vec().get(1) { |
| 22 | if Oid::from_str(value).is_ok() { | 24 | if Oid::from_str(value).is_ok() || value.contains("ref: refs/") { |
| 23 | state.push((name.to_owned(), value.to_owned())); | 25 | state.insert(name.to_owned(), value.to_owned()); |
| 24 | } | 26 | } |
| 25 | } | 27 | } |
| 26 | } | 28 | } |
diff --git a/tests/git_remote_helper.rs b/tests/git_remote_helper.rs index ea76445..fffcaa1 100644 --- a/tests/git_remote_helper.rs +++ b/tests/git_remote_helper.rs | |||
| @@ -51,6 +51,77 @@ fn cli_tester_after_fetch(git_repo: &GitTestRepo) -> Result<CliTester> { | |||
| 51 | Ok(p) | 51 | Ok(p) |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | async fn generate_repo_with_state_event() -> Result<(nostr::Event, GitTestRepo)> { | ||
| 55 | let git_repo = prep_git_repo()?; | ||
| 56 | let commit_id = git_repo.get_tip_of_local_branch("main")?.to_string(); | ||
| 57 | let source_git_repo = GitTestRepo::recreate_as_bare(&git_repo)?; | ||
| 58 | |||
| 59 | let events = vec![ | ||
| 60 | generate_test_key_1_metadata_event("fred"), | ||
| 61 | generate_test_key_1_relay_list_event(), | ||
| 62 | generate_repo_ref_event_with_git_server(source_git_repo.dir.to_str().unwrap()), | ||
| 63 | ]; | ||
| 64 | // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) | ||
| 65 | let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( | ||
| 66 | Relay::new(8051, None, None), | ||
| 67 | Relay::new(8052, None, None), | ||
| 68 | Relay::new(8053, None, None), | ||
| 69 | Relay::new(8055, None, None), | ||
| 70 | Relay::new(8056, None, None), | ||
| 71 | Relay::new(8057, None, None), | ||
| 72 | ); | ||
| 73 | r51.events = events.clone(); | ||
| 74 | r55.events = events; | ||
| 75 | |||
| 76 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 77 | let mut p = cli_tester_after_fetch(&git_repo)?; | ||
| 78 | p.send_line("push refs/heads/main:refs/heads/main")?; | ||
| 79 | p.send_line("")?; | ||
| 80 | p.expect("ok refs/heads/main\r\n")?; | ||
| 81 | p.expect("\r\n")?; | ||
| 82 | |||
| 83 | p.exit()?; | ||
| 84 | for p in [51, 52, 53, 55, 56, 57] { | ||
| 85 | relay::shutdown_relay(8000 + p)?; | ||
| 86 | } | ||
| 87 | Ok(()) | ||
| 88 | }); | ||
| 89 | // launch relays | ||
| 90 | let _ = join!( | ||
| 91 | r51.listen_until_close(), | ||
| 92 | r52.listen_until_close(), | ||
| 93 | r53.listen_until_close(), | ||
| 94 | r55.listen_until_close(), | ||
| 95 | r56.listen_until_close(), | ||
| 96 | r57.listen_until_close(), | ||
| 97 | ); | ||
| 98 | cli_tester_handle.join().unwrap()?; | ||
| 99 | |||
| 100 | let state_event = r56 | ||
| 101 | .events | ||
| 102 | .iter() | ||
| 103 | .find(|e| e.kind().eq(&STATE_KIND)) | ||
| 104 | .context("state event not created")?; | ||
| 105 | |||
| 106 | assert_eq!( | ||
| 107 | state_event | ||
| 108 | .tags | ||
| 109 | .iter() | ||
| 110 | .filter(|t| t.kind().to_string().as_str().ne("d")) | ||
| 111 | .map(|t| t.as_vec().to_vec()) | ||
| 112 | .collect::<HashSet<Vec<String>>>(), | ||
| 113 | HashSet::from([ | ||
| 114 | vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()], | ||
| 115 | vec!["refs/heads/main".to_string(), commit_id,], | ||
| 116 | ]), | ||
| 117 | ); | ||
| 118 | |||
| 119 | // wait for bigger timestamp | ||
| 120 | std::thread::sleep(std::time::Duration::from_millis(1000)); | ||
| 121 | |||
| 122 | Ok((state_event.clone(), source_git_repo)) | ||
| 123 | } | ||
| 124 | |||
| 54 | mod initially_runs_fetch { | 125 | mod initially_runs_fetch { |
| 55 | 126 | ||
| 56 | use super::*; | 127 | use super::*; |
| @@ -523,7 +594,103 @@ mod push { | |||
| 523 | 594 | ||
| 524 | #[tokio::test] | 595 | #[tokio::test] |
| 525 | #[serial] | 596 | #[serial] |
| 526 | async fn sate_event_reflects_git_server_state() -> Result<()> { | 597 | async fn state_event_reflects_git_server_state() -> Result<()> { |
| 598 | async_run_test().await | ||
| 599 | } | ||
| 600 | } | ||
| 601 | } | ||
| 602 | mod existing_state_event { | ||
| 603 | use super::*; | ||
| 604 | |||
| 605 | mod state_on_git_server_published_in_nostr_state_event { | ||
| 606 | |||
| 607 | use super::*; | ||
| 608 | |||
| 609 | async fn async_run_test() -> Result<()> { | ||
| 610 | let (state_event, source_git_repo) = generate_repo_with_state_event().await?; | ||
| 611 | |||
| 612 | let git_repo = prep_git_repo()?; | ||
| 613 | |||
| 614 | std::fs::write(git_repo.dir.join("new.md"), "some content")?; | ||
| 615 | let main_commit_id = git_repo.stage_and_commit("new.md")?; | ||
| 616 | git_repo.create_branch("vnext")?; | ||
| 617 | git_repo.checkout("vnext")?; | ||
| 618 | std::fs::write(git_repo.dir.join("more.md"), "some content")?; | ||
| 619 | let vnext_commit_id = git_repo.stage_and_commit("more.md")?; | ||
| 620 | |||
| 621 | let events = vec![ | ||
| 622 | generate_test_key_1_metadata_event("fred"), | ||
| 623 | generate_test_key_1_relay_list_event(), | ||
| 624 | generate_repo_ref_event_with_git_server( | ||
| 625 | source_git_repo.dir.to_str().unwrap(), | ||
| 626 | ), | ||
| 627 | state_event.clone(), | ||
| 628 | ]; | ||
| 629 | |||
| 630 | // fallback (51,52) user write (53, 55) repo (55, 56) blaster (57) | ||
| 631 | let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = ( | ||
| 632 | Relay::new(8051, None, None), | ||
| 633 | Relay::new(8052, None, None), | ||
| 634 | Relay::new(8053, None, None), | ||
| 635 | Relay::new(8055, None, None), | ||
| 636 | Relay::new(8056, None, None), | ||
| 637 | Relay::new(8057, None, None), | ||
| 638 | ); | ||
| 639 | r51.events = events.clone(); | ||
| 640 | r55.events = events; | ||
| 641 | |||
| 642 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 643 | let mut p = cli_tester_after_fetch(&git_repo)?; | ||
| 644 | p.send_line("push refs/heads/main:refs/heads/main")?; | ||
| 645 | p.send_line("push refs/heads/vnext:refs/heads/vnext")?; | ||
| 646 | p.send_line("")?; | ||
| 647 | p.expect("ok refs/heads/main\r\n")?; | ||
| 648 | p.expect("ok refs/heads/vnext\r\n")?; | ||
| 649 | p.expect("\r\n")?; | ||
| 650 | p.exit()?; | ||
| 651 | for p in [51, 52, 53, 55, 56, 57] { | ||
| 652 | relay::shutdown_relay(8000 + p)?; | ||
| 653 | } | ||
| 654 | Ok(()) | ||
| 655 | }); | ||
| 656 | // launch relays | ||
| 657 | let _ = join!( | ||
| 658 | r51.listen_until_close(), | ||
| 659 | r52.listen_until_close(), | ||
| 660 | r53.listen_until_close(), | ||
| 661 | r55.listen_until_close(), | ||
| 662 | r56.listen_until_close(), | ||
| 663 | r57.listen_until_close(), | ||
| 664 | ); | ||
| 665 | |||
| 666 | cli_tester_handle.join().unwrap()?; | ||
| 667 | |||
| 668 | let state_event = r56 | ||
| 669 | .events | ||
| 670 | .iter() | ||
| 671 | .find(|e| e.kind().eq(&STATE_KIND)) | ||
| 672 | .context("state event not created")?; | ||
| 673 | |||
| 674 | // println!("{:#?}", state_event); | ||
| 675 | assert_eq!( | ||
| 676 | state_event | ||
| 677 | .tags | ||
| 678 | .iter() | ||
| 679 | .filter(|t| t.kind().to_string().as_str().ne("d")) | ||
| 680 | .map(|t| t.as_vec().to_vec()) | ||
| 681 | .collect::<HashSet<Vec<String>>>(), | ||
| 682 | HashSet::from([ | ||
| 683 | vec!["HEAD".to_string(), "ref: refs/heads/main".to_string()], | ||
| 684 | vec!["refs/heads/main".to_string(), main_commit_id.to_string()], | ||
| 685 | vec!["refs/heads/vnext".to_string(), vnext_commit_id.to_string()], | ||
| 686 | ]), | ||
| 687 | ); | ||
| 688 | Ok(()) | ||
| 689 | } | ||
| 690 | |||
| 691 | #[tokio::test] | ||
| 692 | #[serial] | ||
| 693 | async fn state_event_reflects_updated_with_pushed_changes() -> Result<()> { | ||
| 527 | async_run_test().await | 694 | async_run_test().await |
| 528 | } | 695 | } |
| 529 | } | 696 | } |