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