modded7/tools/tasks/changelog/generateModChanges.ts
2024-07-28 23:03:36 +10:00

216 lines
5.9 KiB
TypeScript

import {
ArrayUnique,
cleanupVersion,
compareAndExpandManifestDependencies,
getChangelog,
getFileAtRevision,
getUniqueToArray,
} from "#utils/util.ts";
import {
ModpackManifest,
ModpackManifestFile,
} from "#types/modpackManifest.ts";
import {
ChangelogMessage,
Commit,
ModChangeInfo,
} from "#types/changelogTypes.ts";
import dedent from "dedent-js";
import mustache from "mustache";
import { modChangesAllocations } from "./definitions.ts";
import ChangelogData from "./changelogData.ts";
import { SpecialChangelogFormatting } from "#types/changelogTypes.ts";
import { formatMessage, sortCommitListReverse } from "./pusher.ts";
import { logError } from "#utils/log.ts";
/**
* Mod Changes special formatting
*/
const getModChangesFormatting: (
commits?: Commit[],
) => SpecialChangelogFormatting<Commit[] | undefined> = (commits) => {
return {
formatting: async (message, subMessage, indentation, commits) => {
// Sub messages are details, so make them bold & italic
if (subMessage) return `${indentation}* ***${message}***`;
return formatMessage(message, indentation, commits, subMessage);
},
storage: commits,
} as SpecialChangelogFormatting<Commit[] | undefined>;
};
/**
* Pushes the mod changes, with their relative commits, to their respective sub categories in the specified category.
*/
export default async function generateModChanges(
data: ChangelogData,
): Promise<void> {
const oldManifest: ModpackManifest = JSON.parse(
await getFileAtRevision("manifest.json", data.since),
);
const newManifest: ModpackManifest = JSON.parse(
await getFileAtRevision("manifest.json", data.to),
);
const comparisonResult = await compareAndExpandManifestDependencies(
oldManifest,
newManifest,
);
const commitList = await getChangelog(data.since, data.to, ["manifest.json"]);
const projectIDsToCommits: Map<number, Commit[]> = new Map();
for (const commit of commitList) {
const projectIDs = await getChangedProjectIDs(commit.hash);
projectIDs.forEach((id) => {
if (projectIDsToCommits.has(id))
projectIDsToCommits.get(id)?.push(commit);
else projectIDsToCommits.set(id, [commit]);
});
}
[
{
allocation: modChangesAllocations.added,
list: comparisonResult.added,
},
{
allocation: modChangesAllocations.updated,
list: comparisonResult.modified,
},
{
allocation: modChangesAllocations.removed,
list: comparisonResult.removed,
},
].forEach((block) => {
if (block.list.length == 0) {
return;
}
const list = block.list
// Yeet invalid project names.
.filter((project) => !/project-\d*/.test(project.modName))
.sort()
.map((name) => name);
list.forEach((info) => {
let commits: Commit[] | undefined = undefined;
if (info.projectID && projectIDsToCommits.has(info.projectID)) {
commits = projectIDsToCommits.get(info.projectID) ?? [];
// Sort array so newest commits appear at end instead of start of commit string
sortCommitListReverse(commits);
}
block.allocation.category.changelogSection
?.get(block.allocation.subCategory)
?.push(
getModChangeMessage(info, block.allocation.template, data, commits),
);
});
});
}
/**
* Returns the changelog message, determined by the parameters below.
* @param info The mod change info, containing the mod name and versions.
* @param template The message template to replace in.
* @param data The changelog data
* @param commits The commits
*/
function getModChangeMessage(
info: ModChangeInfo,
template: string,
data: ChangelogData,
commits?: Commit[],
): ChangelogMessage {
const oldVersion = cleanupVersion(info.oldVersion);
const newVersion = cleanupVersion(info.newVersion);
// If not provided with either version, return just mod name
if ((!oldVersion && !newVersion) || !info.projectID)
return {
commitMessage: info.modName,
specialFormatting: getModChangesFormatting(commits),
};
// Replace in template
let text = mustache.render(template, {
modName: info.modName,
oldVersion: oldVersion,
newVersion: newVersion,
});
// Parse Info
let subMessages: ChangelogMessage[] | undefined = undefined;
if (data.modInfoList.has(info.projectID)) {
const modInfo = data.modInfoList.get(info.projectID);
if (modInfo?.info) text = `${text} ***(${modInfo.info})***`;
if (modInfo?.details)
subMessages = modInfo?.details.map((detail) => {
detail.specialFormatting = getModChangesFormatting(commits);
return detail;
});
}
return {
commitMessage: text,
specialFormatting: getModChangesFormatting(commits),
subChangelogMessages: subMessages,
};
}
/**
* Gets what project IDs, in manifest.json, a commit changed.
* @param sha The sha of the commit
*/
async function getChangedProjectIDs(sha: string): Promise<number[]> {
const change = await getCommitChange(sha);
const projectIDs: number[] = [];
if (!change) {
return projectIDs;
}
// Add all unique IDs from both diff lists
change.arr1Unique.forEach((file) => {
if (!projectIDs.includes(file.projectID)) projectIDs.push(file.projectID);
});
change.arr2Unique.forEach((file) => {
if (!projectIDs.includes(file.projectID)) projectIDs.push(file.projectID);
});
return projectIDs;
}
/**
* Gets what parts of the 'manifest.json' file a commit changed.
* @param sha The sha of the commit
*/
async function getCommitChange(
sha: string,
): Promise<ArrayUnique<ModpackManifestFile> | undefined> {
let oldManifest: ModpackManifest, newManifest: ModpackManifest;
try {
oldManifest = JSON.parse(
await getFileAtRevision("manifest.json", `${sha}^`),
) as ModpackManifest;
newManifest = JSON.parse(
await getFileAtRevision("manifest.json", sha),
) as ModpackManifest;
} catch (e) {
logError(dedent`
Failed to parse the manifest.json file at commit ${sha} or the commit before!
Skipping...`);
return undefined;
}
let result: ArrayUnique<ModpackManifestFile> | undefined = undefined;
if (oldManifest && newManifest)
result = getUniqueToArray(
oldManifest.files,
newManifest.files,
(e) => e.fileID,
);
return result;
}