From ea41fe91328858af0b5f5b8c715b17f1af637436 Mon Sep 17 00:00:00 2001 From: Pedro De Ona Date: Sat, 11 May 2019 15:37:18 -0400 Subject: [PATCH] feat(publish): Display uncommitted changes when validation fails (#2066) * Add utils/collect-uncommitted package --- package-lock.json | 68 ++++++++++++---- utils/check-working-tree/CHANGELOG.md | 6 ++ .../__tests__/check-working-tree.test.js | 14 ++++ .../lib/check-working-tree.js | 24 +++--- utils/check-working-tree/package.json | 3 +- utils/collect-uncommitted/CHANGELOG.md | 10 +++ utils/collect-uncommitted/README.md | 25 ++++++ .../__tests__/collect-uncommitted.js | 78 +++++++++++++++++++ .../lib/collect-uncommitted.js | 33 ++++++++ utils/collect-uncommitted/package.json | 39 ++++++++++ 10 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 utils/collect-uncommitted/CHANGELOG.md create mode 100644 utils/collect-uncommitted/README.md create mode 100644 utils/collect-uncommitted/__tests__/collect-uncommitted.js create mode 100644 utils/collect-uncommitted/lib/collect-uncommitted.js create mode 100644 utils/collect-uncommitted/package.json diff --git a/package-lock.json b/package-lock.json index bf9de75e5a..d55ba1e663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -735,6 +735,7 @@ "@lerna/check-working-tree": { "version": "file:utils/check-working-tree", "requires": { + "@lerna/collect-uncommitted": "file:utils/collect-uncommitted", "@lerna/describe-ref": "file:utils/describe-ref", "@lerna/validation-error": "file:core/validation-error" } @@ -769,6 +770,13 @@ "yargs": "^12.0.1" } }, + "@lerna/collect-uncommitted": { + "version": "file:utils/collect-uncommitted", + "requires": { + "@lerna/child-process": "file:core/child-process", + "chalk": "^2.3.1" + } + }, "@lerna/collect-updates": { "version": "file:utils/collect-updates", "requires": { @@ -1267,14 +1275,29 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, "@octokit/endpoint": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-4.1.1.tgz", - "integrity": "sha512-lfphGC9hglBDiIOU84f1xDUzjWE5j3jGkO3Ng/IpDDVARw760A+/x408JOEpdV20ZUj2GRWdDBC0+HPu5qA5gQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-4.2.2.tgz", + "integrity": "sha512-5IZjkUNhx5q0IRN7Juwf5A+Lu2qAso7ULST7C1P2mbGHePuCOk936Stcl/5GdJpB3ovD8M6/Lv3xra6Mn0IKNQ==", "requires": { "deepmerge": "3.2.0", - "is-plain-object": "^2.0.4", + "is-plain-object": "^3.0.0", "universal-user-agent": "^2.0.1", "url-template": "^2.0.8" + }, + "dependencies": { + "is-plain-object": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", + "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", + "requires": { + "isobject": "^4.0.0" + } + }, + "isobject": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" + } } }, "@octokit/plugin-enterprise-rest": { @@ -1283,24 +1306,39 @@ "integrity": "sha512-CTZr64jZYhGWNTDGlSJ2mvIlFsm9OEO3LqWn9I/gmoHI4jRBp4kpHoFYNemG4oA75zUAcmbuWblb7jjP877YZw==" }, "@octokit/request": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-3.0.0.tgz", - "integrity": "sha512-DZqmbm66tq+a9FtcKrn0sjrUpi0UaZ9QPUCxxyk/4CJ2rseTMpAWRf6gCwOSUCzZcx/4XVIsDk+kz5BVdaeenA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-3.0.1.tgz", + "integrity": "sha512-aH61OVkMKMofGW/go2x4mJ44X4U/JF8xsiFFictwkZYtz0psE8OPKpsP2TZBZaJoCg2wmeTyEgqGfY+veg0hGQ==", "requires": { "@octokit/endpoint": "^4.0.0", "deprecation": "^1.0.1", - "is-plain-object": "^2.0.4", + "is-plain-object": "^3.0.0", "node-fetch": "^2.3.0", "once": "^1.4.0", "universal-user-agent": "^2.0.1" + }, + "dependencies": { + "is-plain-object": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", + "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", + "requires": { + "isobject": "^4.0.0" + } + }, + "isobject": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==" + } } }, "@octokit/rest": { - "version": "16.25.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.25.0.tgz", - "integrity": "sha512-QKIzP0gNYjyIGmY3Gpm3beof0WFwxFR+HhRZ+Wi0fYYhkEUvkJiXqKF56Pf5glzzfhEwOrggfluEld5F/ZxsKw==", + "version": "16.25.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.25.1.tgz", + "integrity": "sha512-a1Byzjj07OMQNUQDP5Ng/rChaI7aq6TNMY1ZFf8+zCVEEtYzCgcmrFG9BDerFbLPPKGQ5TAeRRFyLujUUN1HIg==", "requires": { - "@octokit/request": "3.0.0", + "@octokit/request": "3.0.1", "atob-lite": "^2.0.0", "before-after-hook": "^1.4.0", "btoa-lite": "^1.0.0", @@ -6511,9 +6549,9 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-fetch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", - "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.5.0.tgz", + "integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw==" }, "node-fetch-npm": { "version": "2.0.2", diff --git a/utils/check-working-tree/CHANGELOG.md b/utils/check-working-tree/CHANGELOG.md index d30f1ed917..74196b5f79 100644 --- a/utils/check-working-tree/CHANGELOG.md +++ b/utils/check-working-tree/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## Unpublished + +### Features + +* Display uncommitted changes in error message when working tree is dirty. + ## [3.13.3](https://github.com/lerna/lerna/compare/v3.13.2...v3.13.3) (2019-04-17) **Note:** Version bump only for package @lerna/check-working-tree diff --git a/utils/check-working-tree/__tests__/check-working-tree.test.js b/utils/check-working-tree/__tests__/check-working-tree.test.js index 475e3b8d92..4f6386e9d3 100644 --- a/utils/check-working-tree/__tests__/check-working-tree.test.js +++ b/utils/check-working-tree/__tests__/check-working-tree.test.js @@ -1,8 +1,10 @@ "use strict"; jest.mock("@lerna/describe-ref"); +jest.mock("@lerna/collect-uncommitted"); const describeRef = require("@lerna/describe-ref"); +const collectUncommitted = require("@lerna/collect-uncommitted"); const checkWorkingTree = require("../lib/check-working-tree"); describe("check-working-tree", () => { @@ -29,13 +31,25 @@ describe("check-working-tree", () => { it("rejects when working tree has uncommitted changes", async () => { describeRef.mockResolvedValueOnce({ isDirty: true }); + collectUncommitted.mockResolvedValueOnce(["AD file"]); try { await checkWorkingTree(); } catch (err) { expect(err.message).toMatch("Working tree has uncommitted changes"); + expect(err.message).toMatch("\nAD file"); } + expect.assertions(2); + }); + + it("passes cwd to collectUncommitted when working tree has uncommitted changes", async () => { + describeRef.mockResolvedValueOnce({ isDirty: true }); + try { + await checkWorkingTree({ cwd: "foo" }); + } catch (err) { + expect(collectUncommitted).toHaveBeenLastCalledWith({ cwd: "foo" }); + } expect.assertions(1); }); }); diff --git a/utils/check-working-tree/lib/check-working-tree.js b/utils/check-working-tree/lib/check-working-tree.js index 321c1a48ab..21e9a34746 100644 --- a/utils/check-working-tree/lib/check-working-tree.js +++ b/utils/check-working-tree/lib/check-working-tree.js @@ -2,10 +2,12 @@ const describeRef = require("@lerna/describe-ref"); const ValidationError = require("@lerna/validation-error"); +const collectUncommitted = require("@lerna/collect-uncommitted"); module.exports = checkWorkingTree; +module.exports.mkThrowIfUncommitted = mkThrowIfUncommitted; module.exports.throwIfReleased = throwIfReleased; -module.exports.throwIfUncommitted = throwIfUncommitted; +module.exports.throwIfUncommitted = mkThrowIfUncommitted(); function checkWorkingTree({ cwd } = {}) { let chain = Promise.resolve(); @@ -17,7 +19,7 @@ function checkWorkingTree({ cwd } = {}) { // prevent duplicate versioning chain.then(throwIfReleased), // prevent publish of uncommitted changes - chain.then(throwIfUncommitted), + chain.then(mkThrowIfUncommitted({ cwd })), ]; // passes through result of describeRef() to aid composability @@ -33,11 +35,15 @@ function throwIfReleased({ refCount }) { } } -function throwIfUncommitted({ isDirty }) { - if (isDirty) { - throw new ValidationError( - "EUNCOMMIT", - "Working tree has uncommitted changes, please commit or remove changes before continuing." - ); - } +const EUNCOMMIT_MSG = + "Working tree has uncommitted changes, please commit or remove the following changes before continuing:\n"; + +function mkThrowIfUncommitted(options = {}) { + return function throwIfUncommitted({ isDirty }) { + if (isDirty) { + return collectUncommitted(options).then(uncommitted => { + throw new ValidationError("EUNCOMMIT", `${EUNCOMMIT_MSG}${uncommitted.join("\n")}`); + }); + } + }; } diff --git a/utils/check-working-tree/package.json b/utils/check-working-tree/package.json index 983cbbf2f2..a8a0d3065e 100644 --- a/utils/check-working-tree/package.json +++ b/utils/check-working-tree/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@lerna/describe-ref": "file:../describe-ref", - "@lerna/validation-error": "file:../../core/validation-error" + "@lerna/validation-error": "file:../../core/validation-error", + "@lerna/collect-uncommitted": "file:../collect-uncommitted" } } diff --git a/utils/collect-uncommitted/CHANGELOG.md b/utils/collect-uncommitted/CHANGELOG.md new file mode 100644 index 0000000000..79b6870d44 --- /dev/null +++ b/utils/collect-uncommitted/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## Unpublished + +### Features + +* Create `[@lerna](https://github.com/lerna)/collect-uncommitted` diff --git a/utils/collect-uncommitted/README.md b/utils/collect-uncommitted/README.md new file mode 100644 index 0000000000..1054204e5b --- /dev/null +++ b/utils/collect-uncommitted/README.md @@ -0,0 +1,25 @@ +# `@lerna/collect-uncommitted` + +> Check git working tree status and collect uncommitted changes for display + +## Usage + +```js +const collectUncommitted = require("@lerna/collect-uncommitted"); + +// values listed here are their defaults +const options = { + cwd: process.cwd(), +}; + +(async () => { + try { + const results = await collectUncommitted(options); + console.log(`Uncommitted changes on CWD ${options.cwd}: ${results.join("\n")}`); + } catch (err) { + console.error(err.message); + } +})(); +``` + +Install [lerna](https://www.npmjs.com/package/lerna) for access to the `lerna` CLI. diff --git a/utils/collect-uncommitted/__tests__/collect-uncommitted.js b/utils/collect-uncommitted/__tests__/collect-uncommitted.js new file mode 100644 index 0000000000..a07ef8516e --- /dev/null +++ b/utils/collect-uncommitted/__tests__/collect-uncommitted.js @@ -0,0 +1,78 @@ +"use strict"; + +jest.mock("@lerna/child-process"); + +const chalk = require("chalk"); +const childProcess = require("@lerna/child-process"); +const collectUncommitted = require("../lib/collect-uncommitted"); + +const stats = `AD file1 + D file2 + M path/to/file3 +AM path/file4 +MM path/file5 +M file6 +D file7 +UU file8 +?? file9`; + +const GREEN_A = chalk.green("A"); +const GREEN_M = chalk.green("M"); +const GREEN_D = chalk.green("D"); +const RED_D = chalk.red("D"); +const RED_M = chalk.red("M"); +const RED_UU = chalk.red("UU"); +const RED_QQ = chalk.red("??"); + +const colorizedAry = [ + `${GREEN_A}${RED_D} file1`, + ` ${RED_D} file2`, + ` ${RED_M} path/to/file3`, + `${GREEN_A}${RED_M} path/file4`, + `${GREEN_M}${RED_M} path/file5`, + `${GREEN_M} file6`, + `${GREEN_D} file7`, + `${RED_UU} file8`, + `${RED_QQ} file9`, +]; + +childProcess.exec.mockResolvedValue(stats); +childProcess.execSync.mockReturnValue(stats); + +describe("collectUncommitted()", () => { + it("resolves an array of uncommitted changes", async () => { + const result = await collectUncommitted(); + expect(childProcess.exec).toHaveBeenLastCalledWith("git", "status -s", {}); + expect(result).toEqual(colorizedAry); + }); + + it("empty array on clean repo", async () => { + childProcess.exec.mockResolvedValueOnce(""); + const result = await collectUncommitted(); + expect(childProcess.exec).toHaveBeenLastCalledWith("git", "status -s", {}); + expect(result).toEqual([]); + }); + + it("accepts options.cwd", async () => { + const options = { cwd: "foo" }; + await collectUncommitted(options); + + expect(childProcess.exec).toHaveBeenLastCalledWith("git", "status -s", options); + }); + + describe("collectUncommitted.sync()", () => { + it("returns an array of uncommitted changes", async () => { + const result = collectUncommitted.sync(); + + expect(childProcess.execSync).toHaveBeenLastCalledWith("git", "status -s", {}); + expect(result).toEqual(colorizedAry); + }); + + it("accepts options.cwd", async () => { + const options = { cwd: "foo" }; + collectUncommitted.sync(options); + + expect(childProcess.execSync).toHaveBeenLastCalledWith("git", "status -s", options); + }); + }); +}); diff --git a/utils/collect-uncommitted/lib/collect-uncommitted.js b/utils/collect-uncommitted/lib/collect-uncommitted.js new file mode 100644 index 0000000000..17dbf60c80 --- /dev/null +++ b/utils/collect-uncommitted/lib/collect-uncommitted.js @@ -0,0 +1,33 @@ +"use strict"; + +const chalk = require("chalk"); +const { exec, execSync } = require("@lerna/child-process"); + +module.exports = collectUncommitted; +module.exports.sync = sync; + +const maybeColorize = colorize => s => (s !== " " ? colorize(s) : s); +const cRed = maybeColorize(chalk.red); +const cGreen = maybeColorize(chalk.green); + +const replaceStatus = (_, maybeGreen, maybeRed) => `${cGreen(maybeGreen)}${cRed(maybeRed)}`; + +const colorizeStats = stats => + stats.replace(/^([^U]| )([A-Z]| )/gm, replaceStatus).replace(/^\?{2}|U{2}/gm, cRed("$&")); + +const splitOnNewLine = (string = "") => string.split("\n"); + +const filterEmpty = (strings = []) => strings.filter(s => s.length !== 0); + +const o = (l, r) => x => l(r(x)); + +const transformOutput = o(filterEmpty, o(splitOnNewLine, colorizeStats)); + +function collectUncommitted(options = {}) { + return exec("git", "status -s", options).then(transformOutput); +} + +function sync(options = {}) { + const stdout = execSync("git", "status -s", options); + return transformOutput(stdout); +} diff --git a/utils/collect-uncommitted/package.json b/utils/collect-uncommitted/package.json new file mode 100644 index 0000000000..2d6ec5c7e0 --- /dev/null +++ b/utils/collect-uncommitted/package.json @@ -0,0 +1,39 @@ +{ + "name": "@lerna/collect-uncommitted", + "version": "3.13.3", + "description": "Collect uncommitted changes to working tree for display in error messages", + "keywords": [ + "lerna", + "utils", + "git", + "tree" + ], + "author": "Daniel Stockman ", + "contributors": [ + "Pedro De Ona " + ], + "homepage": "https://github.com/lerna/lerna/tree/master/utils/collect-uncommitted#readme", + "license": "MIT", + "main": "lib/collect-uncommitted.js", + "files": [ + "lib" + ], + "engines": { + "node": ">= 6.9.0" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lerna/lerna.git", + "directory": "utils/collect-uncommitted" + }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "dependencies": { + "@lerna/child-process": "file:../../core/child-process", + "chalk": "^2.3.1" + } +}