diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 96a6fe5e6c..e6ac6e428c 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -271,7 +271,7 @@ impl TestServer { let git_hosting_provider_registry = cx.update(GitHostingProviderRegistry::default_global); git_hosting_provider_registry - .register_hosting_provider(Arc::new(git_hosting_providers::Github)); + .register_hosting_provider(Arc::new(git_hosting_providers::Github::new())); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index 6dc846d9f3..b629082e7b 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -2,9 +2,12 @@ mod providers; use std::sync::Arc; +use anyhow::{anyhow, Result}; use git::repository::GitRepository; use git::GitHostingProviderRegistry; use gpui::App; +use url::Url; +use util::maybe; pub use crate::providers::*; @@ -15,7 +18,7 @@ pub fn init(cx: &App) { provider_registry.register_hosting_provider(Arc::new(Chromium)); provider_registry.register_hosting_provider(Arc::new(Codeberg)); provider_registry.register_hosting_provider(Arc::new(Gitee)); - provider_registry.register_hosting_provider(Arc::new(Github)); + provider_registry.register_hosting_provider(Arc::new(Github::new())); provider_registry.register_hosting_provider(Arc::new(Gitlab::new())); provider_registry.register_hosting_provider(Arc::new(Sourcehut)); } @@ -34,5 +37,51 @@ pub fn register_additional_providers( if let Ok(gitlab_self_hosted) = Gitlab::from_remote_url(&origin_url) { provider_registry.register_hosting_provider(Arc::new(gitlab_self_hosted)); + } else if let Ok(github_self_hosted) = Github::from_remote_url(&origin_url) { + provider_registry.register_hosting_provider(Arc::new(github_self_hosted)); + } +} + +pub fn get_host_from_git_remote_url(remote_url: &str) -> Result { + maybe!({ + if let Some(remote_url) = remote_url.strip_prefix("git@") { + if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') { + return Some(host.to_string()); + } + } + + Url::parse(&remote_url) + .ok() + .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string())) + }) + .ok_or_else(|| anyhow!("URL has no host")) +} + +#[cfg(test)] +mod tests { + use super::get_host_from_git_remote_url; + use pretty_assertions::assert_eq; + + #[test] + fn test_get_host_from_git_remote_url() { + let tests = [ + ( + "https://jlannister@github.com/some-org/some-repo.git", + Some("github.com".to_string()), + ), + ( + "git@github.com:zed-industries/zed.git", + Some("github.com".to_string()), + ), + ( + "git@my.super.long.subdomain.com:zed-industries/zed.git", + Some("my.super.long.subdomain.com".to_string()), + ), + ]; + + for (remote_url, expected_host) in tests { + let host = get_host_from_git_remote_url(remote_url).ok(); + assert_eq!(host, expected_host); + } } } diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index f86b586ea8..7d2f20ff70 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -15,6 +15,8 @@ use git::{ PullRequest, RemoteUrl, }; +use crate::get_host_from_git_remote_url; + fn pull_request_number_regex() -> &'static Regex { static PULL_REQUEST_NUMBER_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\(#(\d+)\)$").unwrap()); @@ -43,9 +45,35 @@ struct User { pub avatar_url: String, } -pub struct Github; +pub struct Github { + name: String, + base_url: Url, +} impl Github { + pub fn new() -> Self { + Self { + name: "GitHub".to_string(), + base_url: Url::parse("https://github.com").unwrap(), + } + } + + pub fn from_remote_url(remote_url: &str) -> Result { + let host = get_host_from_git_remote_url(remote_url)?; + + // TODO: detecting self hosted instances by checking whether "github" is in the url or not + // is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more + // information. + if !host.contains("github") { + bail!("not a GitHub URL"); + } + + Ok(Self { + name: "GitHub Self-Hosted".to_string(), + base_url: Url::parse(&format!("https://{}", host))?, + }) + } + async fn fetch_github_commit_author( &self, repo_owner: &str, @@ -53,7 +81,10 @@ impl Github { commit: &str, client: &Arc, ) -> Result> { - let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}"); + let Some(host) = self.base_url.host_str() else { + bail!("failed to get host from github base url"); + }; + let url = format!("https://api.{host}/repos/{repo_owner}/{repo}/commits/{commit}"); let mut request = Request::get(&url) .header("Content-Type", "application/json") @@ -90,15 +121,17 @@ impl Github { #[async_trait] impl GitHostingProvider for Github { fn name(&self) -> String { - "GitHub".to_string() + self.name.clone() } fn base_url(&self) -> Url { - Url::parse("https://github.com").unwrap() + self.base_url.clone() } fn supports_avatars(&self) -> bool { - true + // Avatars are not supported for self-hosted GitHub instances + // See tracking issue: https://github.com/zed-industries/zed/issues/11043 + &self.name == "GitHub" } fn format_line_number(&self, line: u32) -> String { @@ -113,7 +146,7 @@ impl GitHostingProvider for Github { let url = RemoteUrl::from_str(url).ok()?; let host = url.host_str()?; - if host != "github.com" { + if host != self.base_url.host_str()? { return None; } @@ -203,9 +236,69 @@ mod tests { use super::*; + #[test] + fn test_from_remote_url_ssh() { + let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git"; + let github = Github::from_remote_url(remote_url).unwrap(); + + assert!(!github.supports_avatars()); + assert_eq!(github.name, "GitHub Self-Hosted".to_string()); + assert_eq!( + github.base_url, + Url::parse("https://github.my-enterprise.com").unwrap() + ); + } + + #[test] + fn test_from_remote_url_https() { + let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git"; + let github = Github::from_remote_url(remote_url).unwrap(); + + assert!(!github.supports_avatars()); + assert_eq!(github.name, "GitHub Self-Hosted".to_string()); + assert_eq!( + github.base_url, + Url::parse("https://github.my-enterprise.com").unwrap() + ); + } + + #[test] + fn test_parse_remote_url_given_self_hosted_ssh_url() { + let remote_url = "git@github.my-enterprise.com:zed-industries/zed.git"; + let parsed_remote = Github::from_remote_url(remote_url) + .unwrap() + .parse_remote_url(remote_url) + .unwrap(); + + assert_eq!( + parsed_remote, + ParsedGitRemote { + owner: "zed-industries".into(), + repo: "zed".into(), + } + ); + } + + #[test] + fn test_parse_remote_url_given_self_hosted_https_url_with_subgroup() { + let remote_url = "https://github.my-enterprise.com/zed-industries/zed.git"; + let parsed_remote = Github::from_remote_url(remote_url) + .unwrap() + .parse_remote_url(remote_url) + .unwrap(); + + assert_eq!( + parsed_remote, + ParsedGitRemote { + owner: "zed-industries".into(), + repo: "zed".into(), + } + ); + } + #[test] fn test_parse_remote_url_given_ssh_url() { - let parsed_remote = Github + let parsed_remote = Github::new() .parse_remote_url("git@github.com:zed-industries/zed.git") .unwrap(); @@ -220,7 +313,7 @@ mod tests { #[test] fn test_parse_remote_url_given_https_url() { - let parsed_remote = Github + let parsed_remote = Github::new() .parse_remote_url("https://github.com/zed-industries/zed.git") .unwrap(); @@ -235,7 +328,7 @@ mod tests { #[test] fn test_parse_remote_url_given_https_url_with_username() { - let parsed_remote = Github + let parsed_remote = Github::new() .parse_remote_url("https://jlannister@github.com/some-org/some-repo.git") .unwrap(); @@ -254,7 +347,7 @@ mod tests { owner: "zed-industries".into(), repo: "zed".into(), }; - let permalink = Github.build_permalink( + let permalink = Github::new().build_permalink( remote, BuildPermalinkParams { sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7", @@ -269,7 +362,7 @@ mod tests { #[test] fn test_build_github_permalink() { - let permalink = Github.build_permalink( + let permalink = Github::new().build_permalink( ParsedGitRemote { owner: "zed-industries".into(), repo: "zed".into(), @@ -287,7 +380,7 @@ mod tests { #[test] fn test_build_github_permalink_with_single_line_selection() { - let permalink = Github.build_permalink( + let permalink = Github::new().build_permalink( ParsedGitRemote { owner: "zed-industries".into(), repo: "zed".into(), @@ -305,7 +398,7 @@ mod tests { #[test] fn test_build_github_permalink_with_multi_line_selection() { - let permalink = Github.build_permalink( + let permalink = Github::new().build_permalink( ParsedGitRemote { owner: "zed-industries".into(), repo: "zed".into(), @@ -328,8 +421,9 @@ mod tests { repo: "zed".into(), }; + let github = Github::new(); let message = "This does not contain a pull request"; - assert!(Github.extract_pull_request(&remote, message).is_none()); + assert!(github.extract_pull_request(&remote, message).is_none()); // Pull request number at end of first line let message = indoc! {r#" @@ -344,7 +438,7 @@ mod tests { }; assert_eq!( - Github + github .extract_pull_request(&remote, &message) .unwrap() .url @@ -359,6 +453,6 @@ mod tests { See the original PR, this is a fix. "# }; - assert_eq!(Github.extract_pull_request(&remote, &message), None); + assert_eq!(github.extract_pull_request(&remote, &message), None); } } diff --git a/crates/git_hosting_providers/src/providers/gitlab.rs b/crates/git_hosting_providers/src/providers/gitlab.rs index 7910379ef0..abdfc491f4 100644 --- a/crates/git_hosting_providers/src/providers/gitlab.rs +++ b/crates/git_hosting_providers/src/providers/gitlab.rs @@ -1,14 +1,15 @@ use std::str::FromStr; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use url::Url; -use util::maybe; use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, RemoteUrl, }; +use crate::get_host_from_git_remote_url; + #[derive(Debug)] pub struct Gitlab { name: String, @@ -24,19 +25,11 @@ impl Gitlab { } pub fn from_remote_url(remote_url: &str) -> Result { - let host = maybe!({ - if let Some(remote_url) = remote_url.strip_prefix("git@") { - if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') { - return Some(host.to_string()); - } - } - - Url::parse(&remote_url) - .ok() - .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string())) - }) - .ok_or_else(|| anyhow!("URL has no host"))?; + let host = get_host_from_git_remote_url(remote_url)?; + // TODO: detecting self hosted instances by checking whether "gitlab" is in the url or not + // is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more + // information. if !host.contains("gitlab") { bail!("not a GitLab URL"); }