From a520c819d1d22df4966aab9e9fe6fcb86ea76fdc Mon Sep 17 00:00:00 2001 From: Integer Limit <103940576+IntegerLimit@users.noreply.github.com> Date: Sun, 28 Jul 2024 23:03:36 +1000 Subject: [PATCH] Revamp of Commit Authors in Changelog (#829) [SKIP] --- tools/tasks/changelog/generateModChanges.ts | 29 +--- tools/tasks/changelog/pusher.ts | 178 ++++++++++++++------ tools/types/changelogTypes.ts | 2 +- tools/utils/util.ts | 85 ++++++++++ 4 files changed, 216 insertions(+), 78 deletions(-) diff --git a/tools/tasks/changelog/generateModChanges.ts b/tools/tasks/changelog/generateModChanges.ts index e14b5d4..8087f28 100644 --- a/tools/tasks/changelog/generateModChanges.ts +++ b/tools/tasks/changelog/generateModChanges.ts @@ -17,10 +17,10 @@ import { } from "#types/changelogTypes.ts"; import dedent from "dedent-js"; import mustache from "mustache"; -import { modChangesAllocations, repoLink } from "./definitions.ts"; +import { modChangesAllocations } from "./definitions.ts"; import ChangelogData from "./changelogData.ts"; import { SpecialChangelogFormatting } from "#types/changelogTypes.ts"; -import { sortCommitListReverse } from "./pusher.ts"; +import { formatMessage, sortCommitListReverse } from "./pusher.ts"; import { logError } from "#utils/log.ts"; /** @@ -30,32 +30,11 @@ const getModChangesFormatting: ( commits?: Commit[], ) => SpecialChangelogFormatting = (commits) => { return { - formatting: (message, subMessage, indentation, commits) => { + formatting: async (message, subMessage, indentation, commits) => { // Sub messages are details, so make them bold & italic if (subMessage) return `${indentation}* ***${message}***`; - // Edge Case - if (!commits) return `${indentation}* ${message}`; - - if (commits.length > 1) { - const authors: string[] = []; - const formattedCommits: string[] = []; - commits.forEach((commit) => { - if (!authors.includes(commit.author_name)) - authors.push(commit.author_name); - formattedCommits.push( - `[\`${commit.hash.substring(0, 7)}\`](${repoLink}commit/${commit.hash})`, - ); - }); - authors.sort(); - return `${indentation}* ${message} - **${authors.join("**, **")}** (${formattedCommits.join(", ")})`; - } - - const commit = commits[0]; - const shortSHA = commit.hash.substring(0, 7); - const author = commit.author_name; - - return `${indentation}* ${message} - **${author}** ([\`${shortSHA}\`](${repoLink}commit/${commit.hash}))`; + return formatMessage(message, indentation, commits, subMessage); }, storage: commits, } as SpecialChangelogFormatting; diff --git a/tools/tasks/changelog/pusher.ts b/tools/tasks/changelog/pusher.ts index 910fa49..80ef0c6 100644 --- a/tools/tasks/changelog/pusher.ts +++ b/tools/tasks/changelog/pusher.ts @@ -3,7 +3,12 @@ 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"; +import { + formatAuthor, + getIssueURL, + getNewestCommitAuthors, + getNewestIssueURLs, +} from "#utils/util.ts"; let data: ChangelogData; let octokit: Octokit; @@ -14,6 +19,9 @@ 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 { pushTitle(inputData); await pushChangelog(inputData); @@ -75,12 +83,14 @@ export async function pushChangelog(inputData: ChangelogData): Promise { 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"); - data.commitList.forEach((commit) => { - data.builder.push(formatCommit(commit)); - }); + for (const commit of data.commitList) { + data.builder.push(await formatCommit(commit)); + } } } else { // No Commit List = No Changes @@ -118,19 +128,31 @@ async function pushCategory(category: Category) { 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( - list, - (message) => message.commitObject, - (a, b) => a.commitMessage.localeCompare(b.commitMessage), + formatted, + (formatted) => formatted.message.commitObject, + (a, b) => + a.message.commitMessage.localeCompare(b.message.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) + 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)); } } @@ -222,58 +244,110 @@ async function formatChangelogMessage( 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 = new Set(); - const processedSHAs: Set = new Set(); - - 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}))`; + if (!changelogMessage.commitObject || subMessage) { + return formatMessage(message, indentation, undefined, subMessage); } - return `${indentation}* ${message}`; + 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 { + 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 = new Set(); + const processedEmails: Set = new Set(); + const processedSHAs: Set = new Set(); + + 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 */ -function formatCommit(commit: Commit): string { +async function formatCommit(commit: Commit): Promise { const date = new Date(commit.date).toLocaleDateString("en-us", { year: "numeric", month: "short", day: "numeric", }); - const formattedCommit = `${commit.message} - **${commit.author_name}** (${date})`; + const formattedCommit = `${commit.message} - ${await formatAuthor(commit, octokit)} (${date})`; const shortSHA = commit.hash.substring(0, 7); diff --git a/tools/types/changelogTypes.ts b/tools/types/changelogTypes.ts index 28204f9..fe35c83 100644 --- a/tools/types/changelogTypes.ts +++ b/tools/types/changelogTypes.ts @@ -134,7 +134,7 @@ export interface SpecialChangelogFormatting { subMessage: boolean, indentation: string, storage?: T, - ) => string; + ) => Promise; /** * Storage diff --git a/tools/utils/util.ts b/tools/utils/util.ts index 27079b9..3202dbe 100644 --- a/tools/utils/util.ts +++ b/tools/utils/util.ts @@ -593,9 +593,15 @@ export async function getVersionManifest( */ export function cleanupVersion(version?: string): string { if (!version) return ""; + + if (!version.replace(/[\d+.?]+/g, "")) return version; + version = version.replace(/1\.12\.2|1\.12|\.jar/g, ""); const list = version.match(/[\d+.?]+/g); if (!list) return version; + + if (list[list.length - 1] == "0") return version; + return list[list.length - 1]; } @@ -650,20 +656,99 @@ export async function getIssueURL( logError( `Failed to get the Issue/PR Info for Issue/PR #${issueNumber}. Returned Status Code ${issueInfo.status}, expected Status 200.`, ); + issueURLCache.set(issueNumber, ""); return ""; } logInfo( `No Issue URL Cache for Issue Number ${issueNumber}. Retrieved Specifically.`, ); + issueURLCache.set(issueNumber, issueInfo.data.html_url); return issueInfo.data.html_url; } catch (e) { logError( `Failed to get the Issue/PR Info for Issue/PR #${issueNumber}. This may be because this is not a PR or Issue, or could be because of rate limits.`, ); + issueURLCache.set(issueNumber, ""); return ""; } } +// Map of Commit SHA -> Formatted Author +const commitAuthorCache: Map = new Map(); + +/** + * Fills the Commit Author Cache with the newest 100 commits from the repo. + */ +export async function getNewestCommitAuthors(octokit: Octokit): Promise { + if (commitAuthorCache.size > 0) return; + try { + const commits = await octokit.repos.listCommits({ + owner: repoOwner, + repo: repoName, + per_page: 100, + }); + if (commits.status !== 200) { + logError( + `Failed to get all Commit Authors. Returned Status Code ${commits.status}, expected Status 200.`, + ); + return; + } + commits.data.forEach((commit) => { + if (!commitAuthorCache.has(commit.sha)) + commitAuthorCache.set(commit.sha, commit.author?.login ?? ""); + }); + } catch (e) { + logError( + "Failed to get all Commit Authors of Repo. This may be because there are no commits, or because of rate limits.", + ); + } +} + +/** + * Gets the Author, in mentionable form (@login), or default (**Display Name**), from a Commit. + */ +export async function formatAuthor(commit: Commit, octokit: Octokit) { + const defaultFormat = `**${commit.author_name}**`; + + if (commitAuthorCache.has(commit.hash)) { + const login = commitAuthorCache.get(commit.hash); + if (login) return `@${login}`; + return defaultFormat; + } + + try { + const commitInfo = await octokit.repos.getCommit({ + owner: repoOwner, + repo: repoName, + ref: commit.hash, + }); + if (commitInfo.status !== 200) { + logError( + `Failed to get the Author Info for Commit ${commit.hash}. Returned Status Code ${commitInfo.status}, expected Status 200.`, + ); + commitAuthorCache.set(commit.hash, ""); + return defaultFormat; + } + if (!commitInfo.data.author?.login) { + logError( + `Failed to get the Author Info for Commit ${commit.hash}. Returned Null Data, Author or Login.`, + ); + commitAuthorCache.set(commit.hash, ""); + return defaultFormat; + } + logInfo( + `No Author Cache for Commit ${commit.hash}. Retrieved Specifically.`, + ); + return `@${commitInfo.data.author.login}`; + } catch (e) { + logError( + `Failed to get Commit Author for Commit ${commit.hash}. This may be because there are no commits, or because of rate limits.`, + ); + commitAuthorCache.set(commit.hash, ""); + return defaultFormat; + } +} + export const FORGE_VERSION_REG = /forge-(.+)/; export const FORGE_MAVEN = "https://files.minecraftforge.net/maven/";