Integer Limit 9e2fd51b8d
Small Changelog Generation Fixes (#792)
[FIXUP]
[[fixes]]
sha = "bf4055fbe4092d72f68bb40bb5d88c624ce5b074"
changeFixes = false
newBody = '''
[EXPAND]
  [[messages]]
  messageTitle = "Upgrade AE2 Stuff to AE2 Stuff Unofficial"
  messageBody = """
    [BREAKING]
    [DETAILS]
      details = [
        \"Pattern Encoders have been Removed!\",
        \"Any existing Pattern Encoders, whether items, in patterns or placed, have been remapped to AE2 Interfaces.\",
        \"All Usages in Recipes have also been changed to AE2 Interfaces!\",
      ]
    [DETAILS]
    [PRIORITY]
      priority = 25
    [PRIORITY]
  """
  
  [[messages]]
  messageTitle = "Update GT to 2.8.10"
  messageBody = """
    [BREAKING]
    [DETAILS]
      details = [
        \"Allows GregTech Data Sticks to Copy ME Hatch Settings\",
        \"Adds an 'Stocking' Version of ME Hatches and Buses\",
        \"Support for Displaying Power Substation on Central Monitor\",
      ]
    [DETAILS]
    [PRIORITY]
      priority = 20
    [PRIORITY]
    [IGNORE]
      checks = { compareBefore = \"1.7-alpha-4\" } # Only apply this if the commit being compared against is 1.7-alpha-4 or newer
    [IGNORE]
  """
  
  [[messages]]
  messageTitle = "Update Nomi Labs to 0.7.0"
  messageBody = """
    [BREAKING]
    [DETAILS]
      details = [
        \"Improvement of DME Simulation Chamber (now increases Tiers and Data Counts of Models)\",
        \"Custom Implementation of Difficulty Lock, now works on Dedicated Servers\",
        \"Replacement of the Void Dimension, fixing issues with Difficulty Changing\",
        \"Replacement of Custom Window Titles and Logos, Allowing the Removal of Random Patches\",
        \"Improvements to FTB Utils and Effortless Building\",
        \"Allows Setting of Default Keybinds for New Players\",
      ]
    [DETAILS]
    [PRIORITY]
      priority = 15
    [PRIORITY]
    [IGNORE]
      checks = { compareBefore = \"1.7-alpha-4\" } # Only apply this if the commit being compared against is 1.7-alpha-4 or newer
    [IGNORE]
  """
[EXPAND]
'''
[FIXUP]

[MOD INFO]
  [[infos]]
  projectID = 254317
  info = "Replaced by Nomi Labs"

  [[infos]]
  projectID = 285612
  info = "Replaced by Nomi Labs"
  
  [[infos]]
  projectID = 390886
  info = "Replaced by Nomi Labs"
  
  [[infos]]
  projectID = 538092
  info = "Replaced by Betterer P2P"
[MOD INFO]
2024-07-11 21:54:46 +10:00

328 lines
9.7 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 { getIssueURL, 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;
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
) {
sortCommitList(data.commitList, (commit) => commit);
data.builder.push("## Commits");
data.commitList.forEach((commit) => {
data.builder.push(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}:`);
}
// Sort Log
sortCommitList(
list,
(message) => message.commitObject,
(a, b) => a.commitMessage.localeCompare(b.commitMessage),
);
// Push Log
for (const changelogMessage of list) {
categoryLog.push(await formatChangelogMessage(changelogMessage));
// Push Sub Messages
if (changelogMessage.subChangelogMessages) {
for (const subMessage of changelogMessage.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) {
if (data.combineList.has(changelogMessage.commitObject.hash)) {
const commits =
data.combineList.get(changelogMessage.commitObject.hash) ?? [];
commits.push(changelogMessage.commitObject);
// 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 authorEmails: Set<string> = new Set<string>();
const processedSHAs: Set<string> = new Set<string>();
commits.forEach((commit) => {
if (processedSHAs.has(commit.hash)) return;
if (
!authors.includes(commit.author_name) &&
!authorEmails.has(commit.author_email)
) {
authors.push(commit.author_name);
authorEmails.add(commit.author_email);
}
formattedCommits.push(
`[\`${commit.hash.substring(0, 7)}\`](${repoLink}commit/${commit.hash})`,
);
processedSHAs.add(commit.hash);
});
authors.sort();
return `${indentation}* ${message} - **${authors.join("**, **")}** (${formattedCommits.join(", ")})`;
}
const commit = changelogMessage.commitObject;
const shortSHA = commit.hash.substring(0, 7);
const author = commit.author_name;
return `${indentation}* ${message} - **${author}** ([\`${shortSHA}\`](${repoLink}commit/${commit.hash}))`;
}
return `${indentation}* ${message}`;
}
/**
* Returns a formatted commit
*/
function formatCommit(commit: Commit): string {
const date = new Date(commit.date).toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
});
const formattedCommit = `${commit.message} - **${commit.author_name}** (${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;
}