diff --git a/.gitea/ci/render_stats_svgs.py b/.gitea/ci/render_stats_svgs.py new file mode 100644 index 0000000..08a1f2b --- /dev/null +++ b/.gitea/ci/render_stats_svgs.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 + +import argparse +import unicodedata +from dataclasses import dataclass +from pathlib import Path +from typing import List + + +LABEL_BG = "#555555" +DEFAULT_TOTAL_COLOR = "#1f6feb" +DEFAULT_FILES_COLOR = "#2da44e" +DEFAULT_LANGUAGE_COUNT_COLOR = "#8250df" +TEXT_COLOR = "#ffffff" + + +@dataclass +class LanguageStat: + lang_id: str + display_name: str + code_lines: int + formatted_lines: str + file_count: int + color: str + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--output-dir", required=True) + parser.add_argument("--readme-path", required=True) + parser.add_argument("--repo", required=True) + parser.add_argument("--generated-by", required=True) + parser.add_argument("--updated-at", required=True) + parser.add_argument("--total-color", default=DEFAULT_TOTAL_COLOR) + parser.add_argument("--files-color", default=DEFAULT_FILES_COLOR) + parser.add_argument("--language-count-color", default=DEFAULT_LANGUAGE_COUNT_COLOR) + parser.add_argument("--total-code", required=True, type=int) + parser.add_argument("--formatted-total-code", required=True) + parser.add_argument("--total-files", required=True, type=int) + parser.add_argument("--formatted-total-files", required=True) + parser.add_argument("--language-count", required=True, type=int) + parser.add_argument("--exclude-dirs", required=True) + parser.add_argument("--min-lines-threshold", required=True, type=int) + parser.add_argument("--lang-summary", required=True) + return parser.parse_args() + + +def escape_xml(value: str) -> str: + return ( + value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + .replace("'", "'") + ) + + +def display_units(value: str) -> int: + units = 0 + for char in value: + if unicodedata.east_asian_width(char) in {"F", "W"}: + units += 2 + else: + units += 1 + return units + + +def text_width(value: str) -> int: + return display_units(value) * 7 + 20 + + +def normalize_color(value: str, fallback: str) -> str: + if not value: + return fallback + return value if value.startswith("#") else f"#{value}" + + +def parse_language_summary(path: Path) -> List[LanguageStat]: + stats: List[LanguageStat] = [] + + if not path.exists(): + return stats + + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line: + continue + + parts = line.split("|") + if len(parts) < 6: + continue + + while len(parts) < 7: + parts.append("") + + lang_id, display_name, code_lines, formatted_lines, file_count, color, _icon = parts[:7] + stats.append( + LanguageStat( + lang_id=lang_id, + display_name=display_name, + code_lines=int(code_lines), + formatted_lines=formatted_lines, + file_count=int(file_count), + color=normalize_color(color, DEFAULT_TOTAL_COLOR), + ) + ) + + return stats + + +def badge_svg(label: str, message: str, color: str) -> str: + height = 20 + left_width = max(46, text_width(label)) + right_width = max(46, text_width(message)) + total_width = left_width + right_width + left_text_x = left_width / 2 + right_text_x = left_width + right_width / 2 + + return f""" +""" + + +def write_badge(path: Path, label: str, message: str, color: str) -> None: + path.write_text(badge_svg(label, message, color), encoding="utf-8") + + +def render_readme(args: argparse.Namespace, languages: List[LanguageStat], badge_dir_name: str) -> str: + language_rows = [] + + for stat in languages: + share = (stat.code_lines * 100 / args.total_code) if args.total_code else 0.0 + language_rows.append( + f"| **{stat.display_name}** | {stat.formatted_lines} | {stat.file_count} | {share:.1f}% | " + f" |" + ) + + if language_rows: + language_table = "\n".join(language_rows) + else: + language_table = "| _未找到符合条件的语言_ | - | - | - | - |" + + return f"""# 📊 代码统计详细报告 + +> 此分支由 {args.generated_by} 自动生成和维护 +> +> ⚠️ **请勿手动修改此分支的内容!** +> +> 🤖 由 {args.generated_by} 自动生成 +> +> 📅 更新时间: {args.updated_at} + + + + + +## 📈 总体统计 + +| 统计项 | 数值 | 徽章 | +|--------|------|------| +| 💻 总代码行数 | **{args.formatted_total_code}** 行 |  | +| 📁 总文件数 | **{args.formatted_total_files}** 个 |  | +| 🌐 语言种类 | **{args.language_count}** 种 |  | + +## 🌈 语言分布 + +| 语言 | 代码行数 | 文件数 | 占比 | 徽章 | +|------|----------|--------|------|------| +{language_table} + +--- + +## ⚙️ 配置说明 + +### Token 配置 + +- 当前使用: **WORKFLOW** +- 需要在 Settings -> Actions -> Secrets 中配置 `WORKFLOW` + +### 排除规则 + +当前排除的目录: +``` +{args.exclude_dirs} +``` + +### 阈值设置 + +- 最小代码行数阈值: **{args.min_lines_threshold}** 行 +- 低于此阈值的语言不会生成徽章 + +--- + +*📊 统计仓库:`{args.repo}`* +""" + + +def main() -> None: + args = parse_args() + output_dir = Path(args.output_dir) + readme_path = Path(args.readme_path) + + output_dir.mkdir(parents=True, exist_ok=True) + readme_path.parent.mkdir(parents=True, exist_ok=True) + + languages = parse_language_summary(Path(args.lang_summary)) + badge_dir_name = output_dir.name + + write_badge( + output_dir / "total-lines.svg", + "代码", + f"{args.formatted_total_code} 行", + normalize_color(args.total_color, DEFAULT_TOTAL_COLOR), + ) + write_badge( + output_dir / "total-files.svg", + "文件", + f"{args.formatted_total_files} 个", + normalize_color(args.files_color, DEFAULT_FILES_COLOR), + ) + write_badge( + output_dir / "language-count.svg", + "语言", + f"{args.language_count} 种", + normalize_color(args.language_count_color, DEFAULT_LANGUAGE_COUNT_COLOR), + ) + + for stat in languages: + write_badge(output_dir / f"{stat.lang_id}-lines.svg", stat.display_name, f"{stat.formatted_lines} 行", stat.color) + + readme_path.write_text(render_readme(args, languages, badge_dir_name), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/.gitea/workflows/update_stats_badge.yaml b/.gitea/workflows/update_stats_badge.yaml index fa2c79f..8267e16 100644 --- a/.gitea/workflows/update_stats_badge.yaml +++ b/.gitea/workflows/update_stats_badge.yaml @@ -35,17 +35,12 @@ env: SPECIAL_INCLUDES: "" # 示例: 'dist:js,css|vendor:go,mod' - # ===== 徽章颜色配置 ===== + # ===== SVG 徽章颜色配置 ===== COLOR_TOTAL: "blue" COLOR_FILES: "green" - COLOR_DEFAULT: "brightgreen" - - # ===== 徽章样式配置 ===== - BADGE_STYLE: "flat" # 可选: flat, flat-square, plastic, for-the-badge, social + COLOR_LANGUAGE_COUNT: "purple" # ===== 输出配置 ===== - # 是否生成详细报告 - GENERATE_DETAILED_REPORT: "true" # 是否输出到 workflow summary OUTPUT_TO_SUMMARY: "true" # 最小代码行数阈值(低于此值的语言不生成徽章) @@ -55,18 +50,6 @@ env: GIT_USER_NAME: "ci[bot]" GIT_USER_EMAIL: "ci[bot]@tinysoft.com.cn" - # ===== 平台配置 ===== - # 平台类型: github 或 gitea - PLATFORM: "gitea" - # Git 服务器 URL(Gitea 示例: https://gitea.example.com) - GIT_SERVER_URL: "${{ github.server_url }}" - # 仓库路径(格式: owner/repo) - REPO_PATH: "${{ github.repository }}" - # Raw 文件基础 URL - # GitHub: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path} - # Gitea: https://gitea.example.com/{owner}/{repo}/raw/branch/{branch}/{path} - RAW_URL_BASE: '${{ github.server_url }}/${{ github.repository }}/raw/branch' - # ========================================== # 🎨 语言分组配置 # 格式: 组名:后缀列表:显示名称:颜色:图标(可选) @@ -363,19 +346,19 @@ jobs: cat > README.md << 'EOF' # 📊 代码统计徽章数据 - > 此分支由 GitHub Actions 自动生成和维护 + > 此分支由 Gitea Actions 自动生成和维护 > > ⚠️ **请勿手动修改此分支的内容!** ## 📁 目录结构 ``` + README.md # 详细统计报告 badges/ - ├── total-lines.json # 总代码行数徽章 - ├── total-files.json # 总文件数徽章 - ├── {language}-lines.json # 各语言代码行数徽章 - ├── {language}-files.json # 各语言文件数徽章 - └── README.md # 详细统计报告 + ├── total-lines.svg # 总代码行数徽章 + ├── total-files.svg # 总文件数徽章 + ├── language-count.svg # 语言种类徽章 + └── {language}-lines.svg # 各语言代码行数徽章 ``` ## 🔄 更新机制 @@ -389,7 +372,7 @@ jobs: --- - *🤖 由 GitHub Actions 自动维护* + *🤖 由 Gitea Actions 自动维护* EOF # 创建徽章目录 @@ -555,212 +538,50 @@ jobs: echo "======================================" echo "" - - name: 🎨 生成徽章数据 + - name: 🎨 生成 SVG 徽章与统计报告 id: generate_badges run: | echo "======================================" - echo "🎨 生成徽章数据" + echo "🎨 生成 SVG 徽章与统计报告" echo "======================================" cd "${{ env.REPO_DIR }}" - # 确保在统计分支 + # 在主分支渲染产物,避免依赖 stats 分支中的脚本文件 + git checkout ${{ github.ref_name }} + + RENDER_ROOT=$(mktemp -d) + UPDATED_AT=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + + python3 .gitea/ci/render_stats_svgs.py \ + --output-dir "$RENDER_ROOT/${{ env.BADGE_DIR }}" \ + --readme-path "$RENDER_ROOT/README.md" \ + --repo "${{ github.repository }}" \ + --generated-by "Gitea Actions" \ + --updated-at "$UPDATED_AT" \ + --total-color "${{ env.COLOR_TOTAL }}" \ + --files-color "${{ env.COLOR_FILES }}" \ + --language-count-color "${{ env.COLOR_LANGUAGE_COUNT }}" \ + --total-code "${{ steps.total.outputs.total_code }}" \ + --formatted-total-code "${{ steps.total.outputs.formatted_code }}" \ + --total-files "${{ steps.total.outputs.total_files }}" \ + --formatted-total-files "${{ steps.total.outputs.formatted_files }}" \ + --language-count "${{ steps.languages.outputs.language_count }}" \ + --exclude-dirs "${{ env.EXCLUDE_DIRS }}" \ + --min-lines-threshold "${{ env.MIN_LINES_THRESHOLD }}" \ + --lang-summary /tmp/lang_summary.txt + git checkout ${{ env.BADGE_BRANCH }} - - # 确保徽章目录存在 - mkdir -p ${{ env.BADGE_DIR }} - - GENERATED_COUNT=0 - - # 生成总代码行数徽章 - echo "📊 生成总代码行数徽章..." - cat > ${{ env.BADGE_DIR }}/total-lines.json << EOF - { - "schemaVersion": 1, - "label": "代码", - "message": "${{ steps.total.outputs.formatted_code }} 行", - "color": "${{ env.COLOR_TOTAL }}", - "style": "${{ env.BADGE_STYLE }}" - } - EOF - GENERATED_COUNT=$((GENERATED_COUNT + 1)) - - # 生成总文件数徽章 - echo "📁 生成总文件数徽章..." - cat > ${{ env.BADGE_DIR }}/total-files.json << EOF - { - "schemaVersion": 1, - "label": "文件", - "message": "${{ steps.total.outputs.formatted_files }} 个", - "color": "${{ env.COLOR_FILES }}", - "style": "${{ env.BADGE_STYLE }}" - } - EOF - GENERATED_COUNT=$((GENERATED_COUNT + 1)) - - # 生成各语言徽章 - if [ -f /tmp/lang_summary.txt ] && [ -s /tmp/lang_summary.txt ]; then - echo "" - echo "🌐 生成语言徽章..." - - while IFS='|' read -r lang_id display_name code_lines formatted_lines file_count color icon; do - [ -z "$lang_id" ] && continue - - echo " - $display_name" - - # 生成代码行数徽章(使用 heredoc) - if [ -n "$icon" ]; then - # 带图标的徽章 - cat > ${{ env.BADGE_DIR }}/${lang_id}-lines.json << EOFJSON - { - "schemaVersion": 1, - "label": "$display_name", - "message": "$formatted_lines 行", - "color": "$color", - "style": "${{ env.BADGE_STYLE }}", - "namedLogo": "$icon" - } - EOFJSON - else - # 不带图标的徽章 - cat > ${{ env.BADGE_DIR }}/${lang_id}-lines.json << EOFJSON - { - "schemaVersion": 1, - "label": "$display_name", - "message": "$formatted_lines 行", - "color": "$color", - "style": "${{ env.BADGE_STYLE }}" - } - EOFJSON - fi - GENERATED_COUNT=$((GENERATED_COUNT + 1)) - - # 生成文件数徽章 - cat > ${{ env.BADGE_DIR }}/${lang_id}-files.json << EOFJSON - { - "schemaVersion": 1, - "label": "$display_name 文件", - "message": "$file_count 个", - "color": "$color", - "style": "${{ env.BADGE_STYLE }}" - } - EOFJSON - GENERATED_COUNT=$((GENERATED_COUNT + 1)) - done < /tmp/lang_summary.txt - fi + rm -rf "${{ env.BADGE_DIR }}" + mkdir -p "${{ env.BADGE_DIR }}" + cp -f "$RENDER_ROOT/README.md" README.md + cp -f "$RENDER_ROOT/${{ env.BADGE_DIR }}"/* "${{ env.BADGE_DIR }}/" + GENERATED_COUNT=$(find "$RENDER_ROOT/${{ env.BADGE_DIR }}" -maxdepth 1 -type f | wc -l | tr -d ' ') + rm -rf "$RENDER_ROOT" echo "" echo "generated_count=$GENERATED_COUNT" >> $GITHUB_OUTPUT - echo "✅ 已生成 $GENERATED_COUNT 个徽章" - echo "======================================" - echo "" - - - name: 📝 生成详细统计报告 - if: env.GENERATE_DETAILED_REPORT == 'true' - run: | - echo "======================================" - echo "📝 生成详细统计报告" - echo "======================================" - - cd "${{ env.REPO_DIR }}" - - # 确保在统计分支 - git checkout ${{ env.BADGE_BRANCH }} - - # 生成 README - cat > ${{ env.BADGE_DIR }}/README.md << 'EOFMD' - # 📊 代码统计详细报告 - - > 🤖 由 GitHub Actions 自动生成 - > - > 📅 更新时间: TIMESTAMP_PLACEHOLDER - - ## 📈 总体统计 - - | 统计项 | 数值 | 徽章 | - |--------|------|------| - | 💻 总代码行数 | **TOTAL_CODE_PLACEHOLDER** 行 |  | - | 📁 总文件数 | **TOTAL_FILES_PLACEHOLDER** 个 |  | - | 🌐 语言种类 | **LANG_COUNT_PLACEHOLDER** 种 | - | - - ## 🌈 语言分布 - - EOFMD - - # 替换占位符 - sed -i "s|TIMESTAMP_PLACEHOLDER|$(date -u '+%Y-%m-%d %H:%M:%S UTC')|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|TOTAL_CODE_PLACEHOLDER|${{ steps.total.outputs.formatted_code }}|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|TOTAL_FILES_PLACEHOLDER|${{ steps.total.outputs.formatted_files }}|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|LANG_COUNT_PLACEHOLDER|${{ steps.languages.outputs.language_count }}|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|REPO_PLACEHOLDER|${{ github.repository }}|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|BRANCH_PLACEHOLDER|${{ env.BADGE_BRANCH }}|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|BADGE_DIR_PLACEHOLDER|${{ env.BADGE_DIR }}|g" ${{ env.BADGE_DIR }}/README.md - sed -i "s|RAW_URL_PLACEHOLDER|${{ env.RAW_URL_BASE }}|g" ${{ env.BADGE_DIR }}/README.md - - # 添加语言统计表格 - if [ -f /tmp/lang_summary.txt ] && [ -s /tmp/lang_summary.txt ]; then - cat >> ${{ env.BADGE_DIR }}/README.md << 'EOFTABLE' - | 语言 | 代码行数 | 文件数 | 占比 | 徽章 | - |------|----------|--------|------|------| - EOFTABLE - - TOTAL_CODE=${{ steps.total.outputs.total_code }} - while IFS='|' read -r lang_id display_name code_lines formatted_lines file_count color icon; do - [ -z "$lang_id" ] && continue - - if [ "$TOTAL_CODE" -gt 0 ]; then - PERCENT=$(echo "scale=1; $code_lines * 100 / $TOTAL_CODE" | bc) - else - PERCENT="0.0" - fi - - cat >> ${{ env.BADGE_DIR }}/README.md << EOFLANG - | **${display_name}** | ${formatted_lines} | ${file_count} | ${PERCENT}% |  | - EOFLANG - done < /tmp/lang_summary.txt - fi - - # 添加配置说明 - cat >> ${{ env.BADGE_DIR }}/README.md << EOFMD - - --- - - ## ⚙️ 配置说明 - - ### Token 配置 - - - 当前使用: **${{ steps.validate_token.outputs.token_type }}** - - 需要在 Settings -> Secrets 中配置 `WORKFLOW` - - ### 排除规则 - - 当前排除的目录: - ``` - ${{ env.EXCLUDE_DIRS }} - ``` - - ### 阈值设置 - - - 最小代码行数阈值: **${{ env.MIN_LINES_THRESHOLD }}** 行 - - 低于此阈值的语言将不会生成徽章 - - ### 徽章样式 - - - 当前样式: **${{ env.BADGE_STYLE }}** - - 可选样式: flat, flat-square, plastic, for-the-badge, social - - --- - -