kaizen/core/
project_identity.rs1use std::path::Path;
5
6pub fn project_name(workspace: &Path) -> Option<String> {
7 github_origin(workspace)
8 .as_deref()
9 .and_then(project_name_from_github_origin)
10 .or_else(|| fallback_name(workspace))
11}
12
13pub fn project_name_from_github_origin(origin: &str) -> Option<String> {
14 github_path(origin.trim()).and_then(repo_basename)
15}
16
17fn github_origin(workspace: &Path) -> Option<String> {
18 let out = std::process::Command::new("git")
19 .arg("-C")
20 .arg(workspace)
21 .args(["remote", "get-url", "origin"])
22 .output()
23 .ok()?;
24 out.status
25 .success()
26 .then(|| String::from_utf8(out.stdout).ok())?
27 .map(|s| s.trim().to_string())
28 .filter(|s| !s.is_empty())
29}
30
31fn github_path(origin: &str) -> Option<&str> {
32 origin
33 .strip_prefix("https://github.com/")
34 .or_else(|| origin.strip_prefix("http://github.com/"))
35 .or_else(|| origin.strip_prefix("git@github.com:"))
36 .or_else(|| origin.strip_prefix("ssh://git@github.com/"))
37}
38
39fn repo_basename(path: &str) -> Option<String> {
40 path.rsplit('/')
41 .next()
42 .map(|s| s.strip_suffix(".git").unwrap_or(s))
43 .filter(|s| !s.is_empty())
44 .map(str::to_string)
45}
46
47fn fallback_name(workspace: &Path) -> Option<String> {
48 workspace
49 .file_name()
50 .and_then(|s| s.to_str())
51 .filter(|s| !s.is_empty())
52 .map(str::to_string)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn parses_github_origin_forms() {
61 let cases = [
62 ("https://github.com/org/kaizen.git", "kaizen"),
63 ("git@github.com:org/kaizen.git", "kaizen"),
64 ("ssh://git@github.com/org/kaizen.git", "kaizen"),
65 ];
66 for (origin, expected) in cases {
67 assert_eq!(
68 project_name_from_github_origin(origin).as_deref(),
69 Some(expected)
70 );
71 }
72 }
73
74 #[test]
75 fn ignores_non_github_origin() {
76 assert_eq!(
77 project_name_from_github_origin("https://gitlab.com/org/kaizen.git"),
78 None
79 );
80 }
81}