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 parking_lot::RwLock;
9use url::Url;
10
11#[derive(Debug, PartialEq, Eq, Clone)]
12pub struct PullRequest {
13 pub number: u32,
14 pub url: Url,
15}
16
17#[derive(Clone)]
18pub struct GitRemote {
19 pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
20 pub owner: String,
21 pub repo: String,
22}
23
24impl std::fmt::Debug for GitRemote {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("GitRemote")
27 .field("host", &self.host.name())
28 .field("owner", &self.owner)
29 .field("repo", &self.repo)
30 .finish()
31 }
32}
33
34impl GitRemote {
35 pub fn host_supports_avatars(&self) -> bool {
36 self.host.supports_avatars()
37 }
38
39 pub async fn avatar_url(
40 &self,
41 commit: SharedString,
42 client: Arc<dyn HttpClient>,
43 ) -> Option<Url> {
44 self.host
45 .commit_author_avatar_url(&self.owner, &self.repo, commit, client)
46 .await
47 .ok()
48 .flatten()
49 }
50}
51
52pub struct BuildCommitPermalinkParams<'a> {
53 pub sha: &'a str,
54}
55
56pub struct BuildPermalinkParams<'a> {
57 pub sha: &'a str,
58 pub path: &'a str,
59 pub selection: Option<Range<u32>>,
60}
61
62/// A Git hosting provider.
63#[async_trait]
64pub trait GitHostingProvider {
65 /// Returns the name of the provider.
66 fn name(&self) -> String;
67
68 /// Returns the base URL of the provider.
69 fn base_url(&self) -> Url;
70
71 /// Returns a permalink to a Git commit on this hosting provider.
72 fn build_commit_permalink(
73 &self,
74 remote: &ParsedGitRemote,
75 params: BuildCommitPermalinkParams,
76 ) -> Url;
77
78 /// Returns a permalink to a file and/or selection on this hosting provider.
79 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
80
81 /// Returns whether this provider supports avatars.
82 fn supports_avatars(&self) -> bool;
83
84 /// Returns a URL fragment to the given line selection.
85 fn line_fragment(&self, selection: &Range<u32>) -> String {
86 if selection.start == selection.end {
87 let line = selection.start + 1;
88
89 self.format_line_number(line)
90 } else {
91 let start_line = selection.start + 1;
92 let end_line = selection.end + 1;
93
94 self.format_line_numbers(start_line, end_line)
95 }
96 }
97
98 /// Returns a formatted line number to be placed in a permalink URL.
99 fn format_line_number(&self, line: u32) -> String;
100
101 /// Returns a formatted range of line numbers to be placed in a permalink URL.
102 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
103
104 fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote>;
105
106 fn extract_pull_request(
107 &self,
108 _remote: &ParsedGitRemote,
109 _message: &str,
110 ) -> Option<PullRequest> {
111 None
112 }
113
114 async fn commit_author_avatar_url(
115 &self,
116 _repo_owner: &str,
117 _repo: &str,
118 _commit: SharedString,
119 _http_client: Arc<dyn HttpClient>,
120 ) -> Result<Option<Url>> {
121 Ok(None)
122 }
123}
124
125#[derive(Default, Deref, DerefMut)]
126struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
127
128impl Global for GlobalGitHostingProviderRegistry {}
129
130#[derive(Default)]
131struct GitHostingProviderRegistryState {
132 default_providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
133 setting_providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
134}
135
136#[derive(Default)]
137pub struct GitHostingProviderRegistry {
138 state: RwLock<GitHostingProviderRegistryState>,
139}
140
141impl GitHostingProviderRegistry {
142 /// Returns the global [`GitHostingProviderRegistry`].
143 #[track_caller]
144 pub fn global(cx: &App) -> Arc<Self> {
145 cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
146 }
147
148 /// Returns the global [`GitHostingProviderRegistry`], if one is set.
149 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
150 cx.try_global::<GlobalGitHostingProviderRegistry>()
151 .map(|registry| registry.0.clone())
152 }
153
154 /// Returns the global [`GitHostingProviderRegistry`].
155 ///
156 /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
157 pub fn default_global(cx: &mut App) -> Arc<Self> {
158 cx.default_global::<GlobalGitHostingProviderRegistry>()
159 .0
160 .clone()
161 }
162
163 /// Sets the global [`GitHostingProviderRegistry`].
164 pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut App) {
165 cx.set_global(GlobalGitHostingProviderRegistry(registry));
166 }
167
168 /// Returns a new [`GitHostingProviderRegistry`].
169 pub fn new() -> Self {
170 Self {
171 state: RwLock::new(GitHostingProviderRegistryState {
172 setting_providers: Vec::default(),
173 default_providers: Vec::default(),
174 }),
175 }
176 }
177
178 /// Returns the list of all [`GitHostingProvider`]s in the registry.
179 pub fn list_hosting_providers(
180 &self,
181 ) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
182 let state = self.state.read();
183 state
184 .default_providers
185 .iter()
186 .cloned()
187 .chain(state.setting_providers.iter().cloned())
188 .collect()
189 }
190
191 pub fn set_setting_providers(
192 &self,
193 providers: impl IntoIterator<Item = Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
194 ) {
195 let mut state = self.state.write();
196 state.setting_providers.clear();
197 state.setting_providers.extend(providers);
198 }
199
200 /// Adds the provided [`GitHostingProvider`] to the registry.
201 pub fn register_hosting_provider(
202 &self,
203 provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
204 ) {
205 self.state.write().default_providers.push(provider);
206 }
207}
208
209#[derive(Debug, PartialEq)]
210pub struct ParsedGitRemote {
211 pub owner: Arc<str>,
212 pub repo: Arc<str>,
213}
214
215pub fn parse_git_remote_url(
216 provider_registry: Arc<GitHostingProviderRegistry>,
217 url: &str,
218) -> Option<(
219 Arc<dyn GitHostingProvider + Send + Sync + 'static>,
220 ParsedGitRemote,
221)> {
222 provider_registry
223 .list_hosting_providers()
224 .into_iter()
225 .find_map(|provider| {
226 provider
227 .parse_remote_url(url)
228 .map(|parsed_remote| (provider, parsed_remote))
229 })
230}