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}