diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-08-05 14:15:29 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-08-05 14:15:29 +0100 |
| commit | f238fc8c0a122487f4fb71bb78a2e365e147d747 (patch) | |
| tree | 5e8760c192f62fef67189bc82dcae4eef3ce883a | |
| parent | e5750b5b3dfe2c0072902c2523fdf32986aa74b8 (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.rs | 16 | ||||
| -rw-r--r-- | src/git_remote_helper.rs | 339 | ||||
| -rw-r--r-- | tests/git_remote_helper.rs | 265 |
3 files changed, 447 insertions, 173 deletions
| @@ -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)] | ||
| 354 | async fn push( | 357 | async 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 | ||
| 458 | async fn generate_updated_state( | 486 | type HashMapUrlRefspecs = HashMap<String, Vec<String>>; |
| 487 | |||
| 488 | #[allow(clippy::too_many_lines)] | ||
| 489 | fn 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 | |||
| 639 | fn 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 | ||
| 70 | fn 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 | |||
| 67 | async fn generate_repo_with_state_event() -> Result<(nostr::Event, GitTestRepo)> { | 80 | async 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 | ||