use std::path::Path;
use anyhow::{
Context,
Result,
};
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
pub struct VersionInfoConfig {
#[serde(default)]
pub pre_bump_hooks: Vec<String>,
#[serde(default)]
pub post_bump_hooks: Vec<String>,
#[serde(default)]
pub additional_files: Vec<String>,
}
impl VersionInfoConfig {
pub fn from_package(package: &cargo_metadata::Package) -> Self {
package
.metadata
.get("version-info")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default()
}
}
pub fn run_hook(command: &str, version: &str, working_dir: &Path) -> Result<()> {
let expanded = command.replace("{{version}}", version);
#[cfg(unix)]
let status = std::process::Command::new("sh")
.args(["-c", &expanded])
.current_dir(working_dir)
.status()
.with_context(|| format!("Failed to run hook: {}", command))?;
#[cfg(windows)]
let status = std::process::Command::new("cmd")
.args(["/C", &expanded])
.current_dir(working_dir)
.status()
.with_context(|| format!("Failed to run hook: {}", command))?;
if !status.success() {
let exit_code = status
.code()
.map_or("unknown".to_string(), |c| c.to_string());
anyhow::bail!("Hook failed with exit code {}: {}", exit_code, command);
}
Ok(())
}
#[cfg(test)]
mod tests {
#[cfg(unix)]
use tempfile::TempDir;
use super::*;
#[test]
#[cfg(unix)]
fn test_run_hook_success() {
let dir = TempDir::new().unwrap();
run_hook("true", "1.0.0", dir.path()).unwrap();
}
#[test]
#[cfg(unix)]
fn test_run_hook_failure() {
let dir = TempDir::new().unwrap();
let result = run_hook("false", "1.0.0", dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("exit code"));
}
#[test]
#[cfg(unix)]
fn test_run_hook_version_substitution() {
let dir = TempDir::new().unwrap();
let output_file = dir.path().join("version.txt");
let command = format!("echo '{{{{version}}}}' > {}", output_file.display());
run_hook(&command, "2.3.4", dir.path()).unwrap();
let content = std::fs::read_to_string(&output_file).unwrap();
assert_eq!(content.trim(), "2.3.4");
}
#[test]
fn test_version_info_config_default() {
let config = VersionInfoConfig::default();
assert!(config.pre_bump_hooks.is_empty());
assert!(config.post_bump_hooks.is_empty());
assert!(config.additional_files.is_empty());
}
#[test]
fn test_version_info_config_deserialize() {
let json = serde_json::json!({
"pre_bump_hooks": ["./scripts/pre.sh {{version}}"],
"post_bump_hooks": ["./scripts/post.sh"],
"additional_files": ["package.json"]
});
let config: VersionInfoConfig = serde_json::from_value(json).unwrap();
assert_eq!(config.pre_bump_hooks.len(), 1);
assert_eq!(config.post_bump_hooks.len(), 1);
assert_eq!(config.additional_files.len(), 1);
assert!(config.pre_bump_hooks[0].contains("{{version}}"));
}
#[test]
fn test_version_info_config_partial() {
let json = serde_json::json!({
"pre_bump_hooks": ["./scripts/pre.sh"]
});
let config: VersionInfoConfig = serde_json::from_value(json).unwrap();
assert_eq!(config.pre_bump_hooks.len(), 1);
assert!(config.post_bump_hooks.is_empty());
assert!(config.additional_files.is_empty());
}
}