pub mod cargo;
pub mod changelog;
pub mod git;
pub mod version;
pub use cargo::{
get_workspace_version, read_cargo_toml, update_workspace_version, write_cargo_toml,
};
pub use changelog::update_changelog;
pub use git::perform_git_operations;
pub use version::determine_new_version;
use crate::cli::{BumpOpts, OutputFormat};
use crate::error::{CommandExitCode, Result};
use governor_core::domain::version::SemanticVersion;
use serde_json::json;
pub struct BumpService {
workspace_path: String,
opts: BumpOpts,
}
impl BumpService {
pub const fn new(workspace_path: String, opts: BumpOpts) -> Self {
Self {
workspace_path,
opts,
}
}
pub async fn execute(&self, _format: OutputFormat) -> Result<CommandExitCode> {
let start_time = std::time::Instant::now();
let (_content, mut value) = read_cargo_toml(&self.workspace_path)?;
let current_version_str = get_workspace_version(&value)?;
let current = SemanticVersion::parse(¤t_version_str).map_err(|e| {
crate::error::Error::Version(format!("Failed to parse current version: {e}"))
})?;
let new_version = self.determine_new_version(¤t).await?;
let version_str = new_version.to_string();
update_workspace_version(&mut value, &version_str)?;
let dry_run = Self::is_dry_run();
let cargo_lock_path = std::path::PathBuf::from(&self.workspace_path).join("Cargo.lock");
let has_cargo_lock = cargo_lock_path.exists();
if !dry_run {
write_cargo_toml(&self.workspace_path, &value)?;
if has_cargo_lock {
let lock_update_result = std::process::Command::new("cargo")
.args(["generate-lockfile"])
.current_dir(&self.workspace_path)
.output();
if let Err(e) = lock_update_result {
eprintln!("Warning: Failed to update Cargo.lock: {e}");
}
}
}
let changelog_path = std::path::PathBuf::from(&self.workspace_path).join("CHANGELOG.md");
let changelog_updated = if self.opts.no_changelog {
false
} else {
update_changelog(&changelog_path, &version_str, &self.workspace_path, dry_run)?
};
let (commit_hash, tag_created) = perform_git_operations(
&self.workspace_path,
&new_version,
&version_str,
&std::path::PathBuf::from(&self.workspace_path).join("Cargo.toml"),
&changelog_path,
&cargo_lock_path,
self.opts.no_commit,
self.opts.no_tag,
self.opts.commit_template.as_deref(),
self.opts.tag_template.as_deref(),
dry_run,
)
.await?;
let response = json!({
"success": true,
"command": "bump",
"workspace": self.workspace_path,
"result": {
"previous_version": current_version_str,
"new_version": version_str,
"files_modified": if dry_run { vec![] } else {
let mut files = vec!["Cargo.toml".to_string()];
if changelog_updated {
files.push("CHANGELOG.md".to_string());
}
if has_cargo_lock {
files.push("Cargo.lock".to_string());
}
files
},
"commit": {
"created": commit_hash.is_some(),
"hash": commit_hash,
},
"tag": {
"created": tag_created,
},
"dry_run": dry_run,
},
"metrics": {
"execution_time_ms": start_time.elapsed().as_millis(),
"git_operations": if dry_run { 0 } else { 2 },
"api_calls": 0,
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
Ok(CommandExitCode::Success)
}
async fn determine_new_version(&self, current: &SemanticVersion) -> Result<SemanticVersion> {
if self.opts.version.is_some() {
let ver_str = self.opts.version.as_ref().unwrap();
return SemanticVersion::parse(ver_str).map_err(|e| {
crate::error::Error::Version(format!("Failed to parse target version: {e}"))
});
}
determine_new_version(&self.workspace_path, None, current).await
}
fn is_dry_run() -> bool {
std::env::var("CARGO_GOVERNOR_DRY_RUN")
.or_else(|_| std::env::var("DRY_RUN"))
.is_ok()
}
}