| 1 | /** |
| 2 | * changelogithub's `generate()`, minus the 1 MB ceiling. |
| 3 | * |
| 4 | * Upstream reads the commit range with `execSync`, whose default `maxBuffer` |
| 5 | * is 1 MB. `git log … --name-status` prints every changed path of every |
| 6 | * commit in the range, so a release spanning a large diff blows past that and |
| 7 | * the process is killed: |
| 8 | * |
| 9 | * error: spawnSync /bin/sh ENOBUFS (stdout or stderr buffer reached |
| 10 | * maxBuffer size limit) |
| 11 | * |
| 12 | * It is not a pathological case — one repo-wide refactor in the range is |
| 13 | * enough, and the release then fails at the tagging step having already |
| 14 | * bumped versions. |
| 15 | * |
| 16 | * `generate()` is `getGitDiff → parseCommits → resolveAuthors → markdown`, |
| 17 | * and changelogithub exports every piece except the first. So this reads the |
| 18 | * log itself, streaming, and hands the result to the same parser — no |
| 19 | * reimplementation of the changelog format, and no fork. |
| 20 | */ |
| 21 | import type { ChangelogOptions, Commit } from "changelogithub"; |
| 22 | import { |
| 23 | generateMarkdown, |
| 24 | parseCommits, |
| 25 | resolveAuthors, |
| 26 | resolveConfig, |
| 27 | } from "changelogithub"; |
| 28 | |
| 29 | interface RawGitCommit { |
| 30 | message: string; |
| 31 | body: string; |
| 32 | shortHash: string; |
| 33 | author: { name: string; email: string }; |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * The same command and parsing as changelogen's `getGitDiff`, read through a |
| 38 | * pipe instead of a fixed buffer. Kept byte-compatible with upstream: the |
| 39 | * `----` record separator and `|`-delimited header are what `parseCommits` |
| 40 | * expects. |
| 41 | */ |
| 42 | export const getGitDiff = async ( |
| 43 | from: string | undefined, |
| 44 | to = "HEAD", |
| 45 | ): Promise<RawGitCommit[]> => { |
| 46 | const proc = Bun.spawn( |
| 47 | [ |
| 48 | "git", |
| 49 | "--no-pager", |
| 50 | "log", |
| 51 | `${from ? `${from}...` : ""}${to}`, |
| 52 | "--pretty=----%n%s|%h|%an|%ae%n%b", |
| 53 | "--name-status", |
| 54 | ], |
| 55 | { stdout: "pipe", stderr: "pipe" }, |
| 56 | ); |
| 57 | const [raw, err, exitCode] = await Promise.all([ |
| 58 | new Response(proc.stdout).text(), |
| 59 | new Response(proc.stderr).text(), |
| 60 | proc.exited, |
| 61 | ]); |
| 62 | if (exitCode !== 0) { |
| 63 | throw new Error(`git log ${from ?? ""}...${to} failed: ${err.trim()}`); |
| 64 | } |
| 65 | return raw |
| 66 | .trim() |
| 67 | .split("----\n") |
| 68 | .splice(1) |
| 69 | .map((line) => { |
| 70 | const [firstLine, ..._body] = line.split("\n"); |
| 71 | const [message, shortHash, authorName, authorEmail] = ( |
| 72 | firstLine ?? "" |
| 73 | ).split("|"); |
| 74 | return { |
| 75 | message: message ?? "", |
| 76 | shortHash: shortHash ?? "", |
| 77 | author: { name: authorName ?? "", email: authorEmail ?? "" }, |
| 78 | body: _body.join("\n"), |
| 79 | }; |
| 80 | }); |
| 81 | }; |
| 82 | |
| 83 | /** Drop-in for changelogithub's `generate`. */ |
| 84 | export const generate = async (options: ChangelogOptions) => { |
| 85 | const config = await resolveConfig(options); |
| 86 | const commits: Commit[] = parseCommits( |
| 87 | (await getGitDiff(config.from, config.to)) as never, |
| 88 | config, |
| 89 | ); |
| 90 | if (config.contributors) await resolveAuthors(commits, config); |
| 91 | return { config, commits, md: generateMarkdown(commits, config) }; |
| 92 | }; |