mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
244 lines
6.3 KiB
JavaScript
Executable file
244 lines
6.3 KiB
JavaScript
Executable file
#!/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 });
|
||
}
|