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