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