Add support for self-hosted GitLab instances for Git permalinks (#19909)
This PR adds support for self-hosted GitLab instances when generating Git permalinks. If the `origin` Git remote contains `gitlab` in the URL hostname we will then attempt to register it as a self-hosted GitLab instance. A note on this: I don't think relying on specific keywords is going to be a suitable long-term solution to detection. In reality the self-hosted instance could be hosted anywhere (e.g., `vcs.my-company.com`), so we will ultimately need a way to have the user indicate which Git provider they are using (perhaps via a setting). Closes https://github.com/zed-industries/zed/issues/18012. Release Notes: - Added support for self-hosted GitLab instances when generating Git permalinks. - The instance URL must have `gitlab` somewhere in the host in order to be recognized.
This commit is contained in:
parent
3e2f1d733c
commit
322aa41ad6
7 changed files with 142 additions and 23 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4915,6 +4915,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"unindent",
|
"unindent",
|
||||||
"url",
|
"url",
|
||||||
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -14730,6 +14731,7 @@ dependencies = [
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"git",
|
"git",
|
||||||
"git2",
|
"git2",
|
||||||
|
"git_hosting_providers",
|
||||||
"gpui",
|
"gpui",
|
||||||
"http_client",
|
"http_client",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
|
|
@ -111,6 +111,12 @@ impl GitHostingProviderRegistry {
|
||||||
cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
|
cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the global [`GitHostingProviderRegistry`], if one is set.
|
||||||
|
pub fn try_global(cx: &AppContext) -> Option<Arc<Self>> {
|
||||||
|
cx.try_global::<GlobalGitHostingProviderRegistry>()
|
||||||
|
.map(|registry| registry.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the global [`GitHostingProviderRegistry`].
|
/// Returns the global [`GitHostingProviderRegistry`].
|
||||||
///
|
///
|
||||||
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
|
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
|
||||||
|
|
|
@ -22,6 +22,7 @@ regex.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod providers;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use git::repository::GitRepository;
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
|
|
||||||
|
@ -10,17 +11,27 @@ pub use crate::providers::*;
|
||||||
/// Initializes the Git hosting providers.
|
/// Initializes the Git hosting providers.
|
||||||
pub fn init(cx: &AppContext) {
|
pub fn init(cx: &AppContext) {
|
||||||
let provider_registry = GitHostingProviderRegistry::global(cx);
|
let provider_registry = GitHostingProviderRegistry::global(cx);
|
||||||
|
|
||||||
// The providers are stored in a `BTreeMap`, so insertion order matters.
|
|
||||||
// GitHub comes first.
|
|
||||||
provider_registry.register_hosting_provider(Arc::new(Github));
|
|
||||||
|
|
||||||
// Then GitLab.
|
|
||||||
provider_registry.register_hosting_provider(Arc::new(Gitlab));
|
|
||||||
|
|
||||||
// Then the other providers, in the order they were added.
|
|
||||||
provider_registry.register_hosting_provider(Arc::new(Gitee));
|
|
||||||
provider_registry.register_hosting_provider(Arc::new(Bitbucket));
|
provider_registry.register_hosting_provider(Arc::new(Bitbucket));
|
||||||
provider_registry.register_hosting_provider(Arc::new(Sourcehut));
|
|
||||||
provider_registry.register_hosting_provider(Arc::new(Codeberg));
|
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(Gitlab::new()));
|
||||||
|
provider_registry.register_hosting_provider(Arc::new(Sourcehut));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers additional Git hosting providers.
|
||||||
|
///
|
||||||
|
/// These require information from the Git repository to construct, so their
|
||||||
|
/// registration is deferred until we have a Git repository initialized.
|
||||||
|
pub fn register_additional_providers(
|
||||||
|
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||||
|
repository: Arc<dyn GitRepository>,
|
||||||
|
) {
|
||||||
|
let Some(origin_url) = repository.remote_url("origin") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(gitlab_self_hosted) = Gitlab::from_remote_url(&origin_url) {
|
||||||
|
provider_registry.register_hosting_provider(Arc::new(gitlab_self_hosted));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,55 @@
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use util::maybe;
|
||||||
|
|
||||||
use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
|
use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
|
||||||
|
|
||||||
pub struct Gitlab;
|
#[derive(Debug)]
|
||||||
|
pub struct Gitlab {
|
||||||
|
name: String,
|
||||||
|
base_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gitlab {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "GitLab".to_string(),
|
||||||
|
base_url: Url::parse("https://gitlab.com").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))?;
|
||||||
|
|
||||||
|
if !host.contains("gitlab") {
|
||||||
|
bail!("not a GitLab URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name: "GitLab Self-Hosted".to_string(),
|
||||||
|
base_url: Url::parse(&format!("https://{}", host))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl GitHostingProvider for Gitlab {
|
impl GitHostingProvider for Gitlab {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"GitLab".to_string()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_url(&self) -> Url {
|
fn base_url(&self) -> Url {
|
||||||
Url::parse("https://gitlab.com").unwrap()
|
self.base_url.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supports_avatars(&self) -> bool {
|
fn supports_avatars(&self) -> bool {
|
||||||
|
@ -26,10 +65,12 @@ impl GitHostingProvider for Gitlab {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>> {
|
fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>> {
|
||||||
if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
|
let host = self.base_url.host_str()?;
|
||||||
|
|
||||||
|
if url.starts_with(&format!("git@{host}")) || url.starts_with(&format!("https://{host}/")) {
|
||||||
let repo_with_owner = url
|
let repo_with_owner = url
|
||||||
.trim_start_matches("git@gitlab.com:")
|
.trim_start_matches(&format!("git@{host}:"))
|
||||||
.trim_start_matches("https://gitlab.com/")
|
.trim_start_matches(&format!("https://{host}/"))
|
||||||
.trim_end_matches(".git");
|
.trim_end_matches(".git");
|
||||||
|
|
||||||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||||
|
@ -79,6 +120,8 @@ impl GitHostingProvider for Gitlab {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -87,7 +130,7 @@ mod tests {
|
||||||
owner: "zed-industries",
|
owner: "zed-industries",
|
||||||
repo: "zed",
|
repo: "zed",
|
||||||
};
|
};
|
||||||
let permalink = Gitlab.build_permalink(
|
let permalink = Gitlab::new().build_permalink(
|
||||||
remote,
|
remote,
|
||||||
BuildPermalinkParams {
|
BuildPermalinkParams {
|
||||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||||
|
@ -106,7 +149,7 @@ mod tests {
|
||||||
owner: "zed-industries",
|
owner: "zed-industries",
|
||||||
repo: "zed",
|
repo: "zed",
|
||||||
};
|
};
|
||||||
let permalink = Gitlab.build_permalink(
|
let permalink = Gitlab::new().build_permalink(
|
||||||
remote,
|
remote,
|
||||||
BuildPermalinkParams {
|
BuildPermalinkParams {
|
||||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||||
|
@ -125,7 +168,7 @@ mod tests {
|
||||||
owner: "zed-industries",
|
owner: "zed-industries",
|
||||||
repo: "zed",
|
repo: "zed",
|
||||||
};
|
};
|
||||||
let permalink = Gitlab.build_permalink(
|
let permalink = Gitlab::new().build_permalink(
|
||||||
remote,
|
remote,
|
||||||
BuildPermalinkParams {
|
BuildPermalinkParams {
|
||||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||||
|
@ -144,7 +187,7 @@ mod tests {
|
||||||
owner: "zed-industries",
|
owner: "zed-industries",
|
||||||
repo: "zed",
|
repo: "zed",
|
||||||
};
|
};
|
||||||
let permalink = Gitlab.build_permalink(
|
let permalink = Gitlab::new().build_permalink(
|
||||||
remote,
|
remote,
|
||||||
BuildPermalinkParams {
|
BuildPermalinkParams {
|
||||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||||
|
@ -163,7 +206,7 @@ mod tests {
|
||||||
owner: "zed-industries",
|
owner: "zed-industries",
|
||||||
repo: "zed",
|
repo: "zed",
|
||||||
};
|
};
|
||||||
let permalink = Gitlab.build_permalink(
|
let permalink = Gitlab::new().build_permalink(
|
||||||
remote,
|
remote,
|
||||||
BuildPermalinkParams {
|
BuildPermalinkParams {
|
||||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||||
|
@ -182,7 +225,7 @@ mod tests {
|
||||||
owner: "zed-industries",
|
owner: "zed-industries",
|
||||||
repo: "zed",
|
repo: "zed",
|
||||||
};
|
};
|
||||||
let permalink = Gitlab.build_permalink(
|
let permalink = Gitlab::new().build_permalink(
|
||||||
remote,
|
remote,
|
||||||
BuildPermalinkParams {
|
BuildPermalinkParams {
|
||||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||||
|
@ -194,4 +237,48 @@ mod tests {
|
||||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
|
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
|
||||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_gitlab_self_hosted_permalink_from_ssh_url() {
|
||||||
|
let remote = ParsedGitRemote {
|
||||||
|
owner: "zed-industries",
|
||||||
|
repo: "zed",
|
||||||
|
};
|
||||||
|
let gitlab =
|
||||||
|
Gitlab::from_remote_url("git@gitlab.some-enterprise.com:zed-industries/zed.git")
|
||||||
|
.unwrap();
|
||||||
|
let permalink = gitlab.build_permalink(
|
||||||
|
remote,
|
||||||
|
BuildPermalinkParams {
|
||||||
|
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||||
|
path: "crates/editor/src/git/permalink.rs",
|
||||||
|
selection: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_url = "https://gitlab.some-enterprise.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||||
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_gitlab_self_hosted_permalink_from_https_url() {
|
||||||
|
let remote = ParsedGitRemote {
|
||||||
|
owner: "zed-industries",
|
||||||
|
repo: "zed",
|
||||||
|
};
|
||||||
|
let gitlab =
|
||||||
|
Gitlab::from_remote_url("https://gitlab-instance.big-co.com/zed-industries/zed.git")
|
||||||
|
.unwrap();
|
||||||
|
let permalink = gitlab.build_permalink(
|
||||||
|
remote,
|
||||||
|
BuildPermalinkParams {
|
||||||
|
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||||
|
path: "crates/zed/src/main.rs",
|
||||||
|
selection: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_url = "https://gitlab-instance.big-co.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||||
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
|
git_hosting_providers.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
ignore.workspace = true
|
ignore.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
|
|
@ -19,6 +19,7 @@ use futures::{
|
||||||
FutureExt as _, Stream, StreamExt,
|
FutureExt as _, Stream, StreamExt,
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
|
use git::GitHostingProviderRegistry;
|
||||||
use git::{
|
use git::{
|
||||||
repository::{GitFileStatus, GitRepository, RepoPath},
|
repository::{GitFileStatus, GitRepository, RepoPath},
|
||||||
status::GitStatus,
|
status::GitStatus,
|
||||||
|
@ -299,6 +300,7 @@ struct BackgroundScannerState {
|
||||||
removed_entries: HashMap<u64, Entry>,
|
removed_entries: HashMap<u64, Entry>,
|
||||||
changed_paths: Vec<Arc<Path>>,
|
changed_paths: Vec<Arc<Path>>,
|
||||||
prev_snapshot: Snapshot,
|
prev_snapshot: Snapshot,
|
||||||
|
git_hosting_provider_registry: Option<Arc<GitHostingProviderRegistry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -1004,6 +1006,7 @@ impl LocalWorktree {
|
||||||
let share_private_files = self.share_private_files;
|
let share_private_files = self.share_private_files;
|
||||||
let next_entry_id = self.next_entry_id.clone();
|
let next_entry_id = self.next_entry_id.clone();
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
let git_hosting_provider_registry = GitHostingProviderRegistry::try_global(cx);
|
||||||
let settings = self.settings.clone();
|
let settings = self.settings.clone();
|
||||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||||
let background_scanner = cx.background_executor().spawn({
|
let background_scanner = cx.background_executor().spawn({
|
||||||
|
@ -1039,6 +1042,7 @@ impl LocalWorktree {
|
||||||
paths_to_scan: Default::default(),
|
paths_to_scan: Default::default(),
|
||||||
removed_entries: Default::default(),
|
removed_entries: Default::default(),
|
||||||
changed_paths: Default::default(),
|
changed_paths: Default::default(),
|
||||||
|
git_hosting_provider_registry,
|
||||||
}),
|
}),
|
||||||
phase: BackgroundScannerPhase::InitialScan,
|
phase: BackgroundScannerPhase::InitialScan,
|
||||||
share_private_files,
|
share_private_files,
|
||||||
|
@ -2948,6 +2952,13 @@ impl BackgroundScannerState {
|
||||||
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
|
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
|
||||||
let work_directory = RepositoryWorkDirectory(work_dir_path.clone());
|
let work_directory = RepositoryWorkDirectory(work_dir_path.clone());
|
||||||
|
|
||||||
|
if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() {
|
||||||
|
git_hosting_providers::register_additional_providers(
|
||||||
|
git_hosting_provider_registry,
|
||||||
|
repository.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
self.snapshot.repository_entries.insert(
|
self.snapshot.repository_entries.insert(
|
||||||
work_directory.clone(),
|
work_directory.clone(),
|
||||||
RepositoryEntry {
|
RepositoryEntry {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue