Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

git_hosting_providers: Add support for Chromium repositories #24881

Merged
merged 7 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/git_hosting_providers/src/git_hosting_providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub use crate::providers::*;
pub fn init(cx: &App) {
let provider_registry = GitHostingProviderRegistry::global(cx);
provider_registry.register_hosting_provider(Arc::new(Bitbucket));
provider_registry.register_hosting_provider(Arc::new(Chromium));
provider_registry.register_hosting_provider(Arc::new(Codeberg));
provider_registry.register_hosting_provider(Arc::new(Gitee));
provider_registry.register_hosting_provider(Arc::new(Github));
Expand Down
2 changes: 2 additions & 0 deletions crates/git_hosting_providers/src/providers.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
mod bitbucket;
mod chromium;
mod codeberg;
mod gitee;
mod github;
mod gitlab;
mod sourcehut;

pub use bitbucket::*;
pub use chromium::*;
pub use codeberg::*;
pub use gitee::*;
pub use github::*;
Expand Down
302 changes: 302 additions & 0 deletions crates/git_hosting_providers/src/providers/chromium.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
use std::str::FromStr;
use std::sync::{Arc, LazyLock};

use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use futures::AsyncReadExt;
use git::{
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
PullRequest, RemoteUrl,
};
use gpui::SharedString;
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
use regex::Regex;
use serde::Deserialize;
use url::Url;

const CHROMIUM_REVIEW_URL: &str = "https://chromium-review.googlesource.com";

/// Parses Gerrit URLs like
/// https://chromium-review.googlesource.com/c/chromium/src/+/3310961.
fn pull_request_regex() -> &'static Regex {
static PULL_REQUEST_NUMBER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!(
r#"Reviewed-on: ({CHROMIUM_REVIEW_URL}/c/(.*)/\+/(\d+))"#
))
.unwrap()
});
&PULL_REQUEST_NUMBER_REGEX
}

/// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html
#[derive(Debug, Deserialize)]
struct ChangeInfo {
owner: AccountInfo,
}

#[derive(Debug, Deserialize)]
pub struct AccountInfo {
#[serde(rename = "_account_id")]
id: u64,
}

pub struct Chromium;

impl Chromium {
async fn fetch_chromium_commit_author(
&self,
_repo: &str,
commit: &str,
client: &Arc<dyn HttpClient>,
) -> Result<Option<AccountInfo>> {
let url = format!("{CHROMIUM_REVIEW_URL}/changes/{commit}");

let request = Request::get(&url)
.header("Content-Type", "application/json")
.follow_redirects(http_client::RedirectPolicy::FollowAll);

let mut response = client
.send(request.body(AsyncBody::default())?)
.await
.with_context(|| format!("error fetching Gerrit commit details at {:?}", url))?;

let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;

if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}

// Remove XSSI protection prefix.
let body_str = std::str::from_utf8(&body)?.trim_start_matches(")]}'");

serde_json::from_str::<ChangeInfo>(body_str)
.map(|change| Some(change.owner))
.context("failed to deserialize Gerrit change info")
}
}

#[async_trait]
impl GitHostingProvider for Chromium {
fn name(&self) -> String {
"Chromium".to_string()
}

fn base_url(&self) -> Url {
Url::parse("https://chromium.googlesource.com").unwrap()
}

fn supports_avatars(&self) -> bool {
true
}

fn format_line_number(&self, line: u32) -> String {
format!("{line}")
}

fn format_line_numbers(&self, start_line: u32, _end_line: u32) -> String {
format!("{start_line}")
}

fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
let url = RemoteUrl::from_str(url).ok()?;

let host = url.host_str()?;
if host != self.base_url().host_str()? {
return None;
}

let path_segments = url.path_segments()?.collect::<Vec<_>>();
let joined_path = path_segments.join("/");
let repo = joined_path.trim_end_matches(".git");

Some(ParsedGitRemote {
owner: Arc::from(""),
repo: repo.into(),
})
}

fn build_commit_permalink(
&self,
remote: &ParsedGitRemote,
params: BuildCommitPermalinkParams,
) -> Url {
let BuildCommitPermalinkParams { sha } = params;
let ParsedGitRemote { owner: _, repo } = remote;

self.base_url().join(&format!("{repo}/+/{sha}")).unwrap()
}

fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
let ParsedGitRemote { owner: _, repo } = remote;
let BuildPermalinkParams {
sha,
path,
selection,
} = params;

let mut permalink = self
.base_url()
.join(&format!("{repo}/+/{sha}/{path}"))
.unwrap();
permalink.set_fragment(
selection
.map(|selection| self.line_fragment(&selection))
.as_deref(),
);
permalink
}

fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
let capture = pull_request_regex().captures(message)?;
let url = Url::parse(capture.get(1)?.as_str()).unwrap();
let repo = capture.get(2)?.as_str();
if repo != remote.repo.as_ref() {
return None;
}

let number = capture.get(3)?.as_str().parse::<u32>().ok()?;

Some(PullRequest { number, url })
}

async fn commit_author_avatar_url(
&self,
_repo_owner: &str,
repo: &str,
commit: SharedString,
http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
let Some(author) = self
.fetch_chromium_commit_author(repo, &commit, &http_client)
.await?
else {
return Ok(None);
};

let mut avatar_url = Url::parse(&format!(
"{CHROMIUM_REVIEW_URL}/accounts/{}/avatar",
&author.id
))?;
avatar_url.set_query(Some("size=128"));

Ok(Some(avatar_url))
}
}

#[cfg(test)]
mod tests {
use indoc::indoc;
use pretty_assertions::assert_eq;

use super::*;

#[test]
fn test_parse_remote_url_given_https_url() {
let parsed_remote = Chromium
.parse_remote_url("https://chromium.googlesource.com/chromium/src")
.unwrap();

assert_eq!(
parsed_remote,
ParsedGitRemote {
owner: Arc::from(""),
repo: "chromium/src".into(),
}
);
}

#[test]
fn test_build_chromium_permalink() {
let permalink = Chromium.build_permalink(
ParsedGitRemote {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: None,
},
);

let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h";
assert_eq!(permalink.to_string(), expected_url.to_string())
}

#[test]
fn test_build_chromium_permalink_with_single_line_selection() {
let permalink = Chromium.build_permalink(
ParsedGitRemote {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: Some(18..18),
},
);

let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";
assert_eq!(permalink.to_string(), expected_url.to_string())
}

#[test]
fn test_build_chromium_permalink_with_multi_line_selection() {
let permalink = Chromium.build_permalink(
ParsedGitRemote {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: Some(18..30),
},
);

let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";
assert_eq!(permalink.to_string(), expected_url.to_string())
}

#[test]
fn test_chromium_pull_requests() {
let remote = ParsedGitRemote {
owner: Arc::from(""),
repo: "chromium/src".into(),
};

let message = "This does not contain a pull request";
assert!(Chromium.extract_pull_request(&remote, message).is_none());

// Pull request number at end of "Reviewed-on:" line
let message = indoc! {r#"
Test commit header

Test commit description with multiple
lines.

Bug: 1193775, 1270302
Change-Id: Id15e9b4d75cce43ebd5fe34f0fb37d5e1e811b66
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3310961
Reviewed-by: Test reviewer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1054973}
"#
};

assert_eq!(
Chromium
.extract_pull_request(&remote, &message)
.unwrap()
.url
.as_str(),
"https://chromium-review.googlesource.com/c/chromium/src/+/3310961"
);
}
}
Loading