1use std::path::Path;
2
3use anyhow::{Context, bail};
4
5use crate::DslLanguage;
6
7pub fn detect_language(repo_root: &Path) -> anyhow::Result<DslLanguage> {
16 let harmont_dir = repo_root.join(".hm");
17 if !harmont_dir.is_dir() {
18 bail!("no .hm/ directory found in {}", repo_root.display());
19 }
20 let langs = scan_extensions(repo_root)?;
21 if langs.has_ts {
22 Ok(DslLanguage::TypeScript)
24 } else if langs.has_py {
25 Ok(DslLanguage::Python)
26 } else {
27 bail!("no .py or .ts files found in {}", harmont_dir.display())
28 }
29}
30
31pub fn detect_language_python_first(repo_root: &Path) -> anyhow::Result<DslLanguage> {
44 let harmont_dir = repo_root.join(".hm");
45 if !harmont_dir.is_dir() {
46 bail!("no .hm/ directory found in {}", repo_root.display());
47 }
48 let langs = scan_extensions(repo_root)?;
49 if langs.has_py {
50 Ok(DslLanguage::Python)
51 } else if langs.has_ts {
52 Ok(DslLanguage::TypeScript)
53 } else {
54 bail!("no .py or .ts files found in {}", harmont_dir.display())
55 }
56}
57
58#[must_use]
65pub fn has_pipeline_files(repo_root: &Path) -> bool {
66 matches!(scan_extensions(repo_root), Ok(langs) if langs.has_py || langs.has_ts)
67}
68
69struct DetectedLangs {
72 has_py: bool,
73 has_ts: bool,
74}
75
76fn scan_extensions(repo_root: &Path) -> anyhow::Result<DetectedLangs> {
79 let harmont_dir = repo_root.join(".hm");
80 if !harmont_dir.is_dir() {
81 return Ok(DetectedLangs {
82 has_py: false,
83 has_ts: false,
84 });
85 }
86
87 let entries = std::fs::read_dir(&harmont_dir)
88 .with_context(|| format!("failed to read {}", harmont_dir.display()))?;
89
90 let mut has_py = false;
91 let mut has_ts = false;
92 for entry in entries {
93 let entry = entry?;
94 match entry.path().extension().and_then(|e| e.to_str()) {
95 Some("py") => has_py = true,
96 Some("ts") => has_ts = true,
97 _ => {}
98 }
99 }
100 Ok(DetectedLangs { has_py, has_ts })
101}
102
103#[cfg(test)]
104#[allow(clippy::unwrap_used, clippy::expect_used)]
105mod tests {
106 use super::*;
107 use std::fs;
108 use tempfile::TempDir;
109
110 fn setup(files: &[&str]) -> TempDir {
113 let tmp = TempDir::new().unwrap();
114 let harmont = tmp.path().join(".hm");
115 fs::create_dir(&harmont).unwrap();
116 for name in files {
117 fs::write(harmont.join(name), "").unwrap();
118 }
119 tmp
120 }
121
122 #[test]
123 fn python_file_detected() {
124 let tmp = setup(&["ci.py"]);
125 let lang = detect_language(tmp.path()).unwrap();
126 assert_eq!(lang, DslLanguage::Python);
127 }
128
129 #[test]
130 fn typescript_file_detected() {
131 let tmp = setup(&["ci.ts"]);
132 let lang = detect_language(tmp.path()).unwrap();
133 assert_eq!(lang, DslLanguage::TypeScript);
134 }
135
136 #[test]
137 fn mixed_languages_prefers_typescript() {
138 let tmp = setup(&["ci.py", "deploy.ts"]);
139 let lang = detect_language(tmp.path()).unwrap();
140 assert_eq!(lang, DslLanguage::TypeScript);
141 }
142
143 #[test]
144 fn no_harmont_dir_is_error() {
145 let tmp = TempDir::new().unwrap();
146 let err = detect_language(tmp.path()).unwrap_err();
148 let msg = err.to_string();
149 assert!(msg.contains("no .hm/ directory"), "unexpected error: {msg}");
150 }
151
152 #[test]
153 fn empty_harmont_dir_is_error() {
154 let tmp = TempDir::new().unwrap();
155 fs::create_dir(tmp.path().join(".hm")).unwrap();
156 let err = detect_language(tmp.path()).unwrap_err();
157 let msg = err.to_string();
158 assert!(
159 msg.contains("no .py or .ts files"),
160 "unexpected error: {msg}"
161 );
162 }
163
164 #[test]
165 fn python_first_prefers_python_when_mixed() {
166 let tmp = setup(&["ci.py", "deploy.ts"]);
167 assert_eq!(
168 detect_language_python_first(tmp.path()).unwrap(),
169 DslLanguage::Python
170 );
171 }
172
173 #[test]
174 fn python_first_falls_back_to_typescript_when_only_ts() {
175 let tmp = setup(&["ci.ts"]);
176 assert_eq!(
177 detect_language_python_first(tmp.path()).unwrap(),
178 DslLanguage::TypeScript
179 );
180 }
181
182 #[test]
183 fn python_first_no_harmont_dir_is_error() {
184 let tmp = TempDir::new().unwrap();
185 let err = detect_language_python_first(tmp.path()).unwrap_err();
186 assert!(
187 err.to_string().contains("no .hm/ directory"),
188 "unexpected error: {err}"
189 );
190 }
191
192 #[test]
193 fn has_pipeline_files_true_for_py_and_ts() {
194 assert!(has_pipeline_files(setup(&["ci.py"]).path()));
195 assert!(has_pipeline_files(setup(&["ci.ts"]).path()));
196 assert!(has_pipeline_files(setup(&["ci.py", "deploy.ts"]).path()));
197 }
198
199 #[test]
200 fn has_pipeline_files_false_for_missing_or_empty_harmont() {
201 assert!(!has_pipeline_files(TempDir::new().unwrap().path()));
203 assert!(!has_pipeline_files(setup(&["README.md"]).path()));
205 }
206}