@@ -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<String> {
+ 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);
+ }
}
}
@@ -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<Regex> =
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<Self> {
+ 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<dyn HttpClient>,
) -> Result<Option<User>> {
- 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);
}
}
@@ -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<Self> {
- 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");
}