forked from Gitlink/gitlink-cli
122 lines
5.1 KiB
JavaScript
122 lines
5.1 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const dir = process.argv[2] || '_output';
|
||
const lastTag = process.argv[3] || 'v0.0.0';
|
||
|
||
// 读提交日志(优先 git log,后备 API JSON)
|
||
const logFile = path.join(dir, `commits-since-${lastTag}.log`);
|
||
const statsFile = path.join(dir, `stats-since-${lastTag}.log`);
|
||
const apiFile = path.join(dir, 'commits-api.json');
|
||
|
||
let commits = [];
|
||
if (fs.existsSync(logFile)) {
|
||
const content = fs.readFileSync(logFile, 'utf8');
|
||
commits = content.trim().split('\n').filter(Boolean).map(line => {
|
||
const parts = line.split('|');
|
||
return { sha: parts[0] || '', msg: parts[1] || '', author: parts[2] || '', date: parts[3] || '' };
|
||
});
|
||
console.log('✓ 从 git log 读取 ' + commits.length + ' 条提交');
|
||
} else if (fs.existsSync(apiFile)) {
|
||
try {
|
||
const raw = JSON.parse(fs.readFileSync(apiFile, 'utf8'));
|
||
const list = raw.data && raw.data.commits ? raw.data.commits : (raw.data || []);
|
||
commits = list.map(c => ({
|
||
sha: c.sha || '',
|
||
msg: (c.commit_message || '').replace(/\n.*$/, '').trim(),
|
||
author: (c.author && c.author.login) || (c.committer && c.committer.login) || '',
|
||
date: c.commit_time ? new Date(c.commit_time * 1000).toISOString().slice(0, 10) : ''
|
||
}));
|
||
console.log('✓ 从 API 读取 ' + commits.length + ' 条提交');
|
||
} catch(e) {
|
||
console.log('⚠ API 数据解析失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 按 Conventional Commits 分类
|
||
const categories = { feat: [], fix: [], docs: [], refactor: [], perf: [], test: [], chore: [], revert: [], other: [] };
|
||
const typeLabels = {
|
||
feat: '✨ 新功能', fix: '🐛 Bug 修复', docs: '📝 文档',
|
||
refactor: '♻️ 代码重构', perf: '⚡ 性能优化', test: '✅ 测试',
|
||
chore: '🔧 工程配置', revert: '⏪ 回退', other: '📦 其他变更'
|
||
};
|
||
|
||
commits.forEach(c => {
|
||
const m = c.msg.match(/^(\w+)(\([^)]+\))?:/);
|
||
const type = m ? m[1] : 'other';
|
||
if (categories[type]) categories[type].push(c);
|
||
else categories.other.push(c);
|
||
});
|
||
|
||
const totalCommits = commits.length;
|
||
const authors = new Set(commits.map(c => c.author).filter(Boolean));
|
||
const contributorCount = authors.size;
|
||
|
||
// 统计各类型数量
|
||
const counts = {};
|
||
Object.keys(categories).forEach(k => counts[k] = categories[k].length);
|
||
const classifiedTotal = Object.values(counts).reduce((a, b) => a + b, 0);
|
||
|
||
// 统计文件变更(兼容中英文 Git 输出)
|
||
let totalFiles = 0, additions = 0, deletions = 0;
|
||
if (fs.existsSync(statsFile)) {
|
||
const stats = fs.readFileSync(statsFile, 'utf8');
|
||
const lines = stats.split('\n');
|
||
lines.forEach(line => {
|
||
// 匹配 "X file(s) changed" 或 "X 个文件已更改"
|
||
const fm = line.match(/(\d+)\s*(file|个文件)/i);
|
||
if (fm) totalFiles += parseInt(fm[1]) || 0;
|
||
// 匹配 "Y insertion" 或 "Y 行插入"
|
||
const am = line.match(/(\d+)\s*(insertion|行插入)/i);
|
||
if (am) additions += parseInt(am[1]) || 0;
|
||
// 匹配 "Z deletion" 或 "Z 行删除"
|
||
const dm = line.match(/(\d+)\s*(deletion|行删除)/i);
|
||
if (dm) deletions += parseInt(dm[1]) || 0;
|
||
});
|
||
}
|
||
|
||
// 语义化版本推荐
|
||
const hasBreaking = commits.some(c => c.msg.includes('BREAKING CHANGE') || c.msg.includes('!'));
|
||
const hasFeat = counts.feat > 0;
|
||
const oldVer = lastTag.match(/v?(\d+)\.(\d+)\.(\d+)/);
|
||
let newVer = 'v0.1.0';
|
||
if (oldVer) {
|
||
const [_, major, minor, patch] = oldVer.map(Number);
|
||
if (hasBreaking) newVer = `v${major + 1}.0.0`;
|
||
else if (hasFeat) newVer = `v${major}.${minor + 1}.0`;
|
||
else newVer = `v${major}.${minor}.${patch + 1}`;
|
||
}
|
||
|
||
// 生成 Release Notes
|
||
const sections = ['feat', 'fix', 'docs', 'refactor', 'perf', 'test', 'chore', 'revert', 'other']
|
||
.filter(t => counts[t] > 0)
|
||
.map(t => {
|
||
const items = categories[t].map(c => `- ${c.msg}(@${c.author || 'unknown'})`).join('\n');
|
||
const pct = totalCommits > 0 ? Math.round(counts[t] / totalCommits * 100) : 0;
|
||
return `### ${typeLabels[t]}(${counts[t]})\n\n${items}\n\n**占比:** ${pct}%`;
|
||
}).join('\n\n');
|
||
|
||
const notes = `## ${newVer} (${new Date().toISOString().slice(0, 10)})
|
||
|
||
### 📊 版本概览
|
||
- 基于上一版本:${lastTag}
|
||
- 包含 **${totalCommits}** 次提交
|
||
- 涉及 **${totalFiles || '—'}** 个文件
|
||
- 新增 **${additions || '—'}** 行 / 删除 **${deletions || '—'}** 行
|
||
- 贡献者:**${contributorCount}** 人
|
||
|
||
${totalCommits > 0 ? sections : '(暂无提交记录,请确认 tag 名称正确)'}
|
||
|
||
### 📈 统计汇总
|
||
| 类别 | 数量 | 占比 |
|
||
|------|------|------|
|
||
${Object.entries(typeLabels).map(([k, label]) => `| ${label} | ${counts[k]} | ${totalCommits > 0 ? Math.round(counts[k] / totalCommits * 100) : 0}% |`).join('\n')}
|
||
|
||
---
|
||
*自动生成时间:${new Date().toISOString().slice(0, 19).replace('T', ' ')}*
|
||
`;
|
||
|
||
fs.writeFileSync(path.join(dir, `release-notes-${newVer}.md`), notes, 'utf8');
|
||
console.log('Release Notes 已生成: ' + path.join(dir, `release-notes-${newVer}.md`));
|
||
console.log(`版本推荐: ${lastTag} → ${newVer}(${hasBreaking ? 'BREAKING': hasFeat ? '有新功能': '修复'})`);
|