taskgenius_taskgenius-plugin/scripts/custom-changelog.mjs

244 lines
6.3 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import { execSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import semver from "semver";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* 自定义 changelog 生成器,合并所有 beta 版本的提交到单个版本条目
*/
export function generateMergedChangelog(targetVersion, options = {}) {
const { dryRun = false } = options;
// 获取上一个正式版本标签
function getLastStableTag() {
try {
const allTags = execSync("git tag -l", { encoding: "utf8" })
.trim()
.split("\n")
.filter(Boolean);
const stableTags = [];
for (const tag of allTags) {
try {
execSync(`git merge-base --is-ancestor ${tag} HEAD`, {
encoding: "utf8",
});
const versionString = tag.replace(/^v/, "");
const version = semver.valid(versionString);
if (version && !semver.prerelease(version)) {
stableTags.push({ tag, version });
}
} catch (e) {
// 标签不在当前分支历史中,跳过
}
}
if (stableTags.length === 0) {
return "HEAD~30";
}
const sortedTags = stableTags.sort((a, b) => {
return semver.rcompare(a.version, b.version);
});
return sortedTags[0].tag;
} catch (error) {
return "HEAD~30";
}
}
const lastStableTag = getLastStableTag();
console.log(
`📦 Generating changelog from ${lastStableTag} to ${targetVersion}`,
);
// 获取所有提交
const rawCommits = execSync(
`git log ${lastStableTag}..HEAD --pretty=format:"%H|||%s|||%b|||%an|||%ae|||%ad" --no-merges`,
{ encoding: "utf8" },
).trim();
const commits = rawCommits ? rawCommits.split("\n").filter(Boolean) : [];
// 按类型分组提交
const groupedCommits = {
Features: [],
"Bug Fixes": [],
Performance: [],
Refactors: [],
Documentation: [],
Styles: [],
Tests: [],
Reverts: [],
"Breaking Changes": [],
};
const typeMap = {
feat: "Features",
fix: "Bug Fixes",
perf: "Performance",
refactor: "Refactors",
docs: "Documentation",
style: "Styles",
test: "Tests",
revert: "Reverts",
};
// 解析提交
commits.forEach((commit) => {
const parts = commit.split("|||");
if (parts.length < 2) return;
const [hash, subject, body] = parts;
if (!subject) return;
// 解析 conventional commit 格式
const match = subject.match(/^(\w+)(?:\(([^)]+)\))?: (.+)$/);
if (!match) return;
const [, type, scope, description] = match;
// 过滤掉 beta release commits
if (
type === "chore" &&
description &&
(description.includes("beta") ||
description.includes("-beta.") ||
description.match(/v\d+\.\d+\.\d+-beta/))
) {
return;
}
// 检查是否有 BREAKING CHANGE
if (body && body.includes("BREAKING CHANGE")) {
groupedCommits["Breaking Changes"].push({
hash: hash.substring(0, 7),
scope,
description,
body,
});
}
const section = typeMap[type];
if (section) {
groupedCommits[section].push({
hash: hash.substring(0, 7),
scope,
description,
body: body?.trim() || "",
});
}
});
// 生成 changelog 内容
const date = new Date().toISOString().split("T")[0];
const compareUrl = `https://github.com/Quorafind/Obsidian-Task-Genius/compare/${lastStableTag}...${targetVersion}`;
let newChangelog = `## [${targetVersion}](${compareUrl}) (${date})\n\n`;
// 按顺序输出各个分组Breaking Changes 优先)
const sectionOrder = [
"Breaking Changes",
"Features",
"Bug Fixes",
"Performance",
"Refactors",
"Documentation",
"Tests",
"Styles",
"Reverts",
];
sectionOrder.forEach((section) => {
const sectionCommits = groupedCommits[section];
if (sectionCommits && sectionCommits.length > 0) {
newChangelog += `### ${section}\n\n`;
sectionCommits.forEach((commit) => {
const commitUrl = `https://github.com/Quorafind/Obsidian-Task-Genius/commit/${commit.hash}`;
const scopePrefix = commit.scope ? `**${commit.scope}:** ` : "";
newChangelog += `* ${scopePrefix}${commit.description} ([${commit.hash}](${commitUrl}))\n`;
// Add commit body as sub-items if present
if (commit.body) {
if (section === "Breaking Changes") {
// Handle BREAKING CHANGE specially
const breakingDetail = commit.body
.split("BREAKING CHANGE:")[1]
?.trim();
if (breakingDetail) {
newChangelog += ` ${breakingDetail}\n`;
}
} else {
// Parse body lines and add as sub-list items
const bodyLines = commit.body
.split("\n")
.map((line) => line.trim())
.filter(
(line) =>
line && !line.startsWith("BREAKING CHANGE"),
);
if (bodyLines.length > 0) {
bodyLines.forEach((line) => {
// Remove leading "- " if present to avoid double bullets
const cleanLine = line.replace(/^[-*]\s*/, "");
if (cleanLine) {
newChangelog += ` - ${cleanLine}\n`;
}
});
}
}
}
});
newChangelog += "\n";
}
});
// 读取现有的 changelog
const changelogPath = path.join(__dirname, "..", "CHANGELOG.md");
let existingChangelog = "";
try {
existingChangelog = readFileSync(changelogPath, "utf8");
} catch (e) {
// 文件不存在,创建新的
existingChangelog =
"# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
}
// 确保不重复添加相同版本
if (existingChangelog.includes(`## [${targetVersion}]`)) {
console.log(
`⚠️ Version ${targetVersion} already exists in CHANGELOG.md`,
);
return existingChangelog;
}
// 插入新的 changelog 到文件开头(在 header 之后)
const header =
"# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
const restContent = existingChangelog.replace(header, "").trim();
const updatedChangelog = header + "\n" + newChangelog + restContent;
if (!dryRun) {
writeFileSync(changelogPath, updatedChangelog);
console.log(`✅ CHANGELOG.md updated with version ${targetVersion}`);
} else {
console.log("📋 Preview (dry-run mode):");
console.log(newChangelog);
}
return updatedChangelog;
}
// 如果直接运行此脚本
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const version = process.argv[2] || "9.8.0";
const dryRun = process.argv.includes("--dry-run");
generateMergedChangelog(version, { dryRun });
}