diff options
| -rw-r--r-- | src/bin/git_remote_nostr/fetch.rs | 116 | ||||
| -rw-r--r-- | src/bin/git_remote_nostr/utils.rs | 93 | ||||
| -rw-r--r-- | src/lib/git/nostr_url.rs | 25 |
3 files changed, 143 insertions, 91 deletions
diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs index 6836456..f591fed 100644 --- a/src/bin/git_remote_nostr/fetch.rs +++ b/src/bin/git_remote_nostr/fetch.rs | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | use std::io::Stdin; | 1 | use std::io::Stdin; |
| 2 | 2 | ||
| 3 | use anyhow::{anyhow, bail, Context, Result}; | 3 | use anyhow::{bail, 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::{ |
| @@ -16,6 +16,7 @@ use ngit::{ | |||
| 16 | 16 | ||
| 17 | use crate::utils::{ | 17 | use crate::utils::{ |
| 18 | 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, |
| 19 | get_read_protocols_to_try, join_with_and, | ||
| 19 | }; | 20 | }; |
| 20 | 21 | ||
| 21 | pub async fn run_fetch( | 22 | pub async fn run_fetch( |
| @@ -58,10 +59,10 @@ pub async fn run_fetch( | |||
| 58 | && !errors.is_empty() | 59 | && !errors.is_empty() |
| 59 | { | 60 | { |
| 60 | bail!( | 61 | bail!( |
| 61 | "failed to fetch objects in nostr state event from:\r\n{}", | 62 | "fetch: failed to fetch objects in nostr state event from:\r\n{}", |
| 62 | errors | 63 | errors |
| 63 | .iter() | 64 | .iter() |
| 64 | .map(std::string::ToString::to_string) | 65 | .map(|e| format!(" - {e}")) |
| 65 | .collect::<Vec<String>>() | 66 | .collect::<Vec<String>>() |
| 66 | .join("\r\n") | 67 | .join("\r\n") |
| 67 | ); | 68 | ); |
| @@ -114,97 +115,45 @@ fn fetch_from_git_server( | |||
| 114 | ) -> Result<()> { | 115 | ) -> Result<()> { |
| 115 | let server_url = git_server_url.parse::<CloneUrl>()?; | 116 | let server_url = git_server_url.parse::<CloneUrl>()?; |
| 116 | 117 | ||
| 117 | // if protocol is local - just try local | 118 | let protocols_to_attempt = get_read_protocols_to_try(&server_url, decoded_nostr_url); |
| 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 | 119 | ||
| 130 | term.write_line(format!("fetching from {}...", server_url.domain()).as_str())?; | 120 | let mut failed_protocols = vec![]; |
| 121 | let mut success = false; | ||
| 122 | for protocol in &protocols_to_attempt { | ||
| 123 | term.write_line( | ||
| 124 | format!("fetching from {} over {protocol}...", server_url.domain(),).as_str(), | ||
| 125 | )?; | ||
| 131 | 126 | ||
| 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)?; | 127 | 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); | 128 | let res = if [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol) { |
| 129 | fetch_from_git_server_url_unauthenticated(git_repo, oids, &formatted_url) | ||
| 130 | } else { | ||
| 131 | fetch_from_git_server_url(git_repo, oids, &formatted_url) | ||
| 132 | }; | ||
| 136 | term.clear_last_lines(1)?; | 133 | term.clear_last_lines(1)?; |
| 137 | if let Err(error) = res { | 134 | if let Err(error) = res { |
| 138 | term.write_line( | 135 | term.write_line( |
| 139 | format!( | 136 | format!("fetch: {formatted_url} failed over {protocol}: {error}").as_str(), |
| 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 | )?; | 137 | )?; |
| 148 | return Err(error).context(format!("{formatted_url}: failed to fetch")); | 138 | failed_protocols.push(protocol); |
| 149 | } | 139 | } else { |
| 150 | return Ok(()); | 140 | success = true; |
| 151 | } | 141 | if !failed_protocols.is_empty() { |
| 152 | 142 | term.write_line(format!("fetch: succeeded over {protocol}").as_str())?; | |
| 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 | } | 143 | } |
| 183 | } | 144 | } |
| 184 | // try over https authenticated | 145 | } |
| 185 | term.write_line( | 146 | if !success { |
| 186 | format!( | 147 | if decoded_nostr_url.protocol.is_some() { |
| 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( | 148 | term.write_line( |
| 197 | format!("WARNING: {formatted_url} failed to fetch over authenticated https. error:{auth_https_error}",) | 149 | "fetch: protocol override in nostr url so not attempting with any other protocols", |
| 198 | .as_str(), | ||
| 199 | )?; | 150 | )?; |
| 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 | } | 151 | } |
| 152 | bail!( | ||
| 153 | "{} failed over {}", | ||
| 154 | server_url.domain(), | ||
| 155 | join_with_and(&failed_protocols) | ||
| 156 | ); | ||
| 208 | } | 157 | } |
| 209 | Ok(()) | 158 | Ok(()) |
| 210 | } | 159 | } |
| @@ -214,6 +163,9 @@ fn fetch_from_git_server_url( | |||
| 214 | oids: &[String], | 163 | oids: &[String], |
| 215 | git_server_url: &str, | 164 | git_server_url: &str, |
| 216 | ) -> Result<()> { | 165 | ) -> Result<()> { |
| 166 | if git_server_url.parse::<CloneUrl>()?.protocol() == ServerProtocol::Ssh && !check_ssh_keys() { | ||
| 167 | bail!("no ssh keys found"); | ||
| 168 | } | ||
| 217 | let git_config = git_repo.config()?; | 169 | let git_config = git_repo.config()?; |
| 218 | let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; | 170 | let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; |
| 219 | let auth = GitAuthenticator::default(); | 171 | let auth = GitAuthenticator::default(); |
diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs index c53c34f..a31dcbf 100644 --- a/src/bin/git_remote_nostr/utils.rs +++ b/src/bin/git_remote_nostr/utils.rs | |||
| @@ -10,7 +10,10 @@ use ngit::{ | |||
| 10 | get_all_proposal_patch_events_from_cache, get_events_from_cache, | 10 | get_all_proposal_patch_events_from_cache, get_events_from_cache, |
| 11 | get_proposals_and_revisions_from_cache, | 11 | get_proposals_and_revisions_from_cache, |
| 12 | }, | 12 | }, |
| 13 | git::{Repo, RepoActions}, | 13 | git::{ |
| 14 | nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, | ||
| 15 | Repo, RepoActions, | ||
| 16 | }, | ||
| 14 | git_events::{ | 17 | git_events::{ |
| 15 | event_is_revision_root, event_to_cover_letter, get_most_recent_patch_with_ancestors, | 18 | event_is_revision_root, event_to_cover_letter, get_most_recent_patch_with_ancestors, |
| 16 | status_kinds, | 19 | status_kinds, |
| @@ -246,3 +249,91 @@ pub fn find_proposal_and_patches_by_branch_name<'a>( | |||
| 246 | } | 249 | } |
| 247 | }) | 250 | }) |
| 248 | } | 251 | } |
| 252 | |||
| 253 | pub fn join_with_and<T: ToString>(items: &[T]) -> String { | ||
| 254 | match items.len() { | ||
| 255 | 0 => String::new(), | ||
| 256 | 1 => items[0].to_string(), | ||
| 257 | _ => { | ||
| 258 | let last_item = items.last().unwrap().to_string(); | ||
| 259 | let rest = &items[..items.len() - 1]; | ||
| 260 | format!( | ||
| 261 | "{} and {}", | ||
| 262 | rest.iter() | ||
| 263 | .map(std::string::ToString::to_string) | ||
| 264 | .collect::<Vec<_>>() | ||
| 265 | .join(", "), | ||
| 266 | last_item | ||
| 267 | ) | ||
| 268 | } | ||
| 269 | } | ||
| 270 | } | ||
| 271 | |||
| 272 | /// get an ordered vector of server protocols to attempt | ||
| 273 | pub fn get_read_protocols_to_try( | ||
| 274 | server_url: &CloneUrl, | ||
| 275 | decoded_nostr_url: &NostrUrlDecoded, | ||
| 276 | ) -> Vec<ServerProtocol> { | ||
| 277 | if server_url.protocol() == ServerProtocol::Filesystem { | ||
| 278 | vec![(ServerProtocol::Filesystem)] | ||
| 279 | } else if let Some(protocol) = &decoded_nostr_url.protocol { | ||
| 280 | vec![protocol.clone()] | ||
| 281 | } else if server_url.protocol() == ServerProtocol::Http { | ||
| 282 | vec![ | ||
| 283 | ServerProtocol::UnauthHttp, | ||
| 284 | ServerProtocol::Ssh, | ||
| 285 | ServerProtocol::Http, | ||
| 286 | ] | ||
| 287 | } else if server_url.protocol() == ServerProtocol::Ftp { | ||
| 288 | vec![ServerProtocol::Ftp, ServerProtocol::Ssh] | ||
| 289 | } else { | ||
| 290 | vec![ | ||
| 291 | ServerProtocol::UnauthHttps, | ||
| 292 | ServerProtocol::Ssh, | ||
| 293 | ServerProtocol::Https, | ||
| 294 | ] | ||
| 295 | } | ||
| 296 | } | ||
| 297 | |||
| 298 | #[cfg(test)] | ||
| 299 | mod tests { | ||
| 300 | use super::*; | ||
| 301 | mod join_with_and { | ||
| 302 | use super::*; | ||
| 303 | #[test] | ||
| 304 | fn test_empty() { | ||
| 305 | let items: Vec<&str> = vec![]; | ||
| 306 | assert_eq!(join_with_and(&items), ""); | ||
| 307 | } | ||
| 308 | |||
| 309 | #[test] | ||
| 310 | fn test_single_item() { | ||
| 311 | let items = vec!["apple"]; | ||
| 312 | assert_eq!(join_with_and(&items), "apple"); | ||
| 313 | } | ||
| 314 | |||
| 315 | #[test] | ||
| 316 | fn test_two_items() { | ||
| 317 | let items = vec!["apple", "banana"]; | ||
| 318 | assert_eq!(join_with_and(&items), "apple and banana"); | ||
| 319 | } | ||
| 320 | |||
| 321 | #[test] | ||
| 322 | fn test_three_items() { | ||
| 323 | let items = vec!["apple", "banana", "cherry"]; | ||
| 324 | assert_eq!(join_with_and(&items), "apple, banana and cherry"); | ||
| 325 | } | ||
| 326 | |||
| 327 | #[test] | ||
| 328 | fn test_four_items() { | ||
| 329 | let items = vec!["apple", "banana", "cherry", "date"]; | ||
| 330 | assert_eq!(join_with_and(&items), "apple, banana, cherry and date"); | ||
| 331 | } | ||
| 332 | |||
| 333 | #[test] | ||
| 334 | fn test_multiple_items() { | ||
| 335 | let items = vec!["one", "two", "three", "four", "five"]; | ||
| 336 | assert_eq!(join_with_and(&items), "one, two, three, four and five"); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs index c7e1bf2..ea8d221 100644 --- a/src/lib/git/nostr_url.rs +++ b/src/lib/git/nostr_url.rs | |||
| @@ -12,9 +12,11 @@ pub enum ServerProtocol { | |||
| 12 | Http, | 12 | Http, |
| 13 | Git, | 13 | Git, |
| 14 | Ftp, | 14 | Ftp, |
| 15 | Local, | 15 | Filesystem, |
| 16 | #[default] | 16 | #[default] |
| 17 | Unspecified, | 17 | Unspecified, |
| 18 | UnauthHttps, // used for read to enable non-interactive failures over https | ||
| 19 | UnauthHttp, // used for read to enable non-interactive failures over https | ||
| 18 | } | 20 | } |
| 19 | impl fmt::Display for ServerProtocol { | 21 | impl fmt::Display for ServerProtocol { |
| 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| @@ -24,8 +26,10 @@ impl fmt::Display for ServerProtocol { | |||
| 24 | ServerProtocol::Ftp => write!(f, "ftp"), | 26 | ServerProtocol::Ftp => write!(f, "ftp"), |
| 25 | ServerProtocol::Ssh => write!(f, "ssh"), | 27 | ServerProtocol::Ssh => write!(f, "ssh"), |
| 26 | ServerProtocol::Git => write!(f, "git"), | 28 | ServerProtocol::Git => write!(f, "git"), |
| 27 | ServerProtocol::Local => write!(f, "local"), | 29 | ServerProtocol::Filesystem => write!(f, "filesystem"), |
| 28 | ServerProtocol::Unspecified => write!(f, "unsepcified"), | 30 | ServerProtocol::Unspecified => write!(f, "unsepcified"), |
| 31 | ServerProtocol::UnauthHttps => write!(f, "unauthenticated https"), | ||
| 32 | ServerProtocol::UnauthHttp => write!(f, "unauthenticated http"), | ||
| 29 | } | 33 | } |
| 30 | } | 34 | } |
| 31 | } | 35 | } |
| @@ -168,7 +172,7 @@ impl FromStr for CloneUrl { | |||
| 168 | if s.starts_with('/') || s.starts_with("./") || s.starts_with("../") { | 172 | if s.starts_with('/') || s.starts_with("./") || s.starts_with("../") { |
| 169 | return Ok(Self { | 173 | return Ok(Self { |
| 170 | original_string: s.to_string(), | 174 | original_string: s.to_string(), |
| 171 | protocol: ServerProtocol::Local, | 175 | protocol: ServerProtocol::Filesystem, |
| 172 | ..CloneUrl::default() | 176 | ..CloneUrl::default() |
| 173 | }); | 177 | }); |
| 174 | } | 178 | } |
| @@ -248,13 +252,16 @@ fn contains_port(s: &str) -> bool { | |||
| 248 | impl CloneUrl { | 252 | impl CloneUrl { |
| 249 | pub fn format_as(&self, protocol: &ServerProtocol, user: &Option<String>) -> Result<String> { | 253 | pub fn format_as(&self, protocol: &ServerProtocol, user: &Option<String>) -> Result<String> { |
| 250 | // Check for incompatible protocol conversions | 254 | // Check for incompatible protocol conversions |
| 251 | if *protocol == ServerProtocol::Local { | 255 | if *protocol == ServerProtocol::Filesystem { |
| 252 | if self.protocol == ServerProtocol::Local { | 256 | if self.protocol == ServerProtocol::Filesystem { |
| 253 | // If converting from Local to Local, return the original string | 257 | // If converting from Filesystem to Filesystem, return the original string |
| 254 | return Ok(self.original_string.clone()); | 258 | return Ok(self.original_string.clone()); |
| 255 | } else { | 259 | } else { |
| 256 | // If converting to Local from any other protocol, return an error | 260 | // If converting to Filesystem from any other protocol, return an error |
| 257 | bail!("Cannot convert to Local protocol from {:?}", self.protocol); | 261 | bail!( |
| 262 | "Cannot convert to Filesystem protocol from {:?}", | ||
| 263 | self.protocol | ||
| 264 | ); | ||
| 258 | } | 265 | } |
| 259 | } | 266 | } |
| 260 | 267 | ||
| @@ -262,7 +269,9 @@ impl CloneUrl { | |||
| 262 | "{}{}", | 269 | "{}{}", |
| 263 | match protocol { | 270 | match protocol { |
| 264 | ServerProtocol::Https => "https://", | 271 | ServerProtocol::Https => "https://", |
| 272 | ServerProtocol::UnauthHttps => "https://", | ||
| 265 | ServerProtocol::Http => "http://", | 273 | ServerProtocol::Http => "http://", |
| 274 | ServerProtocol::UnauthHttp => "http://", | ||
| 266 | ServerProtocol::Git => "git://", | 275 | ServerProtocol::Git => "git://", |
| 267 | ServerProtocol::Ftp => "ftp://", | 276 | ServerProtocol::Ftp => "ftp://", |
| 268 | ServerProtocol::Ssh => "ssh://", | 277 | ServerProtocol::Ssh => "ssh://", |