diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-06 07:47:13 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-06 07:47:13 +0100 |
| commit | a5a662632b61ef2b35946af1e93f30a885ea1db2 (patch) | |
| tree | 938fac721d85ad771a237999b7212ee6ea72d77d /src | |
| parent | fad3f8fddffb55597432243e129e4012034b3627 (diff) | |
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
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/git_remote_nostr/fetch.rs | 178 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 2 | ||||
| -rw-r--r-- | src/lib/git/mod.rs | 1 | ||||
| -rw-r--r-- | src/lib/git/utils.rs | 25 |
4 files changed, 165 insertions, 41 deletions
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 @@ | |||
| 1 | use std::{collections::HashMap, io::Stdin}; | 1 | use std::io::Stdin; |
| 2 | 2 | ||
| 3 | use anyhow::{anyhow, bail, Result}; | 3 | use anyhow::{anyhow, bail, Context, Result}; |
| 4 | use auth_git2::GitAuthenticator; | 4 | use auth_git2::GitAuthenticator; |
| 5 | use git2::Repository; | 5 | use git2::Repository; |
| 6 | use ngit::{ | 6 | use ngit::{ |
| 7 | git::{Repo, RepoActions}, | 7 | git::{ |
| 8 | nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, | ||
| 9 | utils::check_ssh_keys, | ||
| 10 | Repo, RepoActions, | ||
| 11 | }, | ||
| 8 | git_events::tag_value, | 12 | git_events::tag_value, |
| 9 | login::get_curent_user, | 13 | login::get_curent_user, |
| 10 | repo_ref::RepoRef, | 14 | repo_ref::RepoRef, |
| @@ -12,12 +16,12 @@ use ngit::{ | |||
| 12 | 16 | ||
| 13 | use crate::utils::{ | 17 | use crate::utils::{ |
| 14 | find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, get_open_proposals, | 18 | find_proposal_and_patches_by_branch_name, get_oids_from_fetch_batch, get_open_proposals, |
| 15 | get_short_git_server_name, switch_clone_url_between_ssh_and_https, | ||
| 16 | }; | 19 | }; |
| 17 | 20 | ||
| 18 | pub async fn run_fetch( | 21 | pub async fn run_fetch( |
| 19 | git_repo: &Repo, | 22 | git_repo: &Repo, |
| 20 | repo_ref: &RepoRef, | 23 | repo_ref: &RepoRef, |
| 24 | decoded_nostr_url: &NostrUrlDecoded, | ||
| 21 | stdin: &Stdin, | 25 | stdin: &Stdin, |
| 22 | oid: &str, | 26 | oid: &str, |
| 23 | refstr: &str, | 27 | refstr: &str, |
| @@ -30,43 +34,19 @@ pub async fn run_fetch( | |||
| 30 | .map(|(_, oid)| oid.clone()) | 34 | .map(|(_, oid)| oid.clone()) |
| 31 | .collect::<Vec<String>>(); | 35 | .collect::<Vec<String>>(); |
| 32 | 36 | ||
| 33 | let mut errors = HashMap::new(); | 37 | let mut errors = vec![]; |
| 34 | let term = console::Term::stderr(); | 38 | let term = console::Term::stderr(); |
| 35 | 39 | ||
| 36 | for git_server_url in &repo_ref.git_server { | 40 | for git_server_url in &repo_ref.git_server { |
| 37 | let term = console::Term::stderr(); | 41 | let term = console::Term::stderr(); |
| 38 | let short_name = get_short_git_server_name(git_repo, git_server_url); | 42 | if let Err(error) = fetch_from_git_server( |
| 39 | term.write_line(format!("fetching from {short_name}...").as_str())?; | 43 | &git_repo.git_repo, |
| 40 | let res = fetch_from_git_server(&git_repo.git_repo, &oids_from_git_servers, git_server_url); | 44 | &oids_from_git_servers, |
| 41 | term.clear_last_lines(1)?; | 45 | git_server_url, |
| 42 | if let Err(error1) = res { | 46 | decoded_nostr_url, |
| 43 | if let Ok(alternative_url) = switch_clone_url_between_ssh_and_https(git_server_url) { | 47 | &term, |
| 44 | let res2 = fetch_from_git_server( | 48 | ) { |
| 45 | &git_repo.git_repo, | 49 | errors.push(error); |
| 46 | &oids_from_git_servers, | ||
| 47 | &alternative_url, | ||
| 48 | ); | ||
| 49 | if let Err(error2) = res2 { | ||
| 50 | term.write_line( | ||
| 51 | format!( | ||
| 52 | "WARNING: failed to fetch from {short_name} error:{error1}\r\nand using alternative protocol {alternative_url}: {error2}" | ||
| 53 | ).as_str() | ||
| 54 | )?; | ||
| 55 | errors.insert( | ||
| 56 | short_name.to_string(), | ||
| 57 | anyhow!( | ||
| 58 | "{error1} and using alternative protocol {alternative_url}: {error2}" | ||
| 59 | ), | ||
| 60 | ); | ||
| 61 | } else { | ||
| 62 | break; | ||
| 63 | } | ||
| 64 | } else { | ||
| 65 | term.write_line( | ||
| 66 | format!("WARNING: failed to fetch from {short_name} error:{error1}").as_str(), | ||
| 67 | )?; | ||
| 68 | errors.insert(short_name.to_string(), error1); | ||
| 69 | } | ||
| 70 | } else { | 50 | } else { |
| 71 | break; | 51 | break; |
| 72 | } | 52 | } |
| @@ -81,7 +61,7 @@ pub async fn run_fetch( | |||
| 81 | "failed to fetch objects in nostr state event from:\r\n{}", | 61 | "failed to fetch objects in nostr state event from:\r\n{}", |
| 82 | errors | 62 | errors |
| 83 | .iter() | 63 | .iter() |
| 84 | .map(|(url, error)| format!("{url}: {error}")) | 64 | .map(std::string::ToString::to_string) |
| 85 | .collect::<Vec<String>>() | 65 | .collect::<Vec<String>>() |
| 86 | .join("\r\n") | 66 | .join("\r\n") |
| 87 | ); | 67 | ); |
| @@ -129,17 +109,135 @@ fn fetch_from_git_server( | |||
| 129 | git_repo: &Repository, | 109 | git_repo: &Repository, |
| 130 | oids: &[String], | 110 | oids: &[String], |
| 131 | git_server_url: &str, | 111 | git_server_url: &str, |
| 112 | decoded_nostr_url: &NostrUrlDecoded, | ||
| 113 | term: &console::Term, | ||
| 132 | ) -> Result<()> { | 114 | ) -> Result<()> { |
| 133 | let git_config = git_repo.config()?; | 115 | let server_url = git_server_url.parse::<CloneUrl>()?; |
| 116 | |||
| 117 | // if protocol is local - just try local | ||
| 118 | if server_url.protocol() == ServerProtocol::Local { | ||
| 119 | let formatted_url = server_url.format_as(&ServerProtocol::Local, &None)?; | ||
| 120 | term.write_line(format!("fetching from {formatted_url}...").as_str())?; | ||
| 121 | if let Err(error) = fetch_from_git_server_url(git_repo, oids, &formatted_url) { | ||
| 122 | term.write_line( | ||
| 123 | format!("WARNING: failed to fetch from {formatted_url} error:{error}").as_str(), | ||
| 124 | )?; | ||
| 125 | return Err(error).context(format!("{formatted_url}: failed to fetch")); | ||
| 126 | } | ||
| 127 | return Ok(()); | ||
| 128 | } | ||
| 129 | |||
| 130 | term.write_line(format!("fetching from {}...", server_url.domain()).as_str())?; | ||
| 131 | |||
| 132 | // use overide protocol if specified | ||
| 133 | if let Some(protocol) = &decoded_nostr_url.protocol { | ||
| 134 | let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; | ||
| 135 | let res = fetch_from_git_server_url(git_repo, oids, &formatted_url); | ||
| 136 | term.clear_last_lines(1)?; | ||
| 137 | if let Err(error) = res { | ||
| 138 | term.write_line( | ||
| 139 | format!( | ||
| 140 | "WARNING: {formatted_url} failed to fetch over {protocol}{} as specified in nostr url. error:{error}", | ||
| 141 | if let Some(user) = &decoded_nostr_url.user { | ||
| 142 | format!(" with user '{user}'") | ||
| 143 | } else { | ||
| 144 | String::new() | ||
| 145 | } | ||
| 146 | ).as_str(), | ||
| 147 | )?; | ||
| 148 | return Err(error).context(format!("{formatted_url}: failed to fetch")); | ||
| 149 | } | ||
| 150 | return Ok(()); | ||
| 151 | } | ||
| 152 | |||
| 153 | // Try https unauthenticated | ||
| 154 | let formatted_url = server_url.format_as(&ServerProtocol::Https, &None)?; | ||
| 155 | let res = fetch_from_git_server_url_unauthenticated(git_repo, oids, &formatted_url); | ||
| 156 | term.clear_last_lines(1)?; | ||
| 157 | if let Err(unauth_error) = res { | ||
| 158 | term.write_line( | ||
| 159 | format!( | ||
| 160 | "WARNING: {formatted_url} failed to fetch over unauthenticated https. {unauth_error}", | ||
| 161 | ).as_str(), | ||
| 162 | )?; | ||
| 163 | // TODO what about timeout errors? | ||
| 164 | // try over ssh | ||
| 165 | let mut ssh_error = None; | ||
| 166 | if check_ssh_keys() { | ||
| 167 | term.write_line(format!("fetching from {} over ssh...", server_url.domain()).as_str())?; | ||
| 168 | let formatted_url = server_url.format_as(&ServerProtocol::Ssh, &None)?; | ||
| 169 | let res = fetch_from_git_server_url(git_repo, oids, &formatted_url); | ||
| 170 | term.clear_last_lines(1)?; | ||
| 171 | if let Err(error) = res { | ||
| 172 | term.write_line( | ||
| 173 | format!("WARNING: {formatted_url} failed to fetch over ssh. error:{error}") | ||
| 174 | .as_str(), | ||
| 175 | )?; | ||
| 176 | term.write_line( | ||
| 177 | format!("fetching from {} over ssh...", server_url.domain()).as_str(), | ||
| 178 | )?; | ||
| 179 | ssh_error = Some(error); | ||
| 180 | } else { | ||
| 181 | return Ok(()); | ||
| 182 | } | ||
| 183 | } | ||
| 184 | // try over https authenticated | ||
| 185 | term.write_line( | ||
| 186 | format!( | ||
| 187 | "fetching from {} over authenticated https...", | ||
| 188 | server_url.domain() | ||
| 189 | ) | ||
| 190 | .as_str(), | ||
| 191 | )?; | ||
| 192 | let formatted_url = server_url.format_as(&ServerProtocol::Ssh, &None)?; | ||
| 193 | let res = fetch_from_git_server_url(git_repo, oids, &formatted_url); | ||
| 194 | term.clear_last_lines(1)?; | ||
| 195 | if let Err(auth_https_error) = res { | ||
| 196 | term.write_line( | ||
| 197 | format!("WARNING: {formatted_url} failed to fetch over authenticated https. error:{auth_https_error}",) | ||
| 198 | .as_str(), | ||
| 199 | )?; | ||
| 200 | let error_message = format!( | ||
| 201 | "{} failed to fetch over unauthenticated https ({unauth_error}), ssh ({}) and authenticated https ({auth_https_error})", | ||
| 202 | server_url.format_as(&ServerProtocol::Unspecified, &None)?, | ||
| 203 | ssh_error.unwrap_or(anyhow!("no keys found")) | ||
| 204 | ); | ||
| 205 | |||
| 206 | bail!(error_message) | ||
| 207 | } | ||
| 208 | } | ||
| 209 | Ok(()) | ||
| 210 | } | ||
| 134 | 211 | ||
| 212 | fn fetch_from_git_server_url( | ||
| 213 | git_repo: &Repository, | ||
| 214 | oids: &[String], | ||
| 215 | git_server_url: &str, | ||
| 216 | ) -> Result<()> { | ||
| 217 | let git_config = git_repo.config()?; | ||
| 135 | let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; | 218 | let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; |
| 136 | // authentication may be required (and will be requird if clone url is ssh) | ||
| 137 | let auth = GitAuthenticator::default(); | 219 | let auth = GitAuthenticator::default(); |
| 138 | let mut fetch_options = git2::FetchOptions::new(); | 220 | let mut fetch_options = git2::FetchOptions::new(); |
| 139 | let mut remote_callbacks = git2::RemoteCallbacks::new(); | 221 | let mut remote_callbacks = git2::RemoteCallbacks::new(); |
| 222 | // TODO status update callback | ||
| 140 | remote_callbacks.credentials(auth.credentials(&git_config)); | 223 | remote_callbacks.credentials(auth.credentials(&git_config)); |
| 141 | fetch_options.remote_callbacks(remote_callbacks); | 224 | fetch_options.remote_callbacks(remote_callbacks); |
| 142 | git_server_remote.download(oids, Some(&mut fetch_options))?; | 225 | git_server_remote.download(oids, Some(&mut fetch_options))?; |
| 143 | git_server_remote.disconnect()?; | 226 | git_server_remote.disconnect()?; |
| 144 | Ok(()) | 227 | Ok(()) |
| 145 | } | 228 | } |
| 229 | |||
| 230 | fn fetch_from_git_server_url_unauthenticated( | ||
| 231 | git_repo: &Repository, | ||
| 232 | oids: &[String], | ||
| 233 | git_server_url: &str, | ||
| 234 | ) -> Result<()> { | ||
| 235 | let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; | ||
| 236 | let mut fetch_options = git2::FetchOptions::new(); | ||
| 237 | let remote_callbacks = git2::RemoteCallbacks::new(); | ||
| 238 | // TODO status update callback | ||
| 239 | fetch_options.remote_callbacks(remote_callbacks); | ||
| 240 | git_server_remote.download(oids, Some(&mut fetch_options))?; | ||
| 241 | git_server_remote.disconnect()?; | ||
| 242 | Ok(()) | ||
| 243 | } | ||
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<()> { | |||
| 75 | println!("unsupported"); | 75 | println!("unsupported"); |
| 76 | } | 76 | } |
| 77 | ["fetch", oid, refstr] => { | 77 | ["fetch", oid, refstr] => { |
| 78 | fetch::run_fetch(&git_repo, &repo_ref, &stdin, oid, refstr).await?; | 78 | fetch::run_fetch(&git_repo, &repo_ref, &decoded_nostr_url, &stdin, oid, refstr).await?; |
| 79 | } | 79 | } |
| 80 | ["push", refspec] => { | 80 | ["push", refspec] => { |
| 81 | push::run_push( | 81 | 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}; | |||
| 11 | use crate::git_events::{get_commit_id_from_patch, tag_value}; | 11 | use crate::git_events::{get_commit_id_from_patch, tag_value}; |
| 12 | pub mod identify_ahead_behind; | 12 | pub mod identify_ahead_behind; |
| 13 | pub mod nostr_url; | 13 | pub mod nostr_url; |
| 14 | pub mod utils; | ||
| 14 | 15 | ||
| 15 | pub struct Repo { | 16 | pub struct Repo { |
| 16 | pub git_repo: git2::Repository, | 17 | 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 @@ | |||
| 1 | use std::path::Path; | ||
| 2 | |||
| 3 | use directories::UserDirs; | ||
| 4 | |||
| 5 | pub fn check_ssh_keys() -> bool { | ||
| 6 | // Get the user's home directory using the directories crate | ||
| 7 | if let Some(user_dirs) = UserDirs::new() { | ||
| 8 | let ssh_dir = user_dirs.home_dir().join(".ssh"); | ||
| 9 | let key_files = vec![ | ||
| 10 | "id_rsa", | ||
| 11 | "id_ecdsa", | ||
| 12 | "id_ed25519", | ||
| 13 | "id_rsa.pub", | ||
| 14 | "id_ecdsa.pub", | ||
| 15 | "id_ed25519.pub", | ||
| 16 | ]; | ||
| 17 | |||
| 18 | for key in key_files { | ||
| 19 | if Path::new(&ssh_dir.join(key)).exists() { | ||
| 20 | return true; // At least one key exists | ||
| 21 | } | ||
| 22 | } | ||
| 23 | } | ||
| 24 | false // No keys found | ||
| 25 | } | ||