1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::Config;
4use changepacks_core::{Language, UpdateType, Workspace};
5
6crate::declare_gradle_project!(pub struct GradleWorkspace);
9
10#[async_trait]
11impl Workspace for GradleWorkspace {
12 changepacks_core::impl_basic_accessors!();
18
19 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
24 crate::bump_gradle_version(
25 &mut self.version,
26 &self.path,
27 update_type,
28 crate::version_updater::GradleVersionScope::ScriptAndAllProjects,
29 )
30 .await
31 }
32
33 changepacks_core::impl_language!(Language::Java);
38
39 changepacks_core::impl_const_publish_commands!(
49 crate::PUBLISH_COMMAND,
50 crate::DRY_RUN_PUBLISH_COMMAND
51 );
52
53 crate::impl_gradle_publish_task_flags!();
57
58 async fn publish(&self, config: &Config) -> Result<changepacks_core::publish::PublishOutput> {
63 crate::run_publish_for_path(
64 self.path(),
65 self.relative_path(),
66 self.project_path.as_deref(),
67 config,
68 changepacks_core::publish::WORKSPACE_DIR_NOT_FOUND,
69 )
70 .await
71 }
72
73 async fn dry_run_publish(
74 &self,
75 config: &Config,
76 ) -> Result<Option<changepacks_core::publish::PublishOutput>> {
77 crate::run_dry_run_publish_for_path(
78 self.path(),
79 self.relative_path(),
80 self.project_path.as_deref(),
81 config,
82 changepacks_core::publish::WORKSPACE_DIR_NOT_FOUND,
83 )
84 .await
85 }
86
87 changepacks_core::impl_dependencies_accessors!();
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::test_support::{assert_reported_cwd, captured_argv, create_publish_wrapper};
101 use changepacks_core::UpdateType;
102 use rstest::rstest;
103 use std::fs;
104 use std::path::PathBuf;
105 use tempfile::TempDir;
106 use tokio::fs::read_to_string;
107
108 fn assert_gradle_workspace_defaults(workspace: &GradleWorkspace) {
109 assert_eq!(workspace.name(), Some("test-workspace"));
110 assert_eq!(workspace.version(), Some("1.0.0"));
111 assert_eq!(workspace.path(), PathBuf::from("/test/build.gradle.kts"));
112 assert_eq!(
113 workspace.relative_path(),
114 PathBuf::from("test/build.gradle.kts")
115 );
116 assert_eq!(workspace.language(), Language::Java);
117 assert!(!workspace.is_changed());
118 assert!(workspace.is_publishable_by_default());
119 assert!(workspace.is_dry_run_publishable_by_default());
120 #[cfg(windows)]
121 {
122 assert_eq!(
123 workspace.default_publish_command(),
124 ".\\gradlew.bat publish"
125 );
126 assert_eq!(
127 workspace.default_dry_run_publish_command().as_deref(),
128 Some(".\\gradlew.bat publishToMavenLocal")
129 );
130 }
131 #[cfg(not(windows))]
132 {
133 assert_eq!(workspace.default_publish_command(), "./gradlew publish");
134 assert_eq!(
135 workspace.default_dry_run_publish_command().as_deref(),
136 Some("./gradlew publishToMavenLocal")
137 );
138 }
139 }
140
141 #[tokio::test]
142 async fn test_gradle_workspace_new() {
143 let workspace = GradleWorkspace::new(
144 Some("test-workspace".to_string()),
145 Some("1.0.0".to_string()),
146 PathBuf::from("/test/build.gradle.kts"),
147 PathBuf::from("test/build.gradle.kts"),
148 );
149
150 assert_gradle_workspace_defaults(&workspace);
151 }
152
153 #[test]
154 fn test_gradle_workspace_publishability_tracks_available_tasks() {
155 let remote_only = GradleWorkspace::new_with_publish_tasks(
156 Some("remote-only".to_string()),
157 Some("1.0.0".to_string()),
158 PathBuf::from("/test/build.gradle.kts"),
159 PathBuf::from("test/build.gradle.kts"),
160 true,
161 false,
162 );
163 let local_only = GradleWorkspace::new_with_publish_tasks(
164 Some("local-only".to_string()),
165 Some("1.0.0".to_string()),
166 PathBuf::from("/test/build.gradle.kts"),
167 PathBuf::from("test/build.gradle.kts"),
168 false,
169 true,
170 );
171
172 assert!(remote_only.is_publishable_by_default());
173 assert!(!remote_only.is_dry_run_publishable_by_default());
174 assert!(!local_only.is_publishable_by_default());
175 assert!(local_only.is_dry_run_publishable_by_default());
176 }
177
178 #[tokio::test]
179 async fn test_gradle_workspace_new_without_name_and_version() {
180 let workspace = GradleWorkspace::new(
181 None,
182 None,
183 PathBuf::from("/test/build.gradle.kts"),
184 PathBuf::from("test/build.gradle.kts"),
185 );
186
187 assert_eq!(workspace.name(), None);
188 assert_eq!(workspace.version(), None);
189 }
190
191 #[test]
192 fn test_gradle_workspace_set_changed() {
193 changepacks_core::assert_set_changed_roundtrip!(GradleWorkspace::new(
194 Some("test-workspace".to_string()),
195 Some("1.0.0".to_string()),
196 PathBuf::from("/test/build.gradle.kts"),
197 PathBuf::from("test/build.gradle.kts"),
198 ));
199 }
200
201 #[tokio::test]
202 async fn test_publish_root_project_uses_platform_wrapper_publish_task() {
203 let temp_dir = TempDir::new().unwrap();
204 let root = temp_dir.path().join("repo with spaces");
205 fs::create_dir_all(&root).unwrap();
206 create_publish_wrapper(&root);
207 let manifest = root.join("build.gradle.kts");
208 fs::write(&manifest, "version = \"1.0.0\"\n").unwrap();
209 let workspace = GradleWorkspace::new(
210 Some("root".to_string()),
211 Some("1.0.0".to_string()),
212 manifest,
213 PathBuf::from("build.gradle.kts"),
214 );
215
216 let output = workspace
217 .publish(&changepacks_core::Config::default())
218 .await
219 .unwrap();
220 let dry_run = workspace
221 .dry_run_publish(&changepacks_core::Config::default())
222 .await
223 .unwrap()
224 .unwrap();
225
226 assert!(output.success, "stderr: {}", output.stderr);
227 assert_reported_cwd(&output.stdout, &root);
228 assert_eq!(captured_argv(&output.stdout), ["publish"]);
229 assert!(dry_run.success, "stderr: {}", dry_run.stderr);
230 let dry_run_argv = captured_argv(&dry_run.stdout);
231 assert_eq!(dry_run_argv.len(), 2, "stdout: {}", dry_run.stdout);
232 assert_eq!(dry_run_argv[0], "publishToMavenLocal");
233 assert!(dry_run_argv[1].starts_with("-Dmaven.repo.local="));
234 }
235
236 #[rstest]
237 #[case(UpdateType::Patch, "1.0.1")]
238 #[case(UpdateType::Minor, "1.1.0")]
239 #[case(UpdateType::Major, "2.0.0")]
240 #[tokio::test]
241 async fn test_gradle_workspace_update_version_kts(
242 #[case] update_type: UpdateType,
243 #[case] expected: &str,
244 ) {
245 let temp_dir = TempDir::new().unwrap();
246 let project_dir = temp_dir.path().join("multiproject");
247 fs::create_dir_all(&project_dir).unwrap();
248
249 let build_gradle = project_dir.join("build.gradle.kts");
250 fs::write(
251 &build_gradle,
252 r#"
253plugins {
254 id("java")
255}
256
257group = "com.example"
258version = "1.0.0"
259"#,
260 )
261 .unwrap();
262
263 let mut workspace = GradleWorkspace::new(
264 Some("multiproject".to_string()),
265 Some("1.0.0".to_string()),
266 build_gradle.clone(),
267 PathBuf::from("multiproject/build.gradle.kts"),
268 );
269
270 workspace.update_version(update_type).await.unwrap();
271
272 let content = read_to_string(&build_gradle).await.unwrap();
273 assert!(content.contains(&format!(r#"version = "{expected}""#)));
274 assert_eq!(workspace.version(), Some(expected));
275
276 temp_dir.close().unwrap();
277 }
278
279 #[tokio::test]
280 async fn test_gradle_workspace_update_version_groovy() {
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");
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 mut workspace = GradleWorkspace::new(
300 Some("multiproject".to_string()),
301 Some("1.0.0".to_string()),
302 build_gradle.clone(),
303 PathBuf::from("multiproject/build.gradle"),
304 );
305
306 workspace.update_version(UpdateType::Patch).await.unwrap();
307
308 let content = read_to_string(&build_gradle).await.unwrap();
309 assert!(content.contains("version = '1.0.1'"));
310 assert_eq!(workspace.version(), Some("1.0.1"));
311
312 temp_dir.close().unwrap();
313 }
314
315 #[tokio::test]
316 async fn gradle_workspace_updates_groovy_version_from_sibling_properties() {
317 let temp_dir = TempDir::new().unwrap();
318 let build_gradle = temp_dir.path().join("build.gradle");
319 let properties_path = temp_dir.path().join("gradle.properties");
320 let build_content = b"plugins { id 'java' }\n";
321 let properties_content = b"version : 2.0.0 ! workspace\n";
322 tokio::fs::write(&build_gradle, build_content)
323 .await
324 .unwrap();
325 tokio::fs::write(&properties_path, properties_content)
326 .await
327 .unwrap();
328 let mut workspace = GradleWorkspace::new(
329 Some("multiproject".to_string()),
330 Some("2.0.0".to_string()),
331 build_gradle.clone(),
332 PathBuf::from("build.gradle"),
333 );
334
335 workspace.update_version(UpdateType::Patch).await.unwrap();
336
337 assert_eq!(workspace.version(), Some("2.0.1"));
338 assert_eq!(tokio::fs::read(&build_gradle).await.unwrap(), build_content);
339 assert_eq!(
340 tokio::fs::read(&properties_path).await.unwrap(),
341 b"version : 2.0.1 ! workspace\n"
342 );
343 }
344
345 #[tokio::test]
346 async fn test_gradle_workspace_update_version_without_version() {
347 let temp_dir = TempDir::new().unwrap();
348 let project_dir = temp_dir.path().join("multiproject");
349 fs::create_dir_all(&project_dir).unwrap();
350
351 let build_gradle = project_dir.join("build.gradle.kts");
352 fs::write(
353 &build_gradle,
354 r#"
355plugins {
356 id("java")
357}
358
359group = "com.example"
360version = "0.0.0"
361"#,
362 )
363 .unwrap();
364
365 let mut workspace = GradleWorkspace::new(
366 Some("multiproject".to_string()),
367 None,
368 build_gradle.clone(),
369 PathBuf::from("multiproject/build.gradle.kts"),
370 );
371
372 workspace.update_version(UpdateType::Patch).await.unwrap();
373
374 let content = read_to_string(&build_gradle).await.unwrap();
375 assert!(content.contains(r#"version = "0.0.1""#));
376
377 temp_dir.close().unwrap();
378 }
379
380 #[tokio::test]
381 async fn test_gradle_workspace_update_version_errors_for_unsupported_declaration() {
382 let temp_dir = TempDir::new().unwrap();
383 let build_gradle = temp_dir.path().join("build.gradle.kts");
384 let content = "version = providers.gradleProperty(\"releaseVersion\").get()\n";
385 fs::write(&build_gradle, content).unwrap();
386 let bytes_before = fs::read(&build_gradle).unwrap();
387 let mut workspace = GradleWorkspace::new(
388 Some("multiproject".to_string()),
389 Some("1.0.0".to_string()),
390 build_gradle.clone(),
391 PathBuf::from("build.gradle.kts"),
392 );
393
394 let result = workspace.update_version(UpdateType::Patch).await;
395
396 assert!(result.is_err());
397 assert_eq!(fs::read(&build_gradle).unwrap(), bytes_before);
398 assert_eq!(workspace.version(), Some("1.0.0"));
399 }
400
401 #[tokio::test]
402 async fn test_gradle_workspace_ambiguous_version_keeps_file_and_state_unchanged() {
403 let temp_dir = TempDir::new().unwrap();
404 let build_gradle = temp_dir.path().join("build.gradle");
405 let content = "version = '1.0.0'\nallprojects {\n version = 'duplicate'\n}\n";
406 fs::write(&build_gradle, content).unwrap();
407 let bytes_before = fs::read(&build_gradle).unwrap();
408 let mut workspace = GradleWorkspace::new(
409 Some("multiproject".to_string()),
410 Some("1.0.0".to_string()),
411 build_gradle.clone(),
412 PathBuf::from("build.gradle"),
413 );
414
415 let result = workspace.update_version(UpdateType::Patch).await;
416
417 assert!(result.is_err());
418 assert_eq!(fs::read(&build_gradle).unwrap(), bytes_before);
419 assert_eq!(workspace.version(), Some("1.0.0"));
420 }
421
422 #[tokio::test]
423 async fn test_gradle_workspace_updates_allprojects_version_with_exact_formatting() {
424 let temp_dir = TempDir::new().unwrap();
425 let build_gradle = temp_dir.path().join("build.gradle");
426 let content = "plugins {\r\n version = 'plugin-version'\r\n}\r\nallprojects {\r\n\tversion = \"1.0.0\" // project-wide\r\n}\r\n";
427 fs::write(&build_gradle, content).unwrap();
428 let mut workspace = GradleWorkspace::new(
429 Some("multiproject".to_string()),
430 Some("1.0.0".to_string()),
431 build_gradle.clone(),
432 PathBuf::from("build.gradle"),
433 );
434
435 workspace.update_version(UpdateType::Patch).await.unwrap();
436
437 assert_eq!(
438 fs::read_to_string(&build_gradle).unwrap(),
439 "plugins {\r\n version = 'plugin-version'\r\n}\r\nallprojects {\r\n\tversion = \"1.0.1\" // project-wide\r\n}\r\n"
440 );
441 assert_eq!(workspace.version(), Some("1.0.1"));
442 }
443
444 #[test]
445 fn test_gradle_workspace_dependencies() {
446 changepacks_core::assert_dependencies_roundtrip!(
447 GradleWorkspace::new(
448 Some("test-workspace".to_string()),
449 Some("1.0.0".to_string()),
450 PathBuf::from("/test/build.gradle.kts"),
451 PathBuf::from("test/build.gradle.kts"),
452 ),
453 "core",
454 "utils"
455 );
456 }
457
458 #[test]
459 fn test_set_name() {
460 changepacks_core::assert_set_name_roundtrip!(GradleWorkspace::new(
461 None,
462 Some("1.0.0".to_string()),
463 PathBuf::from("/test/build.gradle.kts"),
464 PathBuf::from("build.gradle.kts"),
465 ));
466 }
467}