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