Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ This works across release lines. For example, with 7.x on `master` and 6.x on a
Merge one release line into another with a merge commit. A squash merge hides the commits, and their pull requests, that it brings in.

The version being prepared (from `package.json`) gets a section for the commits on `--head` that no tag contains yet.
Once that version is tagged those commits are its own, and anything the branch gains afterwards is gathered under an `Unreleased` heading until the next version is prepared, which `template.unreleased` renames.

Without `--tags`, `gren` writes that section and the latest tag `--head` contains, so on a maintenance branch it works on that line's latest release rather than on the highest version in the repository.
`--tags=<new-tag>..<old-tag>` puts everything since the old tag in the new tag's section, taking in the releases between them.
Expand Down
64 changes: 50 additions & 14 deletions lib/src/Gren.js
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ class Gren {
return `${rangeBody}\n`;
}

return "*No changelog for this release.*\n";
return `${this.options.template.noChangelog}\n`;
}

/**
Expand Down Expand Up @@ -1914,17 +1914,30 @@ class Gren {
}

return entries
.map(({ release, frozen, verbatim }) =>
frozen
? verbatim !== undefined && {
.map(({ release, frozen, verbatim }) => {
if (frozen) {
return (
verbatim !== undefined && {
id: release.id,
release: release.name,
name: this.options.prefix + release.name,
published_at: release.date,
verbatim,
}
: blocks.shift(),
)
);
}

const block = blocks.shift();

if (release.version) {
return block;
}

// There is no version for the prefix to belong to in an unreleased section, and no
// reason to write the heading at all when the commits under it have nothing to say:
// they are in the report either way.
return block.body !== this._templateBody([]) && { ...block, name: release.name };
})
.filter(Boolean);
}

Expand Down Expand Up @@ -2016,30 +2029,53 @@ class Gren {
const releases = [];
const unreleasedVersion = parseVersion(version);

if (head && unreleasedVersion && !tags.some((tag) => tag.version === unreleasedVersion)) {
if (head) {
if (!git.refExists(head)) {
throw chalk.red(`\nThe branch "${head}" is not in the local repository.`);
}

const merged = git.tagsMergedInto(head);
const newer = tags.find(
(tag) => merged.has(tag.name) && semver.gte(tag.version, unreleasedVersion),
);

if (newer) {
const taken = unreleasedVersion && tags.some((tag) => tag.version === unreleasedVersion);
// A version equal to a tag is the ordinary state between releases. A version behind one
// is a package.json somebody forgot to bump, which is worth saying out loud.
const behind =
unreleasedVersion &&
!taken &&
tags.find((tag) => merged.has(tag.name) && semver.gt(tag.version, unreleasedVersion));

if (behind) {
console.warn(
chalk.yellow(
`\nSkipping the unreleased ${version} section: ${head} already contains ${newer.name}.`,
`\nThe version being prepared, ${version}, is older than ${behind.name}, which ` +
`${head} already contains. Commits no tag holds are listed as ` +
`${this.options.template.unreleased}.`,
),
);
} else {
}

if (unreleasedVersion && !taken && !behind) {
releases.push({
id: null,
name: version,
version: unreleasedVersion,
ref: head,
date: new Date().toISOString(),
});
} else {
// The version being prepared has been released already, so anything the branch has that
// no tag contains belongs to a release that does not have a name yet.
const excludeRefs = tags.map(({ ref }) => ref);

if (git.releaseCommits(head, excludeRefs).length) {
releases.push({
id: null,
name: this.options.template.unreleased,
version: null,
ref: head,
date: new Date().toISOString(),
excludeRefs,
});
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/src/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export default {
group: "\n### {{heading}}\n",
changelogTitle: "# Changelog\n\n",
release: "## {{release}} ({{date}})\n\n{{body}}",
unreleased: "Unreleased",
noChangelog: "*No changelog for this release.*",
releaseSeparator: "\n---\n\n",
};
95 changes: 86 additions & 9 deletions test/Gren.membership.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,88 @@ describe("Gren release membership", () => {
assert.deepEqual(numbers(byRelease(blocks)["v1.2.0"]), [6, 5, 4, 3]);
});

it("Should skip an unreleased version the branch has already passed", async () => {
it("Should gather commits no tag contains under an unreleased heading", async () => {
// 2.0.0 is released, and master carries c12 beyond every tag.
const blocks = await createGren({ version: "2.0.0" })._getReleaseBlocks();
const unreleased = byRelease(blocks)["Unreleased"];

assert.deepEqual(
blocks.map(({ release }) => release),
["Unreleased", "v2.0.0", "v1.2.1", "v1.2.0", "v1.1.1", "v1.1.0", "v1.0.0"],
);
assert.deepEqual(numbers(unreleased), [9]);
assert.equal(
unreleased.name,
"Unreleased",
"The prefix belongs to a version, and it has none",
);
});

it("Should take the unreleased heading and the empty body from the templates", async () => {
const gren = createGren({
version: "2.0.0",
template: { issue: "{{text}} {{name}} {{url}}", unreleased: "Nog niet uitgebracht" },
});
const blocks = await gren._getReleaseBlocks();

assert.include(
blocks.map(({ release }) => release),
"Nog niet uitgebracht",
);
assert.equal(
createGren({ template: { noChangelog: "_Niets te melden._" } })._templateBody([]),
"_Niets te melden._\n",
);
});

it("Should leave out the unreleased heading when its commits have no pull request", async () => {
// c12 is beyond every tag, but without pull request 9 there is nothing to say about it.
const blocks = await createGren({
version: "2.0.0",
overridePrs: (prs) => prs.filter(({ number }) => number !== 9),
})._getReleaseBlocks();

assert.notInclude(
blocks.map(({ release }) => release),
"Unreleased",
);
assert.include(warnings.join("\n"), repo.sha.c12.slice(0, 10), "It is reported instead");
});

it("Should leave out the unreleased heading when the branch is level with a tag", async () => {
const blocks = await createGren({ head: "1.x", version: "1.2.1" })._getReleaseBlocks();

assert.deepEqual(
blocks.map(({ release }) => release),
["v2.0.0", "v1.2.1", "v1.2.0", "v1.1.1", "v1.1.0", "v1.0.0"],
"Every tag still has a section; 1.x simply has nothing beyond them",
);
});

it("Should still gather them when the branch has passed the version being prepared", async () => {
const blocks = await createGren({ version: "1.1.5" })._getReleaseBlocks();

assert.include(
blocks.map(({ release }) => release),
"Unreleased",
"The work is there whatever package.json says",
);
});

it("Should say when the version being prepared is older than the branch", async () => {
const blocks = await createGren({ version: "1.1.5" })._getReleaseBlocks();

assert.notInclude(
blocks.map(({ release }) => release),
"1.1.5",
);
assert.include(warnings.join("\n"), "Skipping the unreleased 1.1.5 section");
assert.include(warnings.join("\n"), "The version being prepared, 1.1.5, is older than");
});

it("Should say nothing when the version being prepared is simply released", async () => {
await createGren({ version: "2.0.0" })._getReleaseBlocks();

assert.notInclude(warnings.join("\n"), "version being prepared");
});

it("Should generate only the tag given with --tags, or the latest tag", async () => {
Expand All @@ -428,7 +502,8 @@ describe("Gren release membership", () => {

assert.deepEqual(
selected.map(({ release }) => release),
["v1.1.1"],
["Unreleased", "v1.1.1"],
"2.0.0 is released, so master's later commits have no version yet",
);
assert.deepEqual(
latest.map(({ release }) => release),
Expand Down Expand Up @@ -523,16 +598,18 @@ describe("Gren release membership", () => {
}
});

it("Should not ask about the branch when the version is already tagged", async () => {
// Nothing is read from the branch, so it does not matter where GitHub has it.
it("Should not ask about a branch it has nothing to read from", async () => {
// 1.x is level with v1.2.1, so nothing is read from it and where GitHub has it does not
// matter.
const blocks = await createGren({
headSha: "0".repeat(40),
version: "2.0.0",
head: "1.x",
version: "1.2.1",
})._getReleaseBlocks();

assert.deepEqual(
assert.notInclude(
blocks.map(({ release }) => release),
["v2.0.0", "v1.2.1", "v1.2.0", "v1.1.1", "v1.1.0", "v1.0.0"],
"Unreleased",
);
});

Expand Down Expand Up @@ -563,7 +640,7 @@ describe("Gren release membership", () => {

try {
// Every tag is here and points where GitHub says, but the commits between them are not.
await createGren({ tags: "v1.2.0", version: "1.2.0" })._getReleaseBlocks();
await createGren({ tags: "v1.2.0", version: "1.2.0", head: "HEAD" })._getReleaseBlocks();
assert.fail("A shallow clone should stop the changelog");
} catch (error) {
assert.include(String(error), "truncated history");
Expand Down
Loading