1use std::{ops::Range, sync::Arc};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use collections::BTreeMap;
6use derive_more::{Deref, DerefMut};
7use gpui::{App, Global, SharedString};
8use http_client::HttpClient;
9use parking_lot::RwLock;
10use url::Url;
11
12#[derive(Debug, PartialEq, Eq, Clone)]
13pub struct PullRequest {
14 pub number: u32,
15 pub url: Url,
16}
17
18pub struct BuildCommitPermalinkParams<'a> {
19 pub sha: &'a str,
20}
21
22pub struct BuildPermalinkParams<'a> {
23 pub sha: &'a str,
24 pub path: &'a str,
25 pub selection: Option<Range<u32>>,
26}
27
28/// A Git hosting provider.
29#[async_trait]
30pub trait GitHostingProvider {
31 /// Returns the name of the provider.
32 fn name(&self) -> String;
33
34 /// Returns the base URL of the provider.
35 fn base_url(&self) -> Url;
36
37 /// Returns a permalink to a Git commit on this hosting provider.
38 fn build_commit_permalink(
39 &self,
40 remote: &ParsedGitRemote,
41 params: BuildCommitPermalinkParams,
42 ) -> Url;
43
44 /// Returns a permalink to a file and/or selection on this hosting provider.
45 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
46
47 /// Returns whether this provider supports avatars.
48 fn supports_avatars(&self) -> bool;
49
50 /// Returns a URL fragment to the given line selection.
51 fn line_fragment(&self, selection: &Range<u32>) -> String {
52 if selection.start == selection.end {
53 let line = selection.start + 1;
54
55 self.format_line_number(line)
56 } else {
57 let start_line = selection.start + 1;
58 let end_line = selection.end + 1;
59
60 self.format_line_numbers(start_line, end_line)
61 }
62 }
63
64 /// Returns a formatted line number to be placed in a permalink URL.
65 fn format_line_number(&self, line: u32) -> String;
66
67 /// Returns a formatted range of line numbers to be placed in a permalink URL.
68 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
69
70 fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote>;
71
72 fn extract_pull_request(
73 &self,
74 _remote: &ParsedGitRemote,
75 _message: &str,
76 ) -> Option<PullRequest> {
77 None
78 }
79
80 async fn commit_author_avatar_url(
81 &self,
82 _repo_owner: &str,
83 _repo: &str,
84 _commit: SharedString,
85 _http_client: Arc<dyn HttpClient>,
86 ) -> Result<Option<Url>> {
87 Ok(None)
88 }
89}
90
91#[derive(Default, Deref, DerefMut)]
92struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
93
94impl Global for GlobalGitHostingProviderRegistry {}
95
96#[derive(Default)]
97struct GitHostingProviderRegistryState {
98 providers: BTreeMap<String, Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
99}
100
101#[derive(Default)]
102pub struct GitHostingProviderRegistry {
103 state: RwLock<GitHostingProviderRegistryState>,
104}
105
106impl GitHostingProviderRegistry {
107 /// Returns the global [`GitHostingProviderRegistry`].
108 pub fn global(cx: &App) -> Arc<Self> {
109 cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
110 }
111
112 /// Returns the global [`GitHostingProviderRegistry`], if one is set.
113 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
114 cx.try_global::<GlobalGitHostingProviderRegistry>()
115 .map(|registry| registry.0.clone())
116 }
117
118 /// Returns the global [`GitHostingProviderRegistry`].
119 ///
120 /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
121 pub fn default_global(cx: &mut App) -> Arc<Self> {
122 cx.default_global::<GlobalGitHostingProviderRegistry>()
123 .0
124 .clone()
125 }
126
127 /// Sets the global [`GitHostingProviderRegistry`].
128 pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut App) {
129 cx.set_global(GlobalGitHostingProviderRegistry(registry));
130 }
131
132 /// Returns a new [`GitHostingProviderRegistry`].
133 pub fn new() -> Self {
134 Self {
135 state: RwLock::new(GitHostingProviderRegistryState {
136 providers: BTreeMap::default(),
137 }),
138 }
139 }
140
141 /// Returns the list of all [`GitHostingProvider`]s in the registry.
142 pub fn list_hosting_providers(
143 &self,
144 ) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
145 self.state.read().providers.values().cloned().collect()
146 }
147
148 /// Adds the provided [`GitHostingProvider`] to the registry.
149 pub fn register_hosting_provider(
150 &self,
151 provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
152 ) {
153 self.state
154 .write()
155 .providers
156 .insert(provider.name(), provider);
157 }
158}
159
160#[derive(Debug, PartialEq)]
161pub struct ParsedGitRemote {
162 pub owner: Arc<str>,
163 pub repo: Arc<str>,
164}
165
166pub fn parse_git_remote_url(
167 provider_registry: Arc<GitHostingProviderRegistry>,
168 url: &str,
169) -> Option<(
170 Arc<dyn GitHostingProvider + Send + Sync + 'static>,
171 ParsedGitRemote,
172)> {
173 provider_registry
174 .list_hosting_providers()
175 .into_iter()
176 .find_map(|provider| {
177 provider
178 .parse_remote_url(url)
179 .map(|parsed_remote| (provider, parsed_remote))
180 })
181}