diff --git a/scripts/build-release-changelog.ts b/scripts/build-release-changelog.ts index c7868db572..a6c53d34fe 100644 --- a/scripts/build-release-changelog.ts +++ b/scripts/build-release-changelog.ts @@ -431,17 +431,22 @@ async function generateGitHubNotes( } export function parseGitLog(raw: string): Array> { + const fields = raw.split("\0"); + if (fields.at(-1) === "") fields.pop(); + if (fields.length % 3 !== 0) { + throw new Error("git log produced a malformed release commit record"); + } + const commits: Array> = []; - for (const record of raw.split("\x1e")) { - if (!record.trim()) continue; - const [sha, subject, ...bodyParts] = record.replace(/^\n+/, "").split("\x1f"); - if (!sha?.trim() || !subject?.trim()) { + for (let index = 0; index < fields.length; index += 3) { + const [sha, subject, body] = fields.slice(index, index + 3); + if (!sha?.trim() || !subject?.trim() || body === undefined) { throw new Error("git log produced a malformed release commit record"); } commits.push({ sha: sha.trim(), subject: subject.trim(), - body: bodyParts.join("\x1f").trim(), + body: body.trim(), }); } return commits; @@ -464,7 +469,8 @@ async function releaseCommits( "log", "--first-parent", "--reverse", - "--format=%H%x1f%s%x1f%B%x1e", + "-z", + "--format=%H%x00%s%x00%B", range, ]); return parseGitLog(raw); diff --git a/tests/build-release-changelog.test.ts b/tests/build-release-changelog.test.ts index 7baf966d91..e7dc38dd79 100644 --- a/tests/build-release-changelog.test.ts +++ b/tests/build-release-changelog.test.ts @@ -136,10 +136,10 @@ describe("commit helpers", () => { describe("release metadata parsers", () => { test("parses multiline git-log records and trailing separators", () => { const raw = [ - `${sha("a")}\x1ffix(core): first change\x1ffix(core): first change\n\nline one\nline two\x1e`, - `${sha("b")}\x1ffeat(api): second change\x1ffeat(api): second change\x1e`, + sha("a"), "fix(core): first change", "fix(core): first change\n\nline one\nline two", + sha("b"), "feat(api): second change", "feat(api): second change", "", - ].join("\n"); + ].join("\0"); expect(parseGitLog(raw)).toEqual([ { @@ -156,12 +156,26 @@ describe("release metadata parsers", () => { }); test("fails closed on malformed git-log records", () => { - expect(() => parseGitLog(`\x1ffix(core): missing sha\x1fbody\x1e`)).toThrow( + expect(() => parseGitLog(`\0fix(core): missing sha\0body\0`)).toThrow( "malformed release commit record", ); - expect(() => parseGitLog(`${sha("a")}\x1f\x1fbody\x1e`)).toThrow( + expect(() => parseGitLog(`${sha("a")}\0\0body\0`)).toThrow( "malformed release commit record", ); + expect(() => parseGitLog(`${sha("a")}\0fix(core): missing body\0`)).toThrow( + "malformed release commit record", + ); + }); + + test("preserves control bytes in commit subjects and bodies", () => { + const subject = "release: v1.2.3\x1ffix: visible change"; + const body = `${subject}\n\nrecord separator: \x1e`; + + expect(parseGitLog(`${sha("a")}\0${subject}\0${body}\0`)).toEqual([{ + sha: sha("a"), + subject, + body, + }]); }); test("normalizes associated pull metadata safely", () => {