From a5a662632b61ef2b35946af1e93f30a885ea1db2 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 6 Sep 2024 07:47:13 +0100 Subject: feat(remote): fetch protocol selection / fallback enable override from nostr url clone url is local use local otherwise try https unathenticated, ssh, then https authenticated --- src/bin/git_remote_nostr/fetch.rs | 178 +++++++++++++++++++++++++++++--------- src/bin/git_remote_nostr/main.rs | 2 +- src/lib/git/mod.rs | 1 + src/lib/git/utils.rs | 25 ++++++ 4 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 src/lib/git/utils.rs (limited to 'src') diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs index 5fd8816..6836456 100644 --- a/src/bin/git_remote_nostr/fetch.rs +++ b/src/bin/git_remote_nostr/fetch.rs @@ -1,10 +1,14 @@ -use std::{collections::HashMap, io::Stdin}; +use std::io::Stdin; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Context, Result}; use auth_git2::GitAuthenticator; use git2::Repository; use ngit::{ - git::{Repo, RepoActions}, + git::{ + nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, + utils::check_ssh_keys, + Repo, RepoActions, + }, git_events::tag_value, login::get_curent_user, repo_ref::RepoRef, @@ -12,12 +16,12 @@ use ngit::{ use crate::utils::{ find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, get_open_proposals, - get_short_git_server_name, switch_clone_url_between_ssh_and_https, }; pub async fn run_fetch( git_repo: &Repo, repo_ref: &RepoRef, + decoded_nostr_url: &NostrUrlDecoded, stdin: &Stdin, oid: &str, refstr: &str, @@ -30,43 +34,19 @@ pub async fn run_fetch( .map(|(_, oid)| oid.clone()) .collect::>(); - let mut errors = HashMap::new(); + let mut errors = vec![]; let term = console::Term::stderr(); for git_server_url in &repo_ref.git_server { let term = console::Term::stderr(); - let short_name = get_short_git_server_name(git_repo, git_server_url); - term.write_line(format!("fetching from {short_name}...").as_str())?; - let res = fetch_from_git_server(&git_repo.git_repo, &oids_from_git_servers, git_server_url); - term.clear_last_lines(1)?; - if let Err(error1) = res { - if let Ok(alternative_url) = switch_clone_url_between_ssh_and_https(git_server_url) { - let res2 = fetch_from_git_server( - &git_repo.git_repo, - &oids_from_git_servers, - &alternative_url, - ); - if let Err(error2) = res2 { - term.write_line( - format!( - "WARNING: failed to fetch from {short_name} error:{error1}\r\nand using alternative protocol {alternative_url}: {error2}" - ).as_str() - )?; - errors.insert( - short_name.to_string(), - anyhow!( - "{error1} and using alternative protocol {alternative_url}: {error2}" - ), - ); - } else { - break; - } - } else { - term.write_line( - format!("WARNING: failed to fetch from {short_name} error:{error1}").as_str(), - )?; - errors.insert(short_name.to_string(), error1); - } + if let Err(error) = fetch_from_git_server( + &git_repo.git_repo, + &oids_from_git_servers, + git_server_url, + decoded_nostr_url, + &term, + ) { + errors.push(error); } else { break; } @@ -81,7 +61,7 @@ pub async fn run_fetch( "failed to fetch objects in nostr state event from:\r\n{}", errors .iter() - .map(|(url, error)| format!("{url}: {error}")) + .map(std::string::ToString::to_string) .collect::>() .join("\r\n") ); @@ -129,17 +109,135 @@ fn fetch_from_git_server( git_repo: &Repository, oids: &[String], git_server_url: &str, + decoded_nostr_url: &NostrUrlDecoded, + term: &console::Term, ) -> Result<()> { - let git_config = git_repo.config()?; + let server_url = git_server_url.parse::()?; + + // if protocol is local - just try local + if server_url.protocol() == ServerProtocol::Local { + let formatted_url = server_url.format_as(&ServerProtocol::Local, &None)?; + term.write_line(format!("fetching from {formatted_url}...").as_str())?; + if let Err(error) = fetch_from_git_server_url(git_repo, oids, &formatted_url) { + term.write_line( + format!("WARNING: failed to fetch from {formatted_url} error:{error}").as_str(), + )?; + return Err(error).context(format!("{formatted_url}: failed to fetch")); + } + return Ok(()); + } + + term.write_line(format!("fetching from {}...", server_url.domain()).as_str())?; + + // use overide protocol if specified + if let Some(protocol) = &decoded_nostr_url.protocol { + let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; + let res = fetch_from_git_server_url(git_repo, oids, &formatted_url); + term.clear_last_lines(1)?; + if let Err(error) = res { + term.write_line( + format!( + "WARNING: {formatted_url} failed to fetch over {protocol}{} as specified in nostr url. error:{error}", + if let Some(user) = &decoded_nostr_url.user { + format!(" with user '{user}'") + } else { + String::new() + } + ).as_str(), + )?; + return Err(error).context(format!("{formatted_url}: failed to fetch")); + } + return Ok(()); + } + + // Try https unauthenticated + let formatted_url = server_url.format_as(&ServerProtocol::Https, &None)?; + let res = fetch_from_git_server_url_unauthenticated(git_repo, oids, &formatted_url); + term.clear_last_lines(1)?; + if let Err(unauth_error) = res { + term.write_line( + format!( + "WARNING: {formatted_url} failed to fetch over unauthenticated https. {unauth_error}", + ).as_str(), + )?; + // TODO what about timeout errors? + // try over ssh + let mut ssh_error = None; + if check_ssh_keys() { + term.write_line(format!("fetching from {} over ssh...", server_url.domain()).as_str())?; + let formatted_url = server_url.format_as(&ServerProtocol::Ssh, &None)?; + let res = fetch_from_git_server_url(git_repo, oids, &formatted_url); + term.clear_last_lines(1)?; + if let Err(error) = res { + term.write_line( + format!("WARNING: {formatted_url} failed to fetch over ssh. error:{error}") + .as_str(), + )?; + term.write_line( + format!("fetching from {} over ssh...", server_url.domain()).as_str(), + )?; + ssh_error = Some(error); + } else { + return Ok(()); + } + } + // try over https authenticated + term.write_line( + format!( + "fetching from {} over authenticated https...", + server_url.domain() + ) + .as_str(), + )?; + let formatted_url = server_url.format_as(&ServerProtocol::Ssh, &None)?; + let res = fetch_from_git_server_url(git_repo, oids, &formatted_url); + term.clear_last_lines(1)?; + if let Err(auth_https_error) = res { + term.write_line( + format!("WARNING: {formatted_url} failed to fetch over authenticated https. error:{auth_https_error}",) + .as_str(), + )?; + let error_message = format!( + "{} failed to fetch over unauthenticated https ({unauth_error}), ssh ({}) and authenticated https ({auth_https_error})", + server_url.format_as(&ServerProtocol::Unspecified, &None)?, + ssh_error.unwrap_or(anyhow!("no keys found")) + ); + + bail!(error_message) + } + } + Ok(()) +} +fn fetch_from_git_server_url( + git_repo: &Repository, + oids: &[String], + git_server_url: &str, +) -> Result<()> { + let git_config = git_repo.config()?; let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; - // authentication may be required (and will be requird if clone url is ssh) let auth = GitAuthenticator::default(); let mut fetch_options = git2::FetchOptions::new(); let mut remote_callbacks = git2::RemoteCallbacks::new(); + // TODO status update callback remote_callbacks.credentials(auth.credentials(&git_config)); fetch_options.remote_callbacks(remote_callbacks); git_server_remote.download(oids, Some(&mut fetch_options))?; git_server_remote.disconnect()?; Ok(()) } + +fn fetch_from_git_server_url_unauthenticated( + git_repo: &Repository, + oids: &[String], + git_server_url: &str, +) -> Result<()> { + let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; + let mut fetch_options = git2::FetchOptions::new(); + let remote_callbacks = git2::RemoteCallbacks::new(); + // TODO status update callback + fetch_options.remote_callbacks(remote_callbacks); + git_server_remote.download(oids, Some(&mut fetch_options))?; + git_server_remote.disconnect()?; + Ok(()) +} diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs index 7bfe3f4..a3fc5f8 100644 --- a/src/bin/git_remote_nostr/main.rs +++ b/src/bin/git_remote_nostr/main.rs @@ -75,7 +75,7 @@ async fn main() -> Result<()> { println!("unsupported"); } ["fetch", oid, refstr] => { - fetch::run_fetch(&git_repo, &repo_ref, &stdin, oid, refstr).await?; + fetch::run_fetch(&git_repo, &repo_ref, &decoded_nostr_url, &stdin, oid, refstr).await?; } ["push", refspec] => { push::run_push( diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index f92272f..72717f7 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs @@ -11,6 +11,7 @@ use nostr_sdk::hashes::{sha1::Hash as Sha1Hash, Hash}; use crate::git_events::{get_commit_id_from_patch, tag_value}; pub mod identify_ahead_behind; pub mod nostr_url; +pub mod utils; pub struct Repo { pub git_repo: git2::Repository, diff --git a/src/lib/git/utils.rs b/src/lib/git/utils.rs new file mode 100644 index 0000000..4e8f153 --- /dev/null +++ b/src/lib/git/utils.rs @@ -0,0 +1,25 @@ +use std::path::Path; + +use directories::UserDirs; + +pub fn check_ssh_keys() -> bool { + // Get the user's home directory using the directories crate + if let Some(user_dirs) = UserDirs::new() { + let ssh_dir = user_dirs.home_dir().join(".ssh"); + let key_files = vec![ + "id_rsa", + "id_ecdsa", + "id_ed25519", + "id_rsa.pub", + "id_ecdsa.pub", + "id_ed25519.pub", + ]; + + for key in key_files { + if Path::new(&ssh_dir.join(key)).exists() { + return true; // At least one key exists + } + } + } + false // No keys found +} -- cgit v1.2.3