diff --git a/scripts/draft-changelog.test.ts b/scripts/draft-changelog.test.ts index b5ddca59f4..6c9f65550c 100644 --- a/scripts/draft-changelog.test.ts +++ b/scripts/draft-changelog.test.ts @@ -1,4 +1,8 @@ import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { cpSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, it } from "node:test"; import { escapeForMdx, @@ -195,3 +199,60 @@ describe("release tags", () => { assert.deepEqual(renderTags(commits), ["Release", "Core"]); }); }); + +describe("drafting from a checkout that is behind", () => { + const git = (cwd: string, ...args: string[]) => + execFileSync( + "git", + [ + "-c", + "user.name=t", + "-c", + "user.email=t@t", + "-c", + "tag.gpgSign=false", + "-c", + "commit.gpgSign=false", + ...args, + ], + { + cwd, + encoding: "utf-8", + }, + ).trim(); + const land = (cwd: string, subject: string) => { + writeFileSync(join(cwd, "file.txt"), subject); + git(cwd, "add", "."); + git(cwd, "commit", "-m", subject); + }; + + it("lists what merged on the remote since the checkout was last pulled", () => { + const dir = mkdtempSync(join(tmpdir(), "draft-changelog-")); + try { + const origin = join(dir, "origin.git"); + const author = join(dir, "author"); + const stale = join(dir, "stale"); + git(dir, "init", "--bare", "-b", "main", origin); + git(dir, "clone", origin, author); + land(author, "chore: start"); + git(author, "tag", "v0.0.1"); + land(author, "feat: already pulled"); + git(author, "push", "origin", "main", "--tags"); + git(dir, "clone", origin, stale); + land(author, "fix: merged after the pull"); + git(author, "push", "origin", "main"); + cpSync(join(import.meta.dirname, "."), join(stale, "scripts"), { recursive: true }); + writeFileSync(join(stale, "package.json"), '{"type":"module"}'); + + const run = execFileSync( + process.execPath, + ["--import", import.meta.resolve("tsx"), "scripts/draft-changelog.ts", "0.0.2"], + { cwd: stale, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }, + ); + assert.match(run, /merged after the pull/i); + assert.match(run, /already pulled/i); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/scripts/draft-changelog.ts b/scripts/draft-changelog.ts index b66f5db342..f4efaf1ea7 100644 --- a/scripts/draft-changelog.ts +++ b/scripts/draft-changelog.ts @@ -108,8 +108,9 @@ function main() { function createDraft(options: Options): DraftOutput { const versionTag = `v${options.version}`; - const to = options.to ?? (tagExists(versionTag) ? versionTag : "HEAD"); + const to = options.to ?? (tagExists(versionTag) ? versionTag : fetchDraftHead()); const from = options.from ?? resolvePreviousTag(versionTag, to, options.version); + assertHasCommit(from); const commits = getCommits(from, to).filter((commit) => !shouldSkipCommit(commit)); const parsedCommits = commits.map(parseCommit); @@ -204,6 +205,21 @@ function git(args: string[]) { }).trim(); } +/** The remote's main as of now: a stale checkout would otherwise leave out what merged since it was last pulled. */ +function fetchDraftHead() { + git(["fetch", "origin", "main", "--tags", "--force"]); + console.error(`Drafting from origin/main at ${git(["rev-parse", "--short=9", "origin/main"])}`); + return "origin/main"; +} + +function assertHasCommit(ref: string) { + try { + git(["cat-file", "-e", `${ref}^{commit}`]); + } catch { + fail(`This clone has no objects for ${ref}, so the draft would list the wrong commits.`); + } +} + function tagExists(tag: string) { try { git(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}`]);