ZIm/crates/git/src/hosting_provider.rs
Marshall Bowers 5b7fa05a87
Make Git remote URL parsing more robust (#19924)
This PR improves the parsing of Git remote URLs in order to make
features that depend on them more robust.

Previously we were just treating these as plain strings and doing
one-off shotgun parsing to massage them into the right format. This
meant that we weren't accounting for edge cases in URL structure.

One of these cases was HTTPS Git URLs containing a username, which can
arise when using GitHub Enterprise (see
https://github.com/zed-industries/zed/issues/11160).

We now have a `RemoteUrl` typed to represent a parsed Git remote URL and
use the `Url` parser to parse it.

Release Notes:

- Improved the parsing of Git remote URLs to support additional
scenarios.
2024-10-29 16:19:05 -04:00

183 lines
5.1 KiB
Rust

use std::{ops::Range, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use collections::BTreeMap;
use derive_more::{Deref, DerefMut};
use gpui::{AppContext, Global};
use http_client::HttpClient;
use parking_lot::RwLock;
use url::Url;
use crate::Oid;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PullRequest {
pub number: u32,
pub url: Url,
}
pub struct BuildCommitPermalinkParams<'a> {
pub sha: &'a str,
}
pub struct BuildPermalinkParams<'a> {
pub sha: &'a str,
pub path: &'a str,
pub selection: Option<Range<u32>>,
}
/// 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;
self.format_line_number(line)
} else {
let start_line = selection.start + 1;
let end_line = selection.end + 1;
self.format_line_numbers(start_line, end_line)
}
}
/// Returns a formatted line number to be placed in a permalink URL.
fn format_line_number(&self, line: u32) -> String;
/// 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(&self, url: &str) -> Option<ParsedGitRemote>;
fn extract_pull_request(
&self,
_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(None)
}
}
#[derive(Default, Deref, DerefMut)]
struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
impl Global for GlobalGitHostingProviderRegistry {}
#[derive(Default)]
struct GitHostingProviderRegistryState {
providers: BTreeMap<String, Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
}
#[derive(Default)]
pub struct GitHostingProviderRegistry {
state: RwLock<GitHostingProviderRegistryState>,
}
impl GitHostingProviderRegistry {
/// Returns the global [`GitHostingProviderRegistry`].
pub fn global(cx: &AppContext) -> Arc<Self> {
cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
}
/// Returns the global [`GitHostingProviderRegistry`], if one is set.
pub fn try_global(cx: &AppContext) -> Option<Arc<Self>> {
cx.try_global::<GlobalGitHostingProviderRegistry>()
.map(|registry| registry.0.clone())
}
/// Returns the global [`GitHostingProviderRegistry`].
///
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
cx.default_global::<GlobalGitHostingProviderRegistry>()
.0
.clone()
}
/// Sets the global [`GitHostingProviderRegistry`].
pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut AppContext) {
cx.set_global(GlobalGitHostingProviderRegistry(registry));
}
/// Returns a new [`GitHostingProviderRegistry`].
pub fn new() -> Self {
Self {
state: RwLock::new(GitHostingProviderRegistryState {
providers: BTreeMap::default(),
}),
}
}
/// Returns the list of all [`GitHostingProvider`]s in the registry.
pub fn list_hosting_providers(
&self,
) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
self.state.read().providers.values().cloned().collect()
}
/// Adds the provided [`GitHostingProvider`] to the registry.
pub fn register_hosting_provider(
&self,
provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
) {
self.state
.write()
.providers
.insert(provider.name(), provider);
}
}
#[derive(Debug, PartialEq)]
pub struct ParsedGitRemote {
pub owner: Arc<str>,
pub repo: Arc<str>,
}
pub fn parse_git_remote_url(
provider_registry: Arc<GitHostingProviderRegistry>,
url: &str,
) -> Option<(
Arc<dyn GitHostingProvider + Send + Sync + 'static>,
ParsedGitRemote,
)> {
provider_registry
.list_hosting_providers()
.into_iter()
.find_map(|provider| {
provider
.parse_remote_url(url)
.map(|parsed_remote| (provider, parsed_remote))
})
}