upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-01-16 09:23:05 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-10-17 12:18:24 +0100
commitb126e5b7acfce55bd101b06cb5baf7f251cd0fda (patch)
treedd3eb721a49496ce2a686a43ce4d007b26ee7d7c /src/lib
parent6cb9e1f5071a6dee8f8e5b98c3c2e699cafd2921 (diff)
feat!(nostr_url): replace user with ssh_key_file
replaces the "user" in the nostr_url format with "ssh_key_file", to support the original intent, which was to allow users to specify different authentication credentials. most git servers always expect the ssh user to be 'git'. the idiumatic way of specifying logging in as a different user is to specify a different ssh key. the idiomatic way of storing non-default ssh keys is in the location `~/.ssh/key_name`. "ssh_key_file" can be specified as `key_name`, for keys in the default location, or as a relative or absolute custom location eg. `/other_keys/.ssh/nym1` or `../.ssh/nym1`. BREAKING CHANGE: in nostr git url nym1@ssh/npub123/identifer, nym1 is now treated as ssh key file location rather than a ssh user. it can be specified as a file within `~/.ssh` eg `~/.ssh/nym1` or a full or relative path.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/fetch.rs36
-rw-r--r--src/lib/git/nostr_url.rs289
-rw-r--r--src/lib/list.rs37
-rw-r--r--src/lib/push.rs43
-rw-r--r--src/lib/repo_ref.rs2
5 files changed, 246 insertions, 161 deletions
diff --git a/src/lib/fetch.rs b/src/lib/fetch.rs
index 89001d4..eeed8f4 100644
--- a/src/lib/fetch.rs
+++ b/src/lib/fetch.rs
@@ -1,4 +1,6 @@
1use std::{ 1use std::{
2 path::PathBuf,
3 str::FromStr,
2 sync::{Arc, Mutex}, 4 sync::{Arc, Mutex},
3 time::Instant, 5 time::Instant,
4}; 6};
@@ -44,18 +46,28 @@ pub fn fetch_from_git_server(
44 format!("fetching {} over {protocol}...", server_url.short_name(),).as_str(), 46 format!("fetching {} over {protocol}...", server_url.short_name(),).as_str(),
45 )?; 47 )?;
46 48
47 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; 49 let formatted_url = server_url.format_as(protocol)?;
48 let res = fetch_from_git_server_url( 50 let res = fetch_from_git_server_url(
49 &git_repo.git_repo, 51 &git_repo.git_repo,
50 oids, 52 oids,
51 &formatted_url, 53 &formatted_url,
52 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol), 54 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol),
55 decoded_nostr_url.ssh_key_file_path().as_ref(),
53 term, 56 term,
54 ); 57 );
55 if let Err(error) = res { 58 if let Err(error) = res {
56 term.write_line( 59 term.write_line(&format!(
57 format!("fetch: {formatted_url} failed over {protocol}: {error}").as_str(), 60 "fetch: {formatted_url} failed over {protocol}{}: {error}",
58 )?; 61 if protocol == &ServerProtocol::Ssh {
62 if let Some(ssh_key_file) = &decoded_nostr_url.ssh_key_file_path() {
63 format!(" with ssh key from {ssh_key_file}")
64 } else {
65 String::new()
66 }
67 } else {
68 String::new()
69 }
70 ))?;
59 failed_protocols.push(protocol); 71 failed_protocols.push(protocol);
60 } else { 72 } else {
61 success = true; 73 success = true;
@@ -89,6 +101,7 @@ fn fetch_from_git_server_url(
89 oids: &[String], 101 oids: &[String],
90 git_server_url: &str, 102 git_server_url: &str,
91 dont_authenticate: bool, 103 dont_authenticate: bool,
104 ssh_key_file: Option<&String>,
92 term: &console::Term, 105 term: &console::Term,
93) -> Result<()> { 106) -> Result<()> {
94 if git_server_url.parse::<CloneUrl>()?.protocol() == ServerProtocol::Ssh && !check_ssh_keys() { 107 if git_server_url.parse::<CloneUrl>()?.protocol() == ServerProtocol::Ssh && !check_ssh_keys() {
@@ -96,7 +109,20 @@ fn fetch_from_git_server_url(
96 } 109 }
97 let git_config = git_repo.config()?; 110 let git_config = git_repo.config()?;
98 let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?; 111 let mut git_server_remote = git_repo.remote_anonymous(git_server_url)?;
99 let auth = GitAuthenticator::default(); 112 let auth = {
113 if dont_authenticate {
114 GitAuthenticator::default()
115 } else if git_server_url.contains("git@") {
116 if let Some(ssh_key_file) = ssh_key_file {
117 GitAuthenticator::default()
118 .add_ssh_key_from_file(PathBuf::from_str(ssh_key_file)?, None)
119 } else {
120 GitAuthenticator::default()
121 }
122 } else {
123 GitAuthenticator::default()
124 }
125 };
100 let mut fetch_options = git2::FetchOptions::new(); 126 let mut fetch_options = git2::FetchOptions::new();
101 let mut remote_callbacks = git2::RemoteCallbacks::new(); 127 let mut remote_callbacks = git2::RemoteCallbacks::new();
102 let fetch_reporter = Arc::new(Mutex::new(FetchReporter::new(term))); 128 let fetch_reporter = Arc::new(Mutex::new(FetchReporter::new(term)));
diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs
index 5e92a84..0a8338a 100644
--- a/src/lib/git/nostr_url.rs
+++ b/src/lib/git/nostr_url.rs
@@ -2,6 +2,7 @@ use core::fmt;
2use std::{collections::HashMap, str::FromStr}; 2use std::{collections::HashMap, str::FromStr};
3 3
4use anyhow::{Context, Error, Result, anyhow, bail}; 4use anyhow::{Context, Error, Result, anyhow, bail};
5use directories::BaseDirs;
5use nostr::nips::{nip01::Coordinate, nip19::Nip19Coordinate}; 6use nostr::nips::{nip01::Coordinate, nip19::Nip19Coordinate};
6use nostr_sdk::{FromBech32, PublicKey, RelayUrl, ToBech32, Url}; 7use nostr_sdk::{FromBech32, PublicKey, RelayUrl, ToBech32, Url};
7 8
@@ -61,7 +62,7 @@ pub struct NostrUrlDecoded {
61 pub original_string: String, 62 pub original_string: String,
62 pub coordinate: Nip19Coordinate, 63 pub coordinate: Nip19Coordinate,
63 pub protocol: Option<ServerProtocol>, 64 pub protocol: Option<ServerProtocol>,
64 pub user: Option<String>, 65 pub ssh_key_file: Option<String>,
65 pub nip05: Option<String>, 66 pub nip05: Option<String>,
66} 67}
67 68
@@ -71,8 +72,8 @@ impl fmt::Display for NostrUrlDecoded {
71 return write!(f, "{}", self.original_string); 72 return write!(f, "{}", self.original_string);
72 } 73 }
73 write!(f, "nostr://")?; 74 write!(f, "nostr://")?;
74 if let Some(user) = &self.user { 75 if let Some(ssh_key_file) = &self.ssh_key_file {
75 write!(f, "{user}@")?; 76 write!(f, "{ssh_key_file}@")?;
76 } 77 }
77 if let Some(protocol) = &self.protocol { 78 if let Some(protocol) = &self.protocol {
78 write!(f, "{protocol}/")?; 79 write!(f, "{protocol}/")?;
@@ -103,7 +104,7 @@ static INCORRECT_NOSTR_URL_FORMAT_ERROR: &str = "incorrect nostr git url format.
103impl NostrUrlDecoded { 104impl NostrUrlDecoded {
104 pub async fn parse_and_resolve(url: &str, git_repo: &Option<&Repo>) -> Result<Self> { 105 pub async fn parse_and_resolve(url: &str, git_repo: &Option<&Repo>) -> Result<Self> {
105 let mut protocol = None; 106 let mut protocol = None;
106 let mut user = None; 107 let mut ssh_key_file = None;
107 let mut relays = vec![]; 108 let mut relays = vec![];
108 let mut nip05 = None; 109 let mut nip05 = None;
109 110
@@ -130,8 +131,8 @@ impl NostrUrlDecoded {
130 "git" => Some(ServerProtocol::Git), 131 "git" => Some(ServerProtocol::Git),
131 _ => None, 132 _ => None,
132 }; 133 };
133 } else if name == "user" { 134 } else if name == "ssh_key_file" {
134 user = Some(value.to_string()); 135 ssh_key_file = Some(value.to_string());
135 } 136 }
136 } 137 }
137 138
@@ -148,7 +149,7 @@ impl NostrUrlDecoded {
148 let protocol_str = if part.contains('.') { 149 let protocol_str = if part.contains('.') {
149 part 150 part
150 } else if let Some(at_index) = part.find('@') { 151 } else if let Some(at_index) = part.find('@') {
151 user = Some(part[..at_index].to_string()); 152 ssh_key_file = Some(part[..at_index].to_string());
152 &part[at_index + 1..] 153 &part[at_index + 1..]
153 } else { 154 } else {
154 part 155 part
@@ -240,10 +241,30 @@ impl NostrUrlDecoded {
240 original_string: url.to_string(), 241 original_string: url.to_string(),
241 coordinate, 242 coordinate,
242 protocol, 243 protocol,
243 user, 244 ssh_key_file,
244 nip05, 245 nip05,
245 }) 246 })
246 } 247 }
248
249 pub fn ssh_key_file_path(&self) -> Option<String> {
250 if let Some(ssh_key_file) = &self.ssh_key_file {
251 if !ssh_key_file.is_empty() {
252 // checking if path exists would make unit tests harder
253 if is_absoute_or_relative_path(ssh_key_file) {
254 return Some(ssh_key_file.clone());
255 } else if let Some(dirs) = BaseDirs::new() {
256 return Some(
257 dirs.home_dir()
258 .join(".ssh")
259 .join(ssh_key_file)
260 .to_string_lossy()
261 .into_owned(),
262 );
263 }
264 }
265 }
266 None
267 }
247} 268}
248 269
249fn resolve_nip05_from_git_config_cache(nip05: &str, git_repo: &Option<&Repo>) -> Result<PublicKey> { 270fn resolve_nip05_from_git_config_cache(nip05: &str, git_repo: &Option<&Repo>) -> Result<PublicKey> {
@@ -297,6 +318,13 @@ fn load_nip_cache(git_repo: &Option<&Repo>) -> Result<HashMap<String, PublicKey>
297 Ok(h) 318 Ok(h)
298} 319}
299 320
321fn is_absoute_or_relative_path(input: &str) -> bool {
322 ["~", "/", "./", "../", ".\\", "..\\"]
323 .iter()
324 .any(|s| input.starts_with(s))
325 || input.chars().nth(1).unwrap() == ':'
326}
327
300#[derive(Debug, PartialEq, Default)] 328#[derive(Debug, PartialEq, Default)]
301pub struct CloneUrl { 329pub struct CloneUrl {
302 original_string: String, 330 original_string: String,
@@ -324,7 +352,7 @@ impl FromStr for CloneUrl {
324 let url_str = if s.contains("://") { 352 let url_str = if s.contains("://") {
325 s.to_string() // Use the original string 353 s.to_string() // Use the original string
326 } else { 354 } else {
327 let protocol = // Check for the SSH format user@host:path and convert to ssh:// 355 let protocol = // Check for the SSH format git@host:path and convert to ssh://
328 if s.contains('@') && s 356 if s.contains('@') && s
329 .split('@') 357 .split('@')
330 .nth(0) 358 .nth(0)
@@ -395,7 +423,7 @@ fn contains_port(s: &str) -> bool {
395} 423}
396 424
397impl CloneUrl { 425impl CloneUrl {
398 pub fn format_as(&self, protocol: &ServerProtocol, user: &Option<String>) -> Result<String> { 426 pub fn format_as(&self, protocol: &ServerProtocol) -> Result<String> {
399 // Check for incompatible protocol conversions 427 // Check for incompatible protocol conversions
400 if *protocol == ServerProtocol::Filesystem { 428 if *protocol == ServerProtocol::Filesystem {
401 if self.protocol == ServerProtocol::Filesystem { 429 if self.protocol == ServerProtocol::Filesystem {
@@ -450,10 +478,7 @@ impl CloneUrl {
450 let mut formatted_url = url.to_string(); 478 let mut formatted_url = url.to_string();
451 479
452 if *protocol == ServerProtocol::Ssh { 480 if *protocol == ServerProtocol::Ssh {
453 formatted_url = formatted_url.replace( 481 formatted_url = formatted_url.replace("ssh://", "git@");
454 "ssh://",
455 format!("{}@", user.as_deref().unwrap_or("git")).as_str(),
456 );
457 if url.port().is_some() { 482 if url.port().is_some() {
458 formatted_url = format!("ssh://{formatted_url}"); 483 formatted_url = format!("ssh://{formatted_url}");
459 } else { 484 } else {
@@ -543,7 +568,7 @@ fn strip_credentials(url: &str) -> String {
543 return format!("{}{}", protocol, rest_parts[1]); 568 return format!("{}{}", protocol, rest_parts[1]);
544 } 569 }
545 } else if let Some(at_pos) = url.find('@') { 570 } else if let Some(at_pos) = url.find('@') {
546 // Handle user@host:path format 571 // Handle git@host:path format
547 let (_, rest) = url.split_at(at_pos); 572 let (_, rest) = url.split_at(at_pos);
548 // This is a git@ syntax 573 // This is a git@ syntax
549 let host_and_repo = &rest[1..]; // Skip the ':' 574 let host_and_repo = &rest[1..]; // Skip the ':'
@@ -565,52 +590,12 @@ mod tests {
565 mod clone_url_from_str_format_as { 590 mod clone_url_from_str_format_as {
566 use super::*; 591 use super::*;
567 592
568 mod when_user_specified {
569 use super::*;
570
571 mod but_not_in_original_url {
572 use super::*;
573
574 #[test]
575 fn https_to_https_ignores_user() {
576 let result = "https://github.com/user/repo.git"
577 .parse::<CloneUrl>()
578 .unwrap()
579 .format_as(&ServerProtocol::Https, &Some("user1".to_string()))
580 .unwrap();
581 assert_eq!(result, "https://github.com/user/repo.git");
582 }
583 #[test]
584 fn https_to_ssh_uses_specified_user() {
585 let result = "https://github.com/user/repo.git"
586 .parse::<CloneUrl>()
587 .unwrap()
588 .format_as(&ServerProtocol::Ssh, &Some("user1".to_string()))
589 .unwrap();
590 assert_eq!(result, "user1@github.com:user/repo.git");
591 }
592 }
593 mod and_a_different_user_in_original_url {
594 use super::*;
595
596 #[test]
597 fn ssh_uses_specified_user() {
598 let result = "user2@github.com/user/repo.git"
599 .parse::<CloneUrl>()
600 .unwrap()
601 .format_as(&ServerProtocol::Ssh, &Some("user1".to_string()))
602 .unwrap();
603 assert_eq!(result, "user1@github.com:user/repo.git");
604 }
605 }
606 }
607
608 #[test] 593 #[test]
609 fn format_as_ssh_defaults_to_git_user() { 594 fn format_as_ssh_defaults_to_git_user() {
610 let result = "https://github.com/user/repo.git" 595 let result = "https://github.com/user/repo.git"
611 .parse::<CloneUrl>() 596 .parse::<CloneUrl>()
612 .unwrap() 597 .unwrap()
613 .format_as(&ServerProtocol::Ssh, &None) 598 .format_as(&ServerProtocol::Ssh)
614 .unwrap(); 599 .unwrap();
615 assert_eq!(result, "git@github.com:user/repo.git"); 600 assert_eq!(result, "git@github.com:user/repo.git");
616 } 601 }
@@ -623,7 +608,7 @@ mod tests {
623 let result = "https://github.com:1000/user/repo.git" 608 let result = "https://github.com:1000/user/repo.git"
624 .parse::<CloneUrl>() 609 .parse::<CloneUrl>()
625 .unwrap() 610 .unwrap()
626 .format_as(&ServerProtocol::Https, &None) 611 .format_as(&ServerProtocol::Https)
627 .unwrap(); 612 .unwrap();
628 assert_eq!(result, "https://github.com:1000/user/repo.git"); 613 assert_eq!(result, "https://github.com:1000/user/repo.git");
629 } 614 }
@@ -633,7 +618,7 @@ mod tests {
633 let result = "https://github.com:1000/user/repo.git" 618 let result = "https://github.com:1000/user/repo.git"
634 .parse::<CloneUrl>() 619 .parse::<CloneUrl>()
635 .unwrap() 620 .unwrap()
636 .format_as(&ServerProtocol::Ssh, &None) 621 .format_as(&ServerProtocol::Ssh)
637 .unwrap(); 622 .unwrap();
638 assert_eq!(result, "git@github.com:user/repo.git"); 623 assert_eq!(result, "git@github.com:user/repo.git");
639 } 624 }
@@ -644,7 +629,7 @@ mod tests {
644 let result = "ssh://git@github.com:29418/user/repo.git" 629 let result = "ssh://git@github.com:29418/user/repo.git"
645 .parse::<CloneUrl>() 630 .parse::<CloneUrl>()
646 .unwrap() 631 .unwrap()
647 .format_as(&ServerProtocol::Ssh, &None) 632 .format_as(&ServerProtocol::Ssh)
648 .unwrap(); 633 .unwrap();
649 // need this format 634 // need this format
650 assert_eq!(result, "ssh://git@github.com:29418/user/repo.git"); 635 assert_eq!(result, "ssh://git@github.com:29418/user/repo.git");
@@ -655,7 +640,7 @@ mod tests {
655 let result = "ssh://git@github.com:29418/user/repo.git" 640 let result = "ssh://git@github.com:29418/user/repo.git"
656 .parse::<CloneUrl>() 641 .parse::<CloneUrl>()
657 .unwrap() 642 .unwrap()
658 .format_as(&ServerProtocol::Https, &None) 643 .format_as(&ServerProtocol::Https)
659 .unwrap(); 644 .unwrap();
660 // need this format 645 // need this format
661 assert_eq!(result, "https://github.com/user/repo.git"); 646 assert_eq!(result, "https://github.com/user/repo.git");
@@ -667,7 +652,7 @@ mod tests {
667 let result = "git@github.com:29418/user/repo.git" 652 let result = "git@github.com:29418/user/repo.git"
668 .parse::<CloneUrl>() 653 .parse::<CloneUrl>()
669 .unwrap() 654 .unwrap()
670 .format_as(&ServerProtocol::Ssh, &None) 655 .format_as(&ServerProtocol::Ssh)
671 .unwrap(); 656 .unwrap();
672 // need this format 657 // need this format
673 assert_eq!(result, "ssh://git@github.com:29418/user/repo.git"); 658 assert_eq!(result, "ssh://git@github.com:29418/user/repo.git");
@@ -679,7 +664,7 @@ mod tests {
679 let result = "https://github.com/user/repo.git" 664 let result = "https://github.com/user/repo.git"
680 .parse::<CloneUrl>() 665 .parse::<CloneUrl>()
681 .unwrap() 666 .unwrap()
682 .format_as(&ServerProtocol::Unspecified, &None) 667 .format_as(&ServerProtocol::Unspecified)
683 .unwrap(); 668 .unwrap();
684 assert_eq!(result, "github.com/user/repo.git"); 669 assert_eq!(result, "github.com/user/repo.git");
685 } 670 }
@@ -692,7 +677,7 @@ mod tests {
692 let result = "https://github.com/user/repo.git" 677 let result = "https://github.com/user/repo.git"
693 .parse::<CloneUrl>() 678 .parse::<CloneUrl>()
694 .unwrap() 679 .unwrap()
695 .format_as(&ServerProtocol::Https, &None) 680 .format_as(&ServerProtocol::Https)
696 .unwrap(); 681 .unwrap();
697 assert_eq!(result, "https://github.com/user/repo.git"); 682 assert_eq!(result, "https://github.com/user/repo.git");
698 } 683 }
@@ -705,7 +690,7 @@ mod tests {
705 let result = "github.com:1000/user/repo.git" 690 let result = "github.com:1000/user/repo.git"
706 .parse::<CloneUrl>() 691 .parse::<CloneUrl>()
707 .unwrap() 692 .unwrap()
708 .format_as(&ServerProtocol::Https, &None) 693 .format_as(&ServerProtocol::Https)
709 .unwrap(); 694 .unwrap();
710 assert_eq!(result, "https://github.com/user/repo.git"); 695 assert_eq!(result, "https://github.com/user/repo.git");
711 } 696 }
@@ -715,7 +700,7 @@ mod tests {
715 let result = "github.com:user/repo.git" 700 let result = "github.com:user/repo.git"
716 .parse::<CloneUrl>() 701 .parse::<CloneUrl>()
717 .unwrap() 702 .unwrap()
718 .format_as(&ServerProtocol::Https, &None) 703 .format_as(&ServerProtocol::Https)
719 .unwrap(); 704 .unwrap();
720 assert_eq!(result, "https://github.com/user/repo.git"); 705 assert_eq!(result, "https://github.com/user/repo.git");
721 } 706 }
@@ -725,7 +710,7 @@ mod tests {
725 let result = "github.com/user/repo.git#readme" 710 let result = "github.com/user/repo.git#readme"
726 .parse::<CloneUrl>() 711 .parse::<CloneUrl>()
727 .unwrap() 712 .unwrap()
728 .format_as(&ServerProtocol::Https, &None) 713 .format_as(&ServerProtocol::Https)
729 .unwrap(); 714 .unwrap();
730 assert_eq!(result, "https://github.com/user/repo.git#readme"); 715 assert_eq!(result, "https://github.com/user/repo.git#readme");
731 } 716 }
@@ -735,7 +720,7 @@ mod tests {
735 let result = "github.com/user/repo.git?ref=main" 720 let result = "github.com/user/repo.git?ref=main"
736 .parse::<CloneUrl>() 721 .parse::<CloneUrl>()
737 .unwrap() 722 .unwrap()
738 .format_as(&ServerProtocol::Https, &None) 723 .format_as(&ServerProtocol::Https)
739 .unwrap(); 724 .unwrap();
740 assert_eq!(result, "https://github.com/user/repo.git?ref=main"); 725 assert_eq!(result, "https://github.com/user/repo.git?ref=main");
741 } 726 }
@@ -745,7 +730,7 @@ mod tests {
745 let result = "github.com:2222/repo.git?version=1.0#section1" 730 let result = "github.com:2222/repo.git?version=1.0#section1"
746 .parse::<CloneUrl>() 731 .parse::<CloneUrl>()
747 .unwrap() 732 .unwrap()
748 .format_as(&ServerProtocol::Https, &None) 733 .format_as(&ServerProtocol::Https)
749 .unwrap(); 734 .unwrap();
750 assert_eq!(result, "https://github.com/repo.git?version=1.0#section1"); 735 assert_eq!(result, "https://github.com/repo.git?version=1.0#section1");
751 } 736 }
@@ -759,7 +744,7 @@ mod tests {
759 let result = "https://username:password@github.com/user/repo.git" 744 let result = "https://username:password@github.com/user/repo.git"
760 .parse::<CloneUrl>() 745 .parse::<CloneUrl>()
761 .unwrap() 746 .unwrap()
762 .format_as(&ServerProtocol::Https, &None) 747 .format_as(&ServerProtocol::Https)
763 .unwrap(); 748 .unwrap();
764 assert_eq!(result, "https://github.com/user/repo.git"); 749 assert_eq!(result, "https://github.com/user/repo.git");
765 } 750 }
@@ -769,7 +754,7 @@ mod tests {
769 let result = "https://github.com:1000/user/repo.git" 754 let result = "https://github.com:1000/user/repo.git"
770 .parse::<CloneUrl>() 755 .parse::<CloneUrl>()
771 .unwrap() 756 .unwrap()
772 .format_as(&ServerProtocol::Https, &None) 757 .format_as(&ServerProtocol::Https)
773 .unwrap(); 758 .unwrap();
774 assert_eq!(result, "https://github.com:1000/user/repo.git"); 759 assert_eq!(result, "https://github.com:1000/user/repo.git");
775 } 760 }
@@ -779,7 +764,7 @@ mod tests {
779 let result = "https://github.com/user/repo.git#readme" 764 let result = "https://github.com/user/repo.git#readme"
780 .parse::<CloneUrl>() 765 .parse::<CloneUrl>()
781 .unwrap() 766 .unwrap()
782 .format_as(&ServerProtocol::Https, &None) 767 .format_as(&ServerProtocol::Https)
783 .unwrap(); 768 .unwrap();
784 assert_eq!(result, "https://github.com/user/repo.git#readme"); 769 assert_eq!(result, "https://github.com/user/repo.git#readme");
785 } 770 }
@@ -789,7 +774,7 @@ mod tests {
789 let result = "https://github.com/user/repo.git?ref=main" 774 let result = "https://github.com/user/repo.git?ref=main"
790 .parse::<CloneUrl>() 775 .parse::<CloneUrl>()
791 .unwrap() 776 .unwrap()
792 .format_as(&ServerProtocol::Https, &None) 777 .format_as(&ServerProtocol::Https)
793 .unwrap(); 778 .unwrap();
794 assert_eq!(result, "https://github.com/user/repo.git?ref=main"); 779 assert_eq!(result, "https://github.com/user/repo.git?ref=main");
795 } 780 }
@@ -799,7 +784,7 @@ mod tests {
799 let result = "https://github.com:2222/repo.git?version=1.0#section1" 784 let result = "https://github.com:2222/repo.git?version=1.0#section1"
800 .parse::<CloneUrl>() 785 .parse::<CloneUrl>()
801 .unwrap() 786 .unwrap()
802 .format_as(&ServerProtocol::Https, &None) 787 .format_as(&ServerProtocol::Https)
803 .unwrap(); 788 .unwrap();
804 assert_eq!( 789 assert_eq!(
805 result, 790 result,
@@ -813,7 +798,7 @@ mod tests {
813 let result = "http://github.com/user/repo.git" 798 let result = "http://github.com/user/repo.git"
814 .parse::<CloneUrl>() 799 .parse::<CloneUrl>()
815 .unwrap() 800 .unwrap()
816 .format_as(&ServerProtocol::Https, &None) 801 .format_as(&ServerProtocol::Https)
817 .unwrap(); 802 .unwrap();
818 assert_eq!(result, "https://github.com/user/repo.git"); 803 assert_eq!(result, "https://github.com/user/repo.git");
819 } 804 }
@@ -826,56 +811,47 @@ mod tests {
826 let result = "git@github.com:user/repo.git" 811 let result = "git@github.com:user/repo.git"
827 .parse::<CloneUrl>() 812 .parse::<CloneUrl>()
828 .unwrap() 813 .unwrap()
829 .format_as(&ServerProtocol::Https, &None) 814 .format_as(&ServerProtocol::Https)
830 .unwrap(); 815 .unwrap();
831 assert_eq!(result, "https://github.com/user/repo.git"); 816 assert_eq!(result, "https://github.com/user/repo.git");
832 } 817 }
833 818
834 #[test] 819 #[test]
835 fn test_user_at_url() {
836 let result = "user1@github.com:user/repo.git"
837 .parse::<CloneUrl>()
838 .unwrap()
839 .format_as(&ServerProtocol::Https, &None)
840 .unwrap();
841 assert_eq!(result, "https://github.com/user/repo.git");
842 }
843 #[test]
844 fn path_has_colon_slash_prefix() { 820 fn path_has_colon_slash_prefix() {
845 let result = "user1@github.com:/user/repo.git" 821 let result = "git@github.com:/user/repo.git"
846 .parse::<CloneUrl>() 822 .parse::<CloneUrl>()
847 .unwrap() 823 .unwrap()
848 .format_as(&ServerProtocol::Https, &None) 824 .format_as(&ServerProtocol::Https)
849 .unwrap(); 825 .unwrap();
850 assert_eq!(result, "https://github.com/user/repo.git"); 826 assert_eq!(result, "https://github.com/user/repo.git");
851 } 827 }
852 828
853 #[test] 829 #[test]
854 fn path_with_fragment() { 830 fn path_with_fragment() {
855 let result = "user1@github.com:/user/repo.git#readme" 831 let result = "git@github.com:/user/repo.git#readme"
856 .parse::<CloneUrl>() 832 .parse::<CloneUrl>()
857 .unwrap() 833 .unwrap()
858 .format_as(&ServerProtocol::Https, &None) 834 .format_as(&ServerProtocol::Https)
859 .unwrap(); 835 .unwrap();
860 assert_eq!(result, "https://github.com/user/repo.git#readme"); 836 assert_eq!(result, "https://github.com/user/repo.git#readme");
861 } 837 }
862 838
863 #[test] 839 #[test]
864 fn path_with_parameters() { 840 fn path_with_parameters() {
865 let result = "user@github.com:/user/repo.git?ref=main" 841 let result = "git@github.com:/user/repo.git?ref=main"
866 .parse::<CloneUrl>() 842 .parse::<CloneUrl>()
867 .unwrap() 843 .unwrap()
868 .format_as(&ServerProtocol::Https, &None) 844 .format_as(&ServerProtocol::Https)
869 .unwrap(); 845 .unwrap();
870 assert_eq!(result, "https://github.com/user/repo.git?ref=main"); 846 assert_eq!(result, "https://github.com/user/repo.git?ref=main");
871 } 847 }
872 848
873 #[test] 849 #[test]
874 fn port_with_parameters_and_fragment_ssh() { 850 fn port_with_parameters_and_fragment_ssh() {
875 let result = "user@github.com:2222/repo.git?version=1.0#section1" 851 let result = "git@github.com:2222/repo.git?version=1.0#section1"
876 .parse::<CloneUrl>() 852 .parse::<CloneUrl>()
877 .unwrap() 853 .unwrap()
878 .format_as(&ServerProtocol::Ssh, &None) 854 .format_as(&ServerProtocol::Ssh)
879 .unwrap(); 855 .unwrap();
880 assert_eq!( 856 assert_eq!(
881 result, 857 result,
@@ -884,10 +860,10 @@ mod tests {
884 } 860 }
885 #[test] 861 #[test]
886 fn port_with_parameters_and_fragment_https() { 862 fn port_with_parameters_and_fragment_https() {
887 let result = "user@github.com:2222/repo.git?version=1.0#section1" 863 let result = "git@github.com:2222/repo.git?version=1.0#section1"
888 .parse::<CloneUrl>() 864 .parse::<CloneUrl>()
889 .unwrap() 865 .unwrap()
890 .format_as(&ServerProtocol::Https, &None) 866 .format_as(&ServerProtocol::Https)
891 .unwrap(); 867 .unwrap();
892 assert_eq!(result, "https://github.com/repo.git?version=1.0#section1"); 868 assert_eq!(result, "https://github.com/repo.git?version=1.0#section1");
893 } 869 }
@@ -898,7 +874,7 @@ mod tests {
898 let result = "ftp://example.com/repo.git" 874 let result = "ftp://example.com/repo.git"
899 .parse::<CloneUrl>() 875 .parse::<CloneUrl>()
900 .unwrap() 876 .unwrap()
901 .format_as(&ServerProtocol::Https, &None) 877 .format_as(&ServerProtocol::Https)
902 .unwrap(); 878 .unwrap();
903 assert_eq!(result, "https://example.com/repo.git"); 879 assert_eq!(result, "https://example.com/repo.git");
904 } 880 }
@@ -908,7 +884,7 @@ mod tests {
908 let result = "git://example.com/repo.git" 884 let result = "git://example.com/repo.git"
909 .parse::<CloneUrl>() 885 .parse::<CloneUrl>()
910 .unwrap() 886 .unwrap()
911 .format_as(&ServerProtocol::Https, &None) 887 .format_as(&ServerProtocol::Https)
912 .unwrap(); 888 .unwrap();
913 assert_eq!(result, "https://example.com/repo.git"); 889 assert_eq!(result, "https://example.com/repo.git");
914 } 890 }
@@ -925,7 +901,7 @@ mod tests {
925 let result = "/path/to/repo.git" 901 let result = "/path/to/repo.git"
926 .parse::<CloneUrl>() 902 .parse::<CloneUrl>()
927 .unwrap() 903 .unwrap()
928 .format_as(&ServerProtocol::Https, &None); 904 .format_as(&ServerProtocol::Https);
929 assert!(result.is_err()); // Expecting an error when converting to HTTPS 905 assert!(result.is_err()); // Expecting an error when converting to HTTPS
930 } 906 }
931 907
@@ -934,7 +910,7 @@ mod tests {
934 let result = "./path/to/repo.git" 910 let result = "./path/to/repo.git"
935 .parse::<CloneUrl>() 911 .parse::<CloneUrl>()
936 .unwrap() 912 .unwrap()
937 .format_as(&ServerProtocol::Https, &None); 913 .format_as(&ServerProtocol::Https);
938 assert!(result.is_err()); // Expecting an error when converting to HTTPS 914 assert!(result.is_err()); // Expecting an error when converting to HTTPS
939 } 915 }
940 } 916 }
@@ -972,13 +948,6 @@ mod tests {
972 } 948 }
973 949
974 #[test] 950 #[test]
975 fn test_user_at_url() {
976 let url = "user1@github.com:user/repo.git";
977 let result = convert_clone_url_to_https(url).unwrap();
978 assert_eq!(result, "https://github.com/user/repo.git");
979 }
980
981 #[test]
982 fn test_ssh_url() { 951 fn test_ssh_url() {
983 let url = "ssh://github.com/user/repo.git"; 952 let url = "ssh://github.com/user/repo.git";
984 let result = convert_clone_url_to_https(url).unwrap(); 953 let result = convert_clone_url_to_https(url).unwrap();
@@ -1030,7 +999,7 @@ mod tests {
1030 relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()], 999 relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()],
1031 }, 1000 },
1032 protocol: None, 1001 protocol: None,
1033 user: None, 1002 ssh_key_file: None,
1034 nip05: None, 1003 nip05: None,
1035 }), 1004 }),
1036 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", 1005 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit",
@@ -1055,7 +1024,7 @@ mod tests {
1055 relays: vec![], 1024 relays: vec![],
1056 }, 1025 },
1057 protocol: None, 1026 protocol: None,
1058 user: None, 1027 ssh_key_file: None,
1059 nip05: None, 1028 nip05: None,
1060 }), 1029 }),
1061 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit", 1030 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit",
@@ -1080,7 +1049,7 @@ mod tests {
1080 relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()], 1049 relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()],
1081 }, 1050 },
1082 protocol: Some(ServerProtocol::Ssh), 1051 protocol: Some(ServerProtocol::Ssh),
1083 user: None, 1052 ssh_key_file: None,
1084 nip05: None, 1053 nip05: None,
1085 }), 1054 }),
1086 "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", 1055 "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit",
@@ -1089,7 +1058,7 @@ mod tests {
1089 } 1058 }
1090 1059
1091 #[test] 1060 #[test]
1092 fn with_protocol_and_user() -> Result<()> { 1061 fn with_protocol_and_ssh_key_file() -> Result<()> {
1093 assert_eq!( 1062 assert_eq!(
1094 format!("{}", NostrUrlDecoded { 1063 format!("{}", NostrUrlDecoded {
1095 original_string: String::new(), 1064 original_string: String::new(),
@@ -1105,7 +1074,7 @@ mod tests {
1105 relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()], 1074 relays: vec![RelayUrl::parse("wss://nos.lol").unwrap()],
1106 }, 1075 },
1107 protocol: Some(ServerProtocol::Ssh), 1076 protocol: Some(ServerProtocol::Ssh),
1108 user: Some("bla".to_string()), 1077 ssh_key_file: Some("bla".to_string()),
1109 nip05: None, 1078 nip05: None,
1110 }), 1079 }),
1111 "nostr://bla@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", 1080 "nostr://bla@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit",
@@ -1114,27 +1083,27 @@ mod tests {
1114 } 1083 }
1115 } 1084 }
1116 1085
1086 fn get_model_coordinate(relays: bool) -> Nip19Coordinate {
1087 Nip19Coordinate {
1088 coordinate: Coordinate {
1089 identifier: "ngit".to_string(),
1090 public_key: PublicKey::parse(
1091 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr",
1092 )
1093 .unwrap(),
1094 kind: nostr_sdk::Kind::GitRepoAnnouncement,
1095 },
1096 relays: if relays {
1097 vec![RelayUrl::parse("wss://nos.lol").unwrap()]
1098 } else {
1099 vec![]
1100 },
1101 }
1102 }
1103
1117 mod nostr_url_decoded_paramemters_from_str { 1104 mod nostr_url_decoded_paramemters_from_str {
1118 use super::*; 1105 use super::*;
1119 1106
1120 fn get_model_coordinate(relays: bool) -> Nip19Coordinate {
1121 Nip19Coordinate {
1122 coordinate: Coordinate {
1123 identifier: "ngit".to_string(),
1124 public_key: PublicKey::parse(
1125 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr",
1126 )
1127 .unwrap(),
1128 kind: nostr_sdk::Kind::GitRepoAnnouncement,
1129 },
1130 relays: if relays {
1131 vec![RelayUrl::parse("wss://nos.lol").unwrap()]
1132 } else {
1133 vec![]
1134 },
1135 }
1136 }
1137
1138 #[tokio::test] 1107 #[tokio::test]
1139 async fn from_naddr() -> Result<()> { 1108 async fn from_naddr() -> Result<()> {
1140 let url = "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj".to_string(); 1109 let url = "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj".to_string();
@@ -1155,7 +1124,7 @@ mod tests {
1155 * slash */ 1124 * slash */
1156 }, 1125 },
1157 protocol: None, 1126 protocol: None,
1158 user: None, 1127 ssh_key_file: None,
1159 nip05: None, 1128 nip05: None,
1160 }, 1129 },
1161 ); 1130 );
@@ -1176,7 +1145,7 @@ mod tests {
1176 original_string: url.clone(), 1145 original_string: url.clone(),
1177 coordinate: get_model_coordinate(false), 1146 coordinate: get_model_coordinate(false),
1178 protocol: None, 1147 protocol: None,
1179 user: None, 1148 ssh_key_file: None,
1180 nip05: None, 1149 nip05: None,
1181 }, 1150 },
1182 ); 1151 );
@@ -1195,7 +1164,7 @@ mod tests {
1195 original_string: url.clone(), 1164 original_string: url.clone(),
1196 coordinate: get_model_coordinate(true), 1165 coordinate: get_model_coordinate(true),
1197 protocol: None, 1166 protocol: None,
1198 user: None, 1167 ssh_key_file: None,
1199 nip05: None, 1168 nip05: None,
1200 }, 1169 },
1201 ); 1170 );
@@ -1214,7 +1183,7 @@ mod tests {
1214 original_string: url.clone(), 1183 original_string: url.clone(),
1215 coordinate: get_model_coordinate(true), 1184 coordinate: get_model_coordinate(true),
1216 protocol: None, 1185 protocol: None,
1217 user: None, 1186 ssh_key_file: None,
1218 nip05: None, 1187 nip05: None,
1219 }, 1188 },
1220 ); 1189 );
@@ -1247,7 +1216,7 @@ mod tests {
1247 ], 1216 ],
1248 }, 1217 },
1249 protocol: None, 1218 protocol: None,
1250 user: None, 1219 ssh_key_file: None,
1251 nip05: None, 1220 nip05: None,
1252 }, 1221 },
1253 ); 1222 );
@@ -1263,7 +1232,7 @@ mod tests {
1263 original_string: url.clone(), 1232 original_string: url.clone(),
1264 coordinate: get_model_coordinate(false), 1233 coordinate: get_model_coordinate(false),
1265 protocol: Some(ServerProtocol::Ssh), 1234 protocol: Some(ServerProtocol::Ssh),
1266 user: None, 1235 ssh_key_file: None,
1267 nip05: None, 1236 nip05: None,
1268 }, 1237 },
1269 ); 1238 );
@@ -1271,15 +1240,15 @@ mod tests {
1271 } 1240 }
1272 1241
1273 #[tokio::test] 1242 #[tokio::test]
1274 async fn with_server_protocol_and_user() -> Result<()> { 1243 async fn with_server_protocol_and_ssh_key_file() -> Result<()> {
1275 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&user=fred".to_string(); 1244 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&ssh_key_file=fred".to_string();
1276 assert_eq!( 1245 assert_eq!(
1277 NostrUrlDecoded::parse_and_resolve(&url, &None).await?, 1246 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1278 NostrUrlDecoded { 1247 NostrUrlDecoded {
1279 original_string: url.clone(), 1248 original_string: url.clone(),
1280 coordinate: get_model_coordinate(false), 1249 coordinate: get_model_coordinate(false),
1281 protocol: Some(ServerProtocol::Ssh), 1250 protocol: Some(ServerProtocol::Ssh),
1282 user: Some("fred".to_string()), 1251 ssh_key_file: Some("fred".to_string()),
1283 nip05: None, 1252 nip05: None,
1284 }, 1253 },
1285 ); 1254 );
@@ -1299,7 +1268,7 @@ mod tests {
1299 original_string: url.clone(), 1268 original_string: url.clone(),
1300 coordinate: get_model_coordinate(true), 1269 coordinate: get_model_coordinate(true),
1301 protocol: None, 1270 protocol: None,
1302 user: None, 1271 ssh_key_file: None,
1303 nip05: None, 1272 nip05: None,
1304 }, 1273 },
1305 ); 1274 );
@@ -1318,7 +1287,7 @@ mod tests {
1318 original_string: url.clone(), 1287 original_string: url.clone(),
1319 coordinate: get_model_coordinate(true), 1288 coordinate: get_model_coordinate(true),
1320 protocol: None, 1289 protocol: None,
1321 user: None, 1290 ssh_key_file: None,
1322 nip05: None, 1291 nip05: None,
1323 }, 1292 },
1324 ); 1293 );
@@ -1351,7 +1320,7 @@ mod tests {
1351 ], 1320 ],
1352 }, 1321 },
1353 protocol: None, 1322 protocol: None,
1354 user: None, 1323 ssh_key_file: None,
1355 nip05: None, 1324 nip05: None,
1356 }, 1325 },
1357 ); 1326 );
@@ -1367,7 +1336,7 @@ mod tests {
1367 original_string: url.clone(), 1336 original_string: url.clone(),
1368 coordinate: get_model_coordinate(false), 1337 coordinate: get_model_coordinate(false),
1369 protocol: Some(ServerProtocol::Ssh), 1338 protocol: Some(ServerProtocol::Ssh),
1370 user: None, 1339 ssh_key_file: None,
1371 nip05: None, 1340 nip05: None,
1372 }, 1341 },
1373 ); 1342 );
@@ -1375,7 +1344,7 @@ mod tests {
1375 } 1344 }
1376 1345
1377 #[tokio::test] 1346 #[tokio::test]
1378 async fn with_server_protocol_and_user() -> Result<()> { 1347 async fn with_server_protocol_and_ssh_key_file() -> Result<()> {
1379 let url = "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string(); 1348 let url = "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string();
1380 assert_eq!( 1349 assert_eq!(
1381 NostrUrlDecoded::parse_and_resolve(&url, &None).await?, 1350 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
@@ -1383,7 +1352,7 @@ mod tests {
1383 original_string: url.clone(), 1352 original_string: url.clone(),
1384 coordinate: get_model_coordinate(false), 1353 coordinate: get_model_coordinate(false),
1385 protocol: Some(ServerProtocol::Ssh), 1354 protocol: Some(ServerProtocol::Ssh),
1386 user: Some("fred".to_string()), 1355 ssh_key_file: Some("fred".to_string()),
1387 nip05: None, 1356 nip05: None,
1388 }, 1357 },
1389 ); 1358 );
@@ -1392,4 +1361,38 @@ mod tests {
1392 } 1361 }
1393 } 1362 }
1394 } 1363 }
1364 mod nostr_url_ssh_key_file_path {
1365 use super::*;
1366
1367 #[tokio::test]
1368 async fn when_full_file_path_not_detected_default_ssh_dir_is_preppended() -> Result<()> {
1369 let url = "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string();
1370 let decoded = NostrUrlDecoded {
1371 original_string: url.clone(),
1372 coordinate: get_model_coordinate(false),
1373 protocol: Some(ServerProtocol::Ssh),
1374 ssh_key_file: Some("fred".to_string()),
1375 nip05: None,
1376 };
1377 assert!(decoded.ssh_key_file_path().unwrap().ends_with("/.ssh/fred"));
1378 Ok(())
1379 }
1380
1381 #[tokio::test]
1382 async fn when_full_file_path_detected_default_ssh_dir_is_not_preppended() -> Result<()> {
1383 let url = "nostr://~/other/fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string();
1384 let decoded = NostrUrlDecoded {
1385 original_string: url.clone(),
1386 coordinate: get_model_coordinate(false),
1387 protocol: Some(ServerProtocol::Ssh),
1388 ssh_key_file: Some("~/other/fred".to_string()),
1389 nip05: None,
1390 };
1391 assert_eq!(
1392 decoded.ssh_key_file_path(),
1393 Some("~/other/fred".to_string())
1394 );
1395 Ok(())
1396 }
1397 }
1395} 1398}
diff --git a/src/lib/list.rs b/src/lib/list.rs
index b867858..639140e 100644
--- a/src/lib/list.rs
+++ b/src/lib/list.rs
@@ -1,4 +1,4 @@
1use std::collections::HashMap; 1use std::{collections::HashMap, path::PathBuf, str::FromStr};
2 2
3use anyhow::{Result, anyhow}; 3use anyhow::{Result, anyhow};
4use auth_git2::GitAuthenticator; 4use auth_git2::GitAuthenticator;
@@ -59,10 +59,12 @@ pub fn list_from_remote(
59 .as_str(), 59 .as_str(),
60 )?; 60 )?;
61 61
62 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; 62 let formatted_url = server_url.format_as(protocol)?;
63
63 let res = list_from_remote_url( 64 let res = list_from_remote_url(
64 git_repo, 65 git_repo,
65 &formatted_url, 66 &formatted_url,
67 decoded_nostr_url.ssh_key_file_path().as_ref(),
66 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol), 68 [ServerProtocol::UnauthHttps, ServerProtocol::UnauthHttp].contains(protocol),
67 term, 69 term,
68 ); 70 );
@@ -86,9 +88,18 @@ pub fn list_from_remote(
86 } 88 }
87 Err(error) => { 89 Err(error) => {
88 term.clear_last_lines(1)?; 90 term.clear_last_lines(1)?;
89 term.write_line( 91 term.write_line(&format!(
90 format!("list: {formatted_url} failed over {protocol}: {error}").as_str(), 92 "list: {formatted_url} failed over {protocol}{}: {error}",
91 )?; 93 if protocol == &ServerProtocol::Ssh {
94 if let Some(ssh_key_file) = &decoded_nostr_url.ssh_key_file_path() {
95 format!(" with ssh key from {ssh_key_file}")
96 } else {
97 String::new()
98 }
99 } else {
100 String::new()
101 }
102 ))?;
92 failed_protocols.push(protocol); 103 failed_protocols.push(protocol);
93 } 104 }
94 } 105 }
@@ -117,6 +128,7 @@ pub fn list_from_remote(
117fn list_from_remote_url( 128fn list_from_remote_url(
118 git_repo: &Repo, 129 git_repo: &Repo,
119 git_server_remote_url: &str, 130 git_server_remote_url: &str,
131 ssh_key_file: Option<&String>,
120 dont_authenticate: bool, 132 dont_authenticate: bool,
121 term: &console::Term, 133 term: &console::Term,
122) -> Result<HashMap<String, String>> { 134) -> Result<HashMap<String, String>> {
@@ -124,7 +136,20 @@ fn list_from_remote_url(
124 136
125 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_remote_url)?; 137 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_remote_url)?;
126 // authentication may be required 138 // authentication may be required
127 let auth = GitAuthenticator::default(); 139 let auth = {
140 if dont_authenticate {
141 GitAuthenticator::default()
142 } else if git_server_remote_url.contains("git@") {
143 if let Some(ssh_key_file) = ssh_key_file {
144 GitAuthenticator::default()
145 .add_ssh_key_from_file(PathBuf::from_str(ssh_key_file)?, None)
146 } else {
147 GitAuthenticator::default()
148 }
149 } else {
150 GitAuthenticator::default()
151 }
152 };
128 let mut remote_callbacks = git2::RemoteCallbacks::new(); 153 let mut remote_callbacks = git2::RemoteCallbacks::new();
129 if !dont_authenticate { 154 if !dont_authenticate {
130 remote_callbacks.credentials(auth.credentials(&git_config)); 155 remote_callbacks.credentials(auth.credentials(&git_config));
diff --git a/src/lib/push.rs b/src/lib/push.rs
index aafcf35..c9b8791 100644
--- a/src/lib/push.rs
+++ b/src/lib/push.rs
@@ -1,5 +1,6 @@
1use std::{ 1use std::{
2 collections::{HashMap, HashSet}, 2 collections::{HashMap, HashSet},
3 path::PathBuf,
3 str::FromStr, 4 str::FromStr,
4 sync::{Arc, Mutex}, 5 sync::{Arc, Mutex},
5 thread, 6 thread,
@@ -25,7 +26,7 @@ use crate::{
25 client::{Connect, get_repo_ref_from_cache, send_events, sign_draft_event, sign_event}, 26 client::{Connect, get_repo_ref_from_cache, send_events, sign_draft_event, sign_event},
26 git::{ 27 git::{
27 Repo, RepoActions, 28 Repo, RepoActions,
28 nostr_url::{CloneUrl, NostrUrlDecoded}, 29 nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol},
29 oid_to_shorthand_string, 30 oid_to_shorthand_string,
30 }, 31 },
31 git_events::{KIND_PULL_REQUEST_UPDATE, generate_unsigned_pr_or_update_event}, 32 git_events::{KIND_PULL_REQUEST_UPDATE, generate_unsigned_pr_or_update_event},
@@ -60,12 +61,30 @@ pub fn push_to_remote(
60 for protocol in &protocols_to_attempt { 61 for protocol in &protocols_to_attempt {
61 term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?; 62 term.write_line(format!("push: {} over {protocol}...", server_url.short_name(),).as_str())?;
62 63
63 let formatted_url = server_url.format_as(protocol, &decoded_nostr_url.user)?; 64 let formatted_url = server_url.format_as(protocol)?;
64 65
65 match push_to_remote_url(git_repo, &formatted_url, remote_refspecs, term) { 66 match push_to_remote_url(
67 git_repo,
68 &formatted_url,
69 decoded_nostr_url.ssh_key_file_path().as_ref(),
70 remote_refspecs,
71 term,
72 ) {
66 Err(error) => { 73 Err(error) => {
67 term.write_line( 74 term.write_line(
68 format!("push: {formatted_url} failed over {protocol}: {error}").as_str(), 75 format!(
76 "push: {formatted_url} failed over {protocol}{}: {error}",
77 if protocol == &ServerProtocol::Ssh {
78 if let Some(ssh_key_file) = &decoded_nostr_url.ssh_key_file_path() {
79 format!(" with ssh key from {ssh_key_file}")
80 } else {
81 String::new()
82 }
83 } else {
84 String::new()
85 }
86 )
87 .as_str(),
69 )?; 88 )?;
70 failed_protocols.push(protocol); 89 failed_protocols.push(protocol);
71 } 90 }
@@ -127,12 +146,24 @@ pub fn push_to_remote(
127pub fn push_to_remote_url( 146pub fn push_to_remote_url(
128 git_repo: &Repo, 147 git_repo: &Repo,
129 git_server_url: &str, 148 git_server_url: &str,
149 ssh_key_file: Option<&String>,
130 remote_refspecs: &[String], 150 remote_refspecs: &[String],
131 term: &Term, 151 term: &Term,
132) -> Result<HashMap<String, Option<String>>> { 152) -> Result<HashMap<String, Option<String>>> {
133 let git_config = git_repo.git_repo.config()?; 153 let git_config = git_repo.git_repo.config()?;
134 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?; 154 let mut git_server_remote = git_repo.git_repo.remote_anonymous(git_server_url)?;
135 let auth = GitAuthenticator::default(); 155 let auth = {
156 if git_server_url.contains("git@") {
157 if let Some(ssh_key_file) = ssh_key_file {
158 GitAuthenticator::default()
159 .add_ssh_key_from_file(PathBuf::from_str(ssh_key_file)?, None)
160 } else {
161 GitAuthenticator::default()
162 }
163 } else {
164 GitAuthenticator::default()
165 }
166 };
136 let mut push_options = git2::PushOptions::new(); 167 let mut push_options = git2::PushOptions::new();
137 let mut remote_callbacks = git2::RemoteCallbacks::new(); 168 let mut remote_callbacks = git2::RemoteCallbacks::new();
138 let push_reporter = Arc::new(Mutex::new(PushReporter::new(term))); 169 let push_reporter = Arc::new(Mutex::new(PushReporter::new(term)));
@@ -716,7 +747,7 @@ pub async fn push_refs_and_generate_pr_or_pr_update_event(
716 let refspec = format!("{tip}:{git_ref_used}"); 747 let refspec = format!("{tip}:{git_ref_used}");
717 748
718 let res = if is_grasp_server_clone_url(clone_url) { 749 let res = if is_grasp_server_clone_url(clone_url) {
719 push_to_remote_url(git_repo, clone_url, &[refspec], term) 750 push_to_remote_url(git_repo, clone_url, None, &[refspec], term)
720 } else { 751 } else {
721 // anticipated only when pushing to user's own repo or a personal-fork with 752 // anticipated only when pushing to user's own repo or a personal-fork with
722 // non-grasp git servers. this is used to extract prefered protocols / ssh 753 // non-grasp git servers. this is used to extract prefered protocols / ssh
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index b2bd381..eb0964f 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -316,7 +316,7 @@ impl RepoRef {
316 .unwrap_or_default(), 316 .unwrap_or_default(),
317 coordinate: c, 317 coordinate: c,
318 protocol: None, 318 protocol: None,
319 user: None, 319 ssh_key_file: None,
320 } 320 }
321 } 321 }
322 322