feat: enhance version function with automatic version increment (#227)

This commit is contained in:
weisd
2025-07-16 18:09:43 +08:00
committed by GitHub
parent e5d17f5382
commit 749f55d688
3 changed files with 377 additions and 109 deletions
+4 -4
View File
@@ -199,7 +199,7 @@ async fn run(opt: config::Opt) -> Result<()> {
if result.update_available {
if let Some(latest) = &result.latest_version {
info!(
"🚀 New version available: {} -> {} (current: {})",
"🚀 Version check: New version available: {} -> {} (current: {})",
result.current_version, latest.version, result.current_version
);
if let Some(notes) = &latest.release_notes {
@@ -210,14 +210,14 @@ async fn run(opt: config::Opt) -> Result<()> {
}
}
} else {
debug!("✅ Current version is up to date: {}", result.current_version);
debug!(" Version check: Current version is up to date: {}", result.current_version);
}
}
Err(UpdateCheckError::HttpError(e)) => {
debug!("Version check network error (this is normal): {}", e);
debug!("Version check: network error (this is normal): {}", e);
}
Err(e) => {
debug!("Version check failed (this is normal): {}", e);
debug!("Version check: failed (this is normal): {}", e);
}
}
});
+22 -103
View File
@@ -15,7 +15,7 @@
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
use tracing::{debug, error, info, warn};
use tracing::{debug, error, info};
use crate::version;
@@ -86,7 +86,7 @@ impl VersionChecker {
Self {
client,
version_url: "https://version.rustfs.com".to_string(),
version_url: "https://version.rustfs.com/latest.json".to_string(),
timeout: Duration::from_secs(10),
}
}
@@ -139,8 +139,9 @@ impl VersionChecker {
debug!("Retrieved latest version information: {:?}", version_info);
// Compare versions
let update_available = self.is_newer_version(&current_version, &version_info.version)?;
// Compare versions using version.rs functions
let update_available = version::is_newer_version(&current_version, &version_info.version)
.map_err(|e| UpdateCheckError::VersionParseError(e.to_string()))?;
let result = UpdateCheckResult {
update_available,
@@ -161,74 +162,6 @@ impl VersionChecker {
Ok(result)
}
/// Compare version numbers to determine if there's an update
fn is_newer_version(&self, current: &str, latest: &str) -> Result<bool, UpdateCheckError> {
// Clean version numbers, remove prefixes like "v", "RELEASE.", etc.
let current_clean = self.clean_version(current);
let latest_clean = self.clean_version(latest);
debug!("Version comparison: current='{}' vs latest='{}'", current_clean, latest_clean);
// If versions are the same, no update is needed
if current_clean == latest_clean {
return Ok(false);
}
// Try semantic version comparison
match self.compare_semantic_versions(&current_clean, &latest_clean) {
Ok(is_newer) => Ok(is_newer),
Err(_) => {
// If semantic version comparison fails, use string comparison
warn!("Semantic version comparison failed, using string comparison");
Ok(latest_clean > current_clean)
}
}
}
/// Clean version string
fn clean_version(&self, version: &str) -> String {
version
.trim()
.trim_start_matches('v')
.trim_start_matches("RELEASE.")
.trim_start_matches('@')
.to_string()
}
/// Semantic version comparison
fn compare_semantic_versions(&self, current: &str, latest: &str) -> Result<bool, UpdateCheckError> {
let current_parts = self.parse_version_parts(current)?;
let latest_parts = self.parse_version_parts(latest)?;
// Use tuple comparison for lexicographic ordering
Ok(latest_parts > current_parts)
}
/// Parse version parts (major, minor, patch)
fn parse_version_parts(&self, version: &str) -> Result<(u32, u32, u32), UpdateCheckError> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() < 3 {
return Err(UpdateCheckError::VersionParseError(format!("Invalid version format: {version}")));
}
let major = parts[0]
.parse::<u32>()
.map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse major version: {}", parts[0])))?;
let minor = parts[1]
.parse::<u32>()
.map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse minor version: {}", parts[1])))?;
// Patch version may contain other characters, only take numeric part
let patch_str = parts[2].chars().take_while(|c| c.is_numeric()).collect::<String>();
let patch = patch_str
.parse::<u32>()
.map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse patch version: {}", parts[2])))?;
Ok((major, minor, patch))
}
}
/// Get current version number
@@ -253,37 +186,6 @@ pub async fn check_updates_with_url(url: String) -> Result<UpdateCheckResult, Up
mod tests {
use super::*;
#[test]
fn test_clean_version() {
let checker = VersionChecker::new();
assert_eq!(checker.clean_version("v1.0.0"), "1.0.0");
assert_eq!(checker.clean_version("RELEASE.1.0.0"), "1.0.0");
assert_eq!(checker.clean_version("@1.0.0"), "1.0.0");
assert_eq!(checker.clean_version("1.0.0"), "1.0.0");
}
#[test]
fn test_parse_version_parts() {
let checker = VersionChecker::new();
assert_eq!(checker.parse_version_parts("1.0.0").unwrap(), (1, 0, 0));
assert_eq!(checker.parse_version_parts("2.1.3").unwrap(), (2, 1, 3));
assert_eq!(checker.parse_version_parts("1.0.0-beta").unwrap(), (1, 0, 0));
}
#[test]
fn test_version_comparison() {
let checker = VersionChecker::new();
// Test semantic version comparison
assert!(checker.is_newer_version("1.0.0", "1.0.1").unwrap());
assert!(checker.is_newer_version("1.0.0", "1.1.0").unwrap());
assert!(checker.is_newer_version("1.0.0", "2.0.0").unwrap());
assert!(!checker.is_newer_version("1.0.1", "1.0.0").unwrap());
assert!(!checker.is_newer_version("1.0.0", "1.0.0").unwrap());
}
#[tokio::test]
async fn test_get_current_version() {
let version = get_current_version();
@@ -428,4 +330,21 @@ mod tests {
println!("✅ VersionInfo tests passed");
}
#[test]
fn test_version_functions_integration() {
// Test that version functions from version.rs work correctly
assert_eq!(version::clean_version("refs/tags/1.0.0-alpha.17"), "1.0.0-alpha.17");
assert_eq!(version::clean_version("v1.0.0"), "1.0.0");
// Test version comparison
assert!(version::is_newer_version("1.0.0", "1.0.1").unwrap());
assert!(!version::is_newer_version("1.0.1", "1.0.0").unwrap());
// Test version parsing using parse_version
assert_eq!(version::parse_version("1.0.0").unwrap(), (1, 0, 0, None));
assert_eq!(version::parse_version("2.1.3-alpha.1").unwrap(), (2, 1, 3, Some("alpha.1".to_string())));
println!("✅ Version functions integration tests passed");
}
}
+351 -2
View File
@@ -1,13 +1,362 @@
use shadow_rs::shadow;
use std::process::Command;
shadow!(build);
type VersionParseResult = Result<(u32, u32, u32, Option<String>), Box<dyn std::error::Error>>;
#[allow(clippy::const_is_empty)]
pub fn get_version() -> String {
// 获取最新的 tag
if let Ok(latest_tag) = get_latest_tag() {
// 检查当前 commit 是否比最新 tag 更新
if is_head_newer_than_tag(&latest_tag) {
// 如果当前 commit 更新,则提升版本号
if let Ok(new_version) = increment_version(&latest_tag) {
return format!("refs/tags/{new_version}");
}
}
// 如果当前 commit 就是最新 tag,或者版本提升失败,返回当前 tag
return format!("refs/tags/{latest_tag}");
}
// 如果没有 tag,使用原来的逻辑
if !build::TAG.is_empty() {
build::TAG.to_string()
format!("refs/tags/{}", build::TAG)
} else if !build::SHORT_COMMIT.is_empty() {
format!("@{}", build::SHORT_COMMIT)
} else {
build::PKG_VERSION.to_string()
format!("refs/tags/{}", build::PKG_VERSION)
}
}
/// 获取最新的 git tag
fn get_latest_tag() -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new("git").args(["describe", "--tags", "--abbrev=0"]).output()?;
if output.status.success() {
let tag = String::from_utf8(output.stdout)?;
Ok(tag.trim().to_string())
} else {
Err("Failed to get latest tag".into())
}
}
/// 检查当前 HEAD 是否比指定的 tag 更新
fn is_head_newer_than_tag(tag: &str) -> bool {
let output = Command::new("git")
.args(["merge-base", "--is-ancestor", tag, "HEAD"])
.output();
match output {
Ok(result) => result.status.success(),
Err(_) => false,
}
}
/// 提升版本号(增加 patch 版本)
fn increment_version(version: &str) -> Result<String, Box<dyn std::error::Error>> {
// 解析版本号,例如 "1.0.0-alpha.19" -> (1, 0, 0, Some("alpha.19"))
let (major, minor, patch, pre_release) = parse_version(version)?;
// 如果有预发布标识符,则增加预发布版本号
if let Some(pre) = pre_release {
if let Some(new_pre) = increment_pre_release(&pre) {
return Ok(format!("{major}.{minor}.{patch}-{new_pre}"));
}
}
// 否则增加 patch 版本号
Ok(format!("{major}.{minor}.{}", patch + 1))
}
/// 解析版本号
pub fn parse_version(version: &str) -> VersionParseResult {
let parts: Vec<&str> = version.split('-').collect();
let base_version = parts[0];
let pre_release = if parts.len() > 1 { Some(parts[1..].join("-")) } else { None };
let version_parts: Vec<&str> = base_version.split('.').collect();
if version_parts.len() < 3 {
return Err("Invalid version format".into());
}
let major: u32 = version_parts[0].parse()?;
let minor: u32 = version_parts[1].parse()?;
let patch: u32 = version_parts[2].parse()?;
Ok((major, minor, patch, pre_release))
}
/// 增加预发布版本号
fn increment_pre_release(pre_release: &str) -> Option<String> {
// 处理形如 "alpha.19" 的预发布版本
let parts: Vec<&str> = pre_release.split('.').collect();
if parts.len() == 2 {
if let Ok(num) = parts[1].parse::<u32>() {
return Some(format!("{}.{}", parts[0], num + 1));
}
}
// 处理形如 "alpha19" 的预发布版本
if let Some(pos) = pre_release.rfind(|c: char| c.is_alphabetic()) {
let prefix = &pre_release[..=pos];
let suffix = &pre_release[pos + 1..];
if let Ok(num) = suffix.parse::<u32>() {
return Some(format!("{prefix}{}", num + 1));
}
}
None
}
/// Clean version string - removes common prefixes
pub fn clean_version(version: &str) -> String {
version
.trim()
.trim_start_matches("refs/tags/")
.trim_start_matches('v')
.trim_start_matches("RELEASE.")
.trim_start_matches('@')
.to_string()
}
/// Compare two versions to determine if the latest is newer
pub fn is_newer_version(current: &str, latest: &str) -> Result<bool, Box<dyn std::error::Error>> {
// Clean version numbers, remove prefixes like "v", "RELEASE.", etc.
let current_clean = clean_version(current);
let latest_clean = clean_version(latest);
// If versions are the same, no update is needed
if current_clean == latest_clean {
return Ok(false);
}
// Try semantic version comparison using parse_version
match (parse_version(&current_clean), parse_version(&latest_clean)) {
(Ok(current_parts), Ok(latest_parts)) => Ok(compare_version_parts(&current_parts, &latest_parts)),
(Err(_), _) | (_, Err(_)) => {
// If semantic version comparison fails, use string comparison
Ok(latest_clean > current_clean)
}
}
}
/// Compare two version parts tuples (major, minor, patch, pre_release)
fn compare_version_parts(current: &(u32, u32, u32, Option<String>), latest: &(u32, u32, u32, Option<String>)) -> bool {
let (cur_major, cur_minor, cur_patch, cur_pre) = current;
let (lat_major, lat_minor, lat_patch, lat_pre) = latest;
// Compare major version
if lat_major != cur_major {
return lat_major > cur_major;
}
// Compare minor version
if lat_minor != cur_minor {
return lat_minor > cur_minor;
}
// Compare patch version
if lat_patch != cur_patch {
return lat_patch > cur_patch;
}
// Compare pre-release versions
match (cur_pre, lat_pre) {
(None, None) => false, // Same version
(Some(_), None) => true, // Pre-release < release
(None, Some(_)) => false, // Release > pre-release
(Some(cur_pre), Some(lat_pre)) => {
// Both are pre-release, compare them
compare_pre_release(cur_pre, lat_pre)
}
}
}
/// Compare pre-release versions
fn compare_pre_release(current: &str, latest: &str) -> bool {
// Split by dots and compare each part
let current_parts: Vec<&str> = current.split('.').collect();
let latest_parts: Vec<&str> = latest.split('.').collect();
for (cur_part, lat_part) in current_parts.iter().zip(latest_parts.iter()) {
// Try to parse as numbers first
match (cur_part.parse::<u32>(), lat_part.parse::<u32>()) {
(Ok(cur_num), Ok(lat_num)) => {
if cur_num != lat_num {
return lat_num > cur_num;
}
}
_ => {
// If not numbers, compare as strings
if cur_part != lat_part {
return lat_part > cur_part;
}
}
}
}
// If all compared parts are equal, longer version is newer
latest_parts.len() > current_parts.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version() {
// 测试标准版本解析
let (major, minor, patch, pre_release) = parse_version("1.0.0").unwrap();
assert_eq!(major, 1);
assert_eq!(minor, 0);
assert_eq!(patch, 0);
assert_eq!(pre_release, None);
// 测试预发布版本解析
let (major, minor, patch, pre_release) = parse_version("1.0.0-alpha.19").unwrap();
assert_eq!(major, 1);
assert_eq!(minor, 0);
assert_eq!(patch, 0);
assert_eq!(pre_release, Some("alpha.19".to_string()));
}
#[test]
fn test_increment_pre_release() {
// 测试 alpha.19 -> alpha.20
assert_eq!(increment_pre_release("alpha.19"), Some("alpha.20".to_string()));
// 测试 beta.5 -> beta.6
assert_eq!(increment_pre_release("beta.5"), Some("beta.6".to_string()));
// 测试无法解析的情况
assert_eq!(increment_pre_release("unknown"), None);
}
#[test]
fn test_increment_version() {
// 测试预发布版本递增
assert_eq!(increment_version("1.0.0-alpha.19").unwrap(), "1.0.0-alpha.20");
// 测试标准版本递增
assert_eq!(increment_version("1.0.0").unwrap(), "1.0.1");
}
#[test]
fn test_version_format() {
// 测试版本格式是否以 refs/tags/ 开头
let version = get_version();
assert!(version.starts_with("refs/tags/") || version.starts_with("@"));
// 如果是 refs/tags/ 格式,应该包含版本号
if let Some(version_part) = version.strip_prefix("refs/tags/") {
assert!(!version_part.is_empty());
}
}
#[test]
fn test_current_version_output() {
// 显示当前版本输出
let version = get_version();
println!("Current version: {version}");
// 验证版本格式
assert!(version.starts_with("refs/tags/") || version.starts_with("@"));
// 如果是 refs/tags/ 格式,验证版本号不为空
if let Some(version_part) = version.strip_prefix("refs/tags/") {
assert!(!version_part.is_empty());
println!("Version part: {version_part}");
}
}
#[test]
fn test_clean_version() {
assert_eq!(clean_version("v1.0.0"), "1.0.0");
assert_eq!(clean_version("RELEASE.1.0.0"), "1.0.0");
assert_eq!(clean_version("@1.0.0"), "1.0.0");
assert_eq!(clean_version("1.0.0"), "1.0.0");
assert_eq!(clean_version("refs/tags/1.0.0-alpha.17"), "1.0.0-alpha.17");
assert_eq!(clean_version("refs/tags/v1.0.0"), "1.0.0");
}
#[test]
fn test_is_newer_version() {
// Test semantic version comparison
assert!(is_newer_version("1.0.0", "1.0.1").unwrap());
assert!(is_newer_version("1.0.0", "1.1.0").unwrap());
assert!(is_newer_version("1.0.0", "2.0.0").unwrap());
assert!(!is_newer_version("1.0.1", "1.0.0").unwrap());
assert!(!is_newer_version("1.0.0", "1.0.0").unwrap());
// Test version comparison with pre-release identifiers
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-alpha.2").unwrap());
assert!(is_newer_version("1.0.0-alpha.17", "1.0.1").unwrap());
assert!(is_newer_version("refs/tags/1.0.0-alpha.16", "refs/tags/1.0.0-alpha.17").unwrap());
assert!(!is_newer_version("refs/tags/1.0.0-alpha.17", "refs/tags/1.0.0-alpha.16").unwrap());
// Test pre-release vs release comparison
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0").unwrap());
assert!(is_newer_version("1.0.0-beta.1", "1.0.0").unwrap());
assert!(!is_newer_version("1.0.0", "1.0.0-alpha.1").unwrap());
assert!(!is_newer_version("1.0.0", "1.0.0-beta.1").unwrap());
// Test pre-release version ordering
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-alpha.2").unwrap());
assert!(is_newer_version("1.0.0-alpha.19", "1.0.0-alpha.20").unwrap());
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-beta.1").unwrap());
assert!(is_newer_version("1.0.0-beta.1", "1.0.0-rc.1").unwrap());
// Test complex pre-release versions
assert!(is_newer_version("1.0.0-alpha.1.2", "1.0.0-alpha.1.3").unwrap());
assert!(is_newer_version("1.0.0-alpha.1", "1.0.0-alpha.1.1").unwrap());
assert!(!is_newer_version("1.0.0-alpha.1.3", "1.0.0-alpha.1.2").unwrap());
}
#[test]
fn test_compare_version_parts() {
// Test basic version comparison
assert!(compare_version_parts(&(1, 0, 0, None), &(1, 0, 1, None)));
assert!(compare_version_parts(&(1, 0, 0, None), &(1, 1, 0, None)));
assert!(compare_version_parts(&(1, 0, 0, None), &(2, 0, 0, None)));
assert!(!compare_version_parts(&(1, 0, 1, None), &(1, 0, 0, None)));
// Test pre-release vs release
assert!(compare_version_parts(&(1, 0, 0, Some("alpha.1".to_string())), &(1, 0, 0, None)));
assert!(!compare_version_parts(&(1, 0, 0, None), &(1, 0, 0, Some("alpha.1".to_string()))));
// Test pre-release comparison
assert!(compare_version_parts(
&(1, 0, 0, Some("alpha.1".to_string())),
&(1, 0, 0, Some("alpha.2".to_string()))
));
assert!(compare_version_parts(
&(1, 0, 0, Some("alpha.19".to_string())),
&(1, 0, 0, Some("alpha.20".to_string()))
));
assert!(compare_version_parts(
&(1, 0, 0, Some("alpha.1".to_string())),
&(1, 0, 0, Some("beta.1".to_string()))
));
}
#[test]
fn test_compare_pre_release() {
// Test numeric pre-release comparison
assert!(compare_pre_release("alpha.1", "alpha.2"));
assert!(compare_pre_release("alpha.19", "alpha.20"));
assert!(!compare_pre_release("alpha.2", "alpha.1"));
// Test string pre-release comparison
assert!(compare_pre_release("alpha.1", "beta.1"));
assert!(compare_pre_release("beta.1", "rc.1"));
assert!(!compare_pre_release("beta.1", "alpha.1"));
// Test complex pre-release comparison
assert!(compare_pre_release("alpha.1.2", "alpha.1.3"));
assert!(compare_pre_release("alpha.1", "alpha.1.1"));
assert!(!compare_pre_release("alpha.1.3", "alpha.1.2"));
}
}