pub mod args;
pub mod commit;
pub mod diff;
pub mod hooks;
pub mod index;
pub mod readme_update;
pub mod signing;
pub mod tree;
pub mod version_update;
#[cfg(test)]
mod tests;
use anyhow::{
Context,
Result,
};
pub use args::BumpArgs;
use cargo_plugin_utils::common::{
find_package,
get_owner_repo,
};
use crate::github;
use crate::version::{
format_version,
increment_major,
increment_minor,
increment_patch,
parse_version,
};
pub fn bump(args: BumpArgs) -> Result<()> {
use commit::{
AdditionalFile,
FileType,
};
let mut logger = cargo_plugin_utils::logger::Logger::new();
logger.status("Reading", "current version");
let package = find_package(args.manifest_path.as_deref())?;
let current_version = package.version.to_string();
let package_name = package.name.clone();
let hook_config = hooks::VersionInfoConfig::from_package(&package);
logger.finish();
logger.status("Calculating", "target version");
let target_version = calculate_target_version(&args, ¤t_version)?;
logger.finish();
if current_version == target_version {
anyhow::bail!(
"Current version ({}) is already the target version. Nothing to bump.",
current_version
);
}
logger.print_message(&format!(
"Bumping version: {} -> {}",
current_version, target_version
));
logger.status("Updating", "Cargo.toml");
let manifest_path = args
.manifest_path
.as_deref()
.unwrap_or_else(|| std::path::Path::new("./Cargo.toml"));
version_update::update_cargo_toml_version(manifest_path, ¤t_version, &target_version)?;
logger.finish();
let manifest_dir = manifest_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let cargo_lock_path = manifest_dir.join("Cargo.lock");
let cargo_lock_head_content = if !args.no_lock && cargo_lock_path.exists() {
get_file_head_content(manifest_path, &cargo_lock_path).ok()
} else {
None
};
if !args.no_lock {
logger.status("Updating", "Cargo.lock");
let status = std::process::Command::new("cargo")
.args(["update", "--workspace"])
.current_dir(manifest_dir)
.status()
.context("Failed to run cargo update")?;
if !status.success() {
anyhow::bail!("cargo update --workspace failed");
}
logger.finish();
}
let readme_path = manifest_dir.join("README.md");
let readme_head_content = if !args.no_readme && readme_path.exists() {
get_file_head_content(manifest_path, &readme_path).ok()
} else {
None
};
let readme_update = if !args.no_readme {
logger.status("Checking", "README.md");
let result = readme_update::update_readme_file(
&readme_path,
&package_name,
¤t_version,
&target_version,
)?;
logger.finish();
if let Some(ref update) = result
&& update.modified
{
std::fs::write(&readme_path, &update.content)
.with_context(|| format!("Failed to write {}", readme_path.display()))?;
logger.print_message(" Updated version in README.md");
}
result
} else {
None
};
for hook in &hook_config.pre_bump_hooks {
logger.status("Running", &format!("hook: {}", hook));
hooks::run_hook(hook, &target_version, manifest_dir)?;
logger.finish();
}
if !args.no_commit {
logger.status("Committing", "version changes");
let mut additional_files: Vec<AdditionalFile> = Vec::new();
if !args.no_lock && cargo_lock_path.exists() {
let cargo_lock_content = std::fs::read_to_string(&cargo_lock_path)
.with_context(|| format!("Failed to read {}", cargo_lock_path.display()))?;
additional_files.push(AdditionalFile {
path: cargo_lock_path,
working_content: cargo_lock_content,
head_content: cargo_lock_head_content,
file_type: FileType::CargoLock,
});
}
if let Some(update) = readme_update
&& update.modified
{
additional_files.push(AdditionalFile {
path: readme_path,
working_content: update.content,
head_content: readme_head_content,
file_type: FileType::Readme,
});
}
for file_path in &hook_config.additional_files {
let path = manifest_dir.join(file_path);
if path.exists() {
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
let head_content = get_file_head_content(manifest_path, &path).ok();
additional_files.push(AdditionalFile {
path,
working_content: content,
head_content,
file_type: FileType::Other,
});
} else {
logger.print_message(&format!(
"⚠️ Additional file not found: {}",
path.display()
));
}
}
commit::commit_version_changes_with_files(
manifest_path,
&package_name,
¤t_version,
&target_version,
&additional_files,
)?;
logger.finish();
let file_count = additional_files.len() + 1; logger.print_message(&format!(
"✓ Committed version bump: {} -> {} ({} file{})",
current_version,
target_version,
file_count,
if file_count == 1 { "" } else { "s" }
));
for hook in &hook_config.post_bump_hooks {
logger.status("Running", &format!("hook: {}", hook));
hooks::run_hook(hook, &target_version, manifest_dir)?;
logger.finish();
}
} else {
logger.print_message(&format!(
"✓ Updated version to {} (not committed)",
target_version
));
}
Ok(())
}
fn calculate_target_version(args: &BumpArgs, current_version: &str) -> Result<String> {
if let Some(version) = &args.version {
Ok(version.trim().to_string())
} else if args.auto {
let (owner, repo) = get_owner_repo(args.owner.clone(), args.repo.clone())?;
let github_token = args.github_token.as_deref();
let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
let (_latest, next) =
rt.block_on(github::calculate_next_version(&owner, &repo, github_token))?;
Ok(next)
} else {
let (major, minor, patch) = parse_version(current_version)?;
let (new_major, new_minor, new_patch) = if args.major {
increment_major(major, minor, patch)
} else if args.minor {
increment_minor(major, minor, patch)
} else if args.patch {
increment_patch(major, minor, patch)
} else {
increment_patch(major, minor, patch)
};
Ok(format_version(new_major, new_minor, new_patch))
}
}
fn get_file_head_content(
manifest_path: &std::path::Path,
file_path: &std::path::Path,
) -> Result<String> {
use bstr::ByteSlice;
let repo = gix::discover(
manifest_path
.parent()
.unwrap_or_else(|| std::path::Path::new(".")),
)
.context("Not in a git repository")?;
let repo_path = repo.path().parent().context("Invalid repository path")?;
let relative_path = file_path
.strip_prefix(repo_path)
.or_else(|_| file_path.strip_prefix("."))
.unwrap_or(file_path);
let head = repo.head().context("Failed to read HEAD")?;
let head_commit_id = head.id().context("HEAD does not point to a commit")?;
let head_commit = repo
.find_object(head_commit_id)
.context("Failed to find HEAD commit")?
.try_into_commit()
.context("HEAD is not a commit")?;
let head_tree = head_commit.tree().context("Failed to get HEAD tree")?;
let entry = head_tree
.lookup_entry_by_path(relative_path)
.context("Failed to lookup file in HEAD tree")?
.with_context(|| format!("File {} does not exist in HEAD", relative_path.display()))?;
let blob = entry
.object()
.context("Failed to get blob from tree entry")?
.try_into_blob()
.context("Tree entry is not a blob")?;
Ok(blob.data.to_str_lossy().into_owned())
}