use std::path::PathBuf;
use anyhow::{
Context,
Result,
};
use cargo_plugin_utils::common::get_package_version_from_manifest;
use clap::Parser;
#[derive(Parser, Debug)]
pub struct ChangedArgs {
#[arg(long)]
manifest_path: Option<PathBuf>,
#[arg(long, default_value = ".")]
repo_path: PathBuf,
#[arg(long, default_value = "bool")]
format: String,
#[arg(long, env = "GITHUB_OUTPUT")]
github_output: Option<String>,
}
pub fn changed(args: ChangedArgs) -> 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)
.to_string();
let changed = cargo_version != latest_tag_version;
logger.finish();
match args.format.as_str() {
"bool" => println!("{}", changed),
"json" => println!(
"{{\"changed\":{},\"cargo_version\":\"{}\",\"latest_tag_version\":\"{}\"}}",
changed, cargo_version, latest_tag_version
),
"diff" => {
if changed {
println!(
"Version changed: {} -> {}",
latest_tag_version, cargo_version
);
} else {
println!("Version unchanged: {}", cargo_version);
}
}
"github-actions" => {
let output_file = args.github_output.as_deref().unwrap_or("/dev/stdout");
let output = format!(
"changed={}\nversion={}\nlatest_tag_version={}\n",
changed, cargo_version, latest_tag_version
);
std::fs::write(output_file, output)
.with_context(|| format!("Failed to write to {}", output_file))?;
}
_ => anyhow::bail!("Invalid format: {}", args.format),
}
Ok(())
}
#[cfg(test)]
mod tests {
use tempfile::NamedTempFile;
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]
fn test_changed_bool_format() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "0.1.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "bool".to_string(),
github_output: None,
};
let _ = changed(args);
}
#[test]
fn test_changed_json_format() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "1.0.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "json".to_string(),
github_output: None,
};
let _ = changed(args);
}
#[test]
fn test_changed_diff_format() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "2.0.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "diff".to_string(),
github_output: None,
};
let _ = changed(args);
}
#[test]
fn test_changed_github_actions_format() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "3.0.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let output_file = NamedTempFile::new().unwrap();
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "github-actions".to_string(),
github_output: Some(output_file.path().to_string_lossy().to_string()),
};
let result = changed(args);
if result.is_ok() {
let content = std::fs::read_to_string(output_file.path()).unwrap();
assert!(content.contains("changed="));
assert!(content.contains("version="));
assert!(content.contains("latest_tag_version="));
}
}
#[test]
fn test_changed_invalid_format() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
version = "1.0.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "invalid".to_string(),
github_output: None,
};
assert!(changed(args).is_err());
}
#[test]
fn test_changed_file_not_found() {
let args = ChangedArgs {
manifest_path: Some("/nonexistent/Cargo.toml".into()),
repo_path: ".".into(),
format: "bool".to_string(),
github_output: None,
};
assert!(changed(args).is_err());
}
#[test]
fn test_changed_no_version() {
let _dir = create_temp_cargo_project(
r#"
[package]
name = "test"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "bool".to_string(),
github_output: None,
};
assert!(changed(args).is_err());
}
#[test]
fn test_changed_workspace_version() {
let _dir = create_temp_cargo_project(
r#"
[workspace.package]
version = "0.5.0"
"#,
);
let manifest_path = _dir.path().join("Cargo.toml");
let args = ChangedArgs {
manifest_path: Some(manifest_path),
repo_path: ".".into(),
format: "bool".to_string(),
github_output: None,
};
let _ = changed(args);
}
}