upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/git.rs16
-rw-r--r--src/git_remote_helper.rs339
2 files changed, 266 insertions, 89 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