1use std::path::Path;
11use std::process::Command;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Forge {
16 Github,
18 Gitlab,
20}
21
22impl Forge {
23 #[must_use]
25 pub const fn as_str(self) -> &'static str {
26 match self {
27 Self::Github => "github",
28 Self::Gitlab => "gitlab",
29 }
30 }
31
32 #[must_use]
34 pub fn parse(name: &str) -> Option<Self> {
35 match name {
36 "github" => Some(Self::Github),
37 "gitlab" => Some(Self::Gitlab),
38 _ => None,
39 }
40 }
41
42 #[must_use]
44 pub const fn cli(self) -> &'static str {
45 match self {
46 Self::Github => "gh",
47 Self::Gitlab => "glab",
48 }
49 }
50
51 #[must_use]
53 pub const fn cli_override(self) -> &'static str {
54 match self {
55 Self::Github => "RK_GH_BIN",
56 Self::Gitlab => "RK_GLAB_BIN",
57 }
58 }
59
60 #[must_use]
73 pub const fn cli_floor(self) -> (u32, u32, u32) {
74 match self {
75 Self::Github => (2, 19, 0),
76 Self::Gitlab => (1, 114, 0),
77 }
78 }
79
80 #[must_use]
82 pub const fn cli_upgrade(self) -> &'static str {
83 match self {
84 Self::Github => "upgrade gh, or point RK_GH_BIN at a newer binary",
85 Self::Gitlab => "upgrade glab, or point RK_GLAB_BIN at a newer binary",
86 }
87 }
88
89 pub const ALL: [Self; 2] = [Self::Github, Self::Gitlab];
91}
92
93#[derive(Debug, Default)]
96pub struct Detection {
97 pub host: Option<String>,
99 pub repo: Option<String>,
101 pub forge: Option<Forge>,
104}
105
106#[must_use]
112pub fn detect(dir: &Path) -> Detection {
113 let mut command = Command::new(crate::probes::git_bin());
114 for var in crate::maintenance::GIT_HOOK_VARS {
115 command.env_remove(var);
116 }
117 let out = command
118 .args(["-C"])
119 .arg(dir)
120 .args(["remote", "get-url", "origin"])
121 .output();
122 let url = match out {
123 Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_owned(),
124 _ => return Detection::default(),
125 };
126 let Some((host, path)) = split_remote(&url) else {
127 return Detection::default();
128 };
129 let forge = forge_for_host(&host);
130 Detection {
131 host: Some(host),
132 repo: Some(path),
133 forge,
134 }
135}
136
137#[must_use]
141pub fn forge_for_host(host: &str) -> Option<Forge> {
142 if host == "github.com" {
143 return Some(Forge::Github);
144 }
145 if host == "gitlab.com" || host.starts_with("gitlab.") {
146 return Some(Forge::Gitlab);
147 }
148 None
149}
150
151#[must_use]
154pub fn split_remote(url: &str) -> Option<(String, String)> {
155 let (host, raw_path) = if let Some((_, rest)) = url.split_once("://") {
156 let (authority, path) = rest.split_once('/')?;
157 let host = authority
158 .rsplit_once('@')
159 .map_or(authority, |(_, host)| host);
160 let host = host.split(':').next()?;
161 (host.to_owned(), path.to_owned())
162 } else {
163 let (authority, path) = url.split_once(':')?;
164 let host = authority
165 .rsplit_once('@')
166 .map_or(authority, |(_, host)| host);
167 (host.to_owned(), path.to_owned())
168 };
169 let path = raw_path
170 .trim_start_matches('/')
171 .trim_end_matches('/')
172 .trim_end_matches(".git")
173 .to_owned();
174 (!host.is_empty() && !path.is_empty()).then_some((host, path))
175}
176
177#[must_use]
180pub fn tech_of(dir: &Path) -> Option<&'static str> {
181 if dir.join("Cargo.toml").is_file() {
182 Some("rust")
183 } else if dir.join("pyproject.toml").is_file() {
184 Some("python")
185 } else if dir.join("VERSION").is_file() {
186 Some("bash")
187 } else {
188 None
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::{Forge, forge_for_host, split_remote};
195
196 #[test]
197 fn a_remote_splits_into_host_and_path_in_both_forms() {
198 assert_eq!(
199 split_remote("https://github.com/owner/name.git"),
200 Some(("github.com".into(), "owner/name".into()))
201 );
202 assert_eq!(
203 split_remote("git@gitlab.com:group/sub/name.git"),
204 Some(("gitlab.com".into(), "group/sub/name".into()))
205 );
206 assert_eq!(
207 split_remote("ssh://git@github.com:22/owner/name.git"),
208 Some(("github.com".into(), "owner/name".into()))
209 );
210 assert_eq!(split_remote("not a url"), None);
211 }
212
213 #[test]
214 fn a_host_maps_to_its_forge_and_an_unknown_host_to_none() {
215 assert_eq!(forge_for_host("github.com"), Some(Forge::Github));
216 assert_eq!(forge_for_host("gitlab.com"), Some(Forge::Gitlab));
217 assert_eq!(forge_for_host("gitlab.example.org"), Some(Forge::Gitlab));
218 assert_eq!(forge_for_host("codeberg.org"), None);
219 }
220}