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