Revamp of Commit Authors in Changelog (#829)

[SKIP]
This commit is contained in:
Integer Limit 2024-07-28 23:03:36 +10:00 committed by GitHub
parent 51fe4e5a29
commit a520c819d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 216 additions and 78 deletions

View File

@ -17,10 +17,10 @@ import {
} from "#types/changelogTypes.ts"; } from "#types/changelogTypes.ts";
import dedent from "dedent-js"; import dedent from "dedent-js";
import mustache from "mustache"; import mustache from "mustache";
import { modChangesAllocations, repoLink } from "./definitions.ts"; import { modChangesAllocations } from "./definitions.ts";
import ChangelogData from "./changelogData.ts"; import ChangelogData from "./changelogData.ts";
import { SpecialChangelogFormatting } from "#types/changelogTypes.ts"; import { SpecialChangelogFormatting } from "#types/changelogTypes.ts";
import { sortCommitListReverse } from "./pusher.ts"; import { formatMessage, sortCommitListReverse } from "./pusher.ts";
import { logError } from "#utils/log.ts"; import { logError } from "#utils/log.ts";
/** /**
@ -30,32 +30,11 @@ const getModChangesFormatting: (
commits?: Commit[], commits?: Commit[],
) => SpecialChangelogFormatting<Commit[] | undefined> = (commits) => { ) => SpecialChangelogFormatting<Commit[] | undefined> = (commits) => {
return { return {
formatting: (message, subMessage, indentation, commits) => { formatting: async (message, subMessage, indentation, commits) => {
// Sub messages are details, so make them bold & italic // Sub messages are details, so make them bold & italic
if (subMessage) return `${indentation}* ***${message}***`; if (subMessage) return `${indentation}* ***${message}***`;
// Edge Case return formatMessage(message, indentation, commits, subMessage);
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}))`;
}, },
storage: commits, storage: commits,
} as SpecialChangelogFormatting<Commit[] | undefined>; } as SpecialChangelogFormatting<Commit[] | undefined>;

View File

@ -3,7 +3,12 @@ import { categories, defaultIndentation } from "./definitions.ts";
import { Category, ChangelogMessage, Commit } from "#types/changelogTypes.ts"; import { Category, ChangelogMessage, Commit } from "#types/changelogTypes.ts";
import { repoLink } from "./definitions.ts"; import { repoLink } from "./definitions.ts";
import { Octokit } from "@octokit/rest"; 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 data: ChangelogData;
let octokit: Octokit; let octokit: Octokit;
@ -14,6 +19,9 @@ const sectionLinesBeforeCommitLogExcluded = 50;
// How many lines the commit log can be before its is excluded. // How many lines the commit log can be before its is excluded.
const logLinesBeforeCommitLogExcluded = 20; const logLinesBeforeCommitLogExcluded = 20;
// How many commits to include after a message.
const maxIncludeCommits = 3;
export default async function pushAll(inputData: ChangelogData): Promise<void> { export default async function pushAll(inputData: ChangelogData): Promise<void> {
pushTitle(inputData); pushTitle(inputData);
await pushChangelog(inputData); await pushChangelog(inputData);
@ -75,12 +83,14 @@ export async function pushChangelog(inputData: ChangelogData): Promise<void> {
data.builder.length < sectionLinesBeforeCommitLogExcluded && data.builder.length < sectionLinesBeforeCommitLogExcluded &&
data.commitList.length < logLinesBeforeCommitLogExcluded 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); sortCommitList(data.commitList, (commit) => commit);
data.builder.push("## Commits"); data.builder.push("## Commits");
data.commitList.forEach((commit) => { for (const commit of data.commitList) {
data.builder.push(formatCommit(commit)); data.builder.push(await formatCommit(commit));
}); }
} }
} else { } else {
// No Commit List = No Changes // No Commit List = No Changes
@ -118,19 +128,31 @@ async function pushCategory(category: Category) {
categoryLog.push(`### ${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 // Sort Log
sortCommitList( sortCommitList(
list, formatted,
(message) => message.commitObject, (formatted) => formatted.message.commitObject,
(a, b) => a.commitMessage.localeCompare(b.commitMessage), (a, b) =>
a.message.commitMessage.localeCompare(b.message.commitMessage),
); );
// Push Log // Push Log
for (const changelogMessage of list) { for (const format of formatted) {
categoryLog.push(await formatChangelogMessage(changelogMessage)); categoryLog.push(format.formatted);
// Push Sub Messages // Push Sub Messages (No need for Async, Author Info Not Calculated in Sub Messages)
if (changelogMessage.subChangelogMessages) { if (format.message.subChangelogMessages) {
for (const subMessage of changelogMessage.subChangelogMessages) for (const subMessage of format.message.subChangelogMessages)
categoryLog.push(await formatChangelogMessage(subMessage, true)); categoryLog.push(await formatChangelogMessage(subMessage, true));
} }
} }
@ -222,58 +244,110 @@ async function formatChangelogMessage(
changelogMessage.specialFormatting.storage, changelogMessage.specialFormatting.storage,
); );
if (changelogMessage.commitObject && !subMessage) { if (!changelogMessage.commitObject || subMessage) {
if (data.combineList.has(changelogMessage.commitObject.hash)) { return formatMessage(message, indentation, undefined, subMessage);
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}`; 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 * Returns a formatted commit
*/ */
function formatCommit(commit: Commit): string { async function formatCommit(commit: Commit): Promise<string> {
const date = new Date(commit.date).toLocaleDateString("en-us", { const date = new Date(commit.date).toLocaleDateString("en-us", {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", 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); const shortSHA = commit.hash.substring(0, 7);

View File

@ -134,7 +134,7 @@ export interface SpecialChangelogFormatting<T> {
subMessage: boolean, subMessage: boolean,
indentation: string, indentation: string,
storage?: T, storage?: T,
) => string; ) => Promise<string>;
/** /**
* Storage * Storage

View File

@ -593,9 +593,15 @@ export async function getVersionManifest(
*/ */
export function cleanupVersion(version?: string): string { export function cleanupVersion(version?: string): string {
if (!version) return ""; if (!version) return "";
if (!version.replace(/[\d+.?]+/g, "")) return version;
version = version.replace(/1\.12\.2|1\.12|\.jar/g, ""); version = version.replace(/1\.12\.2|1\.12|\.jar/g, "");
const list = version.match(/[\d+.?]+/g); const list = version.match(/[\d+.?]+/g);
if (!list) return version; if (!list) return version;
if (list[list.length - 1] == "0") return version;
return list[list.length - 1]; return list[list.length - 1];
} }
@ -650,20 +656,99 @@ export async function getIssueURL(
logError( logError(
`Failed to get the Issue/PR Info for Issue/PR #${issueNumber}. Returned Status Code ${issueInfo.status}, expected Status 200.`, `Failed to get the Issue/PR Info for Issue/PR #${issueNumber}. Returned Status Code ${issueInfo.status}, expected Status 200.`,
); );
issueURLCache.set(issueNumber, "");
return ""; return "";
} }
logInfo( logInfo(
`No Issue URL Cache for Issue Number ${issueNumber}. Retrieved Specifically.`, `No Issue URL Cache for Issue Number ${issueNumber}. Retrieved Specifically.`,
); );
issueURLCache.set(issueNumber, issueInfo.data.html_url);
return issueInfo.data.html_url; return issueInfo.data.html_url;
} catch (e) { } catch (e) {
logError( 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.`, `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 ""; return "";
} }
} }
// Map of Commit SHA -> Formatted Author
const commitAuthorCache: Map<string, string> = new Map<string, string>();
/**
* Fills the Commit Author Cache with the newest 100 commits from the repo.
*/
export async function getNewestCommitAuthors(octokit: Octokit): Promise<void> {
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_VERSION_REG = /forge-(.+)/;
export const FORGE_MAVEN = "https://files.minecraftforge.net/maven/"; export const FORGE_MAVEN = "https://files.minecraftforge.net/maven/";