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}