diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index fb204fba82..cf07b74ac5 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -1,4 +1,10 @@ +pub mod blame; +pub mod commit; +pub mod diff; mod hosting_provider; +mod remote; +pub mod repository; +pub mod status; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; @@ -7,15 +13,9 @@ use std::fmt; use std::str::FromStr; use std::sync::LazyLock; -pub use git2 as libgit; - pub use crate::hosting_provider::*; - -pub mod blame; -pub mod commit; -pub mod diff; -pub mod repository; -pub mod status; +pub use crate::remote::*; +pub use git2 as libgit; pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git")); pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies")); diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs index 72ed92e8ab..4afbcf42a4 100644 --- a/crates/git/src/hosting_provider.rs +++ b/crates/git/src/hosting_provider.rs @@ -69,7 +69,7 @@ pub trait GitHostingProvider { /// Returns a formatted range of line numbers to be placed in a permalink URL. fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String; - fn parse_remote_url<'a>(&self, url: &'a str) -> Option>; + fn parse_remote_url(&self, url: &str) -> Option; fn extract_pull_request( &self, @@ -159,10 +159,10 @@ impl GitHostingProviderRegistry { } } -#[derive(Debug)] -pub struct ParsedGitRemote<'a> { - pub owner: &'a str, - pub repo: &'a str, +#[derive(Debug, PartialEq)] +pub struct ParsedGitRemote { + pub owner: Arc, + pub repo: Arc, } pub fn parse_git_remote_url( diff --git a/crates/git/src/remote.rs b/crates/git/src/remote.rs new file mode 100644 index 0000000000..430836fcf3 --- /dev/null +++ b/crates/git/src/remote.rs @@ -0,0 +1,85 @@ +use derive_more::Deref; +use url::Url; + +/// The URL to a Git remote. +#[derive(Debug, PartialEq, Eq, Clone, Deref)] +pub struct RemoteUrl(Url); + +impl std::str::FromStr for RemoteUrl { + type Err = url::ParseError; + + fn from_str(input: &str) -> Result { + if input.starts_with("git@") { + // Rewrite remote URLs like `git@github.com:user/repo.git` to `ssh://git@github.com/user/repo.git` + let ssh_url = input.replacen(':', "/", 1).replace("git@", "ssh://git@"); + Ok(RemoteUrl(Url::parse(&ssh_url)?)) + } else { + Ok(RemoteUrl(Url::parse(input)?)) + } + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_parsing_valid_remote_urls() { + let valid_urls = vec![ + ( + "https://github.com/octocat/zed.git", + "https", + "github.com", + "/octocat/zed.git", + ), + ( + "git@github.com:octocat/zed.git", + "ssh", + "github.com", + "/octocat/zed.git", + ), + ( + "ssh://git@github.com/octocat/zed.git", + "ssh", + "github.com", + "/octocat/zed.git", + ), + ( + "file:///path/to/local/zed", + "file", + "", + "/path/to/local/zed", + ), + ]; + + for (input, expected_scheme, expected_host, expected_path) in valid_urls { + let parsed = input.parse::().expect("failed to parse URL"); + let url = parsed.0; + assert_eq!( + url.scheme(), + expected_scheme, + "unexpected scheme for {input:?}", + ); + assert_eq!( + url.host_str().unwrap_or(""), + expected_host, + "unexpected host for {input:?}", + ); + assert_eq!(url.path(), expected_path, "unexpected path for {input:?}"); + } + } + + #[test] + fn test_parsing_invalid_remote_urls() { + let invalid_urls = vec!["not_a_url", "http://"]; + + for url in invalid_urls { + assert!( + url.parse::().is_err(), + "expected \"{url}\" to not parse as a Git remote URL", + ); + } + } +} diff --git a/crates/git_hosting_providers/src/providers/bitbucket.rs b/crates/git_hosting_providers/src/providers/bitbucket.rs index 50c453442f..da95f256da 100644 --- a/crates/git_hosting_providers/src/providers/bitbucket.rs +++ b/crates/git_hosting_providers/src/providers/bitbucket.rs @@ -1,6 +1,11 @@ +use std::str::FromStr; + use url::Url; -use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; +use git::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, + RemoteUrl, +}; pub struct Bitbucket; @@ -25,18 +30,22 @@ impl GitHostingProvider for Bitbucket { format!("lines-{start_line}:{end_line}") } - fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { - if url.contains("bitbucket.org") { - let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?; - let (owner, repo) = repo_with_owner - .trim_start_matches('/') - .trim_start_matches(':') - .split_once('/')?; + fn parse_remote_url(&self, url: &str) -> Option { + let url = RemoteUrl::from_str(url).ok()?; - return Some(ParsedGitRemote { owner, repo }); + let host = url.host_str()?; + if host != "bitbucket.org" { + return None; } - None + let mut path_segments = url.path_segments()?; + let owner = path_segments.next()?; + let repo = path_segments.next()?.trim_end_matches(".git"); + + Some(ParsedGitRemote { + owner: owner.into(), + repo: repo.into(), + }) } fn build_commit_permalink( @@ -88,8 +97,8 @@ mod tests { let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git"; let (provider, parsed) = parse_git_remote_url(provider_registry, url).unwrap(); assert_eq!(provider.name(), "Bitbucket"); - assert_eq!(parsed.owner, "thorstenzed"); - assert_eq!(parsed.repo, "testingrepo"); + assert_eq!(parsed.owner.as_ref(), "thorstenzed"); + assert_eq!(parsed.repo.as_ref(), "testingrepo"); } #[test] @@ -99,8 +108,8 @@ mod tests { let url = "https://bitbucket.org/thorstenzed/testingrepo.git"; let (provider, parsed) = parse_git_remote_url(provider_registry, url).unwrap(); assert_eq!(provider.name(), "Bitbucket"); - assert_eq!(parsed.owner, "thorstenzed"); - assert_eq!(parsed.repo, "testingrepo"); + assert_eq!(parsed.owner.as_ref(), "thorstenzed"); + assert_eq!(parsed.repo.as_ref(), "testingrepo"); } #[test] @@ -110,15 +119,15 @@ mod tests { let url = "git@bitbucket.org:thorstenzed/testingrepo.git"; let (provider, parsed) = parse_git_remote_url(provider_registry, url).unwrap(); assert_eq!(provider.name(), "Bitbucket"); - assert_eq!(parsed.owner, "thorstenzed"); - assert_eq!(parsed.repo, "testingrepo"); + assert_eq!(parsed.owner.as_ref(), "thorstenzed"); + assert_eq!(parsed.repo.as_ref(), "testingrepo"); } #[test] fn test_build_bitbucket_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "thorstenzed", - repo: "testingrepo", + owner: "thorstenzed".into(), + repo: "testingrepo".into(), }; let permalink = Bitbucket.build_permalink( remote, @@ -136,8 +145,8 @@ mod tests { #[test] fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "thorstenzed", - repo: "testingrepo", + owner: "thorstenzed".into(), + repo: "testingrepo".into(), }; let permalink = Bitbucket.build_permalink( remote, @@ -156,8 +165,8 @@ mod tests { #[test] fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "thorstenzed", - repo: "testingrepo", + owner: "thorstenzed".into(), + repo: "testingrepo".into(), }; let permalink = Bitbucket.build_permalink( remote, diff --git a/crates/git_hosting_providers/src/providers/codeberg.rs b/crates/git_hosting_providers/src/providers/codeberg.rs index 3f6a016f68..afd1c564aa 100644 --- a/crates/git_hosting_providers/src/providers/codeberg.rs +++ b/crates/git_hosting_providers/src/providers/codeberg.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::sync::Arc; use anyhow::{bail, Context, Result}; @@ -9,6 +10,7 @@ use url::Url; use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, + RemoteUrl, }; #[derive(Debug, Deserialize)] @@ -103,19 +105,22 @@ impl GitHostingProvider for Codeberg { format!("L{start_line}-L{end_line}") } - fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { - if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") { - let repo_with_owner = url - .trim_start_matches("git@codeberg.org:") - .trim_start_matches("https://codeberg.org/") - .trim_end_matches(".git"); + fn parse_remote_url(&self, url: &str) -> Option { + let url = RemoteUrl::from_str(url).ok()?; - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { owner, repo }); + let host = url.host_str()?; + if host != "codeberg.org" { + return None; } - None + let mut path_segments = url.path_segments()?; + let owner = path_segments.next()?; + let repo = path_segments.next()?.trim_end_matches(".git"); + + Some(ParsedGitRemote { + owner: owner.into(), + repo: repo.into(), + }) } fn build_commit_permalink( @@ -175,8 +180,8 @@ mod tests { #[test] fn test_build_codeberg_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Codeberg.build_permalink( remote, @@ -194,8 +199,8 @@ mod tests { #[test] fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Codeberg.build_permalink( remote, @@ -213,8 +218,8 @@ mod tests { #[test] fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Codeberg.build_permalink( remote, @@ -232,8 +237,8 @@ mod tests { #[test] fn test_build_codeberg_permalink_from_https_url() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Codeberg.build_permalink( remote, @@ -251,8 +256,8 @@ mod tests { #[test] fn test_build_codeberg_permalink_from_https_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Codeberg.build_permalink( remote, @@ -270,8 +275,8 @@ mod tests { #[test] fn test_build_codeberg_permalink_from_https_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Codeberg.build_permalink( remote, diff --git a/crates/git_hosting_providers/src/providers/gitee.rs b/crates/git_hosting_providers/src/providers/gitee.rs index 34d1da262d..2333964e16 100644 --- a/crates/git_hosting_providers/src/providers/gitee.rs +++ b/crates/git_hosting_providers/src/providers/gitee.rs @@ -1,6 +1,11 @@ +use std::str::FromStr; + use url::Url; -use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; +use git::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, + RemoteUrl, +}; pub struct Gitee; @@ -25,19 +30,22 @@ impl GitHostingProvider for Gitee { format!("L{start_line}-{end_line}") } - fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { - if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") { - let repo_with_owner = url - .trim_start_matches("git@gitee.com:") - .trim_start_matches("https://gitee.com/") - .trim_end_matches(".git"); + fn parse_remote_url(&self, url: &str) -> Option { + let url = RemoteUrl::from_str(url).ok()?; - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { owner, repo }); + let host = url.host_str()?; + if host != "gitee.com" { + return None; } - None + let mut path_segments = url.path_segments()?; + let owner = path_segments.next()?; + let repo = path_segments.next()?.trim_end_matches(".git"); + + Some(ParsedGitRemote { + owner: owner.into(), + repo: repo.into(), + }) } fn build_commit_permalink( @@ -81,8 +89,8 @@ mod tests { #[test] fn test_build_gitee_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "libkitten", - repo: "zed", + owner: "libkitten".into(), + repo: "zed".into(), }; let permalink = Gitee.build_permalink( remote, @@ -100,8 +108,8 @@ mod tests { #[test] fn test_build_gitee_permalink_from_ssh_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "libkitten", - repo: "zed", + owner: "libkitten".into(), + repo: "zed".into(), }; let permalink = Gitee.build_permalink( remote, @@ -119,8 +127,8 @@ mod tests { #[test] fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "libkitten", - repo: "zed", + owner: "libkitten".into(), + repo: "zed".into(), }; let permalink = Gitee.build_permalink( remote, @@ -138,8 +146,8 @@ mod tests { #[test] fn test_build_gitee_permalink_from_https_url() { let remote = ParsedGitRemote { - owner: "libkitten", - repo: "zed", + owner: "libkitten".into(), + repo: "zed".into(), }; let permalink = Gitee.build_permalink( remote, @@ -157,8 +165,8 @@ mod tests { #[test] fn test_build_gitee_permalink_from_https_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "libkitten", - repo: "zed", + owner: "libkitten".into(), + repo: "zed".into(), }; let permalink = Gitee.build_permalink( remote, @@ -176,8 +184,8 @@ mod tests { #[test] fn test_build_gitee_permalink_from_https_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "libkitten", - repo: "zed", + owner: "libkitten".into(), + repo: "zed".into(), }; let permalink = Gitee.build_permalink( remote, diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 4078025fa0..1b9d200a7c 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::sync::{Arc, OnceLock}; use anyhow::{bail, Context, Result}; @@ -10,7 +11,7 @@ use url::Url; use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, - PullRequest, + PullRequest, RemoteUrl, }; fn pull_request_number_regex() -> &'static Regex { @@ -107,19 +108,22 @@ impl GitHostingProvider for Github { format!("L{start_line}-L{end_line}") } - fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { - if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") { - let repo_with_owner = url - .trim_start_matches("git@github.com:") - .trim_start_matches("https://github.com/") - .trim_end_matches(".git"); + fn parse_remote_url(&self, url: &str) -> Option { + let url = RemoteUrl::from_str(url).ok()?; - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { owner, repo }); + let host = url.host_str()?; + if host != "github.com" { + return None; } - None + let mut path_segments = url.path_segments()?; + let owner = path_segments.next()?; + let repo = path_segments.next()?.trim_end_matches(".git"); + + Some(ParsedGitRemote { + owner: owner.into(), + repo: repo.into(), + }) } fn build_commit_permalink( @@ -198,11 +202,26 @@ mod tests { use super::*; + #[test] + fn test_parse_remote_url_given_https_url_with_username() { + let parsed_remote = Github + .parse_remote_url("https://jlannister@github.com/some-org/some-repo.git") + .unwrap(); + + assert_eq!( + parsed_remote, + ParsedGitRemote { + owner: "some-org".into(), + repo: "some-repo".into(), + } + ); + } + #[test] fn test_build_github_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Github.build_permalink( remote, @@ -220,8 +239,8 @@ mod tests { #[test] fn test_build_github_permalink_from_ssh_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Github.build_permalink( remote, @@ -239,8 +258,8 @@ mod tests { #[test] fn test_build_github_permalink_from_ssh_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Github.build_permalink( remote, @@ -258,8 +277,8 @@ mod tests { #[test] fn test_build_github_permalink_from_https_url() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Github.build_permalink( remote, @@ -277,8 +296,8 @@ mod tests { #[test] fn test_build_github_permalink_from_https_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Github.build_permalink( remote, @@ -296,8 +315,8 @@ mod tests { #[test] fn test_build_github_permalink_from_https_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Github.build_permalink( remote, @@ -315,8 +334,8 @@ mod tests { #[test] fn test_github_pull_requests() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let message = "This does not contain a pull request"; diff --git a/crates/git_hosting_providers/src/providers/gitlab.rs b/crates/git_hosting_providers/src/providers/gitlab.rs index a8b97182c0..bf97fd4d67 100644 --- a/crates/git_hosting_providers/src/providers/gitlab.rs +++ b/crates/git_hosting_providers/src/providers/gitlab.rs @@ -1,8 +1,13 @@ +use std::str::FromStr; + use anyhow::{anyhow, bail, Result}; use url::Url; use util::maybe; -use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; +use git::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, + RemoteUrl, +}; #[derive(Debug)] pub struct Gitlab { @@ -64,21 +69,22 @@ impl GitHostingProvider for Gitlab { format!("L{start_line}-{end_line}") } - fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { - let host = self.base_url.host_str()?; + fn parse_remote_url(&self, url: &str) -> Option { + let url = RemoteUrl::from_str(url).ok()?; - if url.starts_with(&format!("git@{host}")) || url.starts_with(&format!("https://{host}/")) { - let repo_with_owner = url - .trim_start_matches(&format!("git@{host}:")) - .trim_start_matches(&format!("https://{host}/")) - .trim_end_matches(".git"); - - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { owner, repo }); + let host = url.host_str()?; + if host != self.base_url.host_str()? { + return None; } - None + let mut path_segments = url.path_segments()?; + let owner = path_segments.next()?; + let repo = path_segments.next()?.trim_end_matches(".git"); + + Some(ParsedGitRemote { + owner: owner.into(), + repo: repo.into(), + }) } fn build_commit_permalink( @@ -127,8 +133,8 @@ mod tests { #[test] fn test_build_gitlab_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Gitlab::new().build_permalink( remote, @@ -146,8 +152,8 @@ mod tests { #[test] fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Gitlab::new().build_permalink( remote, @@ -165,8 +171,8 @@ mod tests { #[test] fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Gitlab::new().build_permalink( remote, @@ -184,8 +190,8 @@ mod tests { #[test] fn test_build_gitlab_permalink_from_https_url() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Gitlab::new().build_permalink( remote, @@ -203,8 +209,8 @@ mod tests { #[test] fn test_build_gitlab_permalink_from_https_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Gitlab::new().build_permalink( remote, @@ -222,8 +228,8 @@ mod tests { #[test] fn test_build_gitlab_permalink_from_https_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let permalink = Gitlab::new().build_permalink( remote, @@ -241,8 +247,8 @@ mod tests { #[test] fn test_build_gitlab_self_hosted_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let gitlab = Gitlab::from_remote_url("git@gitlab.some-enterprise.com:zed-industries/zed.git") @@ -263,8 +269,8 @@ mod tests { #[test] fn test_build_gitlab_self_hosted_permalink_from_https_url() { let remote = ParsedGitRemote { - owner: "zed-industries", - repo: "zed", + owner: "zed-industries".into(), + repo: "zed".into(), }; let gitlab = Gitlab::from_remote_url("https://gitlab-instance.big-co.com/zed-industries/zed.git") diff --git a/crates/git_hosting_providers/src/providers/sourcehut.rs b/crates/git_hosting_providers/src/providers/sourcehut.rs index 623b23ab6c..99ab53c8a3 100644 --- a/crates/git_hosting_providers/src/providers/sourcehut.rs +++ b/crates/git_hosting_providers/src/providers/sourcehut.rs @@ -1,6 +1,11 @@ +use std::str::FromStr; + use url::Url; -use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; +use git::{ + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, + RemoteUrl, +}; pub struct Sourcehut; @@ -25,21 +30,27 @@ impl GitHostingProvider for Sourcehut { format!("L{start_line}-{end_line}") } - fn parse_remote_url<'a>(&self, url: &'a str) -> Option> { - if url.starts_with("git@git.sr.ht:") || url.starts_with("https://git.sr.ht/") { - // sourcehut indicates a repo with '.git' suffix as a separate repo. - // For example, "git@git.sr.ht:~username/repo" and "git@git.sr.ht:~username/repo.git" - // are two distinct repositories. - let repo_with_owner = url - .trim_start_matches("git@git.sr.ht:~") - .trim_start_matches("https://git.sr.ht/~"); + fn parse_remote_url(&self, url: &str) -> Option { + let url = RemoteUrl::from_str(url).ok()?; - let (owner, repo) = repo_with_owner.split_once('/')?; - - return Some(ParsedGitRemote { owner, repo }); + let host = url.host_str()?; + if host != "git.sr.ht" { + return None; } - None + let mut path_segments = url.path_segments()?; + let owner = path_segments.next()?; + // We don't trim the `.git` suffix here like we do elsewhere, as + // sourcehut treats a repo with `.git` suffix as a separate repo. + // + // For example, `git@git.sr.ht:~username/repo` and `git@git.sr.ht:~username/repo.git` + // are two distinct repositories. + let repo = path_segments.next()?; + + Some(ParsedGitRemote { + owner: owner.into(), + repo: repo.into(), + }) } fn build_commit_permalink( @@ -83,8 +94,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_ssh_url() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Sourcehut.build_permalink( remote, @@ -102,8 +113,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_ssh_url_with_git_prefix() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed.git", + owner: "rajveermalviya".into(), + repo: "zed.git".into(), }; let permalink = Sourcehut.build_permalink( remote, @@ -121,8 +132,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_ssh_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Sourcehut.build_permalink( remote, @@ -140,8 +151,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_ssh_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Sourcehut.build_permalink( remote, @@ -159,8 +170,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_https_url() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Sourcehut.build_permalink( remote, @@ -178,8 +189,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_https_url_single_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Sourcehut.build_permalink( remote, @@ -197,8 +208,8 @@ mod tests { #[test] fn test_build_sourcehut_permalink_from_https_url_multi_line_selection() { let remote = ParsedGitRemote { - owner: "rajveermalviya", - repo: "zed", + owner: "rajveermalviya".into(), + repo: "zed".into(), }; let permalink = Sourcehut.build_permalink( remote,