hosting_provider.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use anyhow::Result;
  4use async_trait::async_trait;
  5use collections::BTreeMap;
  6use derive_more::{Deref, DerefMut};
  7use gpui::{App, Global, SharedString};
  8use http_client::HttpClient;
  9use parking_lot::RwLock;
 10use url::Url;
 11
 12#[derive(Debug, PartialEq, Eq, Clone)]
 13pub struct PullRequest {
 14    pub number: u32,
 15    pub url: Url,
 16}
 17
 18pub struct BuildCommitPermalinkParams<'a> {
 19    pub sha: &'a str,
 20}
 21
 22pub struct BuildPermalinkParams<'a> {
 23    pub sha: &'a str,
 24    pub path: &'a str,
 25    pub selection: Option<Range<u32>>,
 26}
 27
 28/// A Git hosting provider.
 29#[async_trait]
 30pub trait GitHostingProvider {
 31    /// Returns the name of the provider.
 32    fn name(&self) -> String;
 33
 34    /// Returns the base URL of the provider.
 35    fn base_url(&self) -> Url;
 36
 37    /// Returns a permalink to a Git commit on this hosting provider.
 38    fn build_commit_permalink(
 39        &self,
 40        remote: &ParsedGitRemote,
 41        params: BuildCommitPermalinkParams,
 42    ) -> Url;
 43
 44    /// Returns a permalink to a file and/or selection on this hosting provider.
 45    fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
 46
 47    /// Returns whether this provider supports avatars.
 48    fn supports_avatars(&self) -> bool;
 49
 50    /// Returns a URL fragment to the given line selection.
 51    fn line_fragment(&self, selection: &Range<u32>) -> String {
 52        if selection.start == selection.end {
 53            let line = selection.start + 1;
 54
 55            self.format_line_number(line)
 56        } else {
 57            let start_line = selection.start + 1;
 58            let end_line = selection.end + 1;
 59
 60            self.format_line_numbers(start_line, end_line)
 61        }
 62    }
 63
 64    /// Returns a formatted line number to be placed in a permalink URL.
 65    fn format_line_number(&self, line: u32) -> String;
 66
 67    /// Returns a formatted range of line numbers to be placed in a permalink URL.
 68    fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
 69
 70    fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote>;
 71
 72    fn extract_pull_request(
 73        &self,
 74        _remote: &ParsedGitRemote,
 75        _message: &str,
 76    ) -> Option<PullRequest> {
 77        None
 78    }
 79
 80    async fn commit_author_avatar_url(
 81        &self,
 82        _repo_owner: &str,
 83        _repo: &str,
 84        _commit: SharedString,
 85        _http_client: Arc<dyn HttpClient>,
 86    ) -> Result<Option<Url>> {
 87        Ok(None)
 88    }
 89}
 90
 91#[derive(Default, Deref, DerefMut)]
 92struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
 93
 94impl Global for GlobalGitHostingProviderRegistry {}
 95
 96#[derive(Default)]
 97struct GitHostingProviderRegistryState {
 98    providers: BTreeMap<String, Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
 99}
100
101#[derive(Default)]
102pub struct GitHostingProviderRegistry {
103    state: RwLock<GitHostingProviderRegistryState>,
104}
105
106impl GitHostingProviderRegistry {
107    /// Returns the global [`GitHostingProviderRegistry`].
108    pub fn global(cx: &App) -> Arc<Self> {
109        cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
110    }
111
112    /// Returns the global [`GitHostingProviderRegistry`], if one is set.
113    pub fn try_global(cx: &App) -> Option<Arc<Self>> {
114        cx.try_global::<GlobalGitHostingProviderRegistry>()
115            .map(|registry| registry.0.clone())
116    }
117
118    /// Returns the global [`GitHostingProviderRegistry`].
119    ///
120    /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
121    pub fn default_global(cx: &mut App) -> Arc<Self> {
122        cx.default_global::<GlobalGitHostingProviderRegistry>()
123            .0
124            .clone()
125    }
126
127    /// Sets the global [`GitHostingProviderRegistry`].
128    pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut App) {
129        cx.set_global(GlobalGitHostingProviderRegistry(registry));
130    }
131
132    /// Returns a new [`GitHostingProviderRegistry`].
133    pub fn new() -> Self {
134        Self {
135            state: RwLock::new(GitHostingProviderRegistryState {
136                providers: BTreeMap::default(),
137            }),
138        }
139    }
140
141    /// Returns the list of all [`GitHostingProvider`]s in the registry.
142    pub fn list_hosting_providers(
143        &self,
144    ) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
145        self.state.read().providers.values().cloned().collect()
146    }
147
148    /// Adds the provided [`GitHostingProvider`] to the registry.
149    pub fn register_hosting_provider(
150        &self,
151        provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
152    ) {
153        self.state
154            .write()
155            .providers
156            .insert(provider.name(), provider);
157    }
158}
159
160#[derive(Debug, PartialEq)]
161pub struct ParsedGitRemote {
162    pub owner: Arc<str>,
163    pub repo: Arc<str>,
164}
165
166pub fn parse_git_remote_url(
167    provider_registry: Arc<GitHostingProviderRegistry>,
168    url: &str,
169) -> Option<(
170    Arc<dyn GitHostingProvider + Send + Sync + 'static>,
171    ParsedGitRemote,
172)> {
173    provider_registry
174        .list_hosting_providers()
175        .into_iter()
176        .find_map(|provider| {
177            provider
178                .parse_remote_url(url)
179                .map(|parsed_remote| (provider, parsed_remote))
180        })
181}