use crate::git::{GitRemoteUrl, Repository};
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, strum::EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum CiPlatform {
GitHub,
GitLab,
Gitea,
#[strum(serialize = "azure-devops", serialize = "azuredevops")]
AzureDevOps,
}
fn platform_from_url(url: &str) -> Option<CiPlatform> {
let parsed = GitRemoteUrl::parse(url)?;
if parsed.is_github() {
Some(CiPlatform::GitHub)
} else if parsed.is_gitlab() {
Some(CiPlatform::GitLab)
} else if parsed.is_gitea() {
Some(CiPlatform::Gitea)
} else if parsed.is_azure_devops() {
Some(CiPlatform::AzureDevOps)
} else {
None
}
}
impl Repository {
pub fn ci_platform(&self, remote_hint: Option<&str>) -> Option<CiPlatform> {
if let Some(platform) = self.configured_ci_platform() {
return Some(platform);
}
if let Some(remote) = remote_hint
&& let Some(url) = self.effective_remote_url(remote)
&& let Some(platform) = platform_from_url(&url)
{
log::debug!("Detected CI platform {platform} from remote '{remote}' (hint)");
return Some(platform);
}
if let Ok(remote) = self.primary_remote()
&& let Some(url) = self.effective_remote_url(&remote)
&& let Some(platform) = platform_from_url(&url)
{
log::debug!("Detected CI platform {platform} from remote '{remote}'");
return Some(platform);
}
None
}
fn configured_ci_platform(&self) -> Option<CiPlatform> {
*self.cache.configured_ci_platform.get_or_init(|| {
let raw = self
.project_config()
.ok()
.flatten()?
.forge_platform()
.map(str::to_string)?;
match raw.parse::<CiPlatform>() {
Ok(platform) => {
log::debug!("Using CI platform from config: {platform}");
Some(platform)
}
Err(_) => {
log::warn!(
"Invalid CI platform in config: '{raw}'. Expected 'github', 'gitlab', 'gitea', or 'azure-devops'."
);
None
}
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ci_platform_string_roundtrip() {
assert_eq!(
"github".parse::<CiPlatform>().ok(),
Some(CiPlatform::GitHub)
);
assert_eq!(
"gitlab".parse::<CiPlatform>().ok(),
Some(CiPlatform::GitLab)
);
assert_eq!("gitea".parse::<CiPlatform>().ok(), Some(CiPlatform::Gitea));
assert_eq!(
"azure-devops".parse::<CiPlatform>().ok(),
Some(CiPlatform::AzureDevOps)
);
assert_eq!(
"azuredevops".parse::<CiPlatform>().ok(),
Some(CiPlatform::AzureDevOps)
);
assert_eq!(CiPlatform::GitHub.to_string(), "github");
assert_eq!(CiPlatform::GitLab.to_string(), "gitlab");
assert!("invalid".parse::<CiPlatform>().is_err());
assert!("GITHUB".parse::<CiPlatform>().is_err());
assert!("GitHub".parse::<CiPlatform>().is_err());
}
#[test]
fn test_platform_from_url() {
for url in [
"https://github.com/owner/repo.git",
"git@github.com:owner/repo.git",
"ssh://git@github.com/owner/repo.git",
"https://github.mycompany.com/owner/repo.git",
"http://github.com/owner/repo.git",
"git://github.com/owner/repo.git",
] {
assert_eq!(platform_from_url(url), Some(CiPlatform::GitHub), "{url}");
}
for url in [
"https://gitlab.com/owner/repo.git",
"git@gitlab.com:owner/repo.git",
"https://gitlab.example.com/owner/repo.git",
"http://gitlab.example.com/owner/repo.git",
"git://gitlab.mycompany.com/owner/repo.git",
] {
assert_eq!(platform_from_url(url), Some(CiPlatform::GitLab), "{url}");
}
for url in [
"https://gitea.com/owner/repo.git",
"git@gitea.example.com:owner/repo.git",
] {
assert_eq!(platform_from_url(url), Some(CiPlatform::Gitea), "{url}");
}
for url in [
"https://dev.azure.com/myorg/myproject/_git/myrepo",
"git@ssh.dev.azure.com:v3/myorg/myproject/myrepo",
"https://myorg.visualstudio.com/myproject/_git/myrepo",
] {
assert_eq!(
platform_from_url(url),
Some(CiPlatform::AzureDevOps),
"{url}"
);
}
assert_eq!(
platform_from_url("https://bitbucket.org/owner/repo.git"),
None
);
assert_eq!(
platform_from_url("https://codeberg.org/owner/repo.git"),
None
);
}
}