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 = (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; }; /** * 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 { 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 = 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 { 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 | 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 | undefined = undefined; if (oldManifest && newManifest) result = getUniqueToArray( oldManifest.files, newManifest.files, (e) => e.fileID, ); return result; }