pub mod cmd;
mod deno;
mod flutter;
mod gradle;
mod node;
mod plain;
mod project;
mod python;
mod rust;
use std::fs;
use std::path::{Path, PathBuf};
use standard_version::{
CustomVersionFile, DetectedFile, RegexVersionFile, UpdateResult, VersionFile,
};
use crate::ui;
pub trait Ecosystem {
fn name(&self) -> &'static str;
fn detect(&self, root: &Path) -> bool;
fn version_files(&self) -> &[&str];
fn write_version(&self, root: &Path, new_version: &str) -> WriteOutcome;
fn sync_lock(&self, root: &Path) -> Vec<SyncOutcome>;
fn lock_files(&self) -> &[&str] {
&[]
}
fn version_file_engine(&self) -> Option<Box<dyn VersionFile>> {
None
}
fn is_fallback(&self) -> bool {
false
}
}
pub enum WriteOutcome {
CliModified { files: Vec<PathBuf> },
Fallback { results: Vec<UpdateResult> },
NotDetected,
}
pub enum SyncOutcome {
Synced { lock_file: String },
ToolMissing {
lock_file: String,
tool: String,
hint: String,
},
Failed { lock_file: String, exit_code: i32 },
NoLockFile,
}
pub struct BumpResult {
pub update_results: Vec<UpdateResult>,
pub modified_paths: Vec<PathBuf>,
pub synced_locks: Vec<String>,
}
pub fn native_write(root: &Path, engine: &dyn VersionFile, new_version: &str) -> WriteOutcome {
let mut results = Vec::new();
for filename in engine.filenames() {
let path = root.join(filename);
if !path.exists() {
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
ui::warning(&format!("{}: {e}", path.display()));
continue;
}
};
if !engine.detect(&content) {
continue;
}
let old_version = match engine.read_version(&content) {
Some(v) => v,
None => continue,
};
let updated = match engine.write_version(&content, new_version) {
Ok(u) => u,
Err(e) => {
ui::warning(&format!("{}: {e}", path.display()));
continue;
}
};
let extra = engine.extra_info(&content, &updated);
let actual_new_version = engine
.read_version(&updated)
.unwrap_or_else(|| new_version.to_string());
if fs::write(&path, &updated).is_err() {
ui::warning(&format!("{}: failed to write file", path.display()));
continue;
}
results.push(UpdateResult {
path,
name: engine.name().to_string(),
old_version,
new_version: actual_new_version,
extra,
});
}
if results.is_empty() {
WriteOutcome::NotDetected
} else {
WriteOutcome::Fallback { results }
}
}
pub fn try_sync(root: &Path, lock_file: &str, tool: &str, args: &[&str]) -> SyncOutcome {
if !root.join(lock_file).exists() {
return SyncOutcome::NoLockFile;
}
let hint = format!("{tool} {}", args.join(" "));
match cmd::run_tool(root, tool, args) {
Err(_) => SyncOutcome::ToolMissing {
lock_file: lock_file.to_string(),
tool: tool.to_string(),
hint,
},
Ok(status) if status.success() => SyncOutcome::Synced {
lock_file: lock_file.to_string(),
},
Ok(status) => SyncOutcome::Failed {
lock_file: lock_file.to_string(),
exit_code: status.code().unwrap_or(-1),
},
}
}
fn all_ecosystems() -> Vec<Box<dyn Ecosystem>> {
vec![
Box::new(rust::Rust),
Box::new(node::Node),
Box::new(deno::Deno),
Box::new(python::Python),
Box::new(flutter::Flutter),
Box::new(gradle::Gradle),
Box::new(project::Project),
Box::new(plain::Plain),
]
}
pub fn run_bump(root: &Path, new_version: &str, custom_files: &[CustomVersionFile]) -> BumpResult {
let ecosystems = all_ecosystems();
let mut update_results: Vec<UpdateResult> = Vec::new();
let mut modified_paths: Vec<PathBuf> = Vec::new();
let mut synced_locks: Vec<String> = Vec::new();
let mut any_specific_updated = false;
for eco in &ecosystems {
if eco.is_fallback() && any_specific_updated {
continue;
}
if !eco.detect(root) {
continue;
}
let version_updated = match eco.write_version(root, new_version) {
WriteOutcome::CliModified { files } => {
modified_paths.extend(files);
true
}
WriteOutcome::Fallback { results } => {
for r in &results {
modified_paths.push(r.path.clone());
}
let did_update = !results.is_empty();
update_results.extend(results);
did_update
}
WriteOutcome::NotDetected => false,
};
if version_updated && !eco.is_fallback() {
any_specific_updated = true;
}
if !version_updated {
continue;
}
for outcome in eco.sync_lock(root) {
match outcome {
SyncOutcome::Synced { lock_file } => {
ui::item("Synced:", &lock_file);
synced_locks.push(lock_file);
}
SyncOutcome::ToolMissing {
lock_file, hint, ..
} => {
ui::warning(&format!("{lock_file} not synced \u{2014} run '{hint}'"));
}
SyncOutcome::Failed {
lock_file,
exit_code,
} => {
ui::warning(&format!("{lock_file} sync failed (exit {exit_code})"));
}
SyncOutcome::NoLockFile => {}
}
}
}
let custom_count_before = update_results.len();
process_custom_files(
root,
new_version,
custom_files,
&mut update_results,
&mut modified_paths,
);
let custom_updated = update_results.len() > custom_count_before;
if custom_updated {
for eco in &ecosystems {
if !eco.detect(root) {
continue;
}
if eco
.lock_files()
.iter()
.any(|lf| synced_locks.contains(&lf.to_string()))
{
continue;
}
for outcome in eco.sync_lock(root) {
match outcome {
SyncOutcome::Synced { lock_file } => {
ui::item("Synced:", &lock_file);
synced_locks.push(lock_file);
}
SyncOutcome::ToolMissing {
lock_file, hint, ..
} => {
ui::warning(&format!("{lock_file} not synced \u{2014} run '{hint}'"));
}
SyncOutcome::Failed {
lock_file,
exit_code,
} => {
ui::warning(&format!("{lock_file} sync failed (exit {exit_code})"));
}
SyncOutcome::NoLockFile => {}
}
}
}
}
BumpResult {
update_results,
modified_paths,
synced_locks,
}
}
pub fn dry_run_lock_sync(root: &Path) {
let ecosystems = all_ecosystems();
for eco in &ecosystems {
if !eco.detect(root) {
continue;
}
for lock_file in eco.lock_files() {
if root.join(lock_file).exists() {
ui::item("Would sync:", lock_file);
}
}
}
}
pub fn dry_run_version_files(root: &Path, custom_files: &[CustomVersionFile]) -> Vec<DetectedFile> {
let ecosystems = all_ecosystems();
let mut results: Vec<DetectedFile> = Vec::new();
let mut any_specific_detected = false;
for eco in &ecosystems {
if eco.is_fallback() && any_specific_detected {
continue;
}
if !eco.detect(root) {
continue;
}
let Some(engine) = eco.version_file_engine() else {
continue;
};
let before = results.len();
for filename in engine.filenames() {
let path = root.join(filename);
if !path.exists() {
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
if !engine.detect(&content) {
continue;
}
let Some(old_version) = engine.read_version(&content) else {
continue;
};
results.push(DetectedFile {
path,
name: engine.name().to_string(),
old_version,
});
}
if !eco.is_fallback() && results.len() > before {
any_specific_detected = true;
}
}
for cf in custom_files {
let engine = match RegexVersionFile::new(cf) {
Ok(e) => e,
Err(_) => continue,
};
let path = root.join(engine.path());
if !path.exists() {
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
if !engine.detect(&content) {
continue;
}
let Some(old_version) = engine.read_version(&content) else {
continue;
};
results.push(DetectedFile {
path,
name: engine.name(),
old_version,
});
}
results
}
pub fn dry_run_lock_file_names(root: &Path) -> Vec<String> {
let ecosystems = all_ecosystems();
let mut names = Vec::new();
for eco in &ecosystems {
if !eco.detect(root) {
continue;
}
for lock_file in eco.lock_files() {
if root.join(lock_file).exists() {
names.push(lock_file.to_string());
}
}
}
names
}
fn process_custom_files(
root: &Path,
new_version: &str,
custom_files: &[CustomVersionFile],
results: &mut Vec<UpdateResult>,
paths: &mut Vec<PathBuf>,
) {
for cf in custom_files {
let engine = match RegexVersionFile::new(cf) {
Ok(e) => e,
Err(e) => {
ui::warning(&format!("invalid custom version file regex: {e}"));
continue;
}
};
let path = root.join(engine.path());
if !path.exists() {
ui::warning(&format!("{}: file not found", path.display()));
continue;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
ui::warning(&format!("{}: {e}", path.display()));
continue;
}
};
if !engine.detect(&content) {
ui::warning(&format!(
"{}: pattern did not match file content",
path.display()
));
continue;
}
let old_version = match engine.read_version(&content) {
Some(v) => v,
None => {
ui::warning(&format!(
"{}: could not extract version from matched content",
path.display()
));
continue;
}
};
let updated = match engine.write_version(&content, new_version) {
Ok(u) => u,
Err(e) => {
ui::warning(&format!("{}: {e}", path.display()));
continue;
}
};
let actual_new_version = engine
.read_version(&updated)
.unwrap_or_else(|| new_version.to_string());
if fs::write(&path, &updated).is_err() {
ui::warning(&format!("{}: failed to write file", path.display()));
continue;
}
paths.push(path.clone());
results.push(UpdateResult {
path,
name: engine.name(),
old_version,
new_version: actual_new_version,
extra: None,
});
}
}