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 */
21import type { ChangelogOptions, Commit } from "changelogithub";
22import {
23 generateMarkdown,
24 parseCommits,
25 resolveAuthors,
26 resolveConfig,
27} from "changelogithub";
28
29interface 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 */
42export 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`. */
84export 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};