1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, UpdateType, Workspace};
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 GradleWorkspace {
12 path: PathBuf,
13 relative_path: PathBuf,
14 version: Option<String>,
15 name: Option<String>,
16 is_changed: bool,
17 dependencies: HashSet<String>,
18}
19
20impl GradleWorkspace {
21 pub fn new(
22 name: Option<String>,
23 version: Option<String>,
24 path: PathBuf,
25 relative_path: PathBuf,
26 ) -> Self {
27 Self {
28 path,
29 relative_path,
30 name,
31 version,
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 Workspace for GradleWorkspace {
84 fn name(&self) -> Option<&str> {
85 self.name.as_deref()
86 }
87
88 fn path(&self) -> &Path {
89 &self.path
90 }
91
92 fn version(&self) -> Option<&str> {
93 self.version.as_deref()
94 }
95
96 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
97 let current_version = self.version.as_deref().unwrap_or("0.0.0");
98 let new_version = next_version(current_version, update_type)?;
99
100 let content = read_to_string(&self.path).await?;
101 let file_name = self
102 .path
103 .file_name()
104 .and_then(|f| f.to_str())
105 .unwrap_or_default();
106 let is_kts = file_name.ends_with(".kts");
107
108 let updated_content = if is_kts {
109 update_version_kts(&content, &new_version)
110 } else {
111 update_version_groovy(&content, &new_version)
112 };
113
114 write(&self.path, updated_content).await?;
115 self.version = Some(new_version);
116 Ok(())
117 }
118
119 fn language(&self) -> Language {
120 Language::Java
121 }
122
123 fn is_changed(&self) -> bool {
124 self.is_changed
125 }
126
127 fn set_changed(&mut self, changed: bool) {
128 self.is_changed = changed;
129 }
130
131 fn relative_path(&self) -> &Path {
132 &self.relative_path
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_workspace_new() {
158 let workspace = GradleWorkspace::new(
159 Some("test-workspace".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!(workspace.name(), Some("test-workspace"));
166 assert_eq!(workspace.version(), Some("1.0.0"));
167 assert_eq!(workspace.path(), PathBuf::from("/test/build.gradle.kts"));
168 assert_eq!(
169 workspace.relative_path(),
170 PathBuf::from("test/build.gradle.kts")
171 );
172 assert_eq!(workspace.language(), Language::Java);
173 assert!(!workspace.is_changed());
174 assert_eq!(workspace.default_publish_command(), "./gradlew publish");
175 }
176
177 #[tokio::test]
178 async fn test_gradle_workspace_new_without_name_and_version() {
179 let workspace = GradleWorkspace::new(
180 None,
181 None,
182 PathBuf::from("/test/build.gradle.kts"),
183 PathBuf::from("test/build.gradle.kts"),
184 );
185
186 assert_eq!(workspace.name(), None);
187 assert_eq!(workspace.version(), None);
188 }
189
190 #[tokio::test]
191 async fn test_gradle_workspace_set_changed() {
192 let mut workspace = GradleWorkspace::new(
193 Some("test-workspace".to_string()),
194 Some("1.0.0".to_string()),
195 PathBuf::from("/test/build.gradle.kts"),
196 PathBuf::from("test/build.gradle.kts"),
197 );
198
199 assert!(!workspace.is_changed());
200 workspace.set_changed(true);
201 assert!(workspace.is_changed());
202 workspace.set_changed(false);
203 assert!(!workspace.is_changed());
204 }
205
206 #[tokio::test]
207 async fn test_gradle_workspace_update_version_kts_patch() {
208 let temp_dir = TempDir::new().unwrap();
209 let project_dir = temp_dir.path().join("multiproject");
210 fs::create_dir_all(&project_dir).unwrap();
211
212 let build_gradle = project_dir.join("build.gradle.kts");
213 fs::write(
214 &build_gradle,
215 r#"
216plugins {
217 id("java")
218}
219
220group = "com.example"
221version = "1.0.0"
222"#,
223 )
224 .unwrap();
225
226 let mut workspace = GradleWorkspace::new(
227 Some("multiproject".to_string()),
228 Some("1.0.0".to_string()),
229 build_gradle.clone(),
230 PathBuf::from("multiproject/build.gradle.kts"),
231 );
232
233 workspace.update_version(UpdateType::Patch).await.unwrap();
234
235 let content = read_to_string(&build_gradle).await.unwrap();
236 assert!(content.contains(r#"version = "1.0.1""#));
237
238 temp_dir.close().unwrap();
239 }
240
241 #[tokio::test]
242 async fn test_gradle_workspace_update_version_kts_minor() {
243 let temp_dir = TempDir::new().unwrap();
244 let project_dir = temp_dir.path().join("multiproject");
245 fs::create_dir_all(&project_dir).unwrap();
246
247 let build_gradle = project_dir.join("build.gradle.kts");
248 fs::write(
249 &build_gradle,
250 r#"
251plugins {
252 id("java")
253}
254
255group = "com.example"
256version = "1.0.0"
257"#,
258 )
259 .unwrap();
260
261 let mut workspace = GradleWorkspace::new(
262 Some("multiproject".to_string()),
263 Some("1.0.0".to_string()),
264 build_gradle.clone(),
265 PathBuf::from("multiproject/build.gradle.kts"),
266 );
267
268 workspace.update_version(UpdateType::Minor).await.unwrap();
269
270 let content = read_to_string(&build_gradle).await.unwrap();
271 assert!(content.contains(r#"version = "1.1.0""#));
272
273 temp_dir.close().unwrap();
274 }
275
276 #[tokio::test]
277 async fn test_gradle_workspace_update_version_kts_major() {
278 let temp_dir = TempDir::new().unwrap();
279 let project_dir = temp_dir.path().join("multiproject");
280 fs::create_dir_all(&project_dir).unwrap();
281
282 let build_gradle = project_dir.join("build.gradle.kts");
283 fs::write(
284 &build_gradle,
285 r#"
286plugins {
287 id("java")
288}
289
290group = "com.example"
291version = "1.0.0"
292"#,
293 )
294 .unwrap();
295
296 let mut workspace = GradleWorkspace::new(
297 Some("multiproject".to_string()),
298 Some("1.0.0".to_string()),
299 build_gradle.clone(),
300 PathBuf::from("multiproject/build.gradle.kts"),
301 );
302
303 workspace.update_version(UpdateType::Major).await.unwrap();
304
305 let content = read_to_string(&build_gradle).await.unwrap();
306 assert!(content.contains(r#"version = "2.0.0""#));
307
308 temp_dir.close().unwrap();
309 }
310
311 #[tokio::test]
312 async fn test_gradle_workspace_update_version_groovy() {
313 let temp_dir = TempDir::new().unwrap();
314 let project_dir = temp_dir.path().join("multiproject");
315 fs::create_dir_all(&project_dir).unwrap();
316
317 let build_gradle = project_dir.join("build.gradle");
318 fs::write(
319 &build_gradle,
320 r#"
321plugins {
322 id 'java'
323}
324
325group = 'com.example'
326version = '1.0.0'
327"#,
328 )
329 .unwrap();
330
331 let mut workspace = GradleWorkspace::new(
332 Some("multiproject".to_string()),
333 Some("1.0.0".to_string()),
334 build_gradle.clone(),
335 PathBuf::from("multiproject/build.gradle"),
336 );
337
338 workspace.update_version(UpdateType::Patch).await.unwrap();
339
340 let content = read_to_string(&build_gradle).await.unwrap();
341 assert!(content.contains("version = '1.0.1'"));
342
343 temp_dir.close().unwrap();
344 }
345
346 #[tokio::test]
347 async fn test_gradle_workspace_update_version_without_version() {
348 let temp_dir = TempDir::new().unwrap();
349 let project_dir = temp_dir.path().join("multiproject");
350 fs::create_dir_all(&project_dir).unwrap();
351
352 let build_gradle = project_dir.join("build.gradle.kts");
353 fs::write(
354 &build_gradle,
355 r#"
356plugins {
357 id("java")
358}
359
360group = "com.example"
361version = "0.0.0"
362"#,
363 )
364 .unwrap();
365
366 let mut workspace = GradleWorkspace::new(
367 Some("multiproject".to_string()),
368 None,
369 build_gradle.clone(),
370 PathBuf::from("multiproject/build.gradle.kts"),
371 );
372
373 workspace.update_version(UpdateType::Patch).await.unwrap();
374
375 let content = read_to_string(&build_gradle).await.unwrap();
376 assert!(content.contains(r#"version = "0.0.1""#));
377
378 temp_dir.close().unwrap();
379 }
380
381 #[test]
382 fn test_gradle_workspace_dependencies() {
383 let mut workspace = GradleWorkspace::new(
384 Some("test-workspace".to_string()),
385 Some("1.0.0".to_string()),
386 PathBuf::from("/test/build.gradle.kts"),
387 PathBuf::from("test/build.gradle.kts"),
388 );
389
390 assert!(workspace.dependencies().is_empty());
392
393 workspace.add_dependency("core");
395 workspace.add_dependency("utils");
396
397 let deps = workspace.dependencies();
398 assert_eq!(deps.len(), 2);
399 assert!(deps.contains("core"));
400 assert!(deps.contains("utils"));
401
402 workspace.add_dependency("core");
404 assert_eq!(workspace.dependencies().len(), 2);
405 }
406}