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"
+ }
+}