diff --git a/scripts/github_api.mts b/scripts/github_api.mts index 1f03a51c..12b7d7a6 100644 --- a/scripts/github_api.mts +++ b/scripts/github_api.mts @@ -11,8 +11,8 @@ export async function githubFetch( init = { ...init, headers: { - ...init.headers as Record ?? {}, "Accept": "application/vnd.github+json", + ...init.headers as Record ?? {}, } }; if (process.env.GH_TOKEN) { @@ -24,7 +24,9 @@ export async function githubFetch( console.log(response.headers); throw new Error(`GitHub replied with ${response.status} ${response.statusText}: ${await response.text()}`); } - if (!response.headers.get("Content-Type")?.startsWith("application/json")) { + const accept = init.headers!["Accept"]; + const contentType = accept === "application/vnd.github+json" ? "application/json" : accept; + if (!response.headers.get("Content-Type")?.startsWith(contentType)) { throw new Error( `GitHub replied with ${response.status} ${response.statusText}: ${response.headers.get("Content-Type")}` ); @@ -41,6 +43,15 @@ export async function githubFetchJson( return await response.json(); } +export async function githubFetchText( + path: string, + query: Record = {}, + init: RequestInit & { headers?: Record } = {}, +): Promise { + const response = await githubFetch(path, query, init); + return await response.text(); +} + export async function githubFetchJsonPaginated( path: string, query: Record = {}, @@ -117,6 +128,10 @@ export interface Release { updated_at?: string | null, } +export interface Comparison { + total_commits: number, +} + export interface Commit { sha: string, commit: { diff --git a/scripts/pr_to_changelog.mts b/scripts/pr_to_changelog.mts index a5bbb7bf..7a6324a4 100644 --- a/scripts/pr_to_changelog.mts +++ b/scripts/pr_to_changelog.mts @@ -1,12 +1,22 @@ import { + type Comparison, type Release, type Issue, githubFetchJson, githubFetchJsonPaginated, + githubFetchText, } from "./github_api.mts"; type Changelog = Record; +type Statistics = { + commits: number, + pullRequests: number, + filesChanged: number, + lineAdditions: number, + lineDeletions: number, +}; + const repository = "clusterio/clusterio"; const branch = "master"; @@ -187,10 +197,41 @@ async function fetchIssuesUpdatedSince(since: string) { ); } +async function fetchStatistics(lastRelease: Release, pullRequests: Issue[]): Promise { + const path = `/repos/${repository}/compare/${lastRelease.tag_name}...${branch}`; + const comparison = await githubFetchJson(path, { per_page: "1" }); + // The JSON response lists at most 300 files, the diff has all of them. + const diff = await githubFetchText(path, {}, { headers: { "Accept": "application/vnd.github.diff" } }); + let filesChanged = 0; + let lineAdditions = 0; + let lineDeletions = 0; + let inHunk = false; // Skips the ---/+++ file headers + for (const line of diff.split("\n")) { + if (line.startsWith("diff --git ")) { + filesChanged += 1; + inHunk = false; + } else if (line.startsWith("@@")) { + inHunk = true; + } else if (inHunk && line.startsWith("+")) { + lineAdditions += 1; + } else if (inHunk && line.startsWith("-")) { + lineDeletions += 1; + } + } + return { + commits: comparison.total_commits, + pullRequests: pullRequests.length, + filesChanged, + lineAdditions, + lineDeletions, + }; +} + function printMarkdown( changelog: Changelog, issues: Issue[], users: Issue["user"][], + statistics: Statistics, refText: (issue: Issue) => string, userText: (user: Issue["user"]) => string, ) { @@ -216,8 +257,20 @@ function printMarkdown( } } + console.log("Contributors"); + console.log(); console.log(`Many thanks to the following for contributing to this release:`); - console.log(users.map(userText).join("\n")) + console.log(users.map(userText).join(", ")); + + const format = (value: number) => value.toLocaleString("en-US"); + console.log(); + console.log("Statistics"); + console.log(); + console.log(`Commits: ${format(statistics.commits)}`); + console.log(`Pull Requests: ${format(statistics.pullRequests)}`); + console.log(`Files Changed: ${format(statistics.filesChanged)}`); + console.log(`Line Additions: ${format(statistics.lineAdditions)}`); + console.log(`Line Deletions: ${format(statistics.lineDeletions)}`); } async function main() { @@ -232,17 +285,18 @@ async function main() { const changelog = changelogFromPullRequests(pullRequests, issues); const users = [...new Map(pullRequests.map(issue => [issue.user.login, issue.user])).values()] + const statistics = await fetchStatistics(lastRelease, pullRequests); console.log(); console.log("=== Github Release Markdown ==="); - printMarkdown(changelog, issues, users, + printMarkdown(changelog, issues, users, statistics, issue => `${repository}#${issue.number}`, user => `@${user.login}`, ); console.log(); console.log("=== Discord / Changelog Markdown ==="); - printMarkdown(changelog, issues, users, + printMarkdown(changelog, issues, users, statistics, issue => `[#${issue.number}](<${issue.html_url}>)`, user => `[@${user.login}](<${user.html_url}>)` );