diff --git a/commands/add/__tests__/add-command.test.js b/commands/add/__tests__/add-command.test.js index 71017399c1..3f8fc39ae3 100644 --- a/commands/add/__tests__/add-command.test.js +++ b/commands/add/__tests__/add-command.test.js @@ -290,6 +290,7 @@ describe("AddCommand", () => { ignore: undefined, private: undefined, since: undefined, + excludeDependents: undefined, includeFilteredDependents: undefined, includeFilteredDependencies: undefined, }) diff --git a/commands/add/index.js b/commands/add/index.js index 347f262d3e..b75074189e 100644 --- a/commands/add/index.js +++ b/commands/add/index.js @@ -105,6 +105,7 @@ class AddCommand extends Command { ignore: undefined, private: undefined, since: undefined, + excludeDependents: undefined, includeFilteredDependents: undefined, includeFilteredDependencies: undefined, }); diff --git a/core/filter-options/README.md b/core/filter-options/README.md index e0c7822fc4..016aa18044 100644 --- a/core/filter-options/README.md +++ b/core/filter-options/README.md @@ -51,6 +51,12 @@ $ lerna ls --since some-branch _This can be particularly useful when used in CI, if you can obtain the target branch a PR will be going into, because you can use that as the `ref` to the `--since` option. This works well for PRs going into master as well as feature branches._ +### `--exclude-dependents` + +Exclude all transitive dependents when running a command with `--since`, overriding the default "changed" algorithm. + +This flag has no effect without `--since`, and will throw an error in that case. + ### `--include-filtered-dependents` Include all transitive dependents when running a command regardless of `--scope`, `--ignore`, or `--since`. diff --git a/core/filter-options/__tests__/get-filtered-packages.test.js b/core/filter-options/__tests__/get-filtered-packages.test.js index 4407e656b7..cdafdcaa6e 100644 --- a/core/filter-options/__tests__/get-filtered-packages.test.js +++ b/core/filter-options/__tests__/get-filtered-packages.test.js @@ -169,6 +169,32 @@ test("--scope package-{2,3,4} --since master", async () => { ); }); +test("--exclude-dependents", async () => { + const packageGraph = await buildGraph(cwd); + const execOpts = { cwd }; + const options = parseOptions("--since", "foo", "--exclude-dependents"); + + await getFilteredPackages(packageGraph, execOpts, options); + + expect(collectUpdates).toHaveBeenLastCalledWith( + expect.any(Array), + packageGraph, + execOpts, + expect.objectContaining({ excludeDependents: true }) + ); +}); + +test("--exclude-dependents conflicts with --include-filtered-dependents", async () => { + try { + parseOptions("--exclude-dependents", "--include-filtered-dependents"); + } catch (err) { + expect(err.message).toMatch("exclude-dependents"); + expect(err.message).toMatch("include-filtered-dependents"); + } + + expect.hasAssertions(); +}); + test("--include-filtered-dependents", async () => { const packageGraph = await buildGraph(cwd); const execOpts = { cwd }; diff --git a/core/filter-options/index.js b/core/filter-options/index.js index d1c7d016fc..16f027a3dc 100644 --- a/core/filter-options/index.js +++ b/core/filter-options/index.js @@ -35,11 +35,20 @@ function filterOptions(yargs) { `, type: "string", }, + "exclude-dependents": { + describe: dedent` + Exclude all transitive dependents when running a command + with --since, overriding the default "changed" algorithm. + `, + conflicts: "include-filtered-dependents", + type: "boolean", + }, "include-filtered-dependents": { describe: dedent` Include all transitive dependents when running a command regardless of --scope, --ignore, or --since. `, + conflicts: "exclude-dependents", type: "boolean", }, "include-filtered-dependencies": { diff --git a/core/filter-options/lib/get-filtered-packages.js b/core/filter-options/lib/get-filtered-packages.js index 7a8d5b1f84..2a0c6609f5 100644 --- a/core/filter-options/lib/get-filtered-packages.js +++ b/core/filter-options/lib/get-filtered-packages.js @@ -30,6 +30,10 @@ function getFilteredPackages(packageGraph, execOpts, options) { if (options.since !== undefined) { log.notice("filter", "changed since %j", options.since); + if (options.excludeDependents) { + log.notice("filter", "excluding dependents"); + } + chain = chain.then(filteredPackages => Promise.resolve(collectUpdates(filteredPackages, packageGraph, execOpts, options)).then(updates => { const updated = new Set(updates.map(({ pkg }) => pkg.name)); diff --git a/utils/collect-updates/__tests__/collect-updates.test.js b/utils/collect-updates/__tests__/collect-updates.test.js index 10b222a74a..d2af1e1c51 100644 --- a/utils/collect-updates/__tests__/collect-updates.test.js +++ b/utils/collect-updates/__tests__/collect-updates.test.js @@ -95,6 +95,23 @@ describe("collectUpdates()", () => { ]); }); + it("constrains results by excluded dependents", () => { + changedPackages.add("package-dag-1"); + + const graph = buildGraph(); + const pkgs = graph.rawPackageList; + const execOpts = { cwd: "/test" }; + + const updates = collectUpdates(pkgs, graph, execOpts, { + excludeDependents: true, + }); + + expect(updates).toEqual([ + expect.objectContaining({ name: "package-dag-1" }), + // collectDependents() is skipped + ]); + }); + it("constrains results by filtered packages", () => { changedPackages.add("package-dag-2a"); changedPackages.add("package-dag-3"); diff --git a/utils/collect-updates/collect-updates.js b/utils/collect-updates/collect-updates.js index 7cc327aea3..b3745367e9 100644 --- a/utils/collect-updates/collect-updates.js +++ b/utils/collect-updates/collect-updates.js @@ -13,7 +13,7 @@ module.exports.collectPackages = collectPackages; module.exports.getPackagesForOption = getPackagesForOption; function collectUpdates(filteredPackages, packageGraph, execOpts, commandOptions) { - const { forcePublish, conventionalCommits, conventionalGraduate } = commandOptions; + const { forcePublish, conventionalCommits, conventionalGraduate, excludeDependents } = commandOptions; // If --conventional-commits and --conventional-graduate are both set, ignore --force-publish const useConventionalGraduate = conventionalCommits && conventionalGraduate; @@ -69,6 +69,7 @@ function collectUpdates(filteredPackages, packageGraph, execOpts, commandOptions return collectPackages(packages, { onInclude: name => log.verbose("updated", name), + excludeDependents, }); } @@ -86,5 +87,6 @@ function collectUpdates(filteredPackages, packageGraph, execOpts, commandOptions return collectPackages(packages, { isCandidate: (node, name) => isForced(node, name) || needsBump(node) || hasDiff(node), onInclude: name => log.verbose("updated", name), + excludeDependents, }); } diff --git a/utils/collect-updates/lib/collect-packages.js b/utils/collect-updates/lib/collect-packages.js index 1317a74fa6..061f20f888 100644 --- a/utils/collect-updates/lib/collect-packages.js +++ b/utils/collect-updates/lib/collect-packages.js @@ -4,7 +4,7 @@ const collectDependents = require("./collect-dependents"); module.exports = collectPackages; -function collectPackages(packages, { isCandidate = () => true, onInclude } = {}) { +function collectPackages(packages, { isCandidate = () => true, onInclude, excludeDependents } = {}) { const candidates = new Set(); packages.forEach((node, name) => { @@ -13,8 +13,9 @@ function collectPackages(packages, { isCandidate = () => true, onInclude } = {}) } }); - const dependents = collectDependents(candidates); - dependents.forEach(node => candidates.add(node)); + if (!excludeDependents) { + collectDependents(candidates).forEach(node => candidates.add(node)); + } // The result should always be in the same order as the input const updates = [];