upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-09-06 10:43:34 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-06 11:05:05 +0100
commit935bc0ca630d7964082966e4c0caeb255f5a4f57 (patch)
tree21a2eec2abf81a54714c40c974ecc3c4e9170e11
parent92e06d80540a12d91e23ed6e557cc074d90d4d66 (diff)
fix(remote): improve protocol selction / fallback
abstract the protocols and order to try under different scenarios add some additional scenarios eg hardcoded http tweak error reporting
-rw-r--r--src/bin/git_remote_nostr/fetch.rs116
-rw-r--r--src/bin/git_remote_nostr/utils.rs93
-rw-r--r--src/lib/git/nostr_url.rs25
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 @@
1use std::io::Stdin; 1use std::io::Stdin;
2 2
3use anyhow::{anyhow, bail, Context, Result}; 3use anyhow::{bail, Result};
4use auth_git2::GitAuthenticator; 4use auth_git2::GitAuthenticator;
5use git2::Repository; 5use git2::Repository;
6use ngit::{ 6use ngit::{
@@ -16,6 +16,7 @@ use ngit::{
16 16
17use crate::utils::{ 17use 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
21pub async fn run_fetch( 22pub 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
253pub 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
273pub 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)]
299mod 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}
19impl fmt::Display for ServerProtocol { 21impl 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 {
248impl CloneUrl { 252impl 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://",