1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, Package, UpdateType};
4use changepacks_utils::next_version;
5use regex::Regex;
6use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8use tokio::fs::{read_to_string, write};
9
10#[derive(Debug)]
11pub struct GradlePackage {
12 name: Option<String>,
13 version: Option<String>,
14 path: PathBuf,
15 relative_path: PathBuf,
16 is_changed: bool,
17 dependencies: HashSet<String>,
18}
19
20impl GradlePackage {
21 pub fn new(
22 name: Option<String>,
23 version: Option<String>,
24 path: PathBuf,
25 relative_path: PathBuf,
26 ) -> Self {
27 Self {
28 name,
29 version,
30 path,
31 relative_path,
32 is_changed: false,
33 dependencies: HashSet::new(),
34 }
35 }
36}
37
38fn update_version_kts(content: &str, new_version: &str) -> String {
40 let simple_pattern = Regex::new(r#"(?m)^(version\s*=\s*)"[^"]+""#).unwrap();
42 if simple_pattern.is_match(content) {
43 return simple_pattern
44 .replace(content, format!(r#"${{1}}"{}""#, new_version))
45 .to_string();
46 }
47
48 let fallback_pattern =
50 Regex::new(r#"(?m)^(version\s*=\s*project\.findProperty\([^)]+\)\s*\?:\s*)"[^"]+""#)
51 .unwrap();
52 if fallback_pattern.is_match(content) {
53 return fallback_pattern
54 .replace(content, format!(r#"${{1}}"{}""#, new_version))
55 .to_string();
56 }
57
58 content.to_string()
59}
60
61fn update_version_groovy(content: &str, new_version: &str) -> String {
63 let assign_pattern = Regex::new(r#"(?m)^(version\s*=\s*)['"][^'"]+['"]"#).unwrap();
65 if assign_pattern.is_match(content) {
66 return assign_pattern
67 .replace(content, format!(r#"${{1}}'{}'"#, new_version))
68 .to_string();
69 }
70
71 let space_pattern = Regex::new(r#"(?m)^(version\s+)['"][^'"]+['"]"#).unwrap();
73 if space_pattern.is_match(content) {
74 return space_pattern
75 .replace(content, format!(r#"${{1}}'{}'"#, new_version))
76 .to_string();
77 }
78
79 content.to_string()
80}
81
82#[async_trait]
83impl Package for GradlePackage {
84 fn name(&self) -> Option<&str> {
85 self.name.as_deref()
86 }
87
88 fn version(&self) -> Option<&str> {
89 self.version.as_deref()
90 }
91
92 fn path(&self) -> &Path {
93 &self.path
94 }
95
96 fn relative_path(&self) -> &Path {
97 &self.relative_path
98 }
99
100 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
101 let current_version = self.version.as_deref().unwrap_or("0.0.0");
102 let new_version = next_version(current_version, update_type)?;
103
104 let content = read_to_string(&self.path).await?;
105 let file_name = self
106 .path
107 .file_name()
108 .and_then(|f| f.to_str())
109 .unwrap_or_default();
110 let is_kts = file_name.ends_with(".kts");
111
112 let updated_content = if is_kts {
113 update_version_kts(&content, &new_version)
114 } else {
115 update_version_groovy(&content, &new_version)
116 };
117
118 write(&self.path, updated_content).await?;
119 self.version = Some(new_version);
120 Ok(())
121 }
122
123 fn language(&self) -> Language {
124 Language::Java
125 }
126
127 fn set_changed(&mut self, changed: bool) {
128 self.is_changed = changed;
129 }
130
131 fn is_changed(&self) -> bool {
132 self.is_changed
133 }
134
135 fn default_publish_command(&self) -> String {
136 "./gradlew publish".to_string()
137 }
138
139 fn dependencies(&self) -> &HashSet<String> {
140 &self.dependencies
141 }
142
143 fn add_dependency(&mut self, dependency: &str) {
144 self.dependencies.insert(dependency.to_string());
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use changepacks_core::UpdateType;
152 use std::fs;
153 use tempfile::TempDir;
154 use tokio::fs::read_to_string;
155
156 #[tokio::test]
157 async fn test_gradle_package_new() {
158 let package = GradlePackage::new(
159 Some("test-package".to_string()),
160 Some("1.0.0".to_string()),
161 PathBuf::from("/test/build.gradle.kts"),
162 PathBuf::from("test/build.gradle.kts"),
163 );
164
165 assert_eq!(package.name(), Some("test-package"));
166 assert_eq!(package.version(), Some("1.0.0"));
167 assert_eq!(package.path(), PathBuf::from("/test/build.gradle.kts"));
168 assert_eq!(
169 package.relative_path(),
170 PathBuf::from("test/build.gradle.kts")
171 );
172 assert_eq!(package.language(), Language::Java);
173 assert!(!package.is_changed());
174 assert_eq!(package.default_publish_command(), "./gradlew publish");
175 }
176
177 #[tokio::test]
178 async fn test_gradle_package_set_changed() {
179 let mut package = GradlePackage::new(
180 Some("test-package".to_string()),
181 Some("1.0.0".to_string()),
182 PathBuf::from("/test/build.gradle.kts"),
183 PathBuf::from("test/build.gradle.kts"),
184 );
185
186 assert!(!package.is_changed());
187 package.set_changed(true);
188 assert!(package.is_changed());
189 package.set_changed(false);
190 assert!(!package.is_changed());
191 }
192
193 #[test]
194 fn test_update_version_kts_simple() {
195 let content = r#"
196plugins {
197 id("java")
198}
199
200group = "com.example"
201version = "1.0.0"
202"#;
203 let updated = update_version_kts(content, "1.0.1");
204 assert!(updated.contains(r#"version = "1.0.1""#));
205 }
206
207 #[test]
208 fn test_update_version_kts_with_fallback() {
209 let content = r#"
210group = "com.devfive"
211version = project.findProperty("releaseVersion") ?: "1.0.11"
212"#;
213 let updated = update_version_kts(content, "1.0.12");
214 assert!(
215 updated.contains(r#"version = project.findProperty("releaseVersion") ?: "1.0.12""#)
216 );
217 }
218
219 #[test]
220 fn test_update_version_groovy_assign() {
221 let content = r#"
222group = 'com.example'
223version = '2.0.0'
224"#;
225 let updated = update_version_groovy(content, "2.0.1");
226 assert!(updated.contains("version = '2.0.1'"));
227 }
228
229 #[test]
230 fn test_update_version_groovy_space() {
231 let content = r#"
232group 'com.example'
233version '3.0.0'
234"#;
235 let updated = update_version_groovy(content, "3.0.1");
236 assert!(updated.contains("version '3.0.1'"));
237 }
238
239 #[tokio::test]
240 async fn test_gradle_package_update_version_kts_patch() {
241 let temp_dir = TempDir::new().unwrap();
242 let project_dir = temp_dir.path().join("myproject");
243 fs::create_dir_all(&project_dir).unwrap();
244
245 let build_gradle = project_dir.join("build.gradle.kts");
246 fs::write(
247 &build_gradle,
248 r#"
249plugins {
250 id("java")
251}
252
253group = "com.example"
254version = "1.0.0"
255"#,
256 )
257 .unwrap();
258
259 let mut package = GradlePackage::new(
260 Some("myproject".to_string()),
261 Some("1.0.0".to_string()),
262 build_gradle.clone(),
263 PathBuf::from("myproject/build.gradle.kts"),
264 );
265
266 package.update_version(UpdateType::Patch).await.unwrap();
267
268 let content = read_to_string(&build_gradle).await.unwrap();
269 assert!(content.contains(r#"version = "1.0.1""#));
270
271 temp_dir.close().unwrap();
272 }
273
274 #[tokio::test]
275 async fn test_gradle_package_update_version_kts_minor() {
276 let temp_dir = TempDir::new().unwrap();
277 let project_dir = temp_dir.path().join("myproject");
278 fs::create_dir_all(&project_dir).unwrap();
279
280 let build_gradle = project_dir.join("build.gradle.kts");
281 fs::write(
282 &build_gradle,
283 r#"
284plugins {
285 id("java")
286}
287
288group = "com.example"
289version = "1.0.0"
290"#,
291 )
292 .unwrap();
293
294 let mut package = GradlePackage::new(
295 Some("myproject".to_string()),
296 Some("1.0.0".to_string()),
297 build_gradle.clone(),
298 PathBuf::from("myproject/build.gradle.kts"),
299 );
300
301 package.update_version(UpdateType::Minor).await.unwrap();
302
303 let content = read_to_string(&build_gradle).await.unwrap();
304 assert!(content.contains(r#"version = "1.1.0""#));
305
306 temp_dir.close().unwrap();
307 }
308
309 #[tokio::test]
310 async fn test_gradle_package_update_version_kts_major() {
311 let temp_dir = TempDir::new().unwrap();
312 let project_dir = temp_dir.path().join("myproject");
313 fs::create_dir_all(&project_dir).unwrap();
314
315 let build_gradle = project_dir.join("build.gradle.kts");
316 fs::write(
317 &build_gradle,
318 r#"
319plugins {
320 id("java")
321}
322
323group = "com.example"
324version = "1.0.0"
325"#,
326 )
327 .unwrap();
328
329 let mut package = GradlePackage::new(
330 Some("myproject".to_string()),
331 Some("1.0.0".to_string()),
332 build_gradle.clone(),
333 PathBuf::from("myproject/build.gradle.kts"),
334 );
335
336 package.update_version(UpdateType::Major).await.unwrap();
337
338 let content = read_to_string(&build_gradle).await.unwrap();
339 assert!(content.contains(r#"version = "2.0.0""#));
340
341 temp_dir.close().unwrap();
342 }
343
344 #[tokio::test]
345 async fn test_gradle_package_update_version_groovy() {
346 let temp_dir = TempDir::new().unwrap();
347 let project_dir = temp_dir.path().join("myproject");
348 fs::create_dir_all(&project_dir).unwrap();
349
350 let build_gradle = project_dir.join("build.gradle");
351 fs::write(
352 &build_gradle,
353 r#"
354plugins {
355 id 'java'
356}
357
358group = 'com.example'
359version = '1.0.0'
360"#,
361 )
362 .unwrap();
363
364 let mut package = GradlePackage::new(
365 Some("myproject".to_string()),
366 Some("1.0.0".to_string()),
367 build_gradle.clone(),
368 PathBuf::from("myproject/build.gradle"),
369 );
370
371 package.update_version(UpdateType::Patch).await.unwrap();
372
373 let content = read_to_string(&build_gradle).await.unwrap();
374 assert!(content.contains("version = '1.0.1'"));
375
376 temp_dir.close().unwrap();
377 }
378
379 #[tokio::test]
380 async fn test_gradle_package_update_version_with_fallback() {
381 let temp_dir = TempDir::new().unwrap();
382 let project_dir = temp_dir.path().join("myproject");
383 fs::create_dir_all(&project_dir).unwrap();
384
385 let build_gradle = project_dir.join("build.gradle.kts");
386 fs::write(
387 &build_gradle,
388 r#"
389group = "com.devfive"
390version = project.findProperty("releaseVersion") ?: "1.0.11"
391"#,
392 )
393 .unwrap();
394
395 let mut package = GradlePackage::new(
396 Some("myproject".to_string()),
397 Some("1.0.11".to_string()),
398 build_gradle.clone(),
399 PathBuf::from("myproject/build.gradle.kts"),
400 );
401
402 package.update_version(UpdateType::Patch).await.unwrap();
403
404 let content = read_to_string(&build_gradle).await.unwrap();
405 assert!(content.contains(r#"?: "1.0.12""#));
406
407 temp_dir.close().unwrap();
408 }
409
410 #[test]
411 fn test_gradle_package_dependencies() {
412 let mut package = GradlePackage::new(
413 Some("test-package".to_string()),
414 Some("1.0.0".to_string()),
415 PathBuf::from("/test/build.gradle.kts"),
416 PathBuf::from("test/build.gradle.kts"),
417 );
418
419 assert!(package.dependencies().is_empty());
421
422 package.add_dependency("core");
424 package.add_dependency("utils");
425
426 let deps = package.dependencies();
427 assert_eq!(deps.len(), 2);
428 assert!(deps.contains("core"));
429 assert!(deps.contains("utils"));
430
431 package.add_dependency("core");
433 assert_eq!(package.dependencies().len(), 2);
434 }
435}