changepacks_java/
finder.rs1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use changepacks_core::{Project, ProjectFinder};
4use regex::Regex;
5use std::{
6 collections::HashMap,
7 path::{Path, PathBuf},
8 process::Stdio,
9};
10use tokio::process::Command;
11
12use crate::{package::GradlePackage, workspace::GradleWorkspace};
13
14#[derive(Debug)]
15pub struct GradleProjectFinder {
16 projects: HashMap<PathBuf, Project>,
17 project_files: Vec<&'static str>,
18}
19
20impl Default for GradleProjectFinder {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl GradleProjectFinder {
27 pub fn new() -> Self {
28 Self {
29 projects: HashMap::new(),
30 project_files: vec!["build.gradle.kts", "build.gradle"],
31 }
32 }
33}
34
35#[derive(Debug, Default)]
37struct GradleProperties {
38 name: Option<String>,
39 version: Option<String>,
40}
41
42async fn get_gradle_properties(project_dir: &Path) -> Option<GradleProperties> {
44 let gradlew = if cfg!(windows) {
46 project_dir.join("gradlew.bat")
47 } else {
48 project_dir.join("gradlew")
49 };
50
51 if !gradlew.exists() {
53 return None;
54 }
55
56 let output = Command::new(&gradlew)
58 .args(["properties", "-q"])
59 .current_dir(project_dir)
60 .stdout(Stdio::piped())
61 .stderr(Stdio::null())
62 .output()
63 .await
64 .ok()?;
65
66 if !output.status.success() {
67 return None;
68 }
69
70 let stdout = String::from_utf8_lossy(&output.stdout);
71 let mut props = GradleProperties::default();
72
73 let name_pattern = Regex::new(r"(?m)^name:\s*(.+)$").ok()?;
76 let version_pattern = Regex::new(r"(?m)^version:\s*(.+)$").ok()?;
77
78 if let Some(caps) = name_pattern.captures(&stdout) {
79 let name = caps.get(1).map(|m| m.as_str().trim().to_string());
80 if name.as_deref() != Some("unspecified") {
81 props.name = name;
82 }
83 }
84
85 if let Some(caps) = version_pattern.captures(&stdout) {
86 let version = caps.get(1).map(|m| m.as_str().trim().to_string());
87 if version.as_deref() != Some("unspecified") {
88 props.version = version;
89 }
90 }
91
92 Some(props)
93}
94
95#[async_trait]
96impl ProjectFinder for GradleProjectFinder {
97 fn projects(&self) -> Vec<&Project> {
98 self.projects.values().collect::<Vec<_>>()
99 }
100
101 fn projects_mut(&mut self) -> Vec<&mut Project> {
102 self.projects.values_mut().collect::<Vec<_>>()
103 }
104
105 fn project_files(&self) -> &[&str] {
106 &self.project_files
107 }
108
109 async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()> {
110 if path.is_file()
111 && self.project_files().contains(
112 &path
113 .file_name()
114 .context(format!("File name not found - {}", path.display()))?
115 .to_str()
116 .context(format!("File name not found - {}", path.display()))?,
117 )
118 {
119 if self.projects.contains_key(path) {
120 return Ok(());
121 }
122
123 let project_dir = path
124 .parent()
125 .context(format!("Parent not found - {}", path.display()))?;
126
127 let props = get_gradle_properties(project_dir).await.unwrap_or_default();
129
130 let name = props.name.or_else(|| {
132 project_dir
133 .file_name()
134 .and_then(|n| n.to_str())
135 .map(|s| s.to_string())
136 });
137
138 let version = props.version;
139
140 let is_workspace = project_dir.join("settings.gradle.kts").is_file()
142 || project_dir.join("settings.gradle").is_file();
143
144 let (path, project) = if is_workspace {
145 (
146 path.to_path_buf(),
147 Project::Workspace(Box::new(GradleWorkspace::new(
148 name,
149 version,
150 path.to_path_buf(),
151 relative_path.to_path_buf(),
152 ))),
153 )
154 } else {
155 (
156 path.to_path_buf(),
157 Project::Package(Box::new(GradlePackage::new(
158 name,
159 version,
160 path.to_path_buf(),
161 relative_path.to_path_buf(),
162 ))),
163 )
164 };
165
166 self.projects.insert(path, project);
167 }
168 Ok(())
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use changepacks_core::Project;
176 use std::fs;
177 use tempfile::TempDir;
178
179 #[test]
180 fn test_gradle_project_finder_new() {
181 let finder = GradleProjectFinder::new();
182 assert_eq!(
183 finder.project_files(),
184 &["build.gradle.kts", "build.gradle"]
185 );
186 assert_eq!(finder.projects().len(), 0);
187 }
188
189 #[test]
190 fn test_gradle_project_finder_default() {
191 let finder = GradleProjectFinder::default();
192 assert_eq!(
193 finder.project_files(),
194 &["build.gradle.kts", "build.gradle"]
195 );
196 assert_eq!(finder.projects().len(), 0);
197 }
198
199 #[tokio::test]
200 async fn test_gradle_project_finder_visit_kts_package() {
201 let temp_dir = TempDir::new().unwrap();
202 let project_dir = temp_dir.path().join("myproject");
203 fs::create_dir_all(&project_dir).unwrap();
204
205 let build_gradle = project_dir.join("build.gradle.kts");
206 fs::write(
207 &build_gradle,
208 r#"
209plugins {
210 id("java")
211}
212
213group = "com.example"
214version = "1.0.0"
215"#,
216 )
217 .unwrap();
218
219 let mut finder = GradleProjectFinder::new();
220 finder
221 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
222 .await
223 .unwrap();
224
225 let projects = finder.projects();
226 assert_eq!(projects.len(), 1);
227 match projects[0] {
228 Project::Package(pkg) => {
229 assert_eq!(pkg.name(), Some("myproject"));
231 assert_eq!(pkg.version(), None);
233 }
234 _ => panic!("Expected Package"),
235 }
236
237 temp_dir.close().unwrap();
238 }
239
240 #[tokio::test]
241 async fn test_gradle_project_finder_visit_groovy_package() {
242 let temp_dir = TempDir::new().unwrap();
243 let project_dir = temp_dir.path().join("groovyproject");
244 fs::create_dir_all(&project_dir).unwrap();
245
246 let build_gradle = project_dir.join("build.gradle");
247 fs::write(
248 &build_gradle,
249 r#"
250plugins {
251 id 'java'
252}
253
254group = 'com.example'
255version = '2.0.0'
256"#,
257 )
258 .unwrap();
259
260 let mut finder = GradleProjectFinder::new();
261 finder
262 .visit(&build_gradle, &PathBuf::from("groovyproject/build.gradle"))
263 .await
264 .unwrap();
265
266 let projects = finder.projects();
267 assert_eq!(projects.len(), 1);
268 match projects[0] {
269 Project::Package(pkg) => {
270 assert_eq!(pkg.name(), Some("groovyproject"));
272 }
273 _ => panic!("Expected Package"),
274 }
275
276 temp_dir.close().unwrap();
277 }
278
279 #[tokio::test]
280 async fn test_gradle_project_finder_visit_workspace() {
281 let temp_dir = TempDir::new().unwrap();
282 let project_dir = temp_dir.path().join("multiproject");
283 fs::create_dir_all(&project_dir).unwrap();
284
285 let build_gradle = project_dir.join("build.gradle.kts");
286 fs::write(
287 &build_gradle,
288 r#"
289plugins {
290 id("java")
291}
292
293group = "com.example"
294version = "1.0.0"
295"#,
296 )
297 .unwrap();
298
299 let settings_gradle = project_dir.join("settings.gradle.kts");
301 fs::write(
302 &settings_gradle,
303 r#"
304rootProject.name = "multiproject"
305include("subproject1", "subproject2")
306"#,
307 )
308 .unwrap();
309
310 let mut finder = GradleProjectFinder::new();
311 finder
312 .visit(
313 &build_gradle,
314 &PathBuf::from("multiproject/build.gradle.kts"),
315 )
316 .await
317 .unwrap();
318
319 let projects = finder.projects();
320 assert_eq!(projects.len(), 1);
321 match projects[0] {
322 Project::Workspace(ws) => {
323 assert_eq!(ws.name(), Some("multiproject"));
325 }
326 _ => panic!("Expected Workspace"),
327 }
328
329 temp_dir.close().unwrap();
330 }
331
332 #[tokio::test]
333 async fn test_gradle_project_finder_visit_non_gradle_file() {
334 let temp_dir = TempDir::new().unwrap();
335 let other_file = temp_dir.path().join("other.txt");
336 fs::write(&other_file, "some content").unwrap();
337
338 let mut finder = GradleProjectFinder::new();
339 finder
340 .visit(&other_file, &PathBuf::from("other.txt"))
341 .await
342 .unwrap();
343
344 assert_eq!(finder.projects().len(), 0);
345
346 temp_dir.close().unwrap();
347 }
348
349 #[tokio::test]
350 async fn test_gradle_project_finder_visit_duplicate() {
351 let temp_dir = TempDir::new().unwrap();
352 let project_dir = temp_dir.path().join("myproject");
353 fs::create_dir_all(&project_dir).unwrap();
354
355 let build_gradle = project_dir.join("build.gradle.kts");
356 fs::write(
357 &build_gradle,
358 r#"
359group = "com.example"
360version = "1.0.0"
361"#,
362 )
363 .unwrap();
364
365 let mut finder = GradleProjectFinder::new();
366 finder
367 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
368 .await
369 .unwrap();
370
371 assert_eq!(finder.projects().len(), 1);
372
373 finder
375 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
376 .await
377 .unwrap();
378
379 assert_eq!(finder.projects().len(), 1);
380
381 temp_dir.close().unwrap();
382 }
383
384 #[tokio::test]
385 async fn test_gradle_project_finder_projects_mut() {
386 let temp_dir = TempDir::new().unwrap();
387 let project_dir = temp_dir.path().join("myproject");
388 fs::create_dir_all(&project_dir).unwrap();
389
390 let build_gradle = project_dir.join("build.gradle.kts");
391 fs::write(
392 &build_gradle,
393 r#"
394group = "com.example"
395version = "1.0.0"
396"#,
397 )
398 .unwrap();
399
400 let mut finder = GradleProjectFinder::new();
401 finder
402 .visit(&build_gradle, &PathBuf::from("myproject/build.gradle.kts"))
403 .await
404 .unwrap();
405
406 let mut_projects = finder.projects_mut();
407 assert_eq!(mut_projects.len(), 1);
408
409 temp_dir.close().unwrap();
410 }
411}