402 lines
12 KiB
TypeScript
402 lines
12 KiB
TypeScript
import ChangelogData from "./changelogData.ts";
|
|
import { categories, defaultIndentation } from "./definitions.ts";
|
|
import { Category, ChangelogMessage, Commit } from "#types/changelogTypes.ts";
|
|
import { repoLink } from "./definitions.ts";
|
|
import { Octokit } from "@octokit/rest";
|
|
import {
|
|
formatAuthor,
|
|
getIssueURL,
|
|
getNewestCommitAuthors,
|
|
getNewestIssueURLs,
|
|
} from "#utils/util.ts";
|
|
|
|
let data: ChangelogData;
|
|
let octokit: Octokit;
|
|
|
|
// How many lines the changelog (excluding the commit log) can be before the commit log is excluded.
|
|
const sectionLinesBeforeCommitLogExcluded = 50;
|
|
|
|
// How many lines the commit log can be before its is excluded.
|
|
const logLinesBeforeCommitLogExcluded = 20;
|
|
|
|
// How many commits to include after a message.
|
|
const maxIncludeCommits = 3;
|
|
|
|
export default async function pushAll(inputData: ChangelogData): Promise<void> {
|
|
pushTitle(inputData);
|
|
await pushChangelog(inputData);
|
|
}
|
|
|
|
export async function pushSetup(): Promise<void> {
|
|
octokit = new Octokit({
|
|
auth: process.env.GITHUB_TOKEN,
|
|
});
|
|
|
|
// Save Issue/PR Info to Cache
|
|
await getNewestIssueURLs(octokit);
|
|
}
|
|
|
|
export function pushTitle(inputData: ChangelogData): void {
|
|
data = inputData;
|
|
|
|
// Push the titles.
|
|
// Center Align is replaced by the correct center align style in the respective deployments.
|
|
// Must be triple bracketed, to make mustache not html escape it.
|
|
if (data.releaseType === "Cutting Edge Build") {
|
|
const date = new Date().toLocaleDateString("en-us", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
hour12: true,
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
timeZoneName: "short",
|
|
});
|
|
// noinspection HtmlDeprecatedAttribute
|
|
data.builder.push(
|
|
`<h1 align="center">${data.releaseType} (${date})</h1>`,
|
|
"",
|
|
);
|
|
} else {
|
|
// noinspection HtmlUnknownAttribute
|
|
data.builder.push(
|
|
`<h1 {{{ CENTER_ALIGN }}}>${data.releaseType} ${data.to}</h1>`,
|
|
"",
|
|
);
|
|
data.builder.push("{{{ CF_REDIRECT }}}", "");
|
|
}
|
|
}
|
|
|
|
export async function pushChangelog(inputData: ChangelogData): Promise<void> {
|
|
data = inputData;
|
|
|
|
data.builder.push(`# Changes Since ${data.since}`, "");
|
|
|
|
// Push Sections of Changelog
|
|
for (const category of categories) {
|
|
await pushCategory(category);
|
|
}
|
|
|
|
// Push the commit log
|
|
if (data.commitList.length > 0) {
|
|
if (
|
|
data.builder.length < sectionLinesBeforeCommitLogExcluded &&
|
|
data.commitList.length < logLinesBeforeCommitLogExcluded
|
|
) {
|
|
// Commit List is relatively short, and most commits would have been handled via category pushing anyway.
|
|
// Just retrieve each author info sequentially.
|
|
sortCommitList(data.commitList, (commit) => commit);
|
|
|
|
data.builder.push("## Commits");
|
|
for (const commit of data.commitList) {
|
|
data.builder.push(await formatCommit(commit));
|
|
}
|
|
}
|
|
} else {
|
|
// No Commit List = No Changes
|
|
data.builder.push("");
|
|
data.builder.push("**There haven't been any changes.**");
|
|
}
|
|
|
|
// Push link
|
|
data.builder.push(
|
|
"",
|
|
`**Full Changelog**: [\`${data.since}...${data.to}\`](${repoLink}compare/${data.since}...${data.to})`,
|
|
);
|
|
}
|
|
|
|
export function pushSeperator(inputData: ChangelogData): void {
|
|
data = inputData;
|
|
|
|
data.builder.push("", "<hr>", "");
|
|
}
|
|
|
|
/**
|
|
* Pushes a given category to the builders.
|
|
*/
|
|
async function pushCategory(category: Category) {
|
|
const categoryLog: string[] = [];
|
|
let hasValues = false;
|
|
|
|
// Push All Sub Categories
|
|
for (const subCategory of category.subCategories) {
|
|
// Loop through key list instead of map to produce correct order
|
|
const list = category.changelogSection?.get(subCategory);
|
|
if (list && list.length != 0) {
|
|
// Push Key Name (only pushes if Key Name is not "")
|
|
if (subCategory.keyName) {
|
|
categoryLog.push(`### ${subCategory.keyName}:`);
|
|
}
|
|
|
|
// Format Main Messages (Async so Author Fetch is Fast)
|
|
await getNewestCommitAuthors(octokit);
|
|
const formatted: { message: ChangelogMessage; formatted: string }[] =
|
|
await Promise.all(
|
|
list.map((message) =>
|
|
formatChangelogMessage(message).then((formatted) => {
|
|
return { message, formatted };
|
|
}),
|
|
),
|
|
);
|
|
|
|
// Sort Log
|
|
sortCommitList(
|
|
formatted,
|
|
(formatted) => formatted.message.commitObject,
|
|
(a, b) =>
|
|
a.message.commitMessage.localeCompare(b.message.commitMessage),
|
|
);
|
|
|
|
// Push Log
|
|
for (const format of formatted) {
|
|
categoryLog.push(format.formatted);
|
|
// Push Sub Messages (No need for Async, Author Info Not Calculated in Sub Messages)
|
|
if (format.message.subChangelogMessages) {
|
|
for (const subMessage of format.message.subChangelogMessages)
|
|
categoryLog.push(await formatChangelogMessage(subMessage, true));
|
|
}
|
|
}
|
|
categoryLog.push("");
|
|
hasValues = true;
|
|
}
|
|
}
|
|
await transformAllIssueURLs(categoryLog);
|
|
if (hasValues) {
|
|
// Push Title
|
|
data.builder.push(`## ${category.categoryName}:`);
|
|
|
|
// Push previously made log
|
|
data.builder.push(...categoryLog);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sorts a list that contains commit data
|
|
* @param list A list of type T that contains commit data
|
|
* @param transform A function to turn each element of type T into an element of type Commit
|
|
* @param backup A backup sort, to call when either element does not have a commit object, or when the commit objects' times are the same. Optional, if not set, will just return 0 (equal) or will compare commit messages.
|
|
*/
|
|
function sortCommitList<T>(
|
|
list: T[],
|
|
transform: (obj: T) => Commit | undefined,
|
|
backup?: (a: T, b: T) => number,
|
|
) {
|
|
list.sort((a, b): number => {
|
|
const commitA = transform(a);
|
|
const commitB = transform(b);
|
|
if (!commitA || !commitB) {
|
|
// If either commit is undefined
|
|
if (backup) return backup(a, b);
|
|
return 0;
|
|
}
|
|
const dateA = new Date(commitA.date);
|
|
const dateB = new Date(commitB.date);
|
|
|
|
// This is reversed, so higher priorities go on top
|
|
if (commitB.priority !== commitA.priority)
|
|
return (commitB.priority ?? 0) - (commitA.priority ?? 0);
|
|
// This is reversed, so the newest commits go on top
|
|
if (dateB.getTime() - dateA.getTime() !== 0)
|
|
return dateB.getTime() - dateA.getTime();
|
|
if (backup) return backup(a, b);
|
|
return commitA.message.localeCompare(commitB.message);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sorts a commits list so that newest commits are on the bottom.
|
|
* @param list The commit list.
|
|
*/
|
|
export function sortCommitListReverse(list: Commit[]): void {
|
|
list.sort((a, b) => {
|
|
const dateA = new Date(a.date);
|
|
const dateB = new Date(b.date);
|
|
|
|
// This is reversed, so higher priorities go on top
|
|
if (b.priority !== a.priority) return (b.priority ?? 0) - (a.priority ?? 0); // Priority is still highest first
|
|
if (dateA.getTime() - dateB.getTime() !== 0)
|
|
return dateA.getTime() - dateB.getTime();
|
|
return a.message.localeCompare(b.message);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Formats a Changelog Message
|
|
* @param changelogMessage The message to format.
|
|
* @param subMessage Whether this message is a subMessage (used in details). Set to true to make it a subMessage (different parsing). Defaults to false.
|
|
* @return string Formatted Changelog Message
|
|
*/
|
|
async function formatChangelogMessage(
|
|
changelogMessage: ChangelogMessage,
|
|
subMessage = false,
|
|
): Promise<string> {
|
|
const indentation =
|
|
changelogMessage.indentation == undefined
|
|
? defaultIndentation
|
|
: changelogMessage.indentation;
|
|
const message = changelogMessage.commitMessage.trim();
|
|
|
|
if (changelogMessage.specialFormatting)
|
|
return changelogMessage.specialFormatting.formatting(
|
|
message,
|
|
subMessage,
|
|
indentation,
|
|
changelogMessage.specialFormatting.storage,
|
|
);
|
|
|
|
if (!changelogMessage.commitObject || subMessage) {
|
|
return formatMessage(message, indentation, undefined, subMessage);
|
|
}
|
|
|
|
if (data.combineList.has(changelogMessage.commitObject.hash)) {
|
|
const commits =
|
|
data.combineList.get(changelogMessage.commitObject.hash) ?? [];
|
|
commits.push(changelogMessage.commitObject);
|
|
|
|
return formatMessage(message, indentation, commits, subMessage);
|
|
}
|
|
|
|
return formatMessage(
|
|
message,
|
|
indentation,
|
|
[changelogMessage.commitObject],
|
|
subMessage,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Formats a Changelog Message
|
|
* @param message The message to format.
|
|
* @param indentation Indentation to use.
|
|
* @param commits List of Commits
|
|
* @param subMessage Whether this message is a subMessage (used in details). Set to true to make it a subMessage (different parsing). Defaults to false.
|
|
* @return string Formatted Changelog Message
|
|
*/
|
|
export async function formatMessage(
|
|
message: string,
|
|
indentation: string,
|
|
commits?: Commit[],
|
|
subMessage = false,
|
|
): Promise<string> {
|
|
if (!commits || commits.length == 0 || subMessage) {
|
|
return `${indentation}* ${message}`;
|
|
}
|
|
|
|
if (commits.length === 1) {
|
|
const commit = commits[0];
|
|
const shortSHA = commit.hash.substring(0, 7);
|
|
const formattedCommit = `[\`${shortSHA}\`](${repoLink}commit/${commit.hash})`;
|
|
const author = await formatAuthor(commit, octokit);
|
|
|
|
return `${indentation}* ${message} - ${author} (${formattedCommit})`;
|
|
}
|
|
|
|
// Sort original array so newest commits appear at the end instead of start of commit string
|
|
sortCommitListReverse(commits);
|
|
|
|
const formattedCommits: string[] = [];
|
|
const authors: string[] = [];
|
|
const retrievedAuthors: { commit: Commit; formatted: string }[] =
|
|
await Promise.all(
|
|
commits.map((commit) =>
|
|
formatAuthor(commit, octokit).then((formatted) => {
|
|
return { commit, formatted };
|
|
}),
|
|
),
|
|
);
|
|
|
|
const processedAuthors: Set<string> = new Set<string>();
|
|
const processedEmails: Set<string> = new Set<string>();
|
|
const processedSHAs: Set<string> = new Set<string>();
|
|
|
|
sortCommitList(
|
|
retrievedAuthors,
|
|
(author) => author.commit,
|
|
(a, b) => a.formatted.localeCompare(b.formatted),
|
|
);
|
|
retrievedAuthors.forEach((pAuthor) => {
|
|
if (processedSHAs.has(pAuthor.commit.hash)) return;
|
|
if (
|
|
!processedAuthors.has(pAuthor.formatted) &&
|
|
!processedEmails.has(pAuthor.commit.author_email)
|
|
) {
|
|
authors.push(pAuthor.formatted);
|
|
processedAuthors.add(pAuthor.formatted);
|
|
processedEmails.add(pAuthor.commit.author_email);
|
|
}
|
|
formattedCommits.push(
|
|
`[\`${pAuthor.commit.hash.substring(0, 7)}\`](${repoLink}commit/${pAuthor.commit.hash})`,
|
|
);
|
|
processedSHAs.add(pAuthor.commit.hash);
|
|
});
|
|
|
|
// Delete all Formatted Commits after MaxIncludeCommits elements, replace with '...'
|
|
if (formattedCommits.length > maxIncludeCommits) {
|
|
formattedCommits.splice(maxIncludeCommits, Infinity, "...");
|
|
}
|
|
|
|
return `${indentation}* ${message} - ${authors.join(", ")} (${formattedCommits.join(", ")})`;
|
|
}
|
|
|
|
/**
|
|
* Returns a formatted commit
|
|
*/
|
|
async function formatCommit(commit: Commit): Promise<string> {
|
|
const date = new Date(commit.date).toLocaleDateString("en-us", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
const formattedCommit = `${commit.message} - ${await formatAuthor(commit, octokit)} (${date})`;
|
|
|
|
const shortSHA = commit.hash.substring(0, 7);
|
|
|
|
return `* [\`${shortSHA}\`](${repoLink}commit/${commit.hash}): ${formattedCommit}`;
|
|
}
|
|
|
|
/**
|
|
* Transforms PR/Issue Tags in all strings of the generated changelog.
|
|
* @param changelog The list to transform all PR/Issue Tags of.
|
|
*/
|
|
async function transformAllIssueURLs(changelog: string[]) {
|
|
const promises: Promise<string>[] = [];
|
|
for (let i = 0; i < changelog.length; i++) {
|
|
const categoryFormatted = changelog[i];
|
|
// Transform PR and/or Issue tags into a link.
|
|
promises.push(
|
|
transformTags(categoryFormatted).then(
|
|
(categoryTransformed) => (changelog[i] = categoryTransformed),
|
|
),
|
|
);
|
|
}
|
|
// Apply all Link Changes
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Transforms PR/Issue Tags into Links.
|
|
*/
|
|
async function transformTags(message: string): Promise<string> {
|
|
const promises: Promise<string>[] = [];
|
|
if (message.search(/#\d+/) !== -1) {
|
|
const matched = message.match(/#\d+/g) ?? [];
|
|
for (const match of matched) {
|
|
// Extract digits
|
|
const digitsMatch = match.match(/\d+/);
|
|
if (!digitsMatch) continue;
|
|
const digits = Number.parseInt(digitsMatch[0]);
|
|
|
|
// Get PR/Issue Info (PRs are listed in the Issue API Endpoint)
|
|
promises.push(
|
|
getIssueURL(digits, octokit).then((url) =>
|
|
message.replace(match, `[#${digits}](${url})`),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Resolve all Issue URL Replacements
|
|
await Promise.all(promises);
|
|
return message;
|
|
}
|