变更日志生成脚本?

wen 实用脚本 47

我来提供一个功能完善的变更日志生成脚本,这个脚本可以分析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历史,按版本和类型分类提交,生成结构清晰、美观的变更日志文档。

抱歉,评论功能暂时关闭!