From 749f55d688b9b477750a9025e829c4d74e1214c0 Mon Sep 17 00:00:00 2001 From: weisd Date: Wed, 16 Jul 2025 18:09:43 +0800 Subject: [PATCH] feat: enhance version function with automatic version increment (#227) --- rustfs/src/main.rs | 8 +- rustfs/src/update.rs | 125 +++------------ rustfs/src/version.rs | 353 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 377 insertions(+), 109 deletions(-) diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 469624c16..799fe5d31 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -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); } } }); diff --git a/rustfs/src/update.rs b/rustfs/src/update.rs index 1d145043b..eda63748c 100644 --- a/rustfs/src/update.rs +++ b/rustfs/src/update.rs @@ -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(¤t_version, &version_info.version)?; + // Compare versions using version.rs functions + let update_available = version::is_newer_version(¤t_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 { - // 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(¤t_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 { - 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::() - .map_err(|_| UpdateCheckError::VersionParseError(format!("Cannot parse major version: {}", parts[0])))?; - - let minor = parts[1] - .parse::() - .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::(); - let patch = patch_str - .parse::() - .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), Box>; + #[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> { + 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> { + // 解析版本号,例如 "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 { + // 处理形如 "alpha.19" 的预发布版本 + let parts: Vec<&str> = pre_release.split('.').collect(); + if parts.len() == 2 { + if let Ok(num) = parts[1].parse::() { + 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::() { + 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> { + // 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(¤t_clean), parse_version(&latest_clean)) { + (Ok(current_parts), Ok(latest_parts)) => Ok(compare_version_parts(¤t_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), latest: &(u32, u32, u32, Option)) -> 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::(), lat_part.parse::()) { + (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")); } }