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 whether this provider supports avatars.
 96    fn supports_avatars(&self) -> bool;
 97
 98    /// Returns a URL fragment to the given line selection.
 99    fn line_fragment(&self, selection: &Range<u32>) -> String {
100        if selection.start == selection.end {
101            let line = selection.start + 1;
102
103            self.format_line_number(line)
104        } else {
105            let start_line = selection.start + 1;
106            let end_line = selection.end + 1;
107
108            self.format_line_numbers(start_line, end_line)
109        }
110    }
111
112    /// Returns a formatted line number to be placed in a permalink URL.
113    fn format_line_number(&self, line: u32) -> String;
114
115    /// Returns a formatted range of line numbers to be placed in a permalink URL.
116    fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
117
118    fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote>;
119
120    fn extract_pull_request(
121        &self,
122        _remote: &ParsedGitRemote,
123        _message: &str,
124    ) -> Option<PullRequest> {
125        None
126    }
127
128    async fn commit_author_avatar_url(
129        &self,
130        _repo_owner: &str,
131        _repo: &str,
132        _commit: SharedString,
133        _http_client: Arc<dyn HttpClient>,
134    ) -> Result<Option<Url>> {
135        Ok(None)
136    }
137}
138
139#[derive(Default, Deref, DerefMut)]
140struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
141
142impl Global for GlobalGitHostingProviderRegistry {}
143
144#[derive(Default)]
145struct GitHostingProviderRegistryState {
146    default_providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
147    setting_providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
148}
149
150#[derive(Default)]
151pub struct GitHostingProviderRegistry {
152    state: RwLock<GitHostingProviderRegistryState>,
153}
154
155impl GitHostingProviderRegistry {
156    /// Returns the global [`GitHostingProviderRegistry`].
157    #[track_caller]
158    pub fn global(cx: &App) -> Arc<Self> {
159        cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
160    }
161
162    /// Returns the global [`GitHostingProviderRegistry`], if one is set.
163    pub fn try_global(cx: &App) -> Option<Arc<Self>> {
164        cx.try_global::<GlobalGitHostingProviderRegistry>()
165            .map(|registry| registry.0.clone())
166    }
167
168    /// Returns the global [`GitHostingProviderRegistry`].
169    ///
170    /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
171    pub fn default_global(cx: &mut App) -> Arc<Self> {
172        cx.default_global::<GlobalGitHostingProviderRegistry>()
173            .0
174            .clone()
175    }
176
177    /// Sets the global [`GitHostingProviderRegistry`].
178    pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut App) {
179        cx.set_global(GlobalGitHostingProviderRegistry(registry));
180    }
181
182    /// Returns a new [`GitHostingProviderRegistry`].
183    pub fn new() -> Self {
184        Self {
185            state: RwLock::new(GitHostingProviderRegistryState {
186                setting_providers: Vec::default(),
187                default_providers: Vec::default(),
188            }),
189        }
190    }
191
192    /// Returns the list of all [`GitHostingProvider`]s in the registry.
193    pub fn list_hosting_providers(
194        &self,
195    ) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
196        let state = self.state.read();
197        state
198            .default_providers
199            .iter()
200            .cloned()
201            .chain(state.setting_providers.iter().cloned())
202            .collect()
203    }
204
205    pub fn set_setting_providers(
206        &self,
207        providers: impl IntoIterator<Item = Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
208    ) {
209        let mut state = self.state.write();
210        state.setting_providers.clear();
211        state.setting_providers.extend(providers);
212    }
213
214    /// Adds the provided [`GitHostingProvider`] to the registry.
215    pub fn register_hosting_provider(
216        &self,
217        provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
218    ) {
219        self.state.write().default_providers.push(provider);
220    }
221}
222
223#[derive(Debug, PartialEq)]
224pub struct ParsedGitRemote {
225    pub owner: Arc<str>,
226    pub repo: Arc<str>,
227}
228
229pub fn parse_git_remote_url(
230    provider_registry: Arc<GitHostingProviderRegistry>,
231    url: &str,
232) -> Option<(
233    Arc<dyn GitHostingProvider + Send + Sync + 'static>,
234    ParsedGitRemote,
235)> {
236    provider_registry
237        .list_hosting_providers()
238        .into_iter()
239        .find_map(|provider| {
240            provider
241                .parse_remote_url(url)
242                .map(|parsed_remote| (provider, parsed_remote))
243        })
244}