name: changelog-gen description: 从 git commit 历史自动生成人类可读的 CHANGELOG triggers:
- "generate changelog"
- "生成 changelog"
- "更新变更日志"
- "what changed"
- "有什么变更"
- "changelog since"
CHANGELOG Gen Skill
概述
智能 CHANGELOG 生成器 — 从 git 历史中提取 conventional commits,按类别分组,生成人类可读的 Keep a Changelog 格式变更日志。
触发条件
当用户说出以下内容时激活:
- "生成 changelog"
- "generate changelog"
- "更新变更日志"
- "what changed"
- "有什么变更"
- "changelog since..."
执行流程
第一步:确定 Commit 范围
按以下决策树自动推断范围,避免不必要的询问:
- 用户已指定范围 → 直接使用用户输入(如
v1.0.0..v2.0.0、-n 10、--after="2024-01-01") - 用户未指定范围 → 自动检测 git tag:
git describe --tags --abbrev=0 2>/dev/null- 存在 tag → 自动使用
<last-tag>..HEAD,告知用户所选范围 - 不存在 tag → 询问用户,并提供最近 commit 列表作为参考:
建议用户指定起始 commit hash 或 commit 数量(如# 列出最近 20 个 commit 供用户选择起点 git log -n 20 --oneline-n 10)
- 存在 tag → 自动使用
支持的范围格式
# 从某个 tag 到 HEAD
git log v1.0.0..HEAD --oneline
# 最近 N 个 commit
git log -n 20 --oneline
# 两个 tag 之间
git log v1.0.0..v2.0.0 --oneline
# 基于日期
git log --after="2024-01-01" --oneline
第二步:读取 Git Log(详细格式)
使用以下命令获取结构化 commit 信息:
⚠️ 注意:commit message 中可能包含特殊字符(引号、反斜杠等), 解析时需正确转义。建议使用
%x00(null byte) 作为字段分隔符, 或使用 git log 的--format=tformat:确保每条记录正确分隔。
# 主命令 — 获取完整的 commit 信息(使用 null byte 分隔,避免特殊字符干扰)
git log <range> --format="%H%x00%an%x00%ai%x00%s%x00%b%x00---END---"
# 简化版 — 仅获取 subject 行(使用 null byte 分隔)
git log <range> --format="%h%x00%s%x00%an%x00%ai" --no-merges
# 获取 merge commit(单独处理)
git log <range> --merges --format="%h%x00%s%x00%an%x00%ai"
Merge commit 过滤策略
默认使用 --no-merges 过滤 merge commit,但以下场景应包含 merge commit:
- PR / MR 的 merge commit 中包含有价值的描述(如 squash merge 的汇总信息)
- 项目使用 merge commit 作为主要变更记录(非 conventional commit 工作流)
# 默认:过滤 merge commit
git log <range> --no-merges --format="%h%x00%s%x00%an%x00%ai"
# 可选:包含 merge commit(当 PR 描述有价值时)
git log <range> --format="%h%x00%s%x00%an%x00%ai"
💡 判断方法:先用
git log <range> --merges --oneline查看 merge commit 内容,如果仅为自动生成的Merge branch 'xxx'则过滤;如果包含有意义的变更描述则保留。
第三步:按 Conventional Commits 分类
分类规则表
| 前缀 | CHANGELOG 分类 | 说明 | 显示优先级 |
|---|---|---|---|
feat: / feat(scope): | Added | 新功能 | 1 |
fix: / fix(scope): | Fixed | Bug 修复 | 2 |
perf: / perf(scope): | Performance | 性能优化 | 3 |
refactor: / refactor(scope): | Changed | 重构 | 4 |
docs: / docs(scope): | Documentation | 文档变更 | 5 |
test: / test(scope): | Tests | 测试相关 | 6 |
chore: / chore(scope): | Maintenance | 构建/工具/依赖 | 7 |
style: / style(scope): | Style | 代码格式(不影响功能) | 8 |
ci: / ci(scope): | CI/CD | 持续集成 | 9 |
build: / build(scope): | Build | 构建系统 | 10 |
revert: | Reverted | 回滚 | 3 |
| 无前缀 | Other | 未分类(保留原始信息) | 99 |
解析逻辑
正则匹配 subject 行(Conventional Commits):
^(feat|fix|perf|refactor|docs|test|chore|style|ci|build|revert)(\(.+\))?(!)?:\s*(.+)$
捕获组:
- group 1: type(类型)
- group 2: scope(范围,可选)
- group 3: !(breaking change 标记,可选)
- group 4: description(描述)
非标准 commit 格式处理
不是所有项目都使用 Conventional Commits。在解析前,先检测项目实际的 commit 风格:
# 采样最近 50 个 commit,统计符合 conventional commit 格式的比例
git log -n 50 --format="%s" | grep -cE '^(feat|fix|perf|refactor|docs|test|chore|style|ci|build|revert)(\(.+\))?!?:'
- ≥ 70% 符合 → 按 Conventional Commits 解析
- < 70% → 提示用户项目未使用标准格式,按以下扩展规则处理:
扩展格式支持
| 格式 | 正则 | 处理方式 |
|---|---|---|
[JIRA-123] description | ^\[([A-Z]+-\d+)\]\s*(.+)$ | 提取 ticket ID 作为 scope,描述归入 Other |
#123 description | ^#(\d+)\s+(.+)$ | 提取 issue/PR 号作为引用 |
(tag) description | ^\((\w+)\)\s*(.+)$ | 尝试将 tag 映射到分类表 |
| 纯文本 | 不匹配以上任何格式 | 归入 Other,保留原始 commit message |
💡 归入 Other 的条目保留原始 commit message 全文,不做改写,确保信息不丢失。
第四步:检测 Breaking Changes
Breaking changes 必须醒目标注。检测方式:
- Subject 中的
!标记:feat!: remove legacy API - Body 中的
BREAKING CHANGE:关键字:feat: update auth system BREAKING CHANGE: JWT token format changed, all existing tokens will be invalidated. - Body 中的
BREAKING-CHANGE:关键字(带连字符的变体) - Footer 中的
BREAKING CHANGE:或BREAKING-CHANGE:
# 快速扫描包含 breaking change 的 commit(macOS/BSD grep 兼容)
git log <range> --format="%H %s%n%b" | grep -i -E "BREAKING[-_ ]CHANGE|BREAKING CHANGE|^[a-z]*!:"
⚠️ 注意:正则中
BREAKING.CHANGE的.会匹配任意字符,导致误匹配(如BREAKINGXCHANGE)。 上面已修正为BREAKING[-_ ]CHANGE|BREAKING CHANGE,仅匹配连字符、下划线、空格分隔的合法变体。
第五步:自动检测版本号
按优先级依次检查以下文件:
# 1. package.json (Node.js)
cat package.json 2>/dev/null | grep '"version"' | head -1
# 提取: "version": "1.2.3" → 1.2.3
# ✅ 验证格式:提取结果须匹配 ^\d+\.\d+\.\d+(-[\w.]+)?$
# 2. pyproject.toml (Python)
grep -E '^\s*version\s*=\s*["'"'"']' pyproject.toml 2>/dev/null | head -1 | sed -E "s/.*version\s*=\s*[\"']//" | sed -E "s/[\"'].*//"
# 提取: version = "1.2.3" 或 version = '1.2.3'(支持前导空格和单双引号)→ 1.2.3
# ✅ 验证格式:提取结果须匹配 ^\d+\.\d+\.\d+(-[\w.]+)?$
# 3. Cargo.toml (Rust)
grep -E '^\s*version\s*=\s*["'"'"']' Cargo.toml 2>/dev/null | head -1 | sed -E "s/.*version\s*=\s*[\"']//" | sed -E "s/[\"'].*//"
# 提取: version = "1.2.3" 或 version = '1.2.3'(支持前导空格和单双引号)→ 1.2.3
# ✅ 验证格式:提取结果须匹配 ^\d+\.\d+\.\d+(-[\w.]+)?$
# 4. build.gradle / build.gradle.kts (Java/Kotlin)
cat build.gradle 2>/dev/null | grep 'version' | head -1
# ✅ 验证格式:提取结果须匹配 ^\d+\.\d+\.\d+(-[\w.]+)?$
# 5. pubspec.yaml (Flutter/Dart)
cat pubspec.yaml 2>/dev/null | grep '^version:' | head -1
# ✅ 验证格式:提取结果须匹配 ^\d+\.\d+\.\d+(\+\d+)?$
# 6. Git tag(最近的 tag)
git describe --tags --abbrev=0 2>/dev/null
# ✅ 验证格式:提取结果须匹配 ^v?\d+\.\d+\.\d+(-[\w.]+)?$
⚠️ 每个检测源提取的版本号都必须通过格式验证,不合法的值应跳过并尝试下一个来源。
# 7. 降级方案:如果以上所有来源都未检测到合法版本号
# ❗ 必须明确询问用户指定版本号,不得静默使用日期版本
# 仅在用户明确同意后,才可使用日期格式作为版本号:
date +"%Y.%m.%d"
# ⚠️ 日期格式版本(如 2024.03.08)不符合 semver 规范,需提醒用户确认
如果检测到版本号,建议用户确认或修改。
日期格式规范
本文件中涉及两种日期场景,需明确区分:
| 场景 | 格式 | 示例 | 说明 |
|---|---|---|---|
| 条目日期(版本发布日期) | YYYY-MM-DD | 2024-03-08 | 跟在版本号后面,如 ## [1.2.0] - 2024-03-08 |
| 日期版本号(不推荐) | YYYY.MM.DD | 2024.03.08 | 仅作为无 semver 版本时的降级方案,不符合 semver 规范 |
⚠️ 不要混淆这两种日期。条目日期使用 ISO 8601 短格式(
YYYY-MM-DD,用连字符分隔),始终必须。日期版本号用点号分隔,仅在用户明确同意时使用。
第六步:生成 CHANGELOG 条目
输出模板(Keep a Changelog 格式)
## [版本号] - YYYY-MM-DD
### Breaking Changes
- **scope**: 描述 breaking change 的具体影响 ([commit-hash])
- Migration: 迁移指引(如果 commit body 中有提供)
### Added
- **scope**: 功能描述,用人类可读的语言改写 ([commit-hash])
- 另一个新功能 ([commit-hash])
### Fixed
- **scope**: 修复了什么问题 ([commit-hash])
### Performance
- **scope**: 优化了什么 ([commit-hash])
### Changed
- **scope**: 重构/变更了什么 ([commit-hash])
### Documentation
- 更新了什么文档 ([commit-hash])
### Maintenance
- 依赖更新、构建工具变更等 ([commit-hash])
条目改写规则
- 去掉 conventional commit 前缀(
feat:,fix:等) - 首字母大写
- 如果有 scope,以
**scope**:开头 - 用完整句子描述变更,而不是机械复制 commit message
- 合并相关的小 commit(如多次修复同一个功能的 commit)
- commit hash 使用短格式(7 位),链接到仓库(如果能检测到 remote URL)
改写示例对比
| 原始 commit message | 改写后的 CHANGELOG 条目 |
|---|---|
fix(auth): handle null token | auth: 修复认证模块中 token 为空时的崩溃问题 |
feat: add retry logic | 新增请求失败自动重试机制 |
perf(db): optimize query plan | db: 优化数据库查询计划,减少慢查询 |
fix: typo in error msg + fix: another typo | 修复多处错误提示中的拼写问题 (合并相关 commit) |
refactor(api)!: change response format | api: 重构 API 响应格式为统一的 envelope 结构 |
检测仓库 URL(用于生成链接)
# 获取 remote URL 并转换为 HTTPS 格式
git remote get-url origin 2>/dev/null
# git@github.com:user/repo.git → https://github.com/user/repo
# https://github.com/user/repo.git → https://github.com/user/repo
如果检测到仓库 URL,commit hash 生成为链接格式:
- 功能描述 ([abc1234](https://github.com/user/repo/commit/abc1234))
第七步:追加 vs 新建 CHANGELOG
判断逻辑
# 检查是否存在 CHANGELOG 文件
ls CHANGELOG.md CHANGELOG changelog.md 2>/dev/null
如果 CHANGELOG.md 已存在:
- 读取现有内容
- 备份原文件:
cp CHANGELOG.md CHANGELOG.md.bak - 找到第一个匹配
## [v?\d+\.\d+\.\d+的行(即第一个版本条目,精确匹配版本号格式) - 在该行之前插入新版本条目
- 保留文件头部(标题、描述等)
- 验证插入后的文件格式正确(Markdown 语法无误、版本条目顺序正确)
⚠️ 如果插入失败或格式被破坏,使用备份恢复:
mv CHANGELOG.md.bak CHANGELOG.md成功后可删除备份:rm CHANGELOG.md.bak
如果 CHANGELOG.md 不存在:
- 创建新文件,包含标准头部:
# CHANGELOG
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [版本号] - YYYY-MM-DD
(生成的条目)
第八步:质量检查
生成完成后,执行以下检查:
- 每个 commit 都已被分类(没有遗漏)
- Breaking changes 已被单独标注并放在最前面
- 版本号格式正确(符合 semver)
- 日期格式为 YYYY-MM-DD(条目日期,非版本号中的日期)
- 没有重复条目
- 没有空的分类区块(如果某个分类没有条目,则省略该区块)
- 合并了相关的小 commit(避免冗余)
- 语言风格一致(全英文或全中文,与项目现有 CHANGELOG 保持一致)
- 如果是追加模式,没有破坏现有 CHANGELOG 的格式
- Commit hash 链接正确(如果有仓库 URL)
自动验证命令
# 验证 commit 是否遗漏:对比源 commit 数量与 CHANGELOG 条目数量
# 源 commit 数
git log <range> --no-merges --oneline | wc -l
# CHANGELOG 条目数(合并后应 ≤ 源 commit 数)
grep -c '^\- ' CHANGELOG.md
# 验证版本号格式(semver)
grep -E '## \[' CHANGELOG.md | head -1 | sed -E 's/.*## \[([^]]+)\].*/\1/' | grep -qE '^\d+\.\d+\.\d+(-[\w.]+)?$' && echo "✅ 版本号合法" || echo "❌ 版本号格式不符合 semver"
# 验证日期格式(YYYY-MM-DD)
grep -E '## \[' CHANGELOG.md | head -1 | sed -E 's/.*\] - ([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/' | grep -qE '^\d{4}-\d{2}-\d{2}$' && echo "✅ 日期格式正确" || echo "❌ 日期格式不正确"
# 检查是否存在空的分类区块(### 标题后紧跟另一个 ### 或 ## 或文件结尾)
awk '/^### /{title=$0; empty=1; next} /^- /{empty=0} /^##/{if(empty) print "❌ 空区块: " title}' CHANGELOG.md
# 验证 Markdown 格式(如果安装了 markdownlint)
bunx markdownlint CHANGELOG.md 2>/dev/null || true
特殊处理
Monorepo 支持
检测 monorepo 结构
# 检查常见 monorepo 标志
ls -d packages/ apps/ libs/ modules/ 2>/dev/null
# 检查是否使用 workspace 管理
cat package.json 2>/dev/null | grep -q '"workspaces"' && echo "npm/yarn workspace"
cat pnpm-workspace.yaml 2>/dev/null && echo "pnpm workspace"
cat lerna.json 2>/dev/null && echo "lerna"
生成策略
检测到 monorepo 后,询问用户选择策略:
-
独立生成(推荐):为每个 package 单独生成 CHANGELOG
# 获取某个 package 的 commit git log <range> --no-merges --format="%h%x00%s%x00%an%x00%ai" -- packages/core/每个 package 的 CHANGELOG 写入各自目录:
packages/core/CHANGELOG.md -
合并生成:所有变更写入根目录的 CHANGELOG,按 package 分组
## [1.2.0] - 2024-03-08 ### @myapp/core #### Added - 新增 xxx 功能 ([abc1234]) ### @myapp/utils #### Fixed - 修复 xxx 问题 ([def5678])
跨 package commit 处理
当一个 commit 涉及多个 package 时:
# 检查 commit 涉及的目录
git diff-tree --no-commit-id --name-only -r <commit-hash> | sed 's|/.*||' | sort -u
- 独立生成模式:该 commit 同时出现在所有涉及的 package 的 CHANGELOG 中
- 合并生成模式:归入影响最大的 package(按变更文件数判断),并标注
(also affects: @myapp/utils)
多语言支持
- 检查现有 CHANGELOG.md 的语言
- 如果是中文项目(根据 README 或现有 CHANGELOG 判断),用中文生成条目
- 默认使用英文
使用示例
用户: 生成 changelog
执行:
- 自动检测最近一个 tag 到 HEAD 的范围
- 读取 git log
- 分类、改写、生成
- 追加到 CHANGELOG.md 或创建新文件
用户: generate changelog from v2.0.0 to v3.0.0
执行:
- 使用
v2.0.0..v3.0.0范围 - 读取、分类、生成
- 让用户确认版本号(建议 v3.0.0)
用户: what changed in the last 10 commits
执行:
- 使用
git log -n 10 - 分类展示变更摘要(不写入文件,仅展示)