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}