History

git_hosting_providers: Add support for Chromium repositories (#24881)

Add an implementation of GitHostingProvider for repositories hosted on https://chromium.googlesource.com. Pull requests target the Gerrit instance at https://chromium-review.googlesource.com and avatar images are fetched using the Gerrit REST API. <img width="513" alt="Screenshot 2025-02-20 at 6 43 37 PM" src="https://github.com/user-attachments/assets/867af988-594d-45ea-8482-e40517443c73" /> <img width="511" alt="Screenshot 2025-02-20 at 6 43 51 PM" src="https://github.com/user-attachments/assets/1d412904-048d-4a2d-8494-0837e75f8d61" /> Release Notes: - Added support for repositories hosted on `chromium.googlesource.com` for Git blames and permalinks. ---------

Henrique Ferreiro committed 1 year ago

Commit ec00fb91 parent 5dd35153 files · +305 −0Browse files

Unified · Split

3 files
  1. crates/git_hosting_providers/src/git_hosting_providers.rs +1 −0
  2. crates/git_hosting_providers/src/providers.rs +2 −0
  3. crates/git_hosting_providers/src/providers/chromium.rs +302 −0
crates/git_hosting_providers/src/git_hosting_providers.rs+1−0
@@ -12,6 +12,7 @@ pub use crate::providers::*; · 11 unmodified lines
1212 pub fn init(cx: &App) {
1313 let provider_registry = GitHostingProviderRegistry::global(cx);
1414 provider_registry.register_hosting_provider(Arc::new(Bitbucket));
15+ provider_registry.register_hosting_provider(Arc::new(Chromium));
1516 provider_registry.register_hosting_provider(Arc::new(Codeberg));
1617 provider_registry.register_hosting_provider(Arc::new(Gitee));
1718 provider_registry.register_hosting_provider(Arc::new(Github));
crates/git_hosting_providers/src/providers.rs+2−0
@@ -1,4 +1,5 @@
11 mod bitbucket;
2+mod chromium;
23 mod codeberg;
34 mod gitee;
45 mod github;
@@ -6,6 +7,7 @@ mod gitlab; · 1 unmodified line
67 mod sourcehut;
78
89 pub use bitbucket::*;
10+pub use chromium::*;
911 pub use codeberg::*;
1012 pub use gitee::*;
1113 pub use github::*;
crates/git_hosting_providers/src/providers/chromium.rs+302−0
@@ -0,0 +1,302 @@
1+use std::str::FromStr;
2+use std::sync::{Arc, LazyLock};
3+
4+use anyhow::{bail, Context, Result};
5+use async_trait::async_trait;
6+use futures::AsyncReadExt;
7+use git::{
8+ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
9+ PullRequest, RemoteUrl,
10+};
11+use gpui::SharedString;
12+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
13+use regex::Regex;
14+use serde::Deserialize;
15+use url::Url;
16+
17+const CHROMIUM_REVIEW_URL: &str = "https://chromium-review.googlesource.com";
18+
19+/// Parses Gerrit URLs like
20+/// https://chromium-review.googlesource.com/c/chromium/src/+/3310961.
21+fn pull_request_regex() -> &'static Regex {
22+ static PULL_REQUEST_NUMBER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
23+ Regex::new(&format!(
24+ r#"Reviewed-on: ({CHROMIUM_REVIEW_URL}/c/(.*)/\+/(\d+))"#
25+ ))
26+ .unwrap()
27+ });
28+ &PULL_REQUEST_NUMBER_REGEX
29+}
30+
31+/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html
32+#[derive(Debug, Deserialize)]
33+struct ChangeInfo {
34+ owner: AccountInfo,
35+}
36+
37+#[derive(Debug, Deserialize)]
38+pub struct AccountInfo {
39+ #[serde(rename = "_account_id")]
40+ id: u64,
41+}
42+
43+pub struct Chromium;
44+
45+impl Chromium {
46+ async fn fetch_chromium_commit_author(
47+ &self,
48+ _repo: &str,
49+ commit: &str,
50+ client: &Arc<dyn HttpClient>,
51+ ) -> Result<Option<AccountInfo>> {
52+ let url = format!("{CHROMIUM_REVIEW_URL}/changes/{commit}");
53+
54+ let request = Request::get(&url)
55+ .header("Content-Type", "application/json")
56+ .follow_redirects(http_client::RedirectPolicy::FollowAll);
57+
58+ let mut response = client
59+ .send(request.body(AsyncBody::default())?)
60+ .await
61+ .with_context(|| format!("error fetching Gerrit commit details at {:?}", url))?;
62+
63+ let mut body = Vec::new();
64+ response.body_mut().read_to_end(&mut body).await?;
65+
66+ if response.status().is_client_error() {
67+ let text = String::from_utf8_lossy(body.as_slice());
68+ bail!(
69+ "status error {}, response: {text:?}",
70+ response.status().as_u16()
71+ );
72+ }
73+
74+ // Remove XSSI protection prefix.
75+ let body_str = std::str::from_utf8(&body)?.trim_start_matches(")]}'");
76+
77+ serde_json::from_str::<ChangeInfo>(body_str)
78+ .map(|change| Some(change.owner))
79+ .context("failed to deserialize Gerrit change info")
80+ }
81+}
82+
83+#[async_trait]
84+impl GitHostingProvider for Chromium {
85+ fn name(&self) -> String {
86+ "Chromium".to_string()
87+ }
88+
89+ fn base_url(&self) -> Url {
90+ Url::parse("https://chromium.googlesource.com").unwrap()
91+ }
92+
93+ fn supports_avatars(&self) -> bool {
94+ true
95+ }
96+
97+ fn format_line_number(&self, line: u32) -> String {
98+ format!("{line}")
99+ }
100+
101+ fn format_line_numbers(&self, start_line: u32, _end_line: u32) -> String {
102+ format!("{start_line}")
103+ }
104+
105+ fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
106+ let url = RemoteUrl::from_str(url).ok()?;
107+
108+ let host = url.host_str()?;
109+ if host != self.base_url().host_str()? {
110+ return None;
111+ }
112+
113+ let path_segments = url.path_segments()?.collect::<Vec<_>>();
114+ let joined_path = path_segments.join("/");
115+ let repo = joined_path.trim_end_matches(".git");
116+
117+ Some(ParsedGitRemote {
118+ owner: Arc::from(""),
119+ repo: repo.into(),
120+ })
121+ }
122+
123+ fn build_commit_permalink(
124+ &self,
125+ remote: &ParsedGitRemote,
126+ params: BuildCommitPermalinkParams,
127+ ) -> Url {
128+ let BuildCommitPermalinkParams { sha } = params;
129+ let ParsedGitRemote { owner: _, repo } = remote;
130+
131+ self.base_url().join(&format!("{repo}/+/{sha}")).unwrap()
132+ }
133+
134+ fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
135+ let ParsedGitRemote { owner: _, repo } = remote;
136+ let BuildPermalinkParams {
137+ sha,
138+ path,
139+ selection,
140+ } = params;
141+
142+ let mut permalink = self
143+ .base_url()
144+ .join(&format!("{repo}/+/{sha}/{path}"))
145+ .unwrap();
146+ permalink.set_fragment(
147+ selection
148+ .map(|selection| self.line_fragment(&selection))
149+ .as_deref(),
150+ );
151+ permalink
152+ }
153+
154+ fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
155+ let capture = pull_request_regex().captures(message)?;
156+ let url = Url::parse(capture.get(1)?.as_str()).unwrap();
157+ let repo = capture.get(2)?.as_str();
158+ if repo != remote.repo.as_ref() {
159+ return None;
160+ }
161+
162+ let number = capture.get(3)?.as_str().parse::<u32>().ok()?;
163+
164+ Some(PullRequest { number, url })
165+ }
166+
167+ async fn commit_author_avatar_url(
168+ &self,
169+ _repo_owner: &str,
170+ repo: &str,
171+ commit: SharedString,
172+ http_client: Arc<dyn HttpClient>,
173+ ) -> Result<Option<Url>> {
174+ let commit = commit.to_string();
175+ let Some(author) = self
176+ .fetch_chromium_commit_author(repo, &commit, &http_client)
177+ .await?
178+ else {
179+ return Ok(None);
180+ };
181+
182+ let mut avatar_url = Url::parse(&format!(
183+ "{CHROMIUM_REVIEW_URL}/accounts/{}/avatar",
184+ &author.id
185+ ))?;
186+ avatar_url.set_query(Some("size=128"));
187+
188+ Ok(Some(avatar_url))
189+ }
190+}
191+
192+#[cfg(test)]
193+mod tests {
194+ use indoc::indoc;
195+ use pretty_assertions::assert_eq;
196+
197+ use super::*;
198+
199+ #[test]
200+ fn test_parse_remote_url_given_https_url() {
201+ let parsed_remote = Chromium
202+ .parse_remote_url("https://chromium.googlesource.com/chromium/src")
203+ .unwrap();
204+
205+ assert_eq!(
206+ parsed_remote,
207+ ParsedGitRemote {
208+ owner: Arc::from(""),
209+ repo: "chromium/src".into(),
210+ }
211+ );
212+ }
213+
214+ #[test]
215+ fn test_build_chromium_permalink() {
216+ let permalink = Chromium.build_permalink(
217+ ParsedGitRemote {
218+ owner: Arc::from(""),
219+ repo: "chromium/src".into(),
220+ },
221+ BuildPermalinkParams {
222+ sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
223+ path: "ui/base/cursor/cursor.h",
224+ selection: None,
225+ },
226+ );
227+
228+ let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h";
229+ assert_eq!(permalink.to_string(), expected_url.to_string())
230+ }
231+
232+ #[test]
233+ fn test_build_chromium_permalink_with_single_line_selection() {
234+ let permalink = Chromium.build_permalink(
235+ ParsedGitRemote {
236+ owner: Arc::from(""),
237+ repo: "chromium/src".into(),
238+ },
239+ BuildPermalinkParams {
240+ sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
241+ path: "ui/base/cursor/cursor.h",
242+ selection: Some(18..18),
243+ },
244+ );
245+
246+ let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";
247+ assert_eq!(permalink.to_string(), expected_url.to_string())
248+ }
249+
250+ #[test]
251+ fn test_build_chromium_permalink_with_multi_line_selection() {
252+ let permalink = Chromium.build_permalink(
253+ ParsedGitRemote {
254+ owner: Arc::from(""),
255+ repo: "chromium/src".into(),
256+ },
257+ BuildPermalinkParams {
258+ sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
259+ path: "ui/base/cursor/cursor.h",
260+ selection: Some(18..30),
261+ },
262+ );
263+
264+ let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";
265+ assert_eq!(permalink.to_string(), expected_url.to_string())
266+ }
267+
268+ #[test]
269+ fn test_chromium_pull_requests() {
270+ let remote = ParsedGitRemote {
271+ owner: Arc::from(""),
272+ repo: "chromium/src".into(),
273+ };
274+
275+ let message = "This does not contain a pull request";
276+ assert!(Chromium.extract_pull_request(&remote, message).is_none());
277+
278+ // Pull request number at end of "Reviewed-on:" line
279+ let message = indoc! {r#"
280+ Test commit header
281+
282+ Test commit description with multiple
283+ lines.
284+
285+ Bug: 1193775, 1270302
286+ Change-Id: Id15e9b4d75cce43ebd5fe34f0fb37d5e1e811b66
287+ Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3310961
288+ Reviewed-by: Test reviewer <test@example.com>
289+ Cr-Commit-Position: refs/heads/main@{#1054973}
290+ "#
291+ };
292+
293+ assert_eq!(
294+ Chromium
295+ .extract_pull_request(&remote, &message)
296+ .unwrap()
297+ .url
298+ .as_str(),
299+ "https://chromium-review.googlesource.com/c/chromium/src/+/3310961"
300+ );
301+ }
302+}