hosting_provider.rs

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