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