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 07:43:13 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-06 07:43:13 +0100
commitfad3f8fddffb55597432243e129e4012034b3627 (patch)
treefc4b3d07a1cebc020d5a945956a95722cbd2852f
parentc4c262a5e9bfeb30bc0106d9ea51dfce7e4fa1f3 (diff)
add `CloneUrl`
to enable remote to interact with git servers over a range of specified protocols
-rw-r--r--src/lib/git/nostr_url.rs547
1 files changed, 544 insertions, 3 deletions
diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs
index d1fee2e..f46e751 100644
--- a/src/lib/git/nostr_url.rs
+++ b/src/lib/git/nostr_url.rs
@@ -1,15 +1,33 @@
1use std::collections::HashSet; 1use core::fmt;
2use std::{collections::HashSet, str::FromStr};
2 3
3use anyhow::{bail, Context, Result}; 4use anyhow::{anyhow, bail, Context, Result};
4use nostr::nips::nip01::Coordinate; 5use nostr::nips::nip01::Coordinate;
5use nostr_sdk::{PublicKey, Url}; 6use nostr_sdk::{PublicKey, Url};
6 7
7#[derive(Debug, PartialEq)] 8#[derive(Debug, PartialEq, Default, Clone)]
8pub enum ServerProtocol { 9pub enum ServerProtocol {
9 Ssh, 10 Ssh,
10 Https, 11 Https,
11 Http, 12 Http,
12 Git, 13 Git,
14 Ftp,
15 Local,
16 #[default]
17 Unspecified,
18}
19impl fmt::Display for ServerProtocol {
20 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
21 match self {
22 ServerProtocol::Http => write!(f, "HTTP"),
23 ServerProtocol::Https => write!(f, "HTTPS"),
24 ServerProtocol::Ftp => write!(f, "FTP"),
25 ServerProtocol::Ssh => write!(f, "SSH"),
26 ServerProtocol::Git => write!(f, "GIT"),
27 ServerProtocol::Local => write!(f, "LOCAL"),
28 ServerProtocol::Unspecified => write!(f, "Unsepcified"),
29 }
30 }
13} 31}
14 32
15#[derive(Debug, PartialEq)] 33#[derive(Debug, PartialEq)]
@@ -130,6 +148,185 @@ impl std::str::FromStr for NostrUrlDecoded {
130 } 148 }
131} 149}
132 150
151#[derive(Debug, PartialEq, Default)]
152pub struct CloneUrl {
153 original_string: String,
154 host: String,
155 path: String,
156 parameters: Option<String>,
157 protocol: ServerProtocol,
158 user: Option<String>,
159 port: Option<u16>,
160 fragment: Option<String>,
161}
162
163impl FromStr for CloneUrl {
164 type Err = anyhow::Error;
165
166 fn from_str(s: &str) -> Result<Self> {
167 // Check if the input is a local path
168 if s.starts_with('/') || s.starts_with("./") || s.starts_with("../") {
169 return Ok(Self {
170 original_string: s.to_string(),
171 protocol: ServerProtocol::Local,
172 ..CloneUrl::default()
173 });
174 }
175 let url_str = if s.contains("://") {
176 s.to_string() // Use the original string
177 } else {
178 let protocol = // Check for the SSH format user@host:path and convert to ssh://
179 if s.contains('@') && s
180 .split('@')
181 .nth(0)
182 .map_or(false, |part| !part.contains('/')) {
183 "ssh"
184 }
185 // otherwise assume unspecified
186 else {
187 "unspecified"
188 };
189 format!(
190 "{protocol}://{}",
191 if contains_port(s) {
192 s.to_string()
193 } else {
194 s.replace(":/", "/").replace(':', "/")
195 }
196 )
197 };
198
199 let url = Url::parse(&url_str).context("Failed to parse URL")?;
200
201 let protocol = match url.scheme() {
202 "ssh" => ServerProtocol::Ssh,
203 "https" => ServerProtocol::Https,
204 "http" => ServerProtocol::Http,
205 "git" => ServerProtocol::Git,
206 "ftp" => ServerProtocol::Ftp,
207 "unspecified" => ServerProtocol::Unspecified,
208 _ => return Err(anyhow::anyhow!("Unsupported protocol: {}", url.scheme())),
209 };
210
211 let host = url.host_str().context("Missing host")?.to_string();
212 let path = url.path().to_string();
213 let parameters = url.query().map(|s| s.to_string());
214 let port = url.port();
215
216 let fragment = url.fragment().map(|s| s.to_string());
217
218 let user = if url.username().is_empty() {
219 None
220 } else {
221 Some(url.username().to_string())
222 };
223
224 Ok(CloneUrl {
225 original_string: s.to_string(),
226 host,
227 path,
228 parameters,
229 protocol,
230 user,
231 port,
232 fragment,
233 })
234 }
235}
236
237fn contains_port(s: &str) -> bool {
238 if let Some(after_host) = s.split('@').nth(1).unwrap_or(s).split(':').nth(1) {
239 if let Some(port) = after_host.split('/').next() {
240 if port.parse::<u16>().is_ok() {
241 return true;
242 }
243 }
244 }
245 false
246}
247
248impl CloneUrl {
249 pub fn format_as(&self, protocol: &ServerProtocol, user: &Option<String>) -> Result<String> {
250 // Check for incompatible protocol conversions
251 if *protocol == ServerProtocol::Local {
252 if self.protocol == ServerProtocol::Local {
253 // If converting from Local to Local, return the original string
254 return Ok(self.original_string.clone());
255 } else {
256 // If converting to Local from any other protocol, return an error
257 bail!("Cannot convert to Local protocol from {:?}", self.protocol);
258 }
259 }
260
261 let mut url = Url::parse(&format!(
262 "{}{}",
263 match protocol {
264 ServerProtocol::Https => "https://",
265 ServerProtocol::Http => "http://",
266 ServerProtocol::Git => "git://",
267 ServerProtocol::Ftp => "ftp://",
268 ServerProtocol::Ssh => "ssh://",
269 ServerProtocol::Unspecified => "https://",
270 _ => bail!("unsupported protocol"),
271 },
272 &self.host
273 ))
274 .context("Failed to parse base URL")?; // Start with the specified scheme
275
276 url.set_path(&self.path);
277
278 // Set the port if present
279 if let Some(port) = self.port {
280 url.set_port(Some(port))
281 .map_err(|_| anyhow!("cannot add port"))?;
282 }
283
284 // Set the query parameters if present
285 if let Some(ref parameters) = self.parameters {
286 url.set_query(Some(parameters));
287 }
288
289 // Set the fragment if present
290 if let Some(ref fragment) = self.fragment {
291 url.set_fragment(Some(fragment));
292 }
293
294 let mut formatted_url = url.to_string();
295
296 if *protocol == ServerProtocol::Ssh {
297 formatted_url = formatted_url.replace(
298 "ssh://",
299 format!("{}@", user.as_deref().unwrap_or("git")).as_str(),
300 );
301 if !contains_port(&formatted_url) {
302 formatted_url = replace_first_occurrence(&formatted_url, '/', ':');
303 }
304 } else if *protocol == ServerProtocol::Unspecified {
305 formatted_url = formatted_url.replace("https://", "");
306 }
307
308 Ok(strip_trailing_slash(&formatted_url))
309 }
310 pub fn domain(&self) -> String {
311 self.host.to_string()
312 }
313 pub fn protocol(&self) -> ServerProtocol {
314 self.protocol.clone()
315 }
316}
317
318fn replace_first_occurrence(s: &str, target: char, replacement: char) -> String {
319 let mut result = s.to_string();
320 if let Some(index) = result.find(target) {
321 result.replace_range(index..index + 1, &replacement.to_string());
322 }
323 result
324}
325
326fn strip_trailing_slash(s: &str) -> String {
327 s.strip_suffix('/').unwrap_or(s).to_string()
328}
329
133/** produce error when using local repo or custom protocols */ 330/** produce error when using local repo or custom protocols */
134pub fn convert_clone_url_to_https(url: &str) -> Result<String> { 331pub fn convert_clone_url_to_https(url: &str) -> Result<String> {
135 // Strip credentials if present 332 // Strip credentials if present
@@ -191,6 +388,350 @@ fn strip_credentials(url: &str) -> String {
191#[cfg(test)] 388#[cfg(test)]
192mod tests { 389mod tests {
193 use super::*; 390 use super::*;
391
392 mod clone_url_from_str_format_as {
393 use super::*;
394
395 mod when_user_specified {
396 use super::*;
397
398 mod but_not_in_original_url {
399 use super::*;
400
401 #[test]
402 fn https_to_https_ignores_user() {
403 let result = "https://github.com/user/repo.git"
404 .parse::<CloneUrl>()
405 .unwrap()
406 .format_as(&ServerProtocol::Https, &Some("user1".to_string()))
407 .unwrap();
408 assert_eq!(result, "https://github.com/user/repo.git");
409 }
410 #[test]
411 fn https_to_ssh_uses_specified_user() {
412 let result = "https://github.com/user/repo.git"
413 .parse::<CloneUrl>()
414 .unwrap()
415 .format_as(&ServerProtocol::Ssh, &Some("user1".to_string()))
416 .unwrap();
417 assert_eq!(result, "user1@github.com:user/repo.git");
418 }
419 }
420 mod and_a_different_user_in_original_url {
421 use super::*;
422
423 #[test]
424 fn ssh_uses_specified_user() {
425 let result = "user2@github.com/user/repo.git"
426 .parse::<CloneUrl>()
427 .unwrap()
428 .format_as(&ServerProtocol::Ssh, &Some("user1".to_string()))
429 .unwrap();
430 assert_eq!(result, "user1@github.com:user/repo.git");
431 }
432 }
433 }
434
435 #[test]
436 fn format_as_ssh_defaults_to_git_user() {
437 let result = "https://github.com/user/repo.git"
438 .parse::<CloneUrl>()
439 .unwrap()
440 .format_as(&ServerProtocol::Ssh, &None)
441 .unwrap();
442 assert_eq!(result, "git@github.com:user/repo.git");
443 }
444
445 #[test]
446 fn format_as_ssh_includes_port() {
447 let result = "https://github.com:1000/user/repo.git"
448 .parse::<CloneUrl>()
449 .unwrap()
450 .format_as(&ServerProtocol::Ssh, &None)
451 .unwrap();
452 assert_eq!(result, "git@github.com:1000/user/repo.git");
453 }
454
455 #[test]
456 fn format_as_unspecified_ommits_prefix() {
457 let result = "https://github.com/user/repo.git"
458 .parse::<CloneUrl>()
459 .unwrap()
460 .format_as(&ServerProtocol::Unspecified, &None)
461 .unwrap();
462 assert_eq!(result, "github.com/user/repo.git");
463 }
464
465 mod input_all_formats_to_from_str_and_correctly_format_as_https {
466 use super::*;
467
468 #[test]
469 fn test_https_url() {
470 let result = "https://github.com/user/repo.git"
471 .parse::<CloneUrl>()
472 .unwrap()
473 .format_as(&ServerProtocol::Https, &None)
474 .unwrap();
475 assert_eq!(result, "https://github.com/user/repo.git");
476 }
477
478 mod with_unspecified_and_additional_features {
479 use super::*;
480
481 #[test]
482 fn port() {
483 let result = "github.com:1000/user/repo.git"
484 .parse::<CloneUrl>()
485 .unwrap()
486 .format_as(&ServerProtocol::Https, &None)
487 .unwrap();
488 assert_eq!(result, "https://github.com:1000/user/repo.git");
489 }
490
491 #[test]
492 fn colon() {
493 let result = "github.com:user/repo.git"
494 .parse::<CloneUrl>()
495 .unwrap()
496 .format_as(&ServerProtocol::Https, &None)
497 .unwrap();
498 assert_eq!(result, "https://github.com/user/repo.git");
499 }
500
501 #[test]
502 fn path_with_fragment() {
503 let result = "github.com/user/repo.git#readme"
504 .parse::<CloneUrl>()
505 .unwrap()
506 .format_as(&ServerProtocol::Https, &None)
507 .unwrap();
508 assert_eq!(result, "https://github.com/user/repo.git#readme");
509 }
510
511 #[test]
512 fn path_with_parameters() {
513 let result = "github.com/user/repo.git?ref=main"
514 .parse::<CloneUrl>()
515 .unwrap()
516 .format_as(&ServerProtocol::Https, &None)
517 .unwrap();
518 assert_eq!(result, "https://github.com/user/repo.git?ref=main");
519 }
520
521 #[test]
522 fn port_with_parameters_and_fragment() {
523 let result = "github.com:2222/repo.git?version=1.0#section1"
524 .parse::<CloneUrl>()
525 .unwrap()
526 .format_as(&ServerProtocol::Https, &None)
527 .unwrap();
528 assert_eq!(
529 result,
530 "https://github.com:2222/repo.git?version=1.0#section1"
531 );
532 }
533 }
534
535 mod with_https_and_additional_features {
536 use super::*;
537
538 #[test]
539 fn credentials_and_they_are_stripped() {
540 let result = "https://username:password@github.com/user/repo.git"
541 .parse::<CloneUrl>()
542 .unwrap()
543 .format_as(&ServerProtocol::Https, &None)
544 .unwrap();
545 assert_eq!(result, "https://github.com/user/repo.git");
546 }
547
548 #[test]
549 fn port() {
550 let result = "https://github.com:1000/user/repo.git"
551 .parse::<CloneUrl>()
552 .unwrap()
553 .format_as(&ServerProtocol::Https, &None)
554 .unwrap();
555 assert_eq!(result, "https://github.com:1000/user/repo.git");
556 }
557
558 #[test]
559 fn path_with_fragment() {
560 let result = "https://github.com/user/repo.git#readme"
561 .parse::<CloneUrl>()
562 .unwrap()
563 .format_as(&ServerProtocol::Https, &None)
564 .unwrap();
565 assert_eq!(result, "https://github.com/user/repo.git#readme");
566 }
567
568 #[test]
569 fn path_with_parameters() {
570 let result = "https://github.com/user/repo.git?ref=main"
571 .parse::<CloneUrl>()
572 .unwrap()
573 .format_as(&ServerProtocol::Https, &None)
574 .unwrap();
575 assert_eq!(result, "https://github.com/user/repo.git?ref=main");
576 }
577
578 #[test]
579 fn port_with_parameters_and_fragment() {
580 let result = "https://github.com:2222/repo.git?version=1.0#section1"
581 .parse::<CloneUrl>()
582 .unwrap()
583 .format_as(&ServerProtocol::Https, &None)
584 .unwrap();
585 assert_eq!(
586 result,
587 "https://github.com:2222/repo.git?version=1.0#section1"
588 );
589 }
590 }
591
592 #[test]
593 fn test_http_url() {
594 let result = "http://github.com/user/repo.git"
595 .parse::<CloneUrl>()
596 .unwrap()
597 .format_as(&ServerProtocol::Https, &None)
598 .unwrap();
599 assert_eq!(result, "https://github.com/user/repo.git");
600 }
601
602 mod ssh_input {
603 use super::*;
604
605 #[test]
606 fn test_git_at_url() {
607 let result = "git@github.com:user/repo.git"
608 .parse::<CloneUrl>()
609 .unwrap()
610 .format_as(&ServerProtocol::Https, &None)
611 .unwrap();
612 assert_eq!(result, "https://github.com/user/repo.git");
613 }
614
615 #[test]
616 fn test_user_at_url() {
617 let result = "user1@github.com:user/repo.git"
618 .parse::<CloneUrl>()
619 .unwrap()
620 .format_as(&ServerProtocol::Https, &None)
621 .unwrap();
622 assert_eq!(result, "https://github.com/user/repo.git");
623 }
624 #[test]
625 fn path_has_colon_slash_prefix() {
626 let result = "user1@github.com:/user/repo.git"
627 .parse::<CloneUrl>()
628 .unwrap()
629 .format_as(&ServerProtocol::Https, &None)
630 .unwrap();
631 assert_eq!(result, "https://github.com/user/repo.git");
632 }
633
634 #[test]
635 fn port_specified_with_path() {
636 let result = "user@github.com:2222/repo.git"
637 .parse::<CloneUrl>()
638 .unwrap()
639 .format_as(&ServerProtocol::Https, &None)
640 .unwrap();
641 assert_eq!(result, "https://github.com:2222/repo.git");
642 }
643
644 #[test]
645 fn port_specified_without_path() {
646 let result = "user@github.com:2222"
647 .parse::<CloneUrl>()
648 .unwrap()
649 .format_as(&ServerProtocol::Https, &None)
650 .unwrap();
651 assert_eq!(result, "https://github.com:2222");
652 }
653
654 #[test]
655 fn path_with_fragment() {
656 let result = "user1@github.com:/user/repo.git#readme"
657 .parse::<CloneUrl>()
658 .unwrap()
659 .format_as(&ServerProtocol::Https, &None)
660 .unwrap();
661 assert_eq!(result, "https://github.com/user/repo.git#readme");
662 }
663
664 #[test]
665 fn path_with_parameters() {
666 let result = "user@github.com:/user/repo.git?ref=main"
667 .parse::<CloneUrl>()
668 .unwrap()
669 .format_as(&ServerProtocol::Https, &None)
670 .unwrap();
671 assert_eq!(result, "https://github.com/user/repo.git?ref=main");
672 }
673
674 #[test]
675 fn port_with_parameters_and_fragment() {
676 let result = "user@github.com:2222/repo.git?version=1.0#section1"
677 .parse::<CloneUrl>()
678 .unwrap()
679 .format_as(&ServerProtocol::Https, &None)
680 .unwrap();
681 assert_eq!(
682 result,
683 "https://github.com:2222/repo.git?version=1.0#section1"
684 );
685 }
686 }
687
688 #[test]
689 fn test_ftp_url() {
690 let result = "ftp://example.com/repo.git"
691 .parse::<CloneUrl>()
692 .unwrap()
693 .format_as(&ServerProtocol::Https, &None)
694 .unwrap();
695 assert_eq!(result, "https://example.com/repo.git");
696 }
697
698 #[test]
699 fn test_git_protocol_url() {
700 let result = "git://example.com/repo.git"
701 .parse::<CloneUrl>()
702 .unwrap()
703 .format_as(&ServerProtocol::Https, &None)
704 .unwrap();
705 assert_eq!(result, "https://example.com/repo.git");
706 }
707
708 #[test]
709 fn test_invalid_url() {
710 let clone_url_result = "unsupported://example.com/repo.git".parse::<CloneUrl>();
711 assert!(clone_url_result.is_err());
712 }
713 mod local_addresses_should_return_error {
714 use super::*;
715 #[test]
716 fn test_absolute_local_path() {
717 let result = "/path/to/repo.git"
718 .parse::<CloneUrl>()
719 .unwrap()
720 .format_as(&ServerProtocol::Https, &None);
721 assert!(result.is_err()); // Expecting an error when converting to HTTPS
722 }
723
724 #[test]
725 fn test_relative_local_path() {
726 let result = "./path/to/repo.git"
727 .parse::<CloneUrl>()
728 .unwrap()
729 .format_as(&ServerProtocol::Https, &None);
730 assert!(result.is_err()); // Expecting an error when converting to HTTPS
731 }
732 }
733 }
734 }
194 mod convert_clone_url_to_https { 735 mod convert_clone_url_to_https {
195 use super::*; 736 use super::*;
196 737