Skip to content

Commit

Permalink
feat(publish): Display uncommitted changes when validation fails (#2066)
Browse files Browse the repository at this point in the history
* Add utils/collect-uncommitted package
  • Loading branch information
pdeona authored and evocateur committed May 11, 2019
1 parent 90acdde commit ea41fe9
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 25 deletions.
68 changes: 53 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions utils/check-working-tree/CHANGELOG.md
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions 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", () => {
Expand All @@ -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);
});
});
24 changes: 15 additions & 9 deletions utils/check-working-tree/lib/check-working-tree.js
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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")}`);
});
}
};
}
3 changes: 2 additions & 1 deletion utils/check-working-tree/package.json
Expand Up @@ -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"
}
}
10 changes: 10 additions & 0 deletions 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`
25 changes: 25 additions & 0 deletions 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.
78 changes: 78 additions & 0 deletions 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);
});
});
});
33 changes: 33 additions & 0 deletions 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);
}

0 comments on commit ea41fe9

Please sign in to comment.