diff --git a/lib/createInlinePluginCreator.js b/lib/createInlinePluginCreator.js index 5fb9e347..985ab04f 100644 --- a/lib/createInlinePluginCreator.js +++ b/lib/createInlinePluginCreator.js @@ -1,5 +1,6 @@ const debug = require("debug")("msr:inlinePlugin"); const getCommitsFiltered = require("./getCommitsFiltered"); +const { getTagHead } = require("./git"); const { updateManifestDeps, resolveReleaseType } = require("./updateDeps"); /** @@ -40,11 +41,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. @@ -85,12 +81,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; @@ -151,8 +153,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. diff --git a/lib/getCommitsFiltered.js b/lib/getCommitsFiltered.js index 7bb0d5b7..4aad4dbb 100644 --- a/lib/getCommitsFiltered.js +++ b/lib/getCommitsFiltered.js @@ -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>} 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); @@ -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); diff --git a/lib/git.js b/lib/git.js index d86eb66a..89c63066 100644 --- a/lib/git.js +++ b/lib/git.js @@ -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} 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, }; diff --git a/test/lib/getCommitsFiltered.test.js b/test/lib/getCommitsFiltered.test.js index cc48aa3a..4ae74f05 100644 --- a/test/lib/getCommitsFiltered.test.js +++ b/test/lib/getCommitsFiltered.test.js @@ -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"); @@ -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"); @@ -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(); @@ -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"), }); }); });