auto-version-tool
Version:
根据git commit历史自动修改版本号并生成changelog的CLI工具 (Automatically bump version & generate changelog based on git commits)
265 lines (252 loc) • 9.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangelogService = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const mustache_1 = __importDefault(require("mustache"));
class ChangelogService {
constructor(config) {
this.config = config;
}
async generateChangelog(versionInfo, commits) {
const changelogEntry = this.createChangelogEntry(versionInfo, commits);
return this.renderChangelogEntry(changelogEntry);
}
async updateChangelog(versionInfo, commits) {
const changelogPath = path.resolve(this.config.changelog.outputFile);
const newEntry = await this.generateChangelog(versionInfo, commits);
// 读取现有的changelog
let existingContent = '';
if (fs.existsSync(changelogPath)) {
existingContent = fs.readFileSync(changelogPath, 'utf-8');
}
// 生成新的changelog内容
const updatedContent = this.mergeChangelog(newEntry, existingContent);
// 写入文件
fs.writeFileSync(changelogPath, updatedContent, 'utf-8');
console.log(`✅ 已更新 changelog: ${changelogPath}`);
}
createChangelogEntry(versionInfo, commits) {
const filteredCommits = this.filterCommits(commits);
return {
version: versionInfo.next,
date: new Date().toISOString().split('T')[0],
commits: filteredCommits,
breaking: filteredCommits.filter(c => c.breaking),
features: filteredCommits.filter(c => c.type === 'feat'),
fixes: filteredCommits.filter(c => c.type === 'fix'),
others: filteredCommits.filter(c => c.type && !['feat', 'fix'].includes(c.type) && !c.breaking)
};
}
filterCommits(commits) {
return commits.filter(commit => {
// 过滤掉不需要的提交类型
if (!commit.type)
return false;
return this.config.changelog.includeTypes.includes(commit.type);
});
}
renderChangelogEntry(entry) {
const template = this.getTemplate();
const data = {
version: entry.version,
date: entry.date,
hasBreaking: entry.breaking.length > 0,
breaking: entry.breaking.map(this.formatCommit.bind(this)),
hasFeatures: entry.features.length > 0,
features: entry.features.map(this.formatCommit.bind(this)),
hasFixes: entry.fixes.length > 0,
fixes: entry.fixes.map(this.formatCommit.bind(this)),
hasOthers: entry.others.length > 0,
others: this.groupOtherCommits(entry.others)
};
return mustache_1.default.render(template, data);
}
formatCommit(commit) {
const shortHash = commit.hash.substring(0, 7);
const scope = commit.scope ? `**${commit.scope}**: ` : '';
const subject = commit.subject || commit.message.split('\n')[0];
return {
hash: commit.hash,
shortHash,
scope,
subject,
scopeDisplay: commit.scope,
message: `${scope}${subject}`,
author: commit.author,
type: commit.type,
typeTitle: this.getTypeTitle(commit.type),
emoji: this.getTypeEmoji(commit.type)
};
}
groupOtherCommits(commits) {
if (this.config.changelog.groupBy === 'none') {
return commits.map(this.formatCommit.bind(this));
}
const grouped = {};
commits.forEach(commit => {
const key = this.config.changelog.groupBy === 'type'
? commit.type || 'other'
: commit.scope || 'other';
if (!grouped[key]) {
grouped[key] = [];
}
grouped[key].push(commit);
});
return Object.entries(grouped).map(([key, commits]) => ({
title: this.getGroupTitle(key),
commits: commits.map(this.formatCommit.bind(this))
}));
}
getTypeTitle(type) {
if (!type)
return 'Other';
const typeConfig = this.config.commitTypes[type];
return typeConfig?.title || type.charAt(0).toUpperCase() + type.slice(1);
}
getTypeEmoji(type) {
if (!type)
return '';
const typeConfig = this.config.commitTypes[type];
return typeConfig?.emoji || '';
}
getGroupTitle(group) {
if (this.config.changelog.groupBy === 'type') {
return this.getTypeTitle(group);
}
return group.charAt(0).toUpperCase() + group.slice(1);
}
getTemplate() {
// 如果配置了自定义模板文件,读取它
if (this.config.changelog.template && fs.existsSync(this.config.changelog.template)) {
return fs.readFileSync(this.config.changelog.template, 'utf-8');
}
// 使用默认模板
return this.getDefaultTemplate();
}
getDefaultTemplate() {
return `## [{{version}}]({{compareUrl}}) ({{date}})
{{#hasBreaking}}
### ⚠ BREAKING CHANGES
{{#breaking}}
* {{emoji}} {{message}} ([{{shortHash}}]({{commitUrl}}))
{{/breaking}}
{{/hasBreaking}}
{{#hasFeatures}}
### ✨ Features
{{#features}}
* {{emoji}} {{message}} ([{{shortHash}}]({{commitUrl}}))
{{/features}}
{{/hasFeatures}}
{{#hasFixes}}
### 🐛 Bug Fixes
{{#fixes}}
* {{emoji}} {{message}} ([{{shortHash}}]({{commitUrl}}))
{{/fixes}}
{{/hasFixes}}
{{#hasOthers}}
### 📦 Other Changes
{{#others}}
{{#title}}
#### {{title}}
{{/title}}
{{#commits}}
* {{emoji}} {{message}} ([{{shortHash}}]({{commitUrl}}))
{{/commits}}
{{/others}}
{{/hasOthers}}
`;
}
mergeChangelog(newEntry, existingContent) {
if (!existingContent.trim()) {
return this.createInitialChangelog(newEntry);
}
// 找到第一个版本标题的位置
const versionHeaderRegex = /^## \[.*?\]/m;
const match = existingContent.match(versionHeaderRegex);
if (match && match.index !== undefined) {
// 在第一个版本之前插入新条目
const beforeFirstVersion = existingContent.substring(0, match.index);
const afterFirstVersion = existingContent.substring(match.index);
return beforeFirstVersion + newEntry + '\n' + afterFirstVersion;
}
else {
// 如果没有找到版本标题,说明是空的changelog
return this.createInitialChangelog(newEntry);
}
}
createInitialChangelog(firstEntry) {
return `# 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.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
${firstEntry}`;
}
async getChangelogHistory() {
const changelogPath = path.resolve(this.config.changelog.outputFile);
if (!fs.existsSync(changelogPath)) {
return [];
}
const content = fs.readFileSync(changelogPath, 'utf-8');
return this.parseChangelog(content);
}
parseChangelog(content) {
// 这里可以实现changelog解析逻辑
// 暂时返回空数组
return [];
}
async validateChangelog() {
try {
const changelogPath = path.resolve(this.config.changelog.outputFile);
if (!fs.existsSync(changelogPath)) {
return true; // 文件不存在时视为有效
}
const content = fs.readFileSync(changelogPath, 'utf-8');
// 基本验证:检查是否有有效的markdown格式
const hasHeader = content.includes('# Changelog') || content.includes('# CHANGELOG');
const hasVersions = /^## \[.*?\]/.test(content);
return hasHeader || hasVersions;
}
catch {
return false;
}
}
}
exports.ChangelogService = ChangelogService;
//# sourceMappingURL=ChangelogService.js.map