Skip to content
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
13 changes: 9 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
//! - 🏗️ Host provider info extraction
//! - Easy to implement trait [`GitProvider`](crate::types::provider::GitProvider) for custom provider parsing
//! - Built-in support for multiple Git hosting providers
//! * [Generic](crate::types::provider::GenericProvider) (`git@host:owner/repo.git` style urls)
//! * [GitLab](crate::types::provider::GitLabProvider)
//! * [Azure DevOps](crate::types::provider::AzureDevOpsProvider)
//! * [Generic](crate::types::provider::generic::GenericProvider) (`git@host:owner/repo.git` style urls)
//! * [GitLab](crate::types::provider::gitlab::GitLabProvider)
//! * [Azure DevOps](crate::types::provider::azure_devops::AzureDevOpsProvider)
//!
//! ## Quick Example
//!
Expand Down Expand Up @@ -90,7 +90,12 @@
//! #### `url`
//! (**enabled by default**)
//!
//! Uses [url](https://docs.rs/url/latest/) during parsing for full url validation
//! `GitUrl` parsing finishes with [url](https://docs.rs/url/latest/) during parsing for full url validation
//!
//! [`GitUrl::parse_to_url`] will normalize an ssh-based url and return [`url::Url`](https://docs.rs/url/latest/url/struct.Url.html)
//!
//! You can use `url::Url` with the built-in [`GitProvider`](crate::types::provider::GitProvider) host parsers. See the `url_interop` tests for examples
//!
//!

pub mod types;
Expand Down
109 changes: 78 additions & 31 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,6 @@ pub struct GitUrl {
hint: GitUrlParseHint,
}

/// Build the printable GitUrl from its components
impl fmt::Display for GitUrl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let git_url_str = self.display();

write!(f, "{git_url_str}",)
}
}

impl GitUrl {
/// scheme name (i.e. `scheme://`)
pub fn scheme(&self) -> Option<&str> {
Expand Down Expand Up @@ -130,7 +121,7 @@ impl GitUrl {
}

/// This method rebuilds the printable GitUrl from its components.
/// `url_compat` results in output that can be parsed by the `url` crate
/// `url_compat` results in output that can be parsed by the [`url`](https://docs.rs/url/latest/url/) crate
fn build_string(&self, url_compat: bool) -> String {
let scheme = if self.print_scheme() || url_compat {
if let Some(scheme) = self.scheme() {
Expand Down Expand Up @@ -176,27 +167,7 @@ impl GitUrl {
let git_url_str = format!("{scheme}{auth_info}{host}{port}{path}");
git_url_str
}
}

#[cfg(feature = "url")]
impl TryFrom<&GitUrl> for Url {
type Error = url::ParseError;
fn try_from(value: &GitUrl) -> Result<Self, Self::Error> {
// Since we don't fully implement any spec, we'll rely on the url crate
Url::parse(&value.url_compat_display())
}
}

#[cfg(feature = "url")]
impl TryFrom<GitUrl> for Url {
type Error = url::ParseError;
fn try_from(value: GitUrl) -> Result<Self, Self::Error> {
// Since we don't fully implement any spec, we'll rely on the url crate
Url::parse(&value.url_compat_display())
}
}

impl GitUrl {
/// Returns `GitUrl` after removing all user info values
pub fn trim_auth(&self) -> GitUrl {
let mut new_giturl = self.clone();
Expand All @@ -219,8 +190,16 @@ impl GitUrl {
/// # }
/// ```
pub fn parse(input: &str) -> Result<Self, GitUrlParseError> {
let mut git_url_result = GitUrl::default();
let git_url = Self::parse_to_git_url(input)?;

git_url.is_valid()?;

Ok(git_url)
}

/// Internal parse to `GitUrl` without validation steps
fn parse_to_git_url(input: &str) -> Result<Self, GitUrlParseError> {
let mut git_url_result = GitUrl::default();
// Error if there are null bytes within the url
// https://github.com/tjtelan/git-url-parse-rs/issues/16
if input.contains('\0') {
Expand Down Expand Up @@ -294,6 +273,31 @@ impl GitUrl {
Ok(git_url_result)
}

/// Normalize input into form that can be used by [`Url::parse`](https://docs.rs/url/latest/url/struct.Url.html#method.parse)
///
/// ```
/// use git_url_parse::GitUrl;
/// #[cfg(feature = "url")]
/// use url::Url;
///
/// fn main() -> Result<(), git_url_parse::GitUrlParseError> {
/// let ssh_url = GitUrl::parse_to_url("git@github.com:tjtelan/git-url-parse-rs.git")?;
///
/// assert_eq!(ssh_url.scheme(), "ssh");
/// assert_eq!(ssh_url.username(), "git");
/// assert_eq!(ssh_url.host_str(), Some("github.com"));
/// assert_eq!(ssh_url.path(), "/tjtelan/git-url-parse-rs.git");
/// Ok(())
/// }
/// ```
///
#[cfg(feature = "url")]
pub fn parse_to_url(input: &str) -> Result<Url, GitUrlParseError> {
let git_url = Self::parse_to_git_url(input)?;

Ok(Url::try_from(git_url)?)
}

/// ```
/// use git_url_parse::GitUrl;
/// use git_url_parse::types::provider::GenericProvider;
Expand Down Expand Up @@ -380,3 +384,46 @@ impl GitUrl {
Ok(())
}
}

/// Build the printable GitUrl from its components
impl fmt::Display for GitUrl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let git_url_str = self.display();

write!(f, "{git_url_str}",)
}
}

#[cfg(feature = "url")]
impl TryFrom<&GitUrl> for Url {
type Error = url::ParseError;
fn try_from(value: &GitUrl) -> Result<Self, Self::Error> {
// Since we don't fully implement any spec, we'll rely on the url crate
Url::parse(&value.url_compat_display())
}
}

#[cfg(feature = "url")]
impl TryFrom<GitUrl> for Url {
type Error = url::ParseError;
fn try_from(value: GitUrl) -> Result<Self, Self::Error> {
// Since we don't fully implement any spec, we'll rely on the url crate
Url::parse(&value.url_compat_display())
}
}

#[cfg(feature = "url")]
impl TryFrom<&Url> for GitUrl {
type Error = GitUrlParseError;
fn try_from(value: &Url) -> Result<Self, Self::Error> {
GitUrl::parse(value.as_str())
}
}

#[cfg(feature = "url")]
impl TryFrom<Url> for GitUrl {
type Error = GitUrlParseError;
fn try_from(value: Url) -> Result<Self, Self::Error> {
GitUrl::parse(value.as_str())
}
}
142 changes: 142 additions & 0 deletions src/types/provider/azure_devops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use super::GitProvider;
use crate::types::GitUrlParseHint;
use crate::{GitUrl, GitUrlParseError};

use getset::Getters;
use nom::Parser;
use nom::branch::alt;
use nom::bytes::complete::{is_not, tag, take_until};
use nom::combinator::opt;
use nom::sequence::{preceded, separated_pair, terminated};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "url")]
use url::Url;

/// Azure DevOps repository provider
/// ## Supported URL Formats
///
/// - `https://dev.azure.com/org/project/_git/repo`
/// - `git@ssh.dev.azure.com:v3/org/project/repo`
///
/// Example:
///
/// ```
/// use git_url_parse::{GitUrl, GitUrlParseError};
/// use git_url_parse::types::provider::AzureDevOpsProvider;
///
/// let test_url = "https://CompanyName@dev.azure.com/CompanyName/ProjectName/_git/RepoName";
/// let parsed = GitUrl::parse(test_url).expect("URL parse failed");
///
/// let provider_info: AzureDevOpsProvider = parsed.provider_info().unwrap();
///
/// assert_eq!(provider_info.org(), "CompanyName");
/// assert_eq!(provider_info.project(), "ProjectName");
/// assert_eq!(provider_info.repo(), "RepoName");
/// assert_eq!(provider_info.fullname(), "CompanyName/ProjectName/RepoName");
/// ```
///
#[derive(Debug, PartialEq, Eq, Clone, Getters)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[getset(get = "pub")]
pub struct AzureDevOpsProvider {
/// Azure Devops organization name
org: String,
/// Azure Devops project name
project: String,
/// Azure Devops repo name
repo: String,
}

impl AzureDevOpsProvider {
/// Helper method to get the full name of a repo: `{org}/{project}/{repo}`
pub fn fullname(&self) -> String {
format!("{}/{}/{}", self.org, self.project, self.repo)
}

/// Parse the path of a http url for Azure Devops patterns
fn parse_http_path(input: &str) -> Result<(&str, AzureDevOpsProvider), GitUrlParseError> {
// Handle optional leading /
let (input, _) = opt(tag("/")).parse(input)?;

// Parse org/project/repo
let (input, (org, (project, repo))) = separated_pair(
is_not("/"),
tag("/"),
separated_pair(
is_not("/"),
tag("/"),
preceded(opt(tag("_git/")), is_not("")),
),
)
.parse(input)?;

Ok((
input,
AzureDevOpsProvider {
org: org.to_string(),
project: project.to_string(),
repo: repo.to_string(),
},
))
}

/// Parse the path of an ssh url for Azure Devops patterns
fn parse_ssh_path(input: &str) -> Result<(&str, AzureDevOpsProvider), GitUrlParseError> {
// Handle optional leading /v3/ or v3/ prefix
let (input, _) =
opt(alt((preceded(tag("/"), tag("v3/")), take_until("/")))).parse(input)?;

let (input, _) = opt(tag("/")).parse(input)?;

// Parse org/project/repo
let (input, (org, (project, repo))) = separated_pair(
is_not("/"),
tag("/"),
separated_pair(
is_not("/"),
tag("/"),
terminated(is_not("."), opt(tag(".git"))),
),
)
.parse(input)?;

Ok((
input,
AzureDevOpsProvider {
org: org.to_string(),
project: project.to_string(),
repo: repo.to_string(),
},
))
}
}

impl GitProvider<GitUrl, GitUrlParseError> for AzureDevOpsProvider {
fn from_git_url(url: &GitUrl) -> Result<Self, GitUrlParseError> {
let path = url.path();

let parsed = if url.hint() == GitUrlParseHint::Httplike {
Self::parse_http_path(path)
} else {
Self::parse_ssh_path(path)
};

parsed.map(|(_, provider)| provider)
}
}

#[cfg(feature = "url")]
impl GitProvider<Url, GitUrlParseError> for AzureDevOpsProvider {
fn from_git_url(url: &Url) -> Result<Self, GitUrlParseError> {
let path = url.path();

let parsed = if url.scheme().contains("http") {
Self::parse_http_path(path)
} else {
Self::parse_ssh_path(path)
};

parsed.map(|(_, provider)| provider)
}
}
Loading
Loading