jj_ryu/platform/
detection.rs1use crate::error::{Error, Result};
4use crate::types::{Platform, PlatformConfig};
5use regex::Regex;
6use std::env;
7use std::sync::LazyLock;
8
9static RE_SSH: LazyLock<Regex> =
11 LazyLock::new(|| Regex::new(r"git@[^:]+:(.+?)(?:\.git)?$").unwrap());
12
13static RE_HTTPS: LazyLock<Regex> =
15 LazyLock::new(|| Regex::new(r"https?://[^/]+/(.+?)(?:\.git)?$").unwrap());
16
17pub 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 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 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
43pub fn parse_repo_info(url: &str) -> Result<PlatformConfig> {
45 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 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 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 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 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}