diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-08-30 13:39:05 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-08-30 13:39:05 +0100 |
| commit | 8a9fc6336ae52ed73160719feeae96db9889feb0 (patch) | |
| tree | ff488f991aa49ffa29d2ea773e4d78c1d00457f4 /src | |
| parent | b4459e98d816f6f278a4e9ab76883008d0b946fe (diff) | |
feat(remote): fallback to ssh or https
ssh clone urls are problematic as it requires authentication for
fetching if users don't ave ssh keys setup for authentication with
the git server. so cloning / fetching will fail.
some users only authenticate using https and some only using ssh
so we cant just use https for everything.
this commit falls back to the other protocol if the first one failed
for list, fetch or push
Diffstat (limited to 'src')
| -rw-r--r-- | src/git_remote_helper.rs | 169 |
1 files changed, 133 insertions, 36 deletions
diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs index e2de494..d00ad0e 100644 --- a/src/git_remote_helper.rs +++ b/src/git_remote_helper.rs | |||
| @@ -12,12 +12,13 @@ use std::{ | |||
| 12 | path::{Path, PathBuf}, | 12 | path::{Path, PathBuf}, |
| 13 | }; | 13 | }; |
| 14 | 14 | ||
| 15 | use anyhow::{bail, Context, Result}; | 15 | use anyhow::{anyhow, bail, Context, Result}; |
| 16 | use auth_git2::GitAuthenticator; | 16 | use auth_git2::GitAuthenticator; |
| 17 | use client::{ | 17 | use client::{ |
| 18 | consolidate_fetch_reports, get_events_from_cache, get_repo_ref_from_cache, | 18 | consolidate_fetch_reports, get_events_from_cache, get_repo_ref_from_cache, |
| 19 | get_state_from_cache, sign_event, Connect, STATE_KIND, | 19 | get_state_from_cache, sign_event, Connect, STATE_KIND, |
| 20 | }; | 20 | }; |
| 21 | use console::Term; | ||
| 21 | use git::{nostr_git_url_to_repo_coordinates, sha1_to_oid, RepoActions}; | 22 | use git::{nostr_git_url_to_repo_coordinates, sha1_to_oid, RepoActions}; |
| 22 | use git2::{Oid, Repository}; | 23 | use git2::{Oid, Repository}; |
| 23 | use nostr::nips::{nip01::Coordinate, nip10::Marker}; | 24 | use nostr::nips::{nip01::Coordinate, nip10::Marker}; |
| @@ -290,10 +291,24 @@ fn list_from_remotes( | |||
| 290 | Ok(remote_state) => { | 291 | Ok(remote_state) => { |
| 291 | remote_states.insert(url.clone(), remote_state); | 292 | remote_states.insert(url.clone(), remote_state); |
| 292 | } | 293 | } |
| 293 | Err(error) => { | 294 | Err(error1) => { |
| 294 | term.write_line( | 295 | if let Ok(alternative_url) = switch_clone_url_between_ssh_and_https(url) { |
| 295 | format!("WARNING: {short_name} failed to list refs error: {error}",).as_str(), | 296 | match list_from_remote(git_repo, &alternative_url) { |
| 296 | )?; | 297 | Ok(remote_state) => { |
| 298 | remote_states.insert(url.clone(), remote_state); | ||
| 299 | } | ||
| 300 | Err(error2) => { | ||
| 301 | term.write_line( | ||
| 302 | format!("WARNING: {short_name} failed to list refs error: {error1}\r\nand alternative protocol {alternative_url}: {error2}").as_str(), | ||
| 303 | )?; | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } else { | ||
| 307 | term.write_line( | ||
| 308 | format!("WARNING: {short_name} failed to list refs error: {error1}",) | ||
| 309 | .as_str(), | ||
| 310 | )?; | ||
| 311 | } | ||
| 297 | } | 312 | } |
| 298 | } | 313 | } |
| 299 | term.clear_last_lines(1)?; | 314 | term.clear_last_lines(1)?; |
| @@ -301,6 +316,47 @@ fn list_from_remotes( | |||
| 301 | Ok(remote_states) | 316 | Ok(remote_states) |
| 302 | } | 317 | } |
| 303 | 318 | ||
| 319 | fn switch_clone_url_between_ssh_and_https(url: &str) -> Result<String> { | ||
| 320 | if url.starts_with("https://") { | ||
| 321 | // Convert HTTPS to git@ syntax | ||
| 322 | let parts: Vec<&str> = url.trim_start_matches("https://").split('/').collect(); | ||
| 323 | if parts.len() >= 2 { | ||
| 324 | // Construct the git@ URL | ||
| 325 | Ok(format!("git@{}:{}", parts[0], parts[1..].join("/"))) | ||
| 326 | } else { | ||
| 327 | // If the format is unexpected, return an error | ||
| 328 | bail!("Invalid HTTPS URL format: {}", url); | ||
| 329 | } | ||
| 330 | } else if url.starts_with("ssh://") { | ||
| 331 | // Convert SSH to git@ syntax | ||
| 332 | let parts: Vec<&str> = url.trim_start_matches("ssh://").split('/').collect(); | ||
| 333 | if parts.len() >= 2 { | ||
| 334 | // Construct the git@ URL | ||
| 335 | Ok(format!("git@{}:{}", parts[0], parts[1..].join("/"))) | ||
| 336 | } else { | ||
| 337 | // If the format is unexpected, return an error | ||
| 338 | bail!("Invalid SSH URL format: {}", url); | ||
| 339 | } | ||
| 340 | } else if url.starts_with("git@") { | ||
| 341 | // Convert git@ syntax to HTTPS | ||
| 342 | let parts: Vec<&str> = url.split(':').collect(); | ||
| 343 | if parts.len() == 2 { | ||
| 344 | // Construct the HTTPS URL | ||
| 345 | Ok(format!( | ||
| 346 | "https://{}/{}", | ||
| 347 | parts[0].trim_end_matches('@'), | ||
| 348 | parts[1] | ||
| 349 | )) | ||
| 350 | } else { | ||
| 351 | // If the format is unexpected, return an error | ||
| 352 | bail!("Invalid git@ URL format: {}", url); | ||
| 353 | } | ||
| 354 | } else { | ||
| 355 | // If the URL is neither HTTPS, SSH, nor git@, return an error | ||
| 356 | bail!("Unsupported URL protocol: {}", url); | ||
| 357 | } | ||
| 358 | } | ||
| 359 | |||
| 304 | fn list_from_remote( | 360 | fn list_from_remote( |
| 305 | git_repo: &Repo, | 361 | git_repo: &Repo, |
| 306 | git_server_remote_url: &str, | 362 | git_server_remote_url: &str, |
| @@ -467,15 +523,34 @@ async fn fetch( | |||
| 467 | term.write_line(format!("fetching from {short_name}...").as_str())?; | 523 | term.write_line(format!("fetching from {short_name}...").as_str())?; |
| 468 | let res = fetch_from_git_server(&git_repo.git_repo, &oids_from_git_servers, git_server_url); | 524 | let res = fetch_from_git_server(&git_repo.git_repo, &oids_from_git_servers, git_server_url); |
| 469 | term.clear_last_lines(1)?; | 525 | term.clear_last_lines(1)?; |
| 470 | if let Err(e) = res { | 526 | if let Err(error1) = res { |
| 471 | term.write_line( | 527 | if let Ok(alternative_url) = switch_clone_url_between_ssh_and_https(git_server_url) { |
| 472 | format!( | 528 | let res2 = fetch_from_git_server( |
| 473 | "WARNING: failed to fetch from {short_name} error: | 529 | &git_repo.git_repo, |
| 474 | {e}" | 530 | &oids_from_git_servers, |
| 475 | ) | 531 | &alternative_url, |
| 476 | .as_str(), | 532 | ); |
| 477 | )?; | 533 | if let Err(error2) = res2 { |
| 478 | errors.insert(short_name.to_string(), e); | 534 | term.write_line( |
| 535 | format!( | ||
| 536 | "WARNING: failed to fetch from {short_name} error:{error1}\r\nand using alternative protocol {alternative_url}: {error2}" | ||
| 537 | ).as_str() | ||
| 538 | )?; | ||
| 539 | errors.insert( | ||
| 540 | short_name.to_string(), | ||
| 541 | anyhow!( | ||
| 542 | "{error1} and using alternative protocol {alternative_url}: {error2}" | ||
| 543 | ), | ||
| 544 | ); | ||
| 545 | } else { | ||
| 546 | break; | ||
| 547 | } | ||
| 548 | } else { | ||
| 549 | term.write_line( | ||
| 550 | format!("WARNING: failed to fetch from {short_name} error:{error1}").as_str(), | ||
| 551 | )?; | ||
| 552 | errors.insert(short_name.to_string(), error1); | ||
| 553 | } | ||
| 479 | } else { | 554 | } else { |
| 480 | break; | 555 | break; |
| 481 | } | 556 | } |
| @@ -854,35 +929,26 @@ async fn push( | |||
| 854 | } | 929 | } |
| 855 | 930 | ||
| 856 | // TODO make async - check gitlib2 callbacks work async | 931 | // TODO make async - check gitlib2 callbacks work async |
| 857 | let git_config = git_repo.git_repo.config()?; | ||
| 858 | for (git_server_url, remote_refspecs) in remote_refspecs { | 932 | for (git_server_url, remote_refspecs) in remote_refspecs { |
| 859 | let remote_refspecs = remote_refspecs | 933 | let remote_refspecs = remote_refspecs |
| 860 | .iter() | 934 | .iter() |
| 861 | .filter(|refspec| git_server_refspecs.contains(refspec)) | 935 | .filter(|refspec| git_server_refspecs.contains(refspec)) |
| 862 | .cloned() | 936 | .cloned() |
| 863 | .collect::<Vec<String>>(); | 937 | .collect::<Vec<String>>(); |
| 864 | if !refspecs.is_empty() { | 938 | if !refspecs.is_empty() |
| 865 | if let Ok(mut git_server_remote) = git_repo.git_repo.remote_anonymous(&git_server_url) { | 939 | && push_to_remote(git_repo, &git_server_url, &remote_refspecs, &term).is_err() |
| 866 | let auth = GitAuthenticator::default(); | 940 | { |
| 867 | let mut push_options = git2::PushOptions::new(); | 941 | if let Ok(alternative_url) = switch_clone_url_between_ssh_and_https(&git_server_url) { |
| 868 | let mut remote_callbacks = git2::RemoteCallbacks::new(); | 942 | if push_to_remote(git_repo, &alternative_url, &remote_refspecs, &term).is_err() { |
| 869 | remote_callbacks.credentials(auth.credentials(&git_config)); | 943 | // errors get printed as part of callback |
| 870 | remote_callbacks.push_update_reference(|name, error| { | 944 | // TODO prevent 2 warning messages and instead use one |
| 871 | if let Some(error) = error { | 945 | // to say it didnt work over either https or ssh |
| 872 | term.write_line( | 946 | } else { |
| 873 | format!( | 947 | term.write_line( |
| 874 | "WARNING: {} failed to push {name} error: {error}", | 948 | format!("but succeed over alterantive protocol {alternative_url}",) |
| 875 | get_short_git_server_name(git_repo, &git_server_url), | ||
| 876 | ) | ||
| 877 | .as_str(), | 949 | .as_str(), |
| 878 | ) | 950 | )?; |
| 879 | .unwrap(); | 951 | } |
| 880 | } | ||
| 881 | Ok(()) | ||
| 882 | }); | ||
| 883 | push_options.remote_callbacks(remote_callbacks); | ||
| 884 | let _ = git_server_remote.push(&remote_refspecs, Some(&mut push_options)); | ||
| 885 | let _ = git_server_remote.disconnect(); | ||
| 886 | } | 952 | } |
| 887 | } | 953 | } |
| 888 | } | 954 | } |
| @@ -890,6 +956,37 @@ async fn push( | |||
| 890 | Ok(()) | 956 | Ok(()) |
| 891 | } | 957 | } |
| 892 | 958 | ||
| 959 | fn push_to_remote( | ||
| 960 | git_repo: &Repo, | ||
| 961 | git_server_url: &str, | ||
| 962 | remote_refspecs: &[String], | ||
| 963 | term: &Term, | ||
| 964 | ) -> Result<()> { | ||
| 965 | let git_config = git_repo.git_repo.config()?; | ||
| 966 | let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; | ||
| 967 | let auth = GitAuthenticator::default(); | ||
| 968 | let mut push_options = git2::PushOptions::new(); | ||
| 969 | let mut remote_callbacks = git2::RemoteCallbacks::new(); | ||
| 970 | remote_callbacks.credentials(auth.credentials(&git_config)); | ||
| 971 | remote_callbacks.push_update_reference(|name, error| { | ||
| 972 | if let Some(error) = error { | ||
| 973 | term.write_line( | ||
| 974 | format!( | ||
| 975 | "WARNING: {} failed to push {name} error: {error}", | ||
| 976 | get_short_git_server_name(git_repo, git_server_url), | ||
| 977 | ) | ||
| 978 | .as_str(), | ||
| 979 | ) | ||
| 980 | .unwrap(); | ||
| 981 | } | ||
| 982 | Ok(()) | ||
| 983 | }); | ||
| 984 | push_options.remote_callbacks(remote_callbacks); | ||
| 985 | git_server_remote.push(remote_refspecs, Some(&mut push_options))?; | ||
| 986 | let _ = git_server_remote.disconnect(); | ||
| 987 | Ok(()) | ||
| 988 | } | ||
| 989 | |||
| 893 | fn get_event_root(event: &nostr::Event) -> Result<EventId> { | 990 | fn get_event_root(event: &nostr::Event) -> Result<EventId> { |
| 894 | Ok(EventId::parse( | 991 | Ok(EventId::parse( |
| 895 | event | 992 | event |