use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Forge {
Github,
Gitlab,
}
impl Forge {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Github => "github",
Self::Gitlab => "gitlab",
}
}
#[must_use]
pub fn parse(name: &str) -> Option<Self> {
match name {
"github" => Some(Self::Github),
"gitlab" => Some(Self::Gitlab),
_ => None,
}
}
#[must_use]
pub const fn cli(self) -> &'static str {
match self {
Self::Github => "gh",
Self::Gitlab => "glab",
}
}
pub const ALL: [Self; 2] = [Self::Github, Self::Gitlab];
}
#[derive(Debug, Default)]
pub struct Detection {
pub host: Option<String>,
pub repo: Option<String>,
pub forge: Option<Forge>,
}
#[must_use]
pub fn detect(dir: &Path) -> Detection {
let out = Command::new("git")
.args(["-C"])
.arg(dir)
.args(["remote", "get-url", "origin"])
.output();
let url = match out {
Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_owned(),
_ => return Detection::default(),
};
let Some((host, path)) = split_remote(&url) else {
return Detection::default();
};
let forge = forge_for_host(&host);
Detection {
host: Some(host),
repo: Some(path),
forge,
}
}
#[must_use]
pub fn forge_for_host(host: &str) -> Option<Forge> {
if host == "github.com" {
return Some(Forge::Github);
}
if host == "gitlab.com" || host.starts_with("gitlab.") {
return Some(Forge::Gitlab);
}
None
}
#[must_use]
pub fn split_remote(url: &str) -> Option<(String, String)> {
let (host, raw_path) = if let Some((_, rest)) = url.split_once("://") {
let (authority, path) = rest.split_once('/')?;
let host = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host);
let host = host.split(':').next()?;
(host.to_owned(), path.to_owned())
} else {
let (authority, path) = url.split_once(':')?;
let host = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host);
(host.to_owned(), path.to_owned())
};
let path = raw_path
.trim_start_matches('/')
.trim_end_matches('/')
.trim_end_matches(".git")
.to_owned();
(!host.is_empty() && !path.is_empty()).then_some((host, path))
}
#[must_use]
pub fn tech_of(dir: &Path) -> Option<&'static str> {
if dir.join("Cargo.toml").is_file() {
Some("rust")
} else if dir.join("pyproject.toml").is_file() {
Some("python")
} else if dir.join("VERSION").is_file() {
Some("bash")
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::{Forge, forge_for_host, split_remote};
#[test]
fn a_remote_splits_into_host_and_path_in_both_forms() {
assert_eq!(
split_remote("https://github.com/owner/name.git"),
Some(("github.com".into(), "owner/name".into()))
);
assert_eq!(
split_remote("git@gitlab.com:group/sub/name.git"),
Some(("gitlab.com".into(), "group/sub/name".into()))
);
assert_eq!(
split_remote("ssh://git@github.com:22/owner/name.git"),
Some(("github.com".into(), "owner/name".into()))
);
assert_eq!(split_remote("not a url"), None);
}
#[test]
fn a_host_maps_to_its_forge_and_an_unknown_host_to_none() {
assert_eq!(forge_for_host("github.com"), Some(Forge::Github));
assert_eq!(forge_for_host("gitlab.com"), Some(Forge::Gitlab));
assert_eq!(forge_for_host("gitlab.example.org"), Some(Forge::Gitlab));
assert_eq!(forge_for_host("codeberg.org"), None);
}
}