git_hosting_providers: Allow configuring additional hosting providers via settings (#26879)

khayyam and Marshall Bowers created

Release Notes:

- Added a new `git_hosting_providers` setting for configuring custom Git
hosting providers.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>

Change summary

Cargo.lock                                                |  2 
assets/settings/default.json                              |  8 
crates/git_hosting_providers/Cargo.toml                   |  2 
crates/git_hosting_providers/src/git_hosting_providers.rs |  6 
crates/git_hosting_providers/src/settings.rs              | 84 +++++++++
5 files changed, 101 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -5506,8 +5506,10 @@ dependencies = [
  "indoc",
  "pretty_assertions",
  "regex",
+ "schemars",
  "serde",
  "serde_json",
+ "settings",
  "url",
  "util",
 ]

assets/settings/default.json 🔗

@@ -852,6 +852,14 @@
       // "min_column": 0
     }
   },
+  // The list of custom Git hosting providers.
+  "git_hosting_providers": [
+    // {
+    //   "provider": "github",
+    //   "name": "BigCorp GitHub",
+    //   "base_url": "https://code.big-corp.com"
+    // }
+  ],
   // Configuration for how direnv configuration should be loaded. May take 2 values:
   // 1. Load direnv configuration using `direnv export json` directly.
   //      "load_direnv": "direct"

crates/git_hosting_providers/Cargo.toml 🔗

@@ -19,8 +19,10 @@ git.workspace = true
 gpui.workspace = true
 http_client.workspace = true
 regex.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 url.workspace = true
 util.workspace = true
 

crates/git_hosting_providers/src/git_hosting_providers.rs 🔗

@@ -1,4 +1,5 @@
 mod providers;
+mod settings;
 
 use std::sync::Arc;
 
@@ -10,9 +11,12 @@ use url::Url;
 use util::maybe;
 
 pub use crate::providers::*;
+pub use crate::settings::*;
 
 /// Initializes the Git hosting providers.
-pub fn init(cx: &App) {
+pub fn init(cx: &mut App) {
+    crate::settings::init(cx);
+
     let provider_registry = GitHostingProviderRegistry::global(cx);
     provider_registry.register_hosting_provider(Arc::new(Bitbucket::public_instance()));
     provider_registry.register_hosting_provider(Arc::new(Chromium));

crates/git_hosting_providers/src/settings.rs 🔗

@@ -0,0 +1,84 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use git::GitHostingProviderRegistry;
+use gpui::App;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use url::Url;
+use util::ResultExt as _;
+
+use crate::{Bitbucket, Github, Gitlab};
+
+pub(crate) fn init(cx: &mut App) {
+    GitHostingProviderSettings::register(cx);
+
+    init_git_hosting_provider_settings(cx);
+}
+
+fn init_git_hosting_provider_settings(cx: &mut App) {
+    update_git_hosting_providers_from_settings(cx);
+
+    cx.observe_global::<SettingsStore>(update_git_hosting_providers_from_settings)
+        .detach();
+}
+
+fn update_git_hosting_providers_from_settings(cx: &mut App) {
+    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 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 _,
+        };
+
+        provider_registry.register_hosting_provider(provider);
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitHostingProviderKind {
+    Github,
+    Gitlab,
+    Bitbucket,
+}
+
+/// A custom Git hosting provider.
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GitHostingProviderConfig {
+    /// The type of the provider.
+    ///
+    /// Must be one of `github`, `gitlab`, or `bitbucket`.
+    pub provider: GitHostingProviderKind,
+
+    /// The base URL for the provider (e.g., "https://code.corp.big.com").
+    pub base_url: String,
+
+    /// The display name for the provider (e.g., "BigCorp GitHub").
+    pub name: String,
+}
+
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GitHostingProviderSettings {
+    /// The list of custom Git hosting providers.
+    #[serde(default)]
+    pub git_hosting_providers: Vec<GitHostingProviderConfig>,
+}
+
+impl Settings for GitHostingProviderSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = Self;
+
+    fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
+        sources.json_merge()
+    }
+}