diff --git a/lib/src/Gren.js b/lib/src/Gren.js index 69286b4c..bffe2eb5 100644 --- a/lib/src/Gren.js +++ b/lib/src/Gren.js @@ -35,6 +35,12 @@ const TAGS_LIMIT = 30; // The data sources whose releases are built from the commits each release contains. const MEMBERSHIP_DATA_SOURCES = ["prs", "prs-with-issues"]; +// Where a release section starts. The preamble a changelog opens with ends here, and a +// changelog written without a separator is split here; the two have to agree on the answer. +const RELEASE_HEADING = "#{1,6} +v?\\d+\\.\\d+\\.\\d+"; +const PREAMBLE_END = new RegExp(`^${RELEASE_HEADING}`, "m"); +const SECTION_SPLIT = new RegExp(`(?<=\\n)(?=${RELEASE_HEADING})`); + /** * Get the semver version a tag or release name stands for * @@ -350,12 +356,22 @@ class Gren { content = content.slice(changelogTitle.length); } + // The title in the file is not always the one in the configuration: a banner is added to + // it, or its wording changes. Whatever stands before the first release is title too, and + // leaving it in makes the preamble's first line the heading of the release under it, which + // drops that release from the file this run is about to write over it. + const firstRelease = content.search(PREAMBLE_END); + + if (firstRelease > 0) { + content = content.slice(firstRelease); + } + let current = null; // A changelog written with nothing between its releases can only be split on the headings, // at the start of each one so that the release before it keeps its last line. const chunks = releaseSeparator ? content.split(releaseSeparator) - : content.split(/(?<=\n)(?=#{1,6} +v?\d+\.\d+\.\d+)/); + : content.split(SECTION_SPLIT); chunks.forEach((chunk) => { const heading = chunk.trimStart().split("\n")[0]; diff --git a/test/Gren.membership.spec.js b/test/Gren.membership.spec.js index 83526103..854a2c12 100644 --- a/test/Gren.membership.spec.js +++ b/test/Gren.membership.spec.js @@ -707,6 +707,42 @@ describe("Gren release membership", () => { }); }); + it("Should read the first release under a title the configuration does not spell", () => { + // A banner, or any title edited since the file was written, is still title. Keying the + // first section off the configured changelogTitle alone leaves the leftover preamble + // attached to the release under it, and that release is then dropped from the file the + // next run writes. + fs.writeFileSync( + path.join(repo.dir, "CHANGELOG.md"), + "# Changelog\n\n> Sponsor this project.\n\n" + Object.values(sections).join(separator), + ); + + const gren = createGren({ frozenBefore: "2020-01-16", changelogFilename: "CHANGELOG.md" }); + const read = gren._readChangelogSections(); + + assert.equal(read.get("2.0.0"), sections["2.0.0"]); + assert.equal(read.get("1.2.0"), sections["1.2.0"]); + }); + + it("Should read the first release under an unknown title without a separator", () => { + const plain = { "2.0.0": sections["2.0.0"], "1.2.0": sections["1.2.0"] }; + + fs.writeFileSync( + path.join(repo.dir, "CHANGELOG.md"), + "Release history\n===============\n\n" + Object.values(plain).join(""), + ); + + const gren = createGren({ + frozenBefore: "2020-01-16", + changelogFilename: "CHANGELOG.md", + template: { releaseSeparator: "" }, + }); + const read = gren._readChangelogSections(); + + assert.equal(read.get("2.0.0"), plain["2.0.0"]); + assert.equal(read.get("1.2.0"), plain["1.2.0"]); + }); + it("Should read a changelog with nothing between its releases", async () => { const plain = { "2.0.0": sections["2.0.0"], "1.2.0": sections["1.2.0"] };