1use std::fs;
7use std::path::Path;
8
9use crate::cargo::CargoVersionFile;
10use crate::gradle::GradleVersionFile;
11use crate::json::{DenoVersionFile, JsonVersionFile};
12use crate::project::{ProjectJsonVersionFile, ProjectTomlVersionFile, ProjectYamlVersionFile};
13use crate::pubspec::PubspecVersionFile;
14use crate::pyproject::PyprojectVersionFile;
15use crate::regex_engine::RegexVersionFile;
16use crate::version_file::{
17 CustomVersionFile, DetectedFile, UpdateResult, VersionFile, VersionFileError,
18};
19use crate::version_plain::PlainVersionFile;
20
21fn builtin_engines() -> Vec<Box<dyn VersionFile>> {
23 vec![
24 Box::new(CargoVersionFile),
25 Box::new(PyprojectVersionFile),
26 Box::new(JsonVersionFile),
27 Box::new(DenoVersionFile),
28 Box::new(PubspecVersionFile),
29 Box::new(GradleVersionFile),
30 Box::new(ProjectTomlVersionFile),
31 Box::new(ProjectJsonVersionFile),
32 Box::new(ProjectYamlVersionFile),
33 Box::new(PlainVersionFile),
34 ]
35}
36
37pub fn update_version_files(
53 root: &Path,
54 new_version: &str,
55 custom_files: &[CustomVersionFile],
56) -> Result<Vec<UpdateResult>, VersionFileError> {
57 let custom_engines: Vec<RegexVersionFile> = custom_files
59 .iter()
60 .map(RegexVersionFile::new)
61 .collect::<Result<Vec<_>, _>>()?;
62
63 let engines = builtin_engines();
64 let mut results = Vec::new();
65
66 for engine in &engines {
67 for filename in engine.filenames() {
68 let path = root.join(filename);
69 if !path.exists() {
70 continue;
71 }
72
73 let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
74
75 if !engine.detect(&content) {
76 continue;
77 }
78
79 let old_version = match engine.read_version(&content) {
80 Some(v) => v,
81 None => continue,
82 };
83
84 let updated = engine.write_version(&content, new_version)?;
85 let extra = engine.extra_info(&content, &updated);
86 let actual_new_version = engine
89 .read_version(&updated)
90 .unwrap_or_else(|| new_version.to_string());
91 fs::write(&path, &updated).map_err(VersionFileError::WriteFailed)?;
92
93 results.push(UpdateResult {
94 path,
95 name: engine.name().to_string(),
96 old_version,
97 new_version: actual_new_version,
98 extra,
99 });
100 }
101 }
102
103 for engine in &custom_engines {
105 let path = root.join(engine.path());
106 if !path.exists() {
107 continue;
108 }
109 let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
110 if !engine.detect(&content) {
111 continue;
112 }
113 let old_version = match engine.read_version(&content) {
114 Some(v) => v,
115 None => continue,
116 };
117 let updated = engine.write_version(&content, new_version)?;
118 let actual_new_version = engine
119 .read_version(&updated)
120 .unwrap_or_else(|| new_version.to_string());
121 fs::write(&path, &updated).map_err(VersionFileError::WriteFailed)?;
122 results.push(UpdateResult {
123 path,
124 name: engine.name(),
125 old_version,
126 new_version: actual_new_version,
127 extra: None,
128 });
129 }
130
131 Ok(results)
132}
133
134pub fn detect_version_files(
144 root: &Path,
145 custom_files: &[CustomVersionFile],
146) -> Result<Vec<DetectedFile>, VersionFileError> {
147 let custom_engines: Vec<RegexVersionFile> = custom_files
149 .iter()
150 .map(RegexVersionFile::new)
151 .collect::<Result<Vec<_>, _>>()?;
152
153 let engines = builtin_engines();
154 let mut results = Vec::new();
155
156 for engine in &engines {
157 for filename in engine.filenames() {
158 let path = root.join(filename);
159 if !path.exists() {
160 continue;
161 }
162 let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
163 if !engine.detect(&content) {
164 continue;
165 }
166 let old_version = match engine.read_version(&content) {
167 Some(v) => v,
168 None => continue,
169 };
170 results.push(DetectedFile {
171 path,
172 name: engine.name().to_string(),
173 old_version,
174 });
175 }
176 }
177
178 for engine in &custom_engines {
179 let path = root.join(engine.path());
180 if !path.exists() {
181 continue;
182 }
183 let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
184 if !engine.detect(&content) {
185 continue;
186 }
187 let old_version = match engine.read_version(&content) {
188 Some(v) => v,
189 None => continue,
190 };
191 results.push(DetectedFile {
192 path,
193 name: engine.name(),
194 old_version,
195 });
196 }
197
198 Ok(results)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use std::fs;
205
206 #[test]
207 fn update_version_files_updates_cargo_toml() {
208 let dir = tempfile::tempdir().unwrap();
209 let cargo_toml = dir.path().join("Cargo.toml");
210 fs::write(
211 &cargo_toml,
212 r#"[package]
213name = "example"
214version = "0.1.0"
215edition = "2024"
216"#,
217 )
218 .unwrap();
219
220 let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
221
222 assert_eq!(results.len(), 1);
223 assert_eq!(results[0].old_version, "0.1.0");
224 assert_eq!(results[0].new_version, "2.0.0");
225 assert_eq!(results[0].name, "Cargo.toml");
226 assert_eq!(results[0].path, cargo_toml);
227
228 let on_disk = fs::read_to_string(&cargo_toml).unwrap();
229 assert!(on_disk.contains("version = \"2.0.0\""));
230 }
231
232 #[test]
233 fn update_version_files_skips_missing_file() {
234 let dir = tempfile::tempdir().unwrap();
235 let results = update_version_files(dir.path(), "1.0.0", &[]).unwrap();
237 assert!(results.is_empty());
238 }
239
240 #[test]
241 fn update_version_files_skips_undetected() {
242 let dir = tempfile::tempdir().unwrap();
243 let cargo_toml = dir.path().join("Cargo.toml");
244 fs::write(&cargo_toml, "[dependencies]\nfoo = \"1\"\n").unwrap();
246
247 let results = update_version_files(dir.path(), "1.0.0", &[]).unwrap();
248 assert!(results.is_empty());
249 }
250
251 #[test]
252 fn update_version_files_updates_pyproject_toml() {
253 let dir = tempfile::tempdir().unwrap();
254 let pyproject = dir.path().join("pyproject.toml");
255 fs::write(
256 &pyproject,
257 r#"[project]
258name = "example"
259version = "0.1.0"
260requires-python = ">=3.8"
261"#,
262 )
263 .unwrap();
264
265 let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
266
267 assert_eq!(results.len(), 1);
268 assert_eq!(results[0].old_version, "0.1.0");
269 assert_eq!(results[0].new_version, "2.0.0");
270 assert_eq!(results[0].name, "pyproject.toml");
271 assert_eq!(results[0].path, pyproject);
272
273 let on_disk = fs::read_to_string(&pyproject).unwrap();
274 assert!(on_disk.contains("version = \"2.0.0\""));
275 }
276
277 #[test]
278 fn update_version_files_updates_pubspec_yaml() {
279 let dir = tempfile::tempdir().unwrap();
280 let pubspec = dir.path().join("pubspec.yaml");
281 fs::write(
282 &pubspec,
283 "name: my_app\nversion: 1.0.0\ndescription: test\n",
284 )
285 .unwrap();
286
287 let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
288
289 assert_eq!(results.len(), 1);
290 assert_eq!(results[0].old_version, "1.0.0");
291 assert_eq!(results[0].new_version, "2.0.0");
292 assert_eq!(results[0].name, "pubspec.yaml");
293
294 let on_disk = fs::read_to_string(&pubspec).unwrap();
295 assert!(on_disk.contains("version: 2.0.0"));
296 }
297
298 #[test]
299 fn update_version_files_updates_gradle_properties() {
300 let dir = tempfile::tempdir().unwrap();
301 let gradle = dir.path().join("gradle.properties");
302 fs::write(&gradle, "VERSION_NAME=1.0.0\nVERSION_CODE=10\n").unwrap();
303
304 let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
305
306 assert_eq!(results.len(), 1);
307 assert_eq!(results[0].old_version, "1.0.0");
308 assert_eq!(results[0].name, "gradle.properties");
309 assert_eq!(
310 results[0].extra,
311 Some("VERSION_CODE: 10 \u{2192} 11".to_string()),
312 );
313
314 let on_disk = fs::read_to_string(&gradle).unwrap();
315 assert!(on_disk.contains("VERSION_NAME=2.0.0"));
316 assert!(on_disk.contains("VERSION_CODE=11"));
317 }
318
319 #[test]
320 fn update_version_files_updates_version_file() {
321 let dir = tempfile::tempdir().unwrap();
322 let version = dir.path().join("VERSION");
323 fs::write(&version, "1.0.0\n").unwrap();
324
325 let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
326
327 assert_eq!(results.len(), 1);
328 assert_eq!(results[0].old_version, "1.0.0");
329 assert_eq!(results[0].name, "VERSION");
330
331 let on_disk = fs::read_to_string(&version).unwrap();
332 assert_eq!(on_disk, "2.0.0\n");
333 }
334
335 #[test]
336 fn update_version_files_updates_multiple_files() {
337 let dir = tempfile::tempdir().unwrap();
338 fs::write(
339 dir.path().join("Cargo.toml"),
340 "[package]\nname = \"x\"\nversion = \"1.0.0\"\n",
341 )
342 .unwrap();
343 fs::write(dir.path().join("pubspec.yaml"), "name: x\nversion: 1.0.0\n").unwrap();
344 fs::write(dir.path().join("VERSION"), "1.0.0\n").unwrap();
345
346 let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
347 assert_eq!(results.len(), 3);
348 }
349
350 #[test]
351 fn error_display() {
352 let err = VersionFileError::NoVersionField;
353 assert_eq!(err.to_string(), "no version field found");
354
355 let err = VersionFileError::FileNotFound(std::path::PathBuf::from("/tmp/gone"));
356 assert!(err.to_string().contains("/tmp/gone"));
357 }
358}