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

@@ -8,7 +8,8 @@ use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::generator::ContentGenerator;
use crate::git::tag::{
TagBuilder, VersionBump, bump_version, get_latest_version, suggest_version_bump,
TagBuilder, VersionBump, bump_version, get_latest_version, read_project_version,
suggest_version_bump,
};
use crate::git::{GitRepo, find_repo};
use crate::i18n::Messages;
@@ -63,6 +64,11 @@ pub struct TagCommand {
/// Skip interactive prompts
#[arg(short = 'y', long)]
yes: bool,
/// Auto-detect version from project config (Cargo.toml/pyproject.toml),
/// falling back to commit analysis with confirmation. Mutually exclusive with --bump.
#[arg(short = 'A', long, conflicts_with = "bump")]
auto: bool,
}
impl TagCommand {
@@ -80,6 +86,9 @@ impl TagCommand {
// Determine tag name
let tag_name = if let Some(name) = &self.name {
name.clone()
} else if self.auto {
self.auto_detect_version(&repo, &config.tag.version_prefix, &messages)
.await?
} else if let Some(bump_str) = &self.bump {
// Calculate bumped version
let prefix = &config.tag.version_prefix;
@@ -325,6 +334,57 @@ impl TagCommand {
.await
}
async fn auto_detect_version(
&self,
repo: &GitRepo,
prefix: &str,
messages: &Messages,
) -> Result<String> {
let project_dir = std::env::current_dir()?;
// 1. Try reading from project config files
if let Some(version) = read_project_version(&project_dir) {
let tag_name = format!("{}{}", prefix, version);
println!(
"{} {}",
messages.latest_version(),
tag_name.cyan()
);
return Ok(tag_name);
}
// 2. Fall back to commit analysis
println!("{}", messages.auto_detect_bump());
let commits = repo.get_commits(50)?;
let bump = suggest_version_bump(&commits);
let latest = get_latest_version(repo, prefix)?.unwrap_or_else(|| Version::new(0, 0, 0));
let version = bump_version(&latest, bump, None);
let tag_name = format!("{}{}", prefix, version);
println!(
"{} {:?}{}",
messages.suggested_bump(),
bump,
tag_name.cyan()
);
if !self.yes {
let confirm = Confirm::new()
.with_prompt(messages.use_this_version())
.default(true)
.interact()?;
if !confirm {
// Fall through to interactive version selection
return self
.select_version_interactive(repo, prefix, messages)
.await;
}
}
Ok(tag_name)
}
fn input_message_interactive(&self, version: &str, messages: &Messages) -> Result<String> {
let default_msg = format!("Release {}", version);