Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support merge workflows in generateNotes #55

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
39 changes: 30 additions & 9 deletions lib/createInlinePluginCreator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const debug = require("debug")("msr:inlinePlugin");
const getCommitsFiltered = require("./getCommitsFiltered");
const { getTagHead } = require("./git");
const { updateManifestDeps, resolveReleaseType } = require("./updateDeps");

/**
Expand Down Expand Up @@ -41,11 +42,6 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
);
};

/**
* @var {Commit[]} List of _filtered_ commits that only apply to this package.
*/
let commits;

/**
* @param {object} pluginOptions Options to configure this plugin.
* @param {object} context The semantic-release context.
Expand Down Expand Up @@ -86,12 +82,18 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
* @internal
*/
const analyzeCommits = async (pluginOptions, context) => {
const firstParentBranch = flags.firstParent ? context.branch.name : undefined;
pkg._preRelease = context.branch.prerelease || null;
pkg._branch = context.branch.name;

// Filter commits by directory.
commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead, firstParentBranch);
const firstParentBranch = flags.firstParent ? context.branch.name : undefined;
const commits = await getCommitsFiltered(
cwd,
dir,
context.lastRelease ? context.lastRelease.gitHead : undefined,
context.nextRelease ? context.nextRelease.gitHead : undefined,
firstParentBranch
);

// Set context.commits so analyzeCommits does correct analysis.
context.commits = commits;
Expand Down Expand Up @@ -157,8 +159,27 @@ function createInlinePluginCreator(packages, multiContext, synchronizer, flags)
// Vars.
const notes = [];

// Set context.commits so analyzeCommits does correct analysis.
// We need to redo this because context is a different instance each time.
//get SHA of lastRelease if not already there (should have been done by Semantic Release...)
if (context.lastRelease && context.lastRelease.gitTag) {
if (!context.lastRelease.gitHead || context.lastRelease.gitHead === context.lastRelease.gitTag) {
context.lastRelease.gitHead = await getTagHead(context.lastRelease.gitTag, {
cwd: context.cwd,
env: context.env,
});
}
}

// Filter commits by directory (and release range)
const firstParentBranch = flags.firstParent ? context.branch.name : undefined;
const commits = await getCommitsFiltered(
cwd,
dir,
context.lastRelease ? context.lastRelease.gitHead : undefined,
context.nextRelease ? context.nextRelease.gitHead : undefined,
firstParentBranch
);

// Set context.commits so generateNotes does correct analysis.
context.commits = commits;

// Get subnotes and add to list.
Expand Down
11 changes: 7 additions & 4 deletions lib/getCommitsFiltered.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ const debug = require("debug")("msr:commitsFilter");
*
* @param {string} cwd Absolute path of the working directory the Git repo is in.
* @param {string} dir Path to the target directory to filter by. Either absolute, or relative to cwd param.
* @param {string|void} lastHead The SHA of the previous release
* @param {string|void} lastRelease The SHA of the previous release (default to start of all commits if undefined)
* @param {string|void} nextRelease The SHA of the next release (default to HEAD if undefined)
* @param {string|void} firstParentBranch first-parent to determine which merges went into master
* @return {Promise<Array<Commit>>} The list of commits on the branch `branch` since the last release.
*/
async function getCommitsFiltered(cwd, dir, lastHead = undefined, firstParentBranch) {
async function getCommitsFiltered(cwd, dir, lastRelease, nextRelease, firstParentBranch) {
// Clean paths and make sure directories exist.
check(cwd, "cwd: directory");
check(dir, "dir: path");
cwd = cleanPath(cwd);
dir = cleanPath(dir, cwd);
check(dir, "dir: directory");
check(lastHead, "lastHead: alphanumeric{40}?");
check(lastRelease, "lastRelease: alphanumeric{40}?");
check(nextRelease, "nextRelease: alphanumeric{40}?");

// target must be inside and different than cwd.
if (dir.indexOf(cwd) !== 0) throw new ValueError("dir: Must be inside cwd", dir);
Expand All @@ -45,7 +47,8 @@ async function getCommitsFiltered(cwd, dir, lastHead = undefined, firstParentBra
// Use git-log-parser to get the commits.
const relpath = relative(root, dir);
const firstParentBranchFilter = firstParentBranch ? ["--first-parent", firstParentBranch] : [];
const gitLogFilterQuery = [...firstParentBranchFilter, lastHead ? `${lastHead}..HEAD` : "HEAD", "--", relpath];
const range = (lastRelease ? `${lastRelease}..` : "") + (nextRelease || "HEAD");
const gitLogFilterQuery = [...firstParentBranchFilter, range, "--", relpath];
const stream = gitLogParser.parse({ _: gitLogFilterQuery }, { cwd, env: process.env });
const commits = await getStream.array(stream);

Expand Down
13 changes: 13 additions & 0 deletions lib/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ function getTags(branch, execaOptions, filters) {
return tags.filter((tag) => validateSubstr(tag, filters));
}

/**
* Get the commit sha for a given tag.
*
* @param {String} tagName Tag name for which to retrieve the commit sha.
* @param {Object} [execaOptions] Options to pass to `execa`.
*
* @return {Promise<String>} The commit sha of the tag in parameter or `null`.
*/
async function getTagHead(tagName, execaOptions) {
return (await execa("git", ["rev-list", "-1", tagName], execaOptions)).stdout;
}

module.exports = {
getTags,
getTagHead,
};
49 changes: 43 additions & 6 deletions test/lib/getCommitsFiltered.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { gitInit, gitCommitAll, gitGetCommits } = require("../helpers/git");

// Tests.
describe("getCommitsFiltered()", () => {
test("Works correctly (no lastHead)", async () => {
test("Works correctly (no lastRelease)", async () => {
// Create Git repo with copy of Yarn workspaces fixture.
const cwd = await gitInit();
writeFileSync(`${cwd}/AAA.txt`, "AAA");
Expand All @@ -24,7 +24,7 @@ describe("getCommitsFiltered()", () => {
expect(commits[0].hash).toBe(sha2);
expect(commits[0].subject).toBe("Commit 2");
});
test("Works correctly (with lastHead)", async () => {
test("Works correctly (with lastRelease)", async () => {
// Create Git repo with copy of Yarn workspaces fixture.
const cwd = await gitInit();
writeFileSync(`${cwd}/AAA.txt`, "AAA");
Expand All @@ -40,6 +40,26 @@ describe("getCommitsFiltered()", () => {
const commits = await getCommitsFiltered(cwd, "bbb/", sha3);
expect(commits.length).toBe(0);
});

test("Works correctly (with lastRelease and nextRelease)", async () => {
// Create Git repo with copy of Yarn workspaces fixture.
const cwd = await gitInit();
writeFileSync(`${cwd}/AAA.txt`, "AAA");
const sha1 = await gitCommitAll(cwd, "Commit 1");
mkdirSync(`${cwd}/bbb`);
writeFileSync(`${cwd}/bbb/BBB.txt`, "BBB");
const sha2 = await gitCommitAll(cwd, "Commit 2");
writeFileSync(`${cwd}/bbb/BBB2.txt`, "BBB2");
const sha3 = await gitCommitAll(cwd, "Commit 3");
mkdirSync(`${cwd}/ccc`);
writeFileSync(`${cwd}/ccc/CCC.txt`, "CCC");
const sha4 = await gitCommitAll(cwd, "Commit 4");

// Filter a single directory from sha2 (lastRelease) to sha3 (nextRelease)
const commits = await getCommitsFiltered(cwd, "bbb/", sha2, sha3);
expect(commits.length).toBe(1);
expect(commits[0].hash).toBe(sha3);
});
test("Works correctly (initial commit)", async () => {
// Create Git repo with copy of Yarn workspaces fixture.
const cwd = await gitInit();
Expand Down Expand Up @@ -108,20 +128,37 @@ describe("getCommitsFiltered()", () => {
message: expect.stringMatching("dir: Must be inside cwd"),
});
});
test("TypeError if lastHead is not 40char alphanumeric Git SHA hash", async () => {
test("TypeError if lastRelease is not 40char alphanumeric Git SHA hash", async () => {
const cwd = tempy.directory();
mkdirSync(join(cwd, "dir"));
await expect(getCommitsFiltered(cwd, "dir", false)).rejects.toBeInstanceOf(TypeError);
await expect(getCommitsFiltered(cwd, "dir", false)).rejects.toMatchObject({
message: expect.stringMatching("lastHead: Must be alphanumeric string with size 40 or empty"),
message: expect.stringMatching("lastRelease: Must be alphanumeric string with size 40 or empty"),
});
await expect(getCommitsFiltered(cwd, "dir", 123)).rejects.toBeInstanceOf(TypeError);
await expect(getCommitsFiltered(cwd, "dir", 123)).rejects.toMatchObject({
message: expect.stringMatching("lastHead: Must be alphanumeric string with size 40 or empty"),
message: expect.stringMatching("lastRelease: Must be alphanumeric string with size 40 or empty"),
});
await expect(getCommitsFiltered(cwd, "dir", "nottherightlength")).rejects.toBeInstanceOf(TypeError);
await expect(getCommitsFiltered(cwd, "dir", "nottherightlength")).rejects.toMatchObject({
message: expect.stringMatching("lastHead: Must be alphanumeric string with size 40 or empty"),
message: expect.stringMatching("lastRelease: Must be alphanumeric string with size 40 or empty"),
});
});

test("TypeError if nextRelease is not 40char alphanumeric Git SHA hash", async () => {
const cwd = tempy.directory();
mkdirSync(join(cwd, "dir"));
await expect(getCommitsFiltered(cwd, "dir", undefined, false)).rejects.toBeInstanceOf(TypeError);
await expect(getCommitsFiltered(cwd, "dir", undefined, false)).rejects.toMatchObject({
message: expect.stringMatching("nextRelease: Must be alphanumeric string with size 40 or empty"),
});
await expect(getCommitsFiltered(cwd, "dir", undefined, 123)).rejects.toBeInstanceOf(TypeError);
await expect(getCommitsFiltered(cwd, "dir", undefined, 123)).rejects.toMatchObject({
message: expect.stringMatching("nextRelease: Must be alphanumeric string with size 40 or empty"),
});
await expect(getCommitsFiltered(cwd, "dir", undefined, "nottherightlength")).rejects.toBeInstanceOf(TypeError);
await expect(getCommitsFiltered(cwd, "dir", undefined, "nottherightlength")).rejects.toMatchObject({
message: expect.stringMatching("nextRelease: Must be alphanumeric string with size 40 or empty"),
});
});
});