use std::{ops::Range, sync::Arc}; use anyhow::Result; use async_trait::async_trait; use collections::BTreeMap; use derive_more::{Deref, DerefMut}; use gpui::{App, Global, SharedString}; use http_client::HttpClient; use parking_lot::RwLock; use url::Url; #[derive(Debug, PartialEq, Eq, Clone)] pub struct PullRequest { pub number: u32, pub url: Url, } pub struct BuildCommitPermalinkParams<'a> { pub sha: &'a str, } pub struct BuildPermalinkParams<'a> { pub sha: &'a str, pub path: &'a str, pub selection: Option>, } /// A Git hosting provider. #[async_trait] pub trait GitHostingProvider { /// Returns the name of the provider. fn name(&self) -> String; /// Returns the base URL of the provider. fn base_url(&self) -> Url; /// Returns a permalink to a Git commit on this hosting provider. fn build_commit_permalink( &self, remote: &ParsedGitRemote, params: BuildCommitPermalinkParams, ) -> Url; /// Returns a permalink to a file and/or selection on this hosting provider. fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url; /// Returns whether this provider supports avatars. fn supports_avatars(&self) -> bool; /// Returns a URL fragment to the given line selection. fn line_fragment(&self, selection: &Range) -> String { if selection.start == selection.end { let line = selection.start + 1; self.format_line_number(line) } else { let start_line = selection.start + 1; let end_line = selection.end + 1; self.format_line_numbers(start_line, end_line) } } /// Returns a formatted line number to be placed in a permalink URL. fn format_line_number(&self, line: u32) -> String; /// Returns a formatted range of line numbers to be placed in a permalink URL. fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String; fn parse_remote_url(&self, url: &str) -> Option; fn extract_pull_request( &self, _remote: &ParsedGitRemote, _message: &str, ) -> Option { None } async fn commit_author_avatar_url( &self, _repo_owner: &str, _repo: &str, _commit: SharedString, _http_client: Arc, ) -> Result> { Ok(None) } } #[derive(Default, Deref, DerefMut)] struct GlobalGitHostingProviderRegistry(Arc); impl Global for GlobalGitHostingProviderRegistry {} #[derive(Default)] struct GitHostingProviderRegistryState { providers: BTreeMap>, } #[derive(Default)] pub struct GitHostingProviderRegistry { state: RwLock, } impl GitHostingProviderRegistry { /// Returns the global [`GitHostingProviderRegistry`]. pub fn global(cx: &App) -> Arc { cx.global::().0.clone() } /// Returns the global [`GitHostingProviderRegistry`], if one is set. pub fn try_global(cx: &App) -> Option> { cx.try_global::() .map(|registry| registry.0.clone()) } /// Returns the global [`GitHostingProviderRegistry`]. /// /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist. pub fn default_global(cx: &mut App) -> Arc { cx.default_global::() .0 .clone() } /// Sets the global [`GitHostingProviderRegistry`]. pub fn set_global(registry: Arc, cx: &mut App) { cx.set_global(GlobalGitHostingProviderRegistry(registry)); } /// Returns a new [`GitHostingProviderRegistry`]. pub fn new() -> Self { Self { state: RwLock::new(GitHostingProviderRegistryState { providers: BTreeMap::default(), }), } } /// Returns the list of all [`GitHostingProvider`]s in the registry. pub fn list_hosting_providers( &self, ) -> Vec> { self.state.read().providers.values().cloned().collect() } /// Adds the provided [`GitHostingProvider`] to the registry. pub fn register_hosting_provider( &self, provider: Arc, ) { self.state .write() .providers .insert(provider.name(), provider); } } #[derive(Debug, PartialEq)] pub struct ParsedGitRemote { pub owner: Arc, pub repo: Arc, } pub fn parse_git_remote_url( provider_registry: Arc, url: &str, ) -> Option<( Arc, ParsedGitRemote, )> { provider_registry .list_hosting_providers() .into_iter() .find_map(|provider| { provider .parse_remote_url(url) .map(|parsed_remote| (provider, parsed_remote)) }) }