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.
This commit is contained in:
Marshall Bowers 2024-10-29 16:19:05 -04:00 committed by GitHub
parent d310a1269f
commit 5b7fa05a87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 304 additions and 161 deletions

View file

@ -1,4 +1,10 @@
pub mod blame;
pub mod commit;
pub mod diff;
mod hosting_provider;
mod remote;
pub mod repository;
pub mod status;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
@ -7,15 +13,9 @@ use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
pub use git2 as libgit;
pub use crate::hosting_provider::*;
pub mod blame;
pub mod commit;
pub mod diff;
pub mod repository;
pub mod status;
pub use crate::remote::*;
pub use git2 as libgit;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));

View file

@ -69,7 +69,7 @@ pub trait GitHostingProvider {
/// 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 parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote>;
fn extract_pull_request(
&self,
@ -159,10 +159,10 @@ impl GitHostingProviderRegistry {
}
}
#[derive(Debug)]
pub struct ParsedGitRemote<'a> {
pub owner: &'a str,
pub repo: &'a str,
#[derive(Debug, PartialEq)]
pub struct ParsedGitRemote {
pub owner: Arc<str>,
pub repo: Arc<str>,
}
pub fn parse_git_remote_url(

85
crates/git/src/remote.rs Normal file
View file

@ -0,0 +1,85 @@
use derive_more::Deref;
use url::Url;
/// The URL to a Git remote.
#[derive(Debug, PartialEq, Eq, Clone, Deref)]
pub struct RemoteUrl(Url);
impl std::str::FromStr for RemoteUrl {
type Err = url::ParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input.starts_with("git@") {
// Rewrite remote URLs like `git@github.com:user/repo.git` to `ssh://git@github.com/user/repo.git`
let ssh_url = input.replacen(':', "/", 1).replace("git@", "ssh://git@");
Ok(RemoteUrl(Url::parse(&ssh_url)?))
} else {
Ok(RemoteUrl(Url::parse(input)?))
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_parsing_valid_remote_urls() {
let valid_urls = vec![
(
"https://github.com/octocat/zed.git",
"https",
"github.com",
"/octocat/zed.git",
),
(
"git@github.com:octocat/zed.git",
"ssh",
"github.com",
"/octocat/zed.git",
),
(
"ssh://git@github.com/octocat/zed.git",
"ssh",
"github.com",
"/octocat/zed.git",
),
(
"file:///path/to/local/zed",
"file",
"",
"/path/to/local/zed",
),
];
for (input, expected_scheme, expected_host, expected_path) in valid_urls {
let parsed = input.parse::<RemoteUrl>().expect("failed to parse URL");
let url = parsed.0;
assert_eq!(
url.scheme(),
expected_scheme,
"unexpected scheme for {input:?}",
);
assert_eq!(
url.host_str().unwrap_or(""),
expected_host,
"unexpected host for {input:?}",
);
assert_eq!(url.path(), expected_path, "unexpected path for {input:?}");
}
}
#[test]
fn test_parsing_invalid_remote_urls() {
let invalid_urls = vec!["not_a_url", "http://"];
for url in invalid_urls {
assert!(
url.parse::<RemoteUrl>().is_err(),
"expected \"{url}\" to not parse as a Git remote URL",
);
}
}
}