use std::path::PathBuf;
use anyhow::{
Context,
Result,
};
use cargo_plugin_utils::common::get_package_version_from_manifest;
use clap::Parser;
use crate::version::parse_version;
#[derive(Parser, Debug)]
pub struct PreBumpHookArgs {
#[arg(long)]
manifest_path: Option<PathBuf>,
#[arg(long, default_value = ".")]
repo_path: PathBuf,
#[arg(long, env = "COG_VERSION")]
target_version: Option<String>,
#[arg(long, env = "COG_LATEST")]
current_version: Option<String>,
#[arg(long, default_value = "true")]
exit_on_error: bool,
}
pub fn pre_bump_hook(args: PreBumpHookArgs) -> Result<()> {
let mut logger = cargo_plugin_utils::logger::Logger::new();
logger.status("Reading", "package version");
let manifest_path = args
.manifest_path
.as_deref()
.unwrap_or_else(|| std::path::Path::new("./Cargo.toml"));
let cargo_version = get_package_version_from_manifest(manifest_path)
.with_context(|| format!("Failed to get version from {}", manifest_path.display()))?;
logger.status("Checking", "git tags");
let latest_tag = gix::discover(&args.repo_path)
.ok()
.and_then(|repo| {
repo.references()
.ok()?
.all()
.ok()?
.filter_map(|reference| {
let Ok(reference) = reference else {
return None;
};
let name = reference.name().as_bstr().to_string();
name.strip_prefix("refs/tags/").map(|tag| {
let tag_oid = reference.id();
(tag.to_string(), tag_oid)
})
})
.filter_map(|(tag_name, tag_oid)| {
let commit = repo.find_object(tag_oid).ok()?.try_into_commit().ok()?;
Some((tag_name, commit.id))
})
.max_by_key(|(_, commit_id)| {
Some(*commit_id)
})
.map(|(tag_name, _)| tag_name)
})
.unwrap_or_else(|| "v0.0.0".to_string());
let latest_tag_version = latest_tag
.strip_prefix('v')
.or_else(|| latest_tag.strip_prefix('V'))
.unwrap_or(&latest_tag)
.trim()
.to_string();
logger.finish();
if latest_tag_version != "0.0.0" && cargo_version != latest_tag_version {
eprintln!(
"⚠️ Warning: Cargo.toml version ({}) doesn't match latest git tag ({})",
cargo_version, latest_tag_version
);
if args.exit_on_error {
anyhow::bail!(
"Version mismatch detected. Sync Cargo.toml with git tags before bumping."
);
}
}
if let Some(target) = &args.target_version {
let target_trimmed = target.trim();
if let Ok((target_major, _, _)) = parse_version(target_trimmed)
&& let Ok((current_major, current_minor, current_patch)) = parse_version(&cargo_version)
{
if current_major == 0 && current_minor == 0 && current_patch == 0 && target_major == 1 {
eprintln!(
"⚠️ Warning: Major version bump from 0.0.0 to {}",
target_trimmed
);
eprintln!(" This will change the placeholder version. Continue?");
}
}
}
logger.print_message("✓ Pre-bump checks passed");
logger.print_message(&format!(" Current version: {}", cargo_version));
if let Some(target) = &args.target_version {
logger.print_message(&format!(" Target version: {}", target.trim()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn create_temp_cargo_project(content: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("Cargo.toml");
std::fs::write(&manifest_path, content).unwrap();
dir
}
#[test]
#[serial_test::serial]
fn test_pre_bump_hook_success() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "0.1.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = PreBumpHookArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
target_version: Some("0.1.1".to_string()),
current_version: None,
exit_on_error: true,
};
let _ = pre_bump_hook(args);
}
#[test]
#[serial_test::serial]
fn test_pre_bump_hook_major_bump_warning() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "0.0.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = PreBumpHookArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
target_version: Some("1.0.0".to_string()),
current_version: None,
exit_on_error: false, };
let result = pre_bump_hook(args);
let _ = result;
}
#[test]
#[serial_test::serial]
fn test_pre_bump_hook_no_target_version() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "0.2.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = PreBumpHookArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
target_version: None,
current_version: None,
exit_on_error: true,
};
let _ = pre_bump_hook(args);
}
#[test]
#[serial_test::serial]
fn test_pre_bump_hook_file_not_found() {
let args = PreBumpHookArgs {
manifest_path: Some("/nonexistent/Cargo.toml".into()),
repo_path: ".".into(),
target_version: None,
current_version: None,
exit_on_error: true,
};
assert!(pre_bump_hook(args).is_err());
}
#[test]
#[serial_test::serial]
fn test_pre_bump_hook_no_version() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = PreBumpHookArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
target_version: None,
current_version: None,
exit_on_error: true,
};
assert!(pre_bump_hook(args).is_err());
}
#[test]
#[serial_test::serial]
fn test_pre_bump_hook_workspace_version() {
let _dir = create_temp_cargo_project(
r#"
[workspace.package]
version = "1.0.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = PreBumpHookArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
target_version: Some("1.0.1".to_string()),
current_version: None,
exit_on_error: true,
};
let _ = pre_bump_hook(args);
}
}