diff options
Diffstat (limited to 'src/lib/git/mod.rs')
| -rw-r--r-- | src/lib/git/mod.rs | 266 |
1 files changed, 7 insertions, 259 deletions
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index 5919667..f92272f 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs | |||
| @@ -1,18 +1,16 @@ | |||
| 1 | use std::{ | 1 | use std::{ |
| 2 | collections::HashSet, | ||
| 3 | env::current_dir, | 2 | env::current_dir, |
| 4 | path::{Path, PathBuf}, | 3 | path::{Path, PathBuf}, |
| 5 | }; | 4 | }; |
| 6 | 5 | ||
| 7 | use anyhow::{bail, Context, Result}; | 6 | use anyhow::{bail, Context, Result}; |
| 8 | use git2::{DiffOptions, Oid, Revwalk}; | 7 | use git2::{DiffOptions, Oid, Revwalk}; |
| 9 | use nostr::nips::nip01::Coordinate; | 8 | pub use identify_ahead_behind::identify_ahead_behind; |
| 10 | use nostr_sdk::{ | 9 | use nostr_sdk::hashes::{sha1::Hash as Sha1Hash, Hash}; |
| 11 | hashes::{sha1::Hash as Sha1Hash, Hash}, | ||
| 12 | PublicKey, Url, | ||
| 13 | }; | ||
| 14 | 10 | ||
| 15 | use crate::sub_commands::list::{get_commit_id_from_patch, tag_value}; | 11 | use crate::git_events::{get_commit_id_from_patch, tag_value}; |
| 12 | pub mod identify_ahead_behind; | ||
| 13 | pub mod nostr_url; | ||
| 16 | 14 | ||
| 17 | pub struct Repo { | 15 | pub struct Repo { |
| 18 | pub git_repo: git2::Repository, | 16 | pub git_repo: git2::Repository, |
| @@ -835,188 +833,6 @@ fn extract_sig_from_patch_tags<'a>( | |||
| 835 | .context("failed to create git signature") | 833 | .context("failed to create git signature") |
| 836 | } | 834 | } |
| 837 | 835 | ||
| 838 | #[derive(Debug, PartialEq)] | ||
| 839 | pub enum ServerProtocol { | ||
| 840 | Ssh, | ||
| 841 | Https, | ||
| 842 | Http, | ||
| 843 | Git, | ||
| 844 | } | ||
| 845 | |||
| 846 | #[derive(Debug, PartialEq)] | ||
| 847 | pub struct NostrUrlDecoded { | ||
| 848 | pub coordinates: HashSet<Coordinate>, | ||
| 849 | pub protocol: Option<ServerProtocol>, | ||
| 850 | pub user: Option<String>, | ||
| 851 | } | ||
| 852 | |||
| 853 | static INCORRECT_NOSTR_URL_FORMAT_ERROR: &str = "incorrect nostr git url format. try nostr://naddr123 or nostr://npub123/my-repo or nostr://ssh/npub123/relay.damus.io/my-repo"; | ||
| 854 | |||
| 855 | impl NostrUrlDecoded { | ||
| 856 | pub fn from_str(url: &str) -> Result<Self> { | ||
| 857 | let mut coordinates = HashSet::new(); | ||
| 858 | let mut protocol = None; | ||
| 859 | let mut user = None; | ||
| 860 | let mut relays = vec![]; | ||
| 861 | |||
| 862 | if !url.starts_with("nostr://") { | ||
| 863 | bail!("nostr git url must start with nostr://"); | ||
| 864 | } | ||
| 865 | // process get url parameters if present | ||
| 866 | for (name, value) in Url::parse(url)?.query_pairs() { | ||
| 867 | if name.contains("relay") { | ||
| 868 | let mut decoded = urlencoding::decode(&value) | ||
| 869 | .context("could not parse relays in nostr git url")? | ||
| 870 | .to_string(); | ||
| 871 | if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { | ||
| 872 | decoded = format!("wss://{decoded}"); | ||
| 873 | } | ||
| 874 | let url = | ||
| 875 | Url::parse(&decoded).context("could not parse relays in nostr git url")?; | ||
| 876 | relays.push(url.to_string()); | ||
| 877 | } else if name == "protocol" { | ||
| 878 | protocol = match value.as_ref() { | ||
| 879 | "ssh" => Some(ServerProtocol::Ssh), | ||
| 880 | "https" => Some(ServerProtocol::Https), | ||
| 881 | "http" => Some(ServerProtocol::Http), | ||
| 882 | "git" => Some(ServerProtocol::Git), | ||
| 883 | _ => None, | ||
| 884 | }; | ||
| 885 | } else if name == "user" { | ||
| 886 | user = Some(value.to_string()); | ||
| 887 | } | ||
| 888 | } | ||
| 889 | |||
| 890 | let mut parts: Vec<&str> = url[8..] | ||
| 891 | .split('?') | ||
| 892 | .next() | ||
| 893 | .unwrap_or("") | ||
| 894 | .split('/') | ||
| 895 | .collect(); | ||
| 896 | |||
| 897 | // extract optional protocol | ||
| 898 | if protocol.is_none() { | ||
| 899 | let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; | ||
| 900 | let protocol_str = if let Some(at_index) = part.find('@') { | ||
| 901 | user = Some(part[..at_index].to_string()); | ||
| 902 | &part[at_index + 1..] | ||
| 903 | } else { | ||
| 904 | part | ||
| 905 | }; | ||
| 906 | protocol = match protocol_str { | ||
| 907 | "ssh" => Some(ServerProtocol::Ssh), | ||
| 908 | "https" => Some(ServerProtocol::Https), | ||
| 909 | "http" => Some(ServerProtocol::Http), | ||
| 910 | "git" => Some(ServerProtocol::Git), | ||
| 911 | _ => protocol, | ||
| 912 | }; | ||
| 913 | if protocol.is_some() { | ||
| 914 | parts.remove(0); | ||
| 915 | } | ||
| 916 | } | ||
| 917 | // extract naddr npub/<optional-relays>/identifer | ||
| 918 | let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; | ||
| 919 | // naddr used | ||
| 920 | if let Ok(coordinate) = Coordinate::parse(part) { | ||
| 921 | if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { | ||
| 922 | coordinates.insert(coordinate); | ||
| 923 | } else { | ||
| 924 | bail!("naddr doesnt point to a git repository announcement"); | ||
| 925 | } | ||
| 926 | // npub/<optional-relays>/identifer used | ||
| 927 | } else if let Ok(public_key) = PublicKey::parse(part) { | ||
| 928 | parts.remove(0); | ||
| 929 | let identifier = parts | ||
| 930 | .pop() | ||
| 931 | .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")? | ||
| 932 | .to_string(); | ||
| 933 | for relay in parts { | ||
| 934 | let mut decoded = urlencoding::decode(relay) | ||
| 935 | .context("could not parse relays in nostr git url")? | ||
| 936 | .to_string(); | ||
| 937 | if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { | ||
| 938 | decoded = format!("wss://{decoded}"); | ||
| 939 | } | ||
| 940 | let url = | ||
| 941 | Url::parse(&decoded).context("could not parse relays in nostr git url")?; | ||
| 942 | relays.push(url.to_string()); | ||
| 943 | } | ||
| 944 | coordinates.insert(Coordinate { | ||
| 945 | identifier, | ||
| 946 | public_key, | ||
| 947 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 948 | relays, | ||
| 949 | }); | ||
| 950 | } else { | ||
| 951 | bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR); | ||
| 952 | } | ||
| 953 | |||
| 954 | Ok(Self { | ||
| 955 | coordinates, | ||
| 956 | protocol, | ||
| 957 | user, | ||
| 958 | }) | ||
| 959 | } | ||
| 960 | } | ||
| 961 | |||
| 962 | /** produce error when using local repo or custom protocols */ | ||
| 963 | pub fn convert_clone_url_to_https(url: &str) -> Result<String> { | ||
| 964 | // Strip credentials if present | ||
| 965 | let stripped_url = strip_credentials(url); | ||
| 966 | |||
| 967 | // Check if the URL is already in HTTPS format | ||
| 968 | if stripped_url.starts_with("https://") { | ||
| 969 | return Ok(stripped_url); | ||
| 970 | } | ||
| 971 | // Convert http:// to https:// | ||
| 972 | else if stripped_url.starts_with("http://") { | ||
| 973 | return Ok(stripped_url.replace("http://", "https://")); | ||
| 974 | } | ||
| 975 | // Check if the URL starts with SSH | ||
| 976 | else if stripped_url.starts_with("ssh://") { | ||
| 977 | // Convert SSH to HTTPS | ||
| 978 | let parts: Vec<&str> = stripped_url | ||
| 979 | .trim_start_matches("ssh://") | ||
| 980 | .split('/') | ||
| 981 | .collect(); | ||
| 982 | if parts.len() >= 2 { | ||
| 983 | // Construct the HTTPS URL | ||
| 984 | return Ok(format!("https://{}/{}", parts[0], parts[1..].join("/"))); | ||
| 985 | } | ||
| 986 | bail!("Invalid SSH URL format: {}", url); | ||
| 987 | } | ||
| 988 | // Convert ftp:// to https:// | ||
| 989 | else if stripped_url.starts_with("ftp://") { | ||
| 990 | return Ok(stripped_url.replace("ftp://", "https://")); | ||
| 991 | } | ||
| 992 | // Convert git:// to https:// | ||
| 993 | else if stripped_url.starts_with("git://") { | ||
| 994 | return Ok(stripped_url.replace("git://", "https://")); | ||
| 995 | } | ||
| 996 | |||
| 997 | // If the URL is neither HTTPS, SSH, nor git@, return an error | ||
| 998 | bail!("Unsupported URL protocol: {}", url); | ||
| 999 | } | ||
| 1000 | |||
| 1001 | // Function to strip username and password from the URL | ||
| 1002 | fn strip_credentials(url: &str) -> String { | ||
| 1003 | if let Some(pos) = url.find("://") { | ||
| 1004 | let (protocol, rest) = url.split_at(pos + 3); // Split at "://" | ||
| 1005 | let rest_parts: Vec<&str> = rest.split('@').collect(); | ||
| 1006 | if rest_parts.len() > 1 { | ||
| 1007 | // If there are credentials, return the URL without them | ||
| 1008 | return format!("{}{}", protocol, rest_parts[1]); | ||
| 1009 | } | ||
| 1010 | } else if let Some(at_pos) = url.find('@') { | ||
| 1011 | // Handle user@host:path format | ||
| 1012 | let (_, rest) = url.split_at(at_pos); | ||
| 1013 | // This is a git@ syntax | ||
| 1014 | let host_and_repo = &rest[1..]; // Skip the ':' | ||
| 1015 | return format!("ssh://{}", host_and_repo.replace(':', "/")); | ||
| 1016 | } | ||
| 1017 | url.to_string() // Return the original URL if no credentials are found | ||
| 1018 | } | ||
| 1019 | |||
| 1020 | #[cfg(test)] | 836 | #[cfg(test)] |
| 1021 | mod tests { | 837 | mod tests { |
| 1022 | use std::fs; | 838 | use std::fs; |
| @@ -1813,7 +1629,7 @@ mod tests { | |||
| 1813 | use test_utils::TEST_KEY_1_SIGNER; | 1629 | use test_utils::TEST_KEY_1_SIGNER; |
| 1814 | 1630 | ||
| 1815 | use super::*; | 1631 | use super::*; |
| 1816 | use crate::{repo_ref::RepoRef, sub_commands::send::generate_patch_event}; | 1632 | use crate::{git_events::generate_patch_event, repo_ref::RepoRef}; |
| 1817 | 1633 | ||
| 1818 | async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> { | 1634 | async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> { |
| 1819 | let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); | 1635 | let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); |
| @@ -1959,9 +1775,7 @@ mod tests { | |||
| 1959 | use test_utils::TEST_KEY_1_SIGNER; | 1775 | use test_utils::TEST_KEY_1_SIGNER; |
| 1960 | 1776 | ||
| 1961 | use super::*; | 1777 | use super::*; |
| 1962 | use crate::{ | 1778 | use crate::{git_events::generate_cover_letter_and_patch_events, repo_ref::RepoRef}; |
| 1963 | repo_ref::RepoRef, sub_commands::send::generate_cover_letter_and_patch_events, | ||
| 1964 | }; | ||
| 1965 | 1779 | ||
| 1966 | static BRANCH_NAME: &str = "add-example-feature"; | 1780 | static BRANCH_NAME: &str = "add-example-feature"; |
| 1967 | // returns original_repo, cover_letter_event, patch_events | 1781 | // returns original_repo, cover_letter_event, patch_events |
| @@ -2497,70 +2311,4 @@ mod tests { | |||
| 2497 | Ok(()) | 2311 | Ok(()) |
| 2498 | } | 2312 | } |
| 2499 | } | 2313 | } |
| 2500 | mod convert_clone_url_to_https { | ||
| 2501 | use super::*; | ||
| 2502 | |||
| 2503 | #[test] | ||
| 2504 | fn test_https_url() { | ||
| 2505 | let url = "https://github.com/user/repo.git"; | ||
| 2506 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2507 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2508 | } | ||
| 2509 | |||
| 2510 | #[test] | ||
| 2511 | fn test_http_url() { | ||
| 2512 | let url = "http://github.com/user/repo.git"; | ||
| 2513 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2514 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2515 | } | ||
| 2516 | |||
| 2517 | #[test] | ||
| 2518 | fn test_http_url_with_credentials() { | ||
| 2519 | let url = "http://username:password@github.com/user/repo.git"; | ||
| 2520 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2521 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2522 | } | ||
| 2523 | |||
| 2524 | #[test] | ||
| 2525 | fn test_git_at_url() { | ||
| 2526 | let url = "git@github.com:user/repo.git"; | ||
| 2527 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2528 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2529 | } | ||
| 2530 | |||
| 2531 | #[test] | ||
| 2532 | fn test_user_at_url() { | ||
| 2533 | let url = "user1@github.com:user/repo.git"; | ||
| 2534 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2535 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2536 | } | ||
| 2537 | |||
| 2538 | #[test] | ||
| 2539 | fn test_ssh_url() { | ||
| 2540 | let url = "ssh://github.com/user/repo.git"; | ||
| 2541 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2542 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2543 | } | ||
| 2544 | |||
| 2545 | #[test] | ||
| 2546 | fn test_ftp_url() { | ||
| 2547 | let url = "ftp://example.com/repo.git"; | ||
| 2548 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2549 | assert_eq!(result, "https://example.com/repo.git"); | ||
| 2550 | } | ||
| 2551 | |||
| 2552 | #[test] | ||
| 2553 | fn test_git_protocol_url() { | ||
| 2554 | let url = "git://example.com/repo.git"; | ||
| 2555 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2556 | assert_eq!(result, "https://example.com/repo.git"); | ||
| 2557 | } | ||
| 2558 | |||
| 2559 | #[test] | ||
| 2560 | fn test_invalid_url() { | ||
| 2561 | let url = "unsupported://example.com/repo.git"; | ||
| 2562 | let result = convert_clone_url_to_https(url); | ||
| 2563 | assert!(result.is_err()); | ||
| 2564 | } | ||
| 2565 | } | ||
| 2566 | } | 2314 | } |