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:
2026-07-24 17:41:18 +08:00
parent 995d263a48
commit 206bde0786
7 changed files with 651 additions and 36 deletions

View File

@@ -7,6 +7,7 @@ use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::generator::ContentGenerator;
use crate::git::GitRepo;
use crate::git::find_repo;
use crate::git::{CommitInfo, changelog::*};
use crate::i18n::{Messages, translate_changelog_category};
@@ -120,7 +121,10 @@ impl ChangelogCommand {
// Get commits
println!("{}", messages.fetching_commits());
let commits = generate_from_history(&repo, self.from.as_deref(), Some(&self.to))?;
// Determine from_tag: use explicit --from, or auto-detect from changelog
let from_tag = self.resolve_from_tag(&repo, &output_path, &messages);
let commits = generate_from_history(&repo, from_tag.as_deref(), Some(&self.to))?;
if commits.is_empty() {
bail!("{}", messages.no_commits_found());
@@ -167,33 +171,13 @@ impl ChangelogCommand {
}
}
// Write to file (always prepend to preserve history)
// Write to file (always prepend new entry before existing versions)
if output_path.exists() {
let existing = std::fs::read_to_string(&output_path)?;
let new_content = if existing.is_empty() {
format!("{}{}", CHANGELOG_HEADER, changelog)
} else if existing.starts_with(CHANGELOG_HEADER) {
format!("{}{}", CHANGELOG_HEADER, changelog)
} else if existing.starts_with("# Changelog") {
let lines: Vec<&str> = existing.lines().collect();
let mut header_end = 0;
for (i, line) in lines.iter().enumerate() {
if i == 0 && line.starts_with('#') {
header_end = i + 1;
} else if line.trim().is_empty() {
header_end = i + 1;
} else {
break;
}
}
let header = lines[..header_end].join("\n");
let rest = lines[header_end..].join("\n");
format!("{}\n{}\n{}", header, changelog, rest)
} else {
format!("{}{}", CHANGELOG_HEADER, changelog)
insert_changelog_entry(&existing, &changelog)
};
std::fs::write(&output_path, new_content)?;
} else {
@@ -206,6 +190,28 @@ impl ChangelogCommand {
Ok(())
}
fn resolve_from_tag(
&self,
repo: &GitRepo,
output_path: &PathBuf,
messages: &Messages,
) -> Option<String> {
// Explicit --from always wins
if self.from.is_some() {
return self.from.clone();
}
// Auto-detect: find highest version already in changelog
let existing = std::fs::read_to_string(output_path).ok()?;
let versions = parse_changelog_versions(&existing);
let highest = versions.first()?;
// Match highest version to a git tag
let tag = repo.find_tag_by_version(highest)?;
println!(" {}: {}", messages.version(), tag.name);
Some(tag.name)
}
async fn generate_with_ai(
&self,
version: &str,