Skip to main content

jj_ryu/platform/
detection.rs

1//! Platform detection from remote URLs
2
3use crate::error::{Error, Result};
4use crate::types::{Platform, PlatformConfig};
5use regex::Regex;
6use std::env;
7use std::sync::LazyLock;
8
9/// Regex for SSH URLs: git@host:owner/repo.git
10static RE_SSH: LazyLock<Regex> =
11    LazyLock::new(|| Regex::new(r"git@[^:]+:(.+?)(?:\.git)?$").unwrap());
12
13/// Regex for HTTPS URLs: `https://host/owner/repo.git`
14static RE_HTTPS: LazyLock<Regex> =
15    LazyLock::new(|| Regex::new(r"https?://[^/]+/(.+?)(?:\.git)?$").unwrap());
16
17/// Detect platform (GitHub or GitLab) from a remote URL
18pub fn detect_platform(url: &str) -> Option<Platform> {
19    let gh_host = env::var("GH_HOST").ok();
20    let gitlab_host = env::var("GITLAB_HOST").ok();
21
22    let hostname = extract_hostname(url)?;
23
24    // Check GitHub
25    if hostname == "github.com"
26        || hostname.ends_with(".github.com")
27        || gh_host.as_ref().is_some_and(|h| hostname == *h)
28    {
29        return Some(Platform::GitHub);
30    }
31
32    // Check GitLab
33    if hostname == "gitlab.com"
34        || hostname.ends_with(".gitlab.com")
35        || gitlab_host.as_ref().is_some_and(|h| hostname == *h)
36    {
37        return Some(Platform::GitLab);
38    }
39
40    None
41}
42
43/// Parse repository info (owner/repo) from a remote URL
44pub fn parse_repo_info(url: &str) -> Result<PlatformConfig> {
45    // Normalize: strip trailing slashes
46    let url = url.trim_end_matches('/');
47
48    let platform = detect_platform(url).ok_or(Error::NoSupportedRemotes)?;
49    let hostname = extract_hostname(url);
50
51    let path = RE_SSH
52        .captures(url)
53        .or_else(|| RE_HTTPS.captures(url))
54        .and_then(|c| c.get(1))
55        .map(|m| m.as_str())
56        .ok_or_else(|| Error::Parse(format!("cannot parse remote URL: {url}")))?;
57
58    // Split path into owner and repo (GitLab supports nested groups)
59    let parts: Vec<&str> = path.split('/').collect();
60    if parts.len() < 2 {
61        return Err(Error::Parse(format!("invalid repo path: {path}")));
62    }
63
64    let repo = parts.last().unwrap().to_string();
65    let owner = parts[..parts.len() - 1].join("/");
66
67    // Determine if self-hosted
68    let host = match platform {
69        Platform::GitHub => {
70            if hostname.as_ref().is_some_and(|h| h != "github.com") {
71                hostname
72            } else {
73                None
74            }
75        }
76        Platform::GitLab => {
77            if hostname.as_ref().is_some_and(|h| h != "gitlab.com") {
78                hostname
79            } else {
80                None
81            }
82        }
83    };
84
85    Ok(PlatformConfig {
86        platform,
87        owner,
88        repo,
89        host,
90    })
91}
92
93fn extract_hostname(url: &str) -> Option<String> {
94    // SSH format
95    if url.starts_with("git@") {
96        return url
97            .strip_prefix("git@")
98            .and_then(|s| s.split(':').next())
99            .map(ToString::to_string);
100    }
101
102    // HTTPS format
103    url::Url::parse(url)
104        .ok()
105        .and_then(|u| u.host_str().map(ToString::to_string))
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_detect_github_https() {
114        assert_eq!(
115            detect_platform("https://github.com/owner/repo.git"),
116            Some(Platform::GitHub)
117        );
118    }
119
120    #[test]
121    fn test_detect_github_ssh() {
122        assert_eq!(
123            detect_platform("git@github.com:owner/repo.git"),
124            Some(Platform::GitHub)
125        );
126    }
127
128    #[test]
129    fn test_detect_gitlab_https() {
130        assert_eq!(
131            detect_platform("https://gitlab.com/owner/repo.git"),
132            Some(Platform::GitLab)
133        );
134    }
135
136    #[test]
137    fn test_parse_github_repo() {
138        let config = parse_repo_info("https://github.com/owner/repo.git").unwrap();
139        assert_eq!(config.platform, Platform::GitHub);
140        assert_eq!(config.owner, "owner");
141        assert_eq!(config.repo, "repo");
142        assert!(config.host.is_none());
143    }
144
145    #[test]
146    fn test_parse_gitlab_nested_groups() {
147        let config = parse_repo_info("https://gitlab.com/group/subgroup/repo.git").unwrap();
148        assert_eq!(config.platform, Platform::GitLab);
149        assert_eq!(config.owner, "group/subgroup");
150        assert_eq!(config.repo, "repo");
151    }
152}