Refactor Git hosting providers (#11457)

This PR refactors the code pertaining to Git hosting providers to make
it more uniform and easy to add support for new providers.

There is now a `GitHostingProvider` trait that contains the
functionality specific to an individual Git hosting provider. Each
provider we support has an implementation of this trait.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-05-06 15:44:13 -04:00 committed by GitHub
parent 8871fec2a8
commit bb1817ff31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1443 additions and 883 deletions

View file

@ -1,110 +1,116 @@
use core::fmt;
use std::{ops::Range, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use url::Url;
use util::{codeberg, github, http::HttpClient};
use util::http::HttpClient;
use crate::hosting_providers::{Bitbucket, Codeberg, Gitee, Github, Gitlab, Sourcehut};
use crate::Oid;
#[derive(Clone, Debug, Hash)]
pub enum HostingProvider {
Github,
Gitlab,
Gitee,
Bitbucket,
Sourcehut,
Codeberg,
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PullRequest {
pub number: u32,
pub url: Url,
}
impl HostingProvider {
pub(crate) fn base_url(&self) -> Url {
let base_url = match self {
Self::Github => "https://github.com",
Self::Gitlab => "https://gitlab.com",
Self::Gitee => "https://gitee.com",
Self::Bitbucket => "https://bitbucket.org",
Self::Sourcehut => "https://git.sr.ht",
Self::Codeberg => "https://codeberg.org",
};
pub struct BuildCommitPermalinkParams<'a> {
pub sha: &'a str,
}
Url::parse(&base_url).unwrap()
}
pub struct BuildPermalinkParams<'a> {
pub sha: &'a str,
pub path: &'a str,
pub selection: Option<Range<u32>>,
}
/// Returns the fragment portion of the URL for the selected lines in
/// the representation the [`GitHostingProvider`] expects.
pub(crate) fn line_fragment(&self, selection: &Range<u32>) -> String {
/// A Git hosting provider.
#[async_trait]
pub trait GitHostingProvider {
/// Returns the name of the provider.
fn name(&self) -> String;
/// Returns the base URL of the provider.
fn base_url(&self) -> Url;
/// Returns a permalink to a Git commit on this hosting provider.
fn build_commit_permalink(
&self,
remote: &ParsedGitRemote,
params: BuildCommitPermalinkParams,
) -> Url;
/// Returns a permalink to a file and/or selection on this hosting provider.
fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
/// Returns whether this provider supports avatars.
fn supports_avatars(&self) -> bool;
/// Returns a URL fragment to the given line selection.
fn line_fragment(&self, selection: &Range<u32>) -> String {
if selection.start == selection.end {
let line = selection.start + 1;
match self {
Self::Github | Self::Gitlab | Self::Gitee | Self::Sourcehut | Self::Codeberg => {
format!("L{}", line)
}
Self::Bitbucket => format!("lines-{}", line),
}
self.format_line_number(line)
} else {
let start_line = selection.start + 1;
let end_line = selection.end + 1;
match self {
Self::Github | Self::Codeberg => format!("L{}-L{}", start_line, end_line),
Self::Gitlab | Self::Gitee | Self::Sourcehut => {
format!("L{}-{}", start_line, end_line)
}
Self::Bitbucket => format!("lines-{}:{}", start_line, end_line),
}
self.format_line_numbers(start_line, end_line)
}
}
pub fn supports_avatars(&self) -> bool {
match self {
HostingProvider::Github | HostingProvider::Codeberg => true,
_ => false,
}
}
/// Returns a formatted line number to be placed in a permalink URL.
fn format_line_number(&self, line: u32) -> String;
pub async fn commit_author_avatar_url(
/// Returns a formatted range of line numbers to be placed in a permalink URL.
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>>;
fn extract_pull_request(
&self,
repo_owner: &str,
repo: &str,
commit: Oid,
client: Arc<dyn HttpClient>,
_remote: &ParsedGitRemote,
_message: &str,
) -> Option<PullRequest> {
None
}
async fn commit_author_avatar_url(
&self,
_repo_owner: &str,
_repo: &str,
_commit: Oid,
_http_client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
Ok(match self {
HostingProvider::Github => {
let commit = commit.to_string();
github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
.await?
.map(|author| -> Result<Url, url::ParseError> {
let mut url = Url::parse(&author.avatar_url)?;
url.set_query(Some("size=128"));
Ok(url)
})
.transpose()
}
HostingProvider::Codeberg => {
let commit = commit.to_string();
codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client)
.await?
.map(|author| Url::parse(&author.avatar_url))
.transpose()
}
_ => Ok(None),
}?)
Ok(None)
}
}
impl fmt::Display for HostingProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
HostingProvider::Github => "GitHub",
HostingProvider::Gitlab => "GitLab",
HostingProvider::Gitee => "Gitee",
HostingProvider::Bitbucket => "Bitbucket",
HostingProvider::Sourcehut => "Sourcehut",
HostingProvider::Codeberg => "Codeberg",
};
write!(f, "{}", name)
}
#[derive(Debug)]
pub struct ParsedGitRemote<'a> {
pub owner: &'a str,
pub repo: &'a str,
}
pub fn parse_git_remote_url(
url: &str,
) -> Option<(
Arc<dyn GitHostingProvider + Send + Sync + 'static>,
ParsedGitRemote,
)> {
let providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> = vec![
Arc::new(Github),
Arc::new(Gitlab),
Arc::new(Bitbucket),
Arc::new(Codeberg),
Arc::new(Gitee),
Arc::new(Sourcehut),
];
providers.into_iter().find_map(|provider| {
provider
.parse_remote_url(&url)
.map(|parsed_remote| (provider, parsed_remote))
})
}