From 1307b81721d24341f158bc9b07852eb1ec480d52 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 3 Jun 2025 12:23:01 -0400 Subject: [PATCH] Allow configuring custom git hosting providers in project settings (#31929) Closes #29229 Release Notes: - Extended the support for configuring custom git hosting providers to cover project settings in addition to global settings. --------- Co-authored-by: Anthony Eid --- crates/git/src/hosting_provider.rs | 30 ++++++--- crates/git_hosting_providers/src/settings.rs | 36 +++++++---- crates/project/src/project_tests.rs | 66 ++++++++++++++++++++ crates/settings/src/settings_store.rs | 26 ++++++++ 4 files changed, 138 insertions(+), 20 deletions(-) diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs index 7ed996c73f..5c11cb5504 100644 --- a/crates/git/src/hosting_provider.rs +++ b/crates/git/src/hosting_provider.rs @@ -2,7 +2,6 @@ use std::{ops::Range, sync::Arc}; use anyhow::Result; use async_trait::async_trait; -use collections::BTreeMap; use derive_more::{Deref, DerefMut}; use gpui::{App, Global, SharedString}; use http_client::HttpClient; @@ -130,7 +129,8 @@ impl Global for GlobalGitHostingProviderRegistry {} #[derive(Default)] struct GitHostingProviderRegistryState { - providers: BTreeMap>, + default_providers: Vec>, + setting_providers: Vec>, } #[derive(Default)] @@ -140,6 +140,7 @@ pub struct GitHostingProviderRegistry { impl GitHostingProviderRegistry { /// Returns the global [`GitHostingProviderRegistry`]. + #[track_caller] pub fn global(cx: &App) -> Arc { cx.global::().0.clone() } @@ -168,7 +169,8 @@ impl GitHostingProviderRegistry { pub fn new() -> Self { Self { state: RwLock::new(GitHostingProviderRegistryState { - providers: BTreeMap::default(), + setting_providers: Vec::default(), + default_providers: Vec::default(), }), } } @@ -177,7 +179,22 @@ impl GitHostingProviderRegistry { pub fn list_hosting_providers( &self, ) -> Vec> { - self.state.read().providers.values().cloned().collect() + let state = self.state.read(); + state + .default_providers + .iter() + .cloned() + .chain(state.setting_providers.iter().cloned()) + .collect() + } + + pub fn set_setting_providers( + &self, + providers: impl IntoIterator>, + ) { + let mut state = self.state.write(); + state.setting_providers.clear(); + state.setting_providers.extend(providers); } /// Adds the provided [`GitHostingProvider`] to the registry. @@ -185,10 +202,7 @@ impl GitHostingProviderRegistry { &self, provider: Arc, ) { - self.state - .write() - .providers - .insert(provider.name(), provider); + self.state.write().default_providers.push(provider); } } diff --git a/crates/git_hosting_providers/src/settings.rs b/crates/git_hosting_providers/src/settings.rs index 4273492e2d..91179fea39 100644 --- a/crates/git_hosting_providers/src/settings.rs +++ b/crates/git_hosting_providers/src/settings.rs @@ -25,22 +25,34 @@ fn init_git_hosting_provider_settings(cx: &mut App) { } fn update_git_hosting_providers_from_settings(cx: &mut App) { + let settings_store = cx.global::(); let settings = GitHostingProviderSettings::get_global(cx); let provider_registry = GitHostingProviderRegistry::global(cx); - for provider in settings.git_hosting_providers.iter() { - let Some(url) = Url::parse(&provider.base_url).log_err() else { - continue; - }; + let local_values: Vec = settings_store + .get_all_locals::() + .into_iter() + .flat_map(|(_, _, providers)| providers.git_hosting_providers.clone()) + .collect(); - let provider = match provider.provider { - GitHostingProviderKind::Bitbucket => Arc::new(Bitbucket::new(&provider.name, url)) as _, - GitHostingProviderKind::Github => Arc::new(Github::new(&provider.name, url)) as _, - GitHostingProviderKind::Gitlab => Arc::new(Gitlab::new(&provider.name, url)) as _, - }; + let iter = settings + .git_hosting_providers + .clone() + .into_iter() + .chain(local_values) + .filter_map(|provider| { + let url = Url::parse(&provider.base_url).log_err()?; - provider_registry.register_hosting_provider(provider); - } + Some(match provider.provider { + GitHostingProviderKind::Bitbucket => { + Arc::new(Bitbucket::new(&provider.name, url)) as _ + } + GitHostingProviderKind::Github => Arc::new(Github::new(&provider.name, url)) as _, + GitHostingProviderKind::Gitlab => Arc::new(Gitlab::new(&provider.name, url)) as _, + }) + }); + + provider_registry.set_setting_providers(iter); } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -66,7 +78,7 @@ pub struct GitHostingProviderConfig { pub name: String, } -#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct GitHostingProviderSettings { /// The list of custom Git hosting providers. #[serde(default)] diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index d4ee2aff24..29e24408ee 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -11,6 +11,7 @@ use buffer_diff::{ use fs::FakeFs; use futures::{StreamExt, future}; use git::{ + GitHostingProviderRegistry, repository::RepoPath, status::{StatusCode, TrackedStatus}, }; @@ -216,6 +217,71 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_git_provider_project_setting(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.update(|cx| { + GitHostingProviderRegistry::default_global(cx); + git_hosting_providers::init(cx); + }); + + let fs = FakeFs::new(cx.executor()); + let str_path = path!("/dir"); + let path = Path::new(str_path); + + fs.insert_tree( + path!("/dir"), + json!({ + ".zed": { + "settings.json": r#"{ + "git_hosting_providers": [ + { + "provider": "gitlab", + "base_url": "https://google.com", + "name": "foo" + } + ] + }"# + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let (_worktree, _) = + project.read_with(cx, |project, cx| project.find_worktree(path, cx).unwrap()); + cx.executor().run_until_parked(); + + cx.update(|cx| { + let provider = GitHostingProviderRegistry::global(cx); + assert!( + provider + .list_hosting_providers() + .into_iter() + .any(|provider| provider.name() == "foo") + ); + }); + + fs.atomic_write( + Path::new(path!("/dir/.zed/settings.json")).to_owned(), + "{}".into(), + ) + .await + .unwrap(); + + cx.run_until_parked(); + + cx.update(|cx| { + let provider = GitHostingProviderRegistry::global(cx); + assert!( + !provider + .list_hosting_providers() + .into_iter() + .any(|provider| provider.name() == "foo") + ); + }); +} + #[gpui::test] async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index e6e2a448e0..a7c295fc64 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -250,6 +250,7 @@ trait AnySettingValue: 'static + Send + Sync { cx: &mut App, ) -> Result>; fn value_for_path(&self, path: Option) -> &dyn Any; + fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); fn json_schema( @@ -376,6 +377,24 @@ impl SettingsStore { .expect("no default value for setting type") } + /// Get all values from project specific settings + pub fn get_all_locals(&self) -> Vec<(WorktreeId, Arc, &T)> { + self.setting_values + .get(&TypeId::of::()) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .all_local_values() + .into_iter() + .map(|(id, path, any)| { + ( + id, + path, + any.downcast_ref::() + .expect("wrong value type for setting"), + ) + }) + .collect() + } + /// Override the global value for a setting. /// /// The given value will be overwritten if the user settings file changes. @@ -1235,6 +1254,13 @@ impl AnySettingValue for SettingValue { (key, value) } + fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)> { + self.local_values + .iter() + .map(|(id, path, value)| (*id, path.clone(), value as _)) + .collect() + } + fn value_for_path(&self, path: Option) -> &dyn Any { if let Some(SettingsLocation { worktree_id, path }) = path { for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {