我来提供一个功能完善的变更日志生成脚本,这个脚本可以分析Git提交历史并生成结构化的CHANGELOG.md文件。

完整的变更日志生成脚本
Bash版本(推荐,适用于大多数项目)
#!/bin/bash
# 变更日志生成器
# 用法: ./changelog.sh [tag1] [tag2] 或 ./changelog.sh
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 检查是否在Git仓库中
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo -e "${RED}错误: 不在Git仓库中${NC}"
exit 1
fi
# 获取版本信息
get_version() {
local tag=$1
if [ -z "$tag" ]; then
echo "未发布版本"
else
echo "$tag"
fi
}
# 分类提交信息
classify_commit() {
local msg="$1"
# 转换为小写
local lower_msg=$(echo "$msg" | tr '[:upper:]' '[:lower:]')
# 根据提交信息分类
if echo "$lower_msg" | grep -qE '^(feat|feature|add|new|implement)'; then
echo "features"
elif echo "$lower_msg" | grep -qE '^(fix|bug|bugfix|hotfix|correct|resolve)'; then
echo "fixes"
elif echo "$lower_msg" | grep -qE '^(docs?|document)'; then
echo "documentation"
elif echo "$lower_msg" | grep -qE '^(refactor|refactoring|improve|enhance|optimize)'; then
echo "improvements"
elif echo "$lower_msg" | grep -qE '^(test|testing)'; then
echo "tests"
elif echo "$lower_msg" | grep -qE '^(chore|build|ci|config|deps?)'; then
echo "chores"
elif echo "$lower_msg" | grep -qE '^(style|format|lint)'; then
echo "style"
elif echo "$lower_msg" | grep -qE '^(perf|performance)'; then
echo "performance"
else
echo "other"
fi
}
# 格式化提交信息
format_commit() {
local msg="$1"
local author="$2"
local hash="$3"
# 移除常用的前缀
local formatted=$(echo "$msg" | sed -E 's/^(feat|fix|docs|refactor|test|chore|style|perf)(\([^)]+\))?:\s*//i')
# 添加链接格式
echo "- ${formatted} ([${hash:0:7}](https://github.com/your-repo/commit/${hash}))"
}
# 生成单个版本的变更日志
generate_version_changelog() {
local from_tag="$1"
local to_tag="$2"
local output_file="$3"
# 确定日期范围
local date_range=""
if [ -n "$to_tag" ]; then
local to_date=$(git log -1 --format="%ai" "$to_tag" | cut -d' ' -f1)
date_range=" - $to_date"
else
local to_date=$(date +%Y-%m-%d)
date_range=""
fi
# 获取版本号
local version=$(get_version "$to_tag")
# 获取提交日志
if [ -n "$from_tag" ] && [ -n "$to_tag" ]; then
local commits=$(git log "$from_tag..$to_tag" --oneline --no-merges --format="%h||%s||%an||%ai" 2>/dev/null)
elif [ -n "$to_tag" ]; then
local commits=$(git log "$to_tag" --oneline --no-merges --format="%h||%s||%an||%ai" 2>/dev/null)
else
local commits=$(git log --oneline --no-merges --format="%h||%s||%an||%ai" 2>/dev/null)
fi
if [ -z "$commits" ]; then
echo "没有找到提交记录"
return
fi
# 分类提交
declare -A categories
categories["features"]="🚀 新功能"
categories["fixes"]="🐛 修复"
categories["documentation"]="📝 文档"
categories["improvements"]="🔧 改进与优化"
categories["tests"]="🧪 测试"
categories["chores"]="🔨 杂项"
categories["style"]="🎨 样式"
categories["performance"]="⚡ 性能"
categories["other"]="其他"
# 初始化分类数组
for key in "${!categories[@]}"; do
eval "${key}_commits=()"
done
# 分类处理每个提交
while IFS='||' read -r hash msg author date; do
if [ -n "$hash" ]; then
local category=$(classify_commit "$msg")
local formatted_msg=$(format_commit "$msg" "$author" "$hash")
eval "${category}_commits+=(\"$formatted_msg\")"
fi
done <<< "$commits"
# 写入变更日志
{
echo ""
echo "## [$version]$date_range"
echo ""
# 按类别输出
for key in features fixes documentation improvements tests chores style performance other; do
eval "local count=\${#${key}_commits[@]}"
if [ "$count" -gt 0 ]; then
echo "### ${categories[$key]}"
echo ""
eval "for commit in \"\${${key}_commits[@]}\"; do echo \"\$commit\"; done"
echo ""
fi
done
} >> "$output_file"
}
# 主函数
main() {
local output_file="${1:-CHANGELOG.md}"
local start_tag="$2"
local end_tag="$3"
echo -e "${BLUE}生成变更日志...${NC}"
# 创建或清空输出文件
> "$output_file"
# 写入文件头
{
echo "# 变更日志"
echo ""
echo "项目变更记录"
echo ""
echo "---"
echo ""
} > "$output_file"
if [ -n "$start_tag" ] && [ -n "$end_tag" ]; then
# 生成两个标签之间的变更日志
echo -e "${GREEN}生成标签 $start_tag 到 $end_tag 的变更日志${NC}"
generate_version_changelog "$start_tag" "$end_tag" "$output_file"
elif [ -n "$end_tag" ]; then
# 生成从开始到指定标签的变更日志
echo -e "${GREEN}生成标签 $end_tag 的变更日志${NC}"
generate_version_changelog "" "$end_tag" "$output_file"
else
# 生成所有标签的变更日志
echo -e "${GREEN}生成Git仓库的完整变更日志${NC}"
# 获取所有标签(按版本排序)
local tags=$(git tag --sort=-version:refname)
local prev_tag=""
local first_tag=""
if [ -n "$tags" ]; then
# 处理标签间的变更
for tag in $tags; do
if [ -z "$prev_tag" ]; then
first_tag="$tag"
else
echo -e "${YELLOW}处理标签 $tag 到 $prev_tag${NC}"
generate_version_changelog "$tag" "$prev_tag" "$output_file"
fi
prev_tag="$tag"
done
# 处理第一个标签之前的变更
if [ -n "$first_tag" ]; then
echo -e "${YELLOW}处理初始版本到 $first_tag${NC}"
generate_version_changelog "" "$first_tag" "$output_file"
fi
else
# 没有标签,生成全部变更
echo -e "${YELLOW}未找到标签,生成全部变更日志${NC}"
generate_version_changelog "" "" "$output_file"
fi
fi
echo -e "${GREEN}✓ 变更日志已生成到 $output_file${NC}"
}
# 显示帮助
show_help() {
echo "用法: $0 [选项] [输出文件] [起始标签] [结束标签]"
echo ""
echo "选项:"
echo " -h, --help 显示帮助信息"
echo ""
echo "示例:"
echo " $0 生成完整的变更日志"
echo " $0 CHANGELOG.md 指定输出文件"
echo " $0 CHANGELOG.md v1.0.0 v2.0.0 生成两个版本之间的变更"
echo " $0 CHANGELOG.md '' v1.0.0 生成到指定版本的变更"
}
# 参数解析
case "$1" in
-h|--help)
show_help
exit 0
;;
*)
main "$@"
;;
esac
Python版本(更强大的功能)
#!/usr/bin/env python3
"""
变更日志生成器 - Python版本
自动从Git提交历史生成结构化CHANGELOG.md
"""
import re
import subprocess
import sys
from datetime import datetime
from collections import defaultdict
import os
class ChangelogGenerator:
def __init__(self, repo_path="."):
self.repo_path = repo_path
self.categories = {
'features': '🚀 新功能',
'fixes': '🐛 修复',
'documentation': '📝 文档',
'improvements': '🔧 改进与优化',
'tests': '🧪 测试',
'chores': '🔨 杂项',
'style': '🎨 样式',
'performance': '⚡ 性能',
'breaking': '💥 破坏性变更',
'other': '其他'
}
def run_git_command(self, command):
"""运行Git命令并返回输出"""
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
cwd=self.repo_path
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Git命令失败: {e}")
return ""
def get_tags(self):
"""获取所有标签"""
tags = self.run_git_command("git tag --sort=-version:refname")
return tags.split('\n') if tags else []
def classify_commit(self, message):
"""分类提交信息"""
# 检出破坏性变更
if re.search(r'breaking|BREAKING|!:', message):
return 'breaking'
# 使用Conventional Commits标准
type_map = {
'feat': 'features',
'feature': 'features',
'fix': 'fixes',
'bugfix': 'fixes',
'docs': 'documentation',
'doc': 'documentation',
'refactor': 'improvements',
'improve': 'improvements',
'enhance': 'improvements',
'optimize': 'improvements',
'test': 'tests',
'testing': 'tests',
'chore': 'chores',
'build': 'chores',
'ci': 'chores',
'style': 'style',
'perf': 'performance',
'performance': 'performance',
'add': 'features',
'new': 'features',
'implement': 'features',
'remove': 'breaking',
'delete': 'breaking',
}
commit_type = re.match(r'^(\w+)', message.lower())
if commit_type:
return type_map.get(commit_type.group(1), 'other')
return 'other'
def format_commit(self, hash, message, author):
"""格式化提交信息"""
# 移除前缀
formatted = re.sub(
r'^(feat|fix|docs|refactor|test|chore|style|perf)(\([^)]+\))?:\s*',
'',
message,
flags=re.IGNORECASE
)
# 移除尾随的空格
formatted = formatted.strip()
# 生成链接(请替换为实际的仓库URL)
repo_url = self.get_repo_url()
commit_url = f"{repo_url}/commit/{hash}" if repo_url else f"#{hash}"
return f"- {formatted} ([{hash[:7]}]({commit_url}))"
def get_repo_url(self):
"""获取仓库URL"""
remote = self.run_git_command("git remote get-url origin")
if remote:
# 转换SSH格式到HTTPS
remote = re.sub(r'git@([^:]+):', r'https://\1/', remote)
remote = re.sub(r'\.git$', '', remote)
return remote
return ""
def get_commits_between(self, from_tag=None, to_tag=None):
"""获取两个标签之间的提交"""
if from_tag and to_tag:
cmd = f"git log {from_tag}..{to_tag} --oneline --no-merges --format='%h||%s||%an||%ai'"
elif to_tag:
cmd = f"git log {to_tag} --oneline --no-merges --format='%h||%s||%an||%ai'"
else:
cmd = "git log --oneline --no-merges --format='%h||%s||%an||%ai'"
result = self.run_git_command(cmd)
commits = []
for line in result.split('\n'):
if line:
parts = line.split('||')
if len(parts) >= 4:
commits.append({
'hash': parts[0],
'message': parts[1],
'author': parts[2],
'date': parts[3]
})
return commits
def generate_version_section(self, version, commits, date=None):
"""生成单个版本的变更日志部分"""
section = []
# 版本标题
if date:
section.append(f"\n## [{version}] - {date}\n")
else:
section.append(f"\n## [{version}]\n")
# 分类提交
classified = defaultdict(list)
for commit in commits:
category = self.classify_commit(commit['message'])
formatted = self.format_commit(
commit['hash'],
commit['message'],
commit['author']
)
classified[category].append(formatted)
# 按类别输出
for category, label in self.categories.items():
if category in classified and classified[category]:
section.append(f"\n### {label}\n")
for commit in classified[category]:
section.append(commit)
section.append("")
return '\n'.join(section)
def generate(self, output_file="CHANGELOG.md"):
"""生成完整的变更日志"""
# 获取所有标签
tags = self.get_tags()
# 初始化变更日志内容
changelog = [
"# 变更日志\n",
"本项目所有值得注意的变更都将记录在此文件中。\n",
"格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),",
"版本遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。\n",
"---\n"
]
# 处理标签之间的提交
prev_tag = None
all_commits_processed = set()
if tags:
# 最新版本
latest_commits = self.get_commits_between(to_tag=tags[0])
if latest_commits:
date = latest_commits[0]['date'][:10]
version_section = self.generate_version_section(
tags[0], latest_commits, date
)
changelog.append(version_section)
all_commits_processed.update(
c['hash'] for c in latest_commits
)
# 中间版本
for i in range(len(tags) - 1):
from_tag = tags[i + 1]
to_tag = tags[i]
version_commits = self.get_commits_between(from_tag, to_tag)
# 过滤已处理的提交
new_commits = [
c for c in version_commits
if c['hash'] not in all_commits_processed
]
if new_commits:
date = new_commits[0]['date'][:10]
version_section = self.generate_version_section(
to_tag, new_commits, date
)
changelog.append(version_section)
all_commits_processed.update(
c['hash'] for c in new_commits
)
# 初始版本
initial_commits = self.get_commits_between(to_tag=tags[-1])
# 过滤已处理的
new_initial = [
c for c in initial_commits
if c['hash'] not in all_commits_processed
]
if new_initial:
version_section = self.generate_version_section(
"初始版本", new_initial
)
changelog.append(version_section)
else:
# 没有标签,处理所有提交
all_commits = self.get_commits_between()
if all_commits:
version_section = self.generate_version_section(
"Unreleased", all_commits
)
changelog.append(version_section)
# 写入文件
with open(output_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(changelog))
print(f"✓ 变更日志已生成到 {output_file}")
return output_file
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(
description='生成Git仓库的变更日志'
)
parser.add_argument(
'-o', '--output',
default='CHANGELOG.md',
help='输出文件路径 (默认: CHANGELOG.md)'
)
parser.add_argument(
'-p', '--path',
default='.',
help='Git仓库路径 (默认: 当前目录)'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='显示详细输出'
)
args = parser.parse_args()
generator = ChangelogGenerator(args.path)
if args.verbose:
print(f"生成变更日志...")
print(f"仓库路径: {args.path}")
print(f"输出文件: {args.output}")
generator.generate(args.output)
if __name__ == "__main__":
main()
使用说明
基本使用
# 设置执行权限 chmod +x changelog.sh # 生成完整的变更日志 ./changelog.sh # 指定输出文件名 ./changelog.sh HISTORY.md # 生成两个版本之间的变更 ./changelog.sh CHANGELOG.md v1.0.0 v2.0.0 # Python版本 python3 changelog.py python3 changelog.py -o CHANGELOG.md -v
提交信息规范建议
为了获得最佳效果,建议使用 Conventional Commits 规范:
feat: 新功能 fix: 修复bug docs: 文档变更 style: 样式调整 refactor: 代码重构 test: 测试相关 chore: 构建/配置变更 perf: 性能优化 BREAKING CHANGE: 破坏性变更
Git钩子集成
可以将其添加到 post-commit 钩子中自动更新:
#!/bin/bash # .git/hooks/post-commit # 自动更新变更日志 /path/to/changelog.sh
这个脚本会自动分析Git历史,按版本和类型分类提交,生成结构清晰、美观的变更日志文档。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播