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::{AppContext, 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<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>>;
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: &AppContext) -> Arc<Self> {
111 cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
112 }
113
114 /// Returns the global [`GitHostingProviderRegistry`].
115 ///
116 /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
117 pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
118 cx.default_global::<GlobalGitHostingProviderRegistry>()
119 .0
120 .clone()
121 }
122
123 /// Sets the global [`GitHostingProviderRegistry`].
124 pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut AppContext) {
125 cx.set_global(GlobalGitHostingProviderRegistry(registry));
126 }
127
128 /// Returns a new [`GitHostingProviderRegistry`].
129 pub fn new() -> Self {
130 Self {
131 state: RwLock::new(GitHostingProviderRegistryState {
132 providers: BTreeMap::default(),
133 }),
134 }
135 }
136
137 /// Returns the list of all [`GitHostingProvider`]s in the registry.
138 pub fn list_hosting_providers(
139 &self,
140 ) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
141 self.state.read().providers.values().cloned().collect()
142 }
143
144 /// Adds the provided [`GitHostingProvider`] to the registry.
145 pub fn register_hosting_provider(
146 &self,
147 provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
148 ) {
149 self.state
150 .write()
151 .providers
152 .insert(provider.name(), provider);
153 }
154}
155
156#[derive(Debug)]
157pub struct ParsedGitRemote<'a> {
158 pub owner: &'a str,
159 pub repo: &'a str,
160}
161
162pub fn parse_git_remote_url(
163 provider_registry: Arc<GitHostingProviderRegistry>,
164 url: &str,
165) -> Option<(
166 Arc<dyn GitHostingProvider + Send + Sync + 'static>,
167 ParsedGitRemote,
168)> {
169 provider_registry
170 .list_hosting_providers()
171 .into_iter()
172 .find_map(|provider| {
173 provider
174 .parse_remote_url(url)
175 .map(|parsed_remote| (provider, parsed_remote))
176 })
177}