fix: 修复 tag/changelog 生成范围,增加 --auto 模式
- fix(get_tags): 使用 find_object+peel_to_commit 支持 annotated tag - fix(changelog): insert_changelog_entry 替换覆盖逻辑,保留已有章节 - feat: parse_changelog_versions 按 semver 降序提取版本 - feat: sort_tags_by_semver 取代纯时间排序 - feat(tag --auto): 优先读 Cargo.toml/pyproject.toml,回退 commit 分析 - feat(changelog): 无 --from 时自动检测 changelog 最高已有版本 - refactor: TagInfo::version_name(), GitRepo::find_tag_by_version()
This commit is contained in:
191
src/git/tag.rs
191
src/git/tag.rs
@@ -1,6 +1,7 @@
|
||||
use super::GitRepo;
|
||||
use anyhow::{Result, bail};
|
||||
use semver::Version;
|
||||
use std::path::Path;
|
||||
|
||||
/// Tag builder for creating tags
|
||||
pub struct TagBuilder {
|
||||
@@ -327,3 +328,193 @@ pub fn list_tags(
|
||||
Ok(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort tags by semver version descending, then by time descending.
|
||||
/// Non-semver tags are placed after semver tags, sorted by time descending.
|
||||
pub fn sort_tags_by_semver(tags: &mut [super::TagInfo]) {
|
||||
use semver::Version;
|
||||
|
||||
tags.sort_by(|a, b| {
|
||||
match (
|
||||
Version::parse(a.version_name()),
|
||||
Version::parse(b.version_name()),
|
||||
) {
|
||||
(Ok(va), Ok(vb)) => {
|
||||
// Both semver: version descending, then time descending as tiebreaker
|
||||
vb.cmp(&va).then_with(|| b.time.cmp(&a.time))
|
||||
}
|
||||
(Ok(_), Err(_)) => std::cmp::Ordering::Less, // semver tags first
|
||||
(Err(_), Ok(_)) => std::cmp::Ordering::Greater, // semver tags first
|
||||
(Err(_), Err(_)) => b.time.cmp(&a.time), // both non-semver: time descending
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Read the project version from Cargo.toml or pyproject.toml.
|
||||
/// Returns None if no project config file is found or version cannot be read.
|
||||
pub fn read_project_version(project_dir: &Path) -> Option<Version> {
|
||||
// Try Cargo.toml first
|
||||
let cargo_path = project_dir.join("Cargo.toml");
|
||||
if cargo_path.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(&cargo_path) {
|
||||
if let Ok(value) = content.parse::<toml::Value>() {
|
||||
if let Some(version) = value
|
||||
.get("package")
|
||||
.and_then(|p| p.get("version"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
if let Ok(ver) = Version::parse(version) {
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try pyproject.toml
|
||||
let pyproject_path = project_dir.join("pyproject.toml");
|
||||
if pyproject_path.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(&pyproject_path) {
|
||||
if let Ok(value) = content.parse::<toml::Value>() {
|
||||
if let Some(version) = value
|
||||
.get("project")
|
||||
.and_then(|p| p.get("version"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
if let Ok(ver) = Version::parse(version) {
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::git::TagInfo;
|
||||
|
||||
fn make_tag(name: &str, time: i64) -> TagInfo {
|
||||
TagInfo {
|
||||
name: name.to_string(),
|
||||
target: "abc123".to_string(),
|
||||
message: String::new(),
|
||||
time,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by_semver_descending() {
|
||||
let mut tags = vec![
|
||||
make_tag("v1.0.0", 100),
|
||||
make_tag("v2.0.0", 200),
|
||||
make_tag("v1.5.0", 150),
|
||||
];
|
||||
sort_tags_by_semver(&mut tags);
|
||||
let names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["v2.0.0", "v1.5.0", "v1.0.0"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_semver_ties_broken_by_time() {
|
||||
let mut tags = vec![
|
||||
make_tag("v1.0.0", 100),
|
||||
make_tag("v1.0.0", 300),
|
||||
make_tag("v1.0.0", 200),
|
||||
];
|
||||
sort_tags_by_semver(&mut tags);
|
||||
let times: Vec<i64> = tags.iter().map(|t| t.time).collect();
|
||||
assert_eq!(times, vec![300, 200, 100]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_non_semver_tags_at_end() {
|
||||
let mut tags = vec![
|
||||
make_tag("release-2024", 400),
|
||||
make_tag("v1.0.0", 100),
|
||||
make_tag("staging", 300),
|
||||
make_tag("v0.2.0", 200),
|
||||
];
|
||||
sort_tags_by_semver(&mut tags);
|
||||
let names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
|
||||
assert_eq!(names[0], "v1.0.0");
|
||||
assert_eq!(names[1], "v0.2.0");
|
||||
assert_eq!(names[2], "release-2024");
|
||||
assert_eq!(names[3], "staging");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_no_prefix_semver() {
|
||||
let mut tags = vec![
|
||||
make_tag("1.0.0", 100),
|
||||
make_tag("2.0.0", 200),
|
||||
make_tag("0.1.0", 50),
|
||||
];
|
||||
sort_tags_by_semver(&mut tags);
|
||||
let names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["2.0.0", "1.0.0", "0.1.0"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_all_non_semver_by_time() {
|
||||
let mut tags = vec![
|
||||
make_tag("release-2024", 100),
|
||||
make_tag("release-2023", 300),
|
||||
make_tag("beta", 200),
|
||||
];
|
||||
sort_tags_by_semver(&mut tags);
|
||||
let times: Vec<i64> = tags.iter().map(|t| t.time).collect();
|
||||
assert_eq!(times, vec![300, 200, 100]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_project_version_from_cargo_toml() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("Cargo.toml"),
|
||||
"[package]\nname = \"test\"\nversion = \"0.3.0\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let version = read_project_version(dir.path());
|
||||
assert_eq!(version, Some(Version::new(0, 3, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_project_version_from_pyproject_toml() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("pyproject.toml"),
|
||||
"[project]\nname = \"test\"\nversion = \"0.2.1\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let version = read_project_version(dir.path());
|
||||
assert_eq!(version, Some(Version::new(0, 2, 1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_project_version_no_config_files() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let version = read_project_version(dir.path());
|
||||
assert_eq!(version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_project_version_cargo_takes_priority() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("Cargo.toml"),
|
||||
"[package]\nname = \"test\"\nversion = \"0.3.0\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("pyproject.toml"),
|
||||
"[project]\nname = \"test\"\nversion = \"0.2.1\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let version = read_project_version(dir.path());
|
||||
assert_eq!(version, Some(Version::new(0, 3, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user