use std::fs;
use std::path::Path;
use crate::cargo::CargoVersionFile;
use crate::gradle::GradleVersionFile;
use crate::json::{DenoVersionFile, JsonVersionFile};
use crate::project::{ProjectJsonVersionFile, ProjectTomlVersionFile, ProjectYamlVersionFile};
use crate::pubspec::PubspecVersionFile;
use crate::pyproject::PyprojectVersionFile;
use crate::regex_engine::RegexVersionFile;
use crate::version_file::{
CustomVersionFile, DetectedFile, UpdateResult, VersionFile, VersionFileError,
};
use crate::version_plain::PlainVersionFile;
fn builtin_engines() -> Vec<Box<dyn VersionFile>> {
vec![
Box::new(CargoVersionFile),
Box::new(PyprojectVersionFile),
Box::new(JsonVersionFile),
Box::new(DenoVersionFile),
Box::new(PubspecVersionFile),
Box::new(GradleVersionFile),
Box::new(ProjectTomlVersionFile),
Box::new(ProjectJsonVersionFile),
Box::new(ProjectYamlVersionFile),
Box::new(PlainVersionFile),
]
}
pub fn update_version_files(
root: &Path,
new_version: &str,
custom_files: &[CustomVersionFile],
) -> Result<Vec<UpdateResult>, VersionFileError> {
let custom_engines: Vec<RegexVersionFile> = custom_files
.iter()
.map(RegexVersionFile::new)
.collect::<Result<Vec<_>, _>>()?;
let engines = builtin_engines();
let mut results = Vec::new();
for engine in &engines {
for filename in engine.filenames() {
let path = root.join(filename);
if !path.exists() {
continue;
}
let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
if !engine.detect(&content) {
continue;
}
let old_version = match engine.read_version(&content) {
Some(v) => v,
None => continue,
};
let updated = engine.write_version(&content, new_version)?;
let extra = engine.extra_info(&content, &updated);
let actual_new_version = engine
.read_version(&updated)
.unwrap_or_else(|| new_version.to_string());
fs::write(&path, &updated).map_err(VersionFileError::WriteFailed)?;
results.push(UpdateResult {
path,
name: engine.name().to_string(),
old_version,
new_version: actual_new_version,
extra,
});
}
}
for engine in &custom_engines {
let path = root.join(engine.path());
if !path.exists() {
continue;
}
let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
if !engine.detect(&content) {
continue;
}
let old_version = match engine.read_version(&content) {
Some(v) => v,
None => continue,
};
let updated = engine.write_version(&content, new_version)?;
let actual_new_version = engine
.read_version(&updated)
.unwrap_or_else(|| new_version.to_string());
fs::write(&path, &updated).map_err(VersionFileError::WriteFailed)?;
results.push(UpdateResult {
path,
name: engine.name(),
old_version,
new_version: actual_new_version,
extra: None,
});
}
Ok(results)
}
pub fn detect_version_files(
root: &Path,
custom_files: &[CustomVersionFile],
) -> Result<Vec<DetectedFile>, VersionFileError> {
let custom_engines: Vec<RegexVersionFile> = custom_files
.iter()
.map(RegexVersionFile::new)
.collect::<Result<Vec<_>, _>>()?;
let engines = builtin_engines();
let mut results = Vec::new();
for engine in &engines {
for filename in engine.filenames() {
let path = root.join(filename);
if !path.exists() {
continue;
}
let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
if !engine.detect(&content) {
continue;
}
let old_version = match engine.read_version(&content) {
Some(v) => v,
None => continue,
};
results.push(DetectedFile {
path,
name: engine.name().to_string(),
old_version,
});
}
}
for engine in &custom_engines {
let path = root.join(engine.path());
if !path.exists() {
continue;
}
let content = fs::read_to_string(&path).map_err(VersionFileError::ReadFailed)?;
if !engine.detect(&content) {
continue;
}
let old_version = match engine.read_version(&content) {
Some(v) => v,
None => continue,
};
results.push(DetectedFile {
path,
name: engine.name(),
old_version,
});
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn update_version_files_updates_cargo_toml() {
let dir = tempfile::tempdir().unwrap();
let cargo_toml = dir.path().join("Cargo.toml");
fs::write(
&cargo_toml,
r#"[package]
name = "example"
version = "0.1.0"
edition = "2024"
"#,
)
.unwrap();
let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].old_version, "0.1.0");
assert_eq!(results[0].new_version, "2.0.0");
assert_eq!(results[0].name, "Cargo.toml");
assert_eq!(results[0].path, cargo_toml);
let on_disk = fs::read_to_string(&cargo_toml).unwrap();
assert!(on_disk.contains("version = \"2.0.0\""));
}
#[test]
fn update_version_files_skips_missing_file() {
let dir = tempfile::tempdir().unwrap();
let results = update_version_files(dir.path(), "1.0.0", &[]).unwrap();
assert!(results.is_empty());
}
#[test]
fn update_version_files_skips_undetected() {
let dir = tempfile::tempdir().unwrap();
let cargo_toml = dir.path().join("Cargo.toml");
fs::write(&cargo_toml, "[dependencies]\nfoo = \"1\"\n").unwrap();
let results = update_version_files(dir.path(), "1.0.0", &[]).unwrap();
assert!(results.is_empty());
}
#[test]
fn update_version_files_updates_pyproject_toml() {
let dir = tempfile::tempdir().unwrap();
let pyproject = dir.path().join("pyproject.toml");
fs::write(
&pyproject,
r#"[project]
name = "example"
version = "0.1.0"
requires-python = ">=3.8"
"#,
)
.unwrap();
let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].old_version, "0.1.0");
assert_eq!(results[0].new_version, "2.0.0");
assert_eq!(results[0].name, "pyproject.toml");
assert_eq!(results[0].path, pyproject);
let on_disk = fs::read_to_string(&pyproject).unwrap();
assert!(on_disk.contains("version = \"2.0.0\""));
}
#[test]
fn update_version_files_updates_pubspec_yaml() {
let dir = tempfile::tempdir().unwrap();
let pubspec = dir.path().join("pubspec.yaml");
fs::write(
&pubspec,
"name: my_app\nversion: 1.0.0\ndescription: test\n",
)
.unwrap();
let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].old_version, "1.0.0");
assert_eq!(results[0].new_version, "2.0.0");
assert_eq!(results[0].name, "pubspec.yaml");
let on_disk = fs::read_to_string(&pubspec).unwrap();
assert!(on_disk.contains("version: 2.0.0"));
}
#[test]
fn update_version_files_updates_gradle_properties() {
let dir = tempfile::tempdir().unwrap();
let gradle = dir.path().join("gradle.properties");
fs::write(&gradle, "VERSION_NAME=1.0.0\nVERSION_CODE=10\n").unwrap();
let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].old_version, "1.0.0");
assert_eq!(results[0].name, "gradle.properties");
assert_eq!(
results[0].extra,
Some("VERSION_CODE: 10 \u{2192} 11".to_string()),
);
let on_disk = fs::read_to_string(&gradle).unwrap();
assert!(on_disk.contains("VERSION_NAME=2.0.0"));
assert!(on_disk.contains("VERSION_CODE=11"));
}
#[test]
fn update_version_files_updates_version_file() {
let dir = tempfile::tempdir().unwrap();
let version = dir.path().join("VERSION");
fs::write(&version, "1.0.0\n").unwrap();
let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].old_version, "1.0.0");
assert_eq!(results[0].name, "VERSION");
let on_disk = fs::read_to_string(&version).unwrap();
assert_eq!(on_disk, "2.0.0\n");
}
#[test]
fn update_version_files_updates_multiple_files() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"x\"\nversion = \"1.0.0\"\n",
)
.unwrap();
fs::write(dir.path().join("pubspec.yaml"), "name: x\nversion: 1.0.0\n").unwrap();
fs::write(dir.path().join("VERSION"), "1.0.0\n").unwrap();
let results = update_version_files(dir.path(), "2.0.0", &[]).unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn error_display() {
let err = VersionFileError::NoVersionField;
assert_eq!(err.to_string(), "no version field found");
let err = VersionFileError::FileNotFound(std::path::PathBuf::from("/tmp/gone"));
assert!(err.to_string().contains("/tmp/gone"));
}
}