diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/git.rs | 16 | ||||
| -rw-r--r-- | src/git_remote_helper.rs | 339 |
2 files changed, 266 insertions, 89 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 | ||