From 44dabb21de678822188929fd5effe27ddd7f1e6c Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 29 Jun 2022 22:38:29 -0400 Subject: [PATCH] feat(diff): add Lerna `diff` as optional command --- README.md | 29 +-- package.json | 2 + .../__tests__/cli-diff-commands.spec.ts | 14 ++ .../cli/src/cli-commands/cli-diff-commands.ts | 26 +++ packages/cli/src/lerna-entry.ts | 2 + packages/core/src/command.ts | 2 + packages/core/src/models/command-options.ts | 8 + packages/diff/README.md | 38 ++++ packages/diff/package.json | 36 ++++ .../__tests__/__fixtures__/basic/lerna.json | 3 + .../__tests__/__fixtures__/basic/package.json | 3 + .../basic/packages/package-1/package.json | 5 + .../basic/packages/package-2/package.json | 8 + .../__snapshots__/diff-command.spec.ts.snap | 99 +++++++++++ .../diff/src/__tests__/diff-command.spec.ts | 166 ++++++++++++++++++ packages/diff/src/diff-command.ts | 62 +++++++ packages/diff/src/index.ts | 1 + packages/diff/src/lib/get-last-commit.ts | 34 ++++ packages/diff/src/lib/has-commit.ts | 20 +++ packages/diff/tsconfig.bundle.json | 8 + pnpm-lock.yaml | 8 + 21 files changed, 561 insertions(+), 13 deletions(-) create mode 100644 packages/cli/src/cli-commands/__tests__/cli-diff-commands.spec.ts create mode 100644 packages/cli/src/cli-commands/cli-diff-commands.ts create mode 100644 packages/diff/README.md create mode 100644 packages/diff/package.json create mode 100644 packages/diff/src/__tests__/__fixtures__/basic/lerna.json create mode 100644 packages/diff/src/__tests__/__fixtures__/basic/package.json create mode 100644 packages/diff/src/__tests__/__fixtures__/basic/packages/package-1/package.json create mode 100644 packages/diff/src/__tests__/__fixtures__/basic/packages/package-2/package.json create mode 100644 packages/diff/src/__tests__/__snapshots__/diff-command.spec.ts.snap create mode 100644 packages/diff/src/__tests__/diff-command.spec.ts create mode 100644 packages/diff/src/diff-command.ts create mode 100644 packages/diff/src/index.ts create mode 100644 packages/diff/src/lib/get-last-commit.ts create mode 100644 packages/diff/src/lib/has-commit.ts create mode 100644 packages/diff/tsconfig.bundle.json diff --git a/README.md b/README.md index a5eba1c8..9cca1a24 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - 📑 [`version`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version#readme) - create new version for each workspace packages - optional (**separate install**, refer to [installation](#installation) table shown below) - 🕜 [`changed`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/changed#readme) - list local packages that changed since last tagged release + - 🌓 [`diff`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/diff#readme) - git diff all packages or a single package since the last release - 👷 [`exec`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/exec#readme) - execute shell command in each workspace package - 📖 [`list`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/list#readme) - list local packages - 🏃 [`run`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/run#readme) - run npm script in each workspace packages @@ -137,6 +138,7 @@ If you are new to Lerna-Lite, you could also run the [lerna init](https://github | 📑 [version](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version#readme) | `npm i @lerna-lite/cli -D -W` | create new version for each workspace package | Yes | | ☁️ [publish](https://github.com/ghiscoding/lerna-lite/tree/main/packages/publish#readme) | `npm i @lerna-lite/cli -D -W` | publish each workspace package | Yes | | 🕜 [changed](https://github.com/ghiscoding/lerna-lite/tree/main/packages/changed#readme) | `npm i @lerna-lite/changed -D -W` | list local packages changed since last release | Optional | +| 🌓 [diff](https://github.com/ghiscoding/lerna-lite/tree/main/packages/diff#readme) | `npm i @lerna-lite/diff -D -W` | git diff all packages since the last release | Optional | | 👷 [exec](https://github.com/ghiscoding/lerna-lite/tree/main/packages/exec#readme) | `npm i @lerna-lite/exec -D -W` | execute an command in each workspace package | Optional | | 📖 [list](https://github.com/ghiscoding/lerna-lite/tree/main/packages/list#readme) | `npm i @lerna-lite/list -D -W` | list local packages | Optional | | 🏃 [run](https://github.com/ghiscoding/lerna-lite/tree/main/packages/run#readme) | `npm i @lerna-lite/run -D -W` | run npm script in each workspace package | Optional | @@ -228,17 +230,18 @@ If you have problems running the lib and your problems are related to Git comman ## Lerna-Lite Full List of Packages -| Package Name | Version | Description | Changes | -| ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| [@lerna-lite/cli](https://github.com/ghiscoding/lerna-lite/tree/main/packages/cli) | [![npm](https://img.shields.io/npm/v/@lerna-lite/cli.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/cli) | Lerna-Lite Init/Info/Version/Publish comands CLI | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/cli/CHANGELOG.md) | -| [@lerna-lite/core](https://github.com/ghiscoding/lerna-lite/tree/main/packages/core) | [![npm](https://img.shields.io/npm/v/@lerna-lite/core.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/core) | Lerna-Lite core & shared methods (internal use) | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/core/CHANGELOG.md) | -| [@lerna-lite/info](https://github.com/ghiscoding/lerna-lite/tree/main/packages/info) | [![npm](https://img.shields.io/npm/v/@lerna-lite/info.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/info) | Print local environment information | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/info/CHANGELOG.md) | -| [@lerna-lite/init](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init) | [![npm](https://img.shields.io/npm/v/@lerna-lite/init.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/init) | create a new Lerna-Lite repo | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/init/CHANGELOG.md) | -| [@lerna-lite/publish](https://github.com/ghiscoding/lerna-lite/tree/main/packages/publish) | [![npm](https://img.shields.io/npm/v/@lerna-lite/publish.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/publish) | Publish packages in the current workspace | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/publish/CHANGELOG.md) | -| [@lerna-lite/version](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version) | [![npm](https://img.shields.io/npm/v/@lerna-lite/version.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/version) | Bump Version & write Changelogs | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/version/CHANGELOG.md) | -| [@lerna-lite/exec](https://github.com/ghiscoding/lerna-lite/tree/main/packages/exec) | [![npm](https://img.shields.io/npm/v/@lerna-lite/exec.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/exec) | Execute shell command in current workspace | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/exec/CHANGELOG.md) | -| [@lerna-lite/changed](https://github.com/ghiscoding/lerna-lite/tree/main/packages/changed) | [![npm](https://img.shields.io/npm/v/@lerna-lite/changed.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/changed) | List local packages that changed since last release | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/changed/CHANGELOG.md) | -| [@lerna-lite/list](https://github.com/ghiscoding/lerna-lite/tree/main/packages/list) | [![npm](https://img.shields.io/npm/v/@lerna-lite/list.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/list) | List local packages | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/list/CHANGELOG.md) | -| [@lerna-lite/listable](https://github.com/ghiscoding/lerna-lite/tree/main/packages/listable) | [![npm](https://img.shields.io/npm/v/@lerna-lite/listable.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/listable) | Listable utils used by `list` and `changed` commands | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/listable/CHANGELOG.md) | -| [@lerna-lite/run](https://github.com/ghiscoding/lerna-lite/tree/main/packages/run) | [![npm](https://img.shields.io/npm/v/@lerna-lite/run.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/run) | Run npm scripts in current workspace | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/run/CHANGELOG.md) | +| Package Name | Version | Description | Changes | +| ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| [@lerna-lite/cli](https://github.com/ghiscoding/lerna-lite/tree/main/packages/cli) | [![npm](https://img.shields.io/npm/v/@lerna-lite/cli.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/cli) | Lerna-Lite Init/Info/Version/Publish comands CLI | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/cli/CHANGELOG.md) | +| [@lerna-lite/core](https://github.com/ghiscoding/lerna-lite/tree/main/packages/core) | [![npm](https://img.shields.io/npm/v/@lerna-lite/core.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/core) | Lerna-Lite core & shared methods (internal use) | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/core/CHANGELOG.md) | +| [@lerna-lite/info](https://github.com/ghiscoding/lerna-lite/tree/main/packages/info) | [![npm](https://img.shields.io/npm/v/@lerna-lite/info.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/info) | Print local environment information | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/info/CHANGELOG.md) | +| [@lerna-lite/init](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init) | [![npm](https://img.shields.io/npm/v/@lerna-lite/init.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/init) | create a new Lerna-Lite repo | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/init/CHANGELOG.md) | +| [@lerna-lite/publish](https://github.com/ghiscoding/lerna-lite/tree/main/packages/publish) | [![npm](https://img.shields.io/npm/v/@lerna-lite/publish.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/publish) | Publish packages in the current workspace | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/publish/CHANGELOG.md) | +| [@lerna-lite/version](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version) | [![npm](https://img.shields.io/npm/v/@lerna-lite/version.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/version) | Bump Version & write Changelogs | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/version/CHANGELOG.md) | +| [@lerna-lite/exec](https://github.com/ghiscoding/lerna-lite/tree/main/packages/exec) | [![npm](https://img.shields.io/npm/v/@lerna-lite/exec.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/exec) | Execute shell command in current workspace | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/exec/CHANGELOG.md) | +| [@lerna-lite/changed](https://github.com/ghiscoding/lerna-lite/tree/main/packages/changed) | [![npm](https://img.shields.io/npm/v/@lerna-lite/changed.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/changed) | List local packages that changed since last release | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/changed/CHANGELOG.md) | +| [@lerna-lite/diff](https://github.com/ghiscoding/lerna-lite/tree/main/packages/diff) | [![npm](https://img.shields.io/npm/v/@lerna-lite/diff.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/diff) | Diff all packages or a single package since the last release | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/changed/CHANGELOG.md) | +| [@lerna-lite/list](https://github.com/ghiscoding/lerna-lite/tree/main/packages/list) | [![npm](https://img.shields.io/npm/v/@lerna-lite/list.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/list) | List local packages | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/list/CHANGELOG.md) | +| [@lerna-lite/listable](https://github.com/ghiscoding/lerna-lite/tree/main/packages/listable) | [![npm](https://img.shields.io/npm/v/@lerna-lite/listable.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/listable) | Listable utils used by `list` and `changed` commands | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/listable/CHANGELOG.md) | +| [@lerna-lite/run](https://github.com/ghiscoding/lerna-lite/tree/main/packages/run) | [![npm](https://img.shields.io/npm/v/@lerna-lite/run.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/run) | Run npm scripts in current workspace | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/run/CHANGELOG.md) | | [@lerna-lite/optional-cmd-common](https://github.com/ghiscoding/lerna-lite/tree/main/packages/optional-cmd-common) | [![npm](https://img.shields.io/npm/v/@lerna-lite/optional-cmd-common.svg?logo=npm&logoColor=fff&label=npm)](https://www.npmjs.com/package/@lerna-lite/optional-cmd-common) | Lerna-Lite common utils for optional commands Exec/List/Run (internal use) | [changelog](https://github.com/ghiscoding/lerna-lite/blob/main/packages/optional-cmd-common/CHANGELOG.md) | diff --git a/package.json b/package.json index fd4b1924..be72066b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dist-info-cmd": "node ./packages/cli/dist/cli.js info", "dist-init-cmd": "node ./packages/cli/dist/cli.js init --independent --exact --use-workspaces", "dist-changed-cmd": "node ./packages/cli/dist/cli.js changed --all", + "dist-diff-cmd": "node ./packages/cli/dist/cli.js diff", "dist-list-cmd": "node ./packages/cli/dist/cli.js list --all", "dist-roll-version": "node ./packages/cli/dist/cli.js version", "dist-roll-publish": "node ./packages/cli/dist/cli.js publish from-package", @@ -49,6 +50,7 @@ "@lerna-lite/changed": "workspace:*", "@lerna-lite/cli": "workspace:*", "@lerna-lite/core": "workspace:*", + "@lerna-lite/diff": "workspace:*", "@lerna-lite/exec": "workspace:*", "@lerna-lite/info": "workspace:*", "@lerna-lite/list": "workspace:*", diff --git a/packages/cli/src/cli-commands/__tests__/cli-diff-commands.spec.ts b/packages/cli/src/cli-commands/__tests__/cli-diff-commands.spec.ts new file mode 100644 index 00000000..851e9583 --- /dev/null +++ b/packages/cli/src/cli-commands/__tests__/cli-diff-commands.spec.ts @@ -0,0 +1,14 @@ +jest.mock('@lerna-lite/diff', () => null); +const cliDiff = require('../cli-diff-commands'); + +describe('DiffCommand CLI options', () => { + it('should log a console error when DiffCommand is not provided', async () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + await cliDiff.handler(); + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('"@lerna-lite/diff" is optional and was not found.') + ); + }); +}); diff --git a/packages/cli/src/cli-commands/cli-diff-commands.ts b/packages/cli/src/cli-commands/cli-diff-commands.ts new file mode 100644 index 00000000..10329a54 --- /dev/null +++ b/packages/cli/src/cli-commands/cli-diff-commands.ts @@ -0,0 +1,26 @@ +/** + * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module + */ +exports.command = 'diff [pkgName]'; +exports.describe = 'Diff all packages or a single package since the last release'; + +exports.builder = { + 'ignore-changes': { + group: 'Command Options:', + describe: 'Ignore changes in files matched by glob(s).', + type: 'array', + }, +}; + +exports.handler = async function handler(argv) { + try { + // @ts-ignore + // eslint-disable-next-line import/no-unresolved + const { DiffCommand } = await import('@lerna-lite/diff'); + new DiffCommand(argv); + } catch (e) { + console.error( + '"@lerna-lite/diff" is optional and was not found. Please install it with `npm install @lerna-lite/diff -D -W`' + ); + } +}; diff --git a/packages/cli/src/lerna-entry.ts b/packages/cli/src/lerna-entry.ts index b9fecd8e..7a8950fb 100644 --- a/packages/cli/src/lerna-entry.ts +++ b/packages/cli/src/lerna-entry.ts @@ -2,6 +2,7 @@ const cli = require('./lerna-cli'); const pkg = require('../package.json'); const changedCmd = require('./cli-commands/cli-changed-commands'); +const diffCmd = require('./cli-commands/cli-diff-commands'); const execCmd = require('./cli-commands/cli-exec-commands'); const initCmd = require('./cli-commands/cli-init-commands'); const infoCmd = require('./cli-commands/cli-info-commands'); @@ -17,6 +18,7 @@ export function lerna(argv: any[]) { return cli() .command(changedCmd) + .command(diffCmd) .command(execCmd) .command(infoCmd) .command(initCmd) diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index 4aa4373c..c99aab3d 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -14,6 +14,7 @@ import { ValidationError } from './validation-error'; import { ChangedCommandOption, CommandType, + DiffCommandOption, ExecCommandOption, ExecOpts, InitCommandOption, @@ -29,6 +30,7 @@ const DEFAULT_CONCURRENCY = os.cpus().length; type AvailableCommandOption = | ChangedCommandOption + | DiffCommandOption | ExecCommandOption | InitCommandOption | ListCommandOption diff --git a/packages/core/src/models/command-options.ts b/packages/core/src/models/command-options.ts index 14598ca0..1bd672bb 100644 --- a/packages/core/src/models/command-options.ts +++ b/packages/core/src/models/command-options.ts @@ -15,6 +15,14 @@ export interface ChangedCommandOption { includeMergedTags?: boolean; } +export interface DiffCommandOption { + /** ignore changes in files matched by glob(s) when detecting changed packages. Pass --no-ignore-changes to completely disable. */ + ignoreChanges: string[]; + + /** package name */ + pkgName: string; +} + export interface ExecCommandOption { /** command to execute by the command */ cmd?: string; diff --git a/packages/diff/README.md b/packages/diff/README.md new file mode 100644 index 00000000..470268cf --- /dev/null +++ b/packages/diff/README.md @@ -0,0 +1,38 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) +[![npm](https://img.shields.io/npm/dy/@lerna-lite/diff?color=forest)](https://www.npmjs.com/package/@lerna-lite/diff) +[![npm](https://img.shields.io/npm/v/@lerna-lite/diff.svg?logo=npm&logoColor=fff)](https://www.npmjs.com/package/@lerna-lite/diff) + +# @lerna-lite/diff + +## (`lerna diff`) - Diff command [optional] 🌓 + +Diff all packages or a single package since the last release + +--- + +## Installation + +```sh +npm install @lerna-lite/diff -D -W + +# then use it (see usage below) +lerna diff + +# OR use npx +npx lerna diff +``` + +## Usage + +```sh +$ lerna diff [package] + +$ lerna diff +# diff a specific package +$ lerna diff package-name +``` + +Diff all packages or a single package since the last release. + +> Similar to `lerna changed`. This command runs `git diff`. diff --git a/packages/diff/package.json b/packages/diff/package.json new file mode 100644 index 00000000..31e66956 --- /dev/null +++ b/packages/diff/package.json @@ -0,0 +1,36 @@ +{ + "name": "@lerna-lite/diff", + "description": "Lerna-Lite diff commmand - Diff all packages or a single package since the last release", + "version": "1.5.1", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "typings": "dist/index.d.ts", + "files": [ + "/dist" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc --project tsconfig.bundle.json --newLine LF", + "pack-tarball": "npm pack" + }, + "license": "MIT", + "author": "Ghislain B.", + "homepage": "https://github.com/ghiscoding/lerna-lite", + "repository": { + "type": "git", + "url": "https://github.com/ghiscoding/lerna-lite.git", + "directory": "packages/diff" + }, + "bugs": { + "url": "https://github.com/ghiscoding/lerna-lite/issues" + }, + "engines": { + "node": ">=14.15.0", + "npm": ">=8.0.0" + }, + "dependencies": { + "@lerna-lite/core": "workspace:*" + } +} diff --git a/packages/diff/src/__tests__/__fixtures__/basic/lerna.json b/packages/diff/src/__tests__/__fixtures__/basic/lerna.json new file mode 100644 index 00000000..1587a669 --- /dev/null +++ b/packages/diff/src/__tests__/__fixtures__/basic/lerna.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} diff --git a/packages/diff/src/__tests__/__fixtures__/basic/package.json b/packages/diff/src/__tests__/__fixtures__/basic/package.json new file mode 100644 index 00000000..46358b16 --- /dev/null +++ b/packages/diff/src/__tests__/__fixtures__/basic/package.json @@ -0,0 +1,3 @@ +{ + "name": "independent" +} diff --git a/packages/diff/src/__tests__/__fixtures__/basic/packages/package-1/package.json b/packages/diff/src/__tests__/__fixtures__/basic/packages/package-1/package.json new file mode 100644 index 00000000..f0a0409c --- /dev/null +++ b/packages/diff/src/__tests__/__fixtures__/basic/packages/package-1/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-1", + "changed": 0, + "version": "1.0.0" +} \ No newline at end of file diff --git a/packages/diff/src/__tests__/__fixtures__/basic/packages/package-2/package.json b/packages/diff/src/__tests__/__fixtures__/basic/packages/package-2/package.json new file mode 100644 index 00000000..febdbdf2 --- /dev/null +++ b/packages/diff/src/__tests__/__fixtures__/basic/packages/package-2/package.json @@ -0,0 +1,8 @@ +{ + "name": "package-2", + "changed": 0, + "version": "1.0.0", + "dependencies": { + "package-1": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/diff/src/__tests__/__snapshots__/diff-command.spec.ts.snap b/packages/diff/src/__tests__/__snapshots__/diff-command.spec.ts.snap new file mode 100644 index 00000000..dfe26ec4 --- /dev/null +++ b/packages/diff/src/__tests__/__snapshots__/diff-command.spec.ts.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Diff Command passes diff exclude globs configured with --ignored-changes 1`] = ` +diff --git a/packages/package-1/package.json b/packages/package-1/package.json +index SHA..SHA 100644 +--- a/packages/package-1/package.json ++++ b/packages/package-1/package.json +@@ -1,5 +1,5 @@ + { + "name": "package-1", +- "changed": 0, ++ "changed": 1, + "version": "1.0.0" +-} +\\ No newline at end of file ++} +`; + +exports[`Diff Command should diff a specific package 1`] = ` +diff --git a/packages/package-2/package.json b/packages/package-2/package.json +index SHA..SHA 100644 +--- a/packages/package-2/package.json ++++ b/packages/package-2/package.json +@@ -1,8 +1,8 @@ + { + "name": "package-2", +- "changed": 0, ++ "changed": 1, + "version": "1.0.0", + "dependencies": { + "package-1": "^1.0.0" + } +-} +\\ No newline at end of file ++} +`; + +exports[`Diff Command should diff packages from the first commit 1`] = ` +diff --git a/packages/package-1/package.json b/packages/package-1/package.json +index SHA..SHA 100644 +--- a/packages/package-1/package.json ++++ b/packages/package-1/package.json +@@ -1,5 +1,5 @@ + { + "name": "package-1", +- "changed": 0, ++ "changed": 1, + "version": "1.0.0" +-} +\\ No newline at end of file ++} +`; + +exports[`Diff Command should diff packages from the first commit from DiffCommand class 1`] = ` +diff --git a/packages/package-1/package.json b/packages/package-1/package.json +index SHA..SHA 100644 +--- a/packages/package-1/package.json ++++ b/packages/package-1/package.json +@@ -1,5 +1,5 @@ + { + "name": "package-1", +- "changed": 0, ++ "changed": 1, + "version": "1.0.0" +-} +\\ No newline at end of file ++} +`; + +exports[`Diff Command should diff packages from the first commit from factory 1`] = ` +diff --git a/packages/package-1/package.json b/packages/package-1/package.json +index SHA..SHA 100644 +--- a/packages/package-1/package.json ++++ b/packages/package-1/package.json +@@ -1,5 +1,5 @@ + { + "name": "package-1", +- "changed": 0, ++ "changed": 1, + "version": "1.0.0" +-} +\\ No newline at end of file ++} +`; + +exports[`Diff Command should diff packages from the most recent tag 1`] = ` +diff --git a/packages/package-1/package.json b/packages/package-1/package.json +index SHA..SHA 100644 +--- a/packages/package-1/package.json ++++ b/packages/package-1/package.json +@@ -1,5 +1,6 @@ + { + "name": "package-1", + "changed": 1, +- "version": "1.0.0" ++ "version": "1.0.0", ++ "sinceLastTag": true + } +`; diff --git a/packages/diff/src/__tests__/diff-command.spec.ts b/packages/diff/src/__tests__/diff-command.spec.ts new file mode 100644 index 00000000..64cf7e15 --- /dev/null +++ b/packages/diff/src/__tests__/diff-command.spec.ts @@ -0,0 +1,166 @@ +import execa from 'execa'; +import fs from 'fs-extra'; +import path from 'path'; + +jest.mock('@lerna-lite/core', () => ({ + ...(jest.requireActual('@lerna-lite/core') as any), // return the other real methods, below we'll mock only 2 of the methods + logOutput: jest.requireActual('../../../core/src/__mocks__/output').logOutput, + collectUpdates: jest.requireActual('../../../core/src/__mocks__/collect-updates').collectUpdates, + getPackages: jest.requireActual('../../../core/src/project').getPackages, + spawn: jest.fn(() => Promise.resolve({ exitCode: 0 })), +})); + +// mocked modules +let coreChildProcess = require('@lerna-lite/core'); + +// helpers +const initFixture = require('@lerna-test/init-fixture')(__dirname); +const { getPackages } = require('@lerna-lite/core'); +import { gitAdd } from '@lerna-test/git-add'; +import { gitCommit } from '@lerna-test/git-commit'; +import { gitInit } from '@lerna-test/git-init'; +import { gitTag } from '@lerna-test/git-tag'; + +// file under test +const lernaDiff = require('@lerna-test/command-runner')(require('../../../cli/src/cli-commands/cli-diff-commands')); +import { DiffCommand } from '../index'; +import { factory } from '../diff-command'; + +// file under test +const yargParser = require('yargs-parser'); + +const createArgv = (cwd: string, ...args: string[]) => { + args.unshift('diff'); + const parserArgs = args.map(String); + const argv = yargParser(parserArgs); + argv['$0'] = cwd; + argv['loglevel'] = 'silent'; + return argv; +}; + +// stabilize commit SHA +expect.addSnapshotSerializer(require('@lerna-test/serialize-git-sha')); + +describe('Diff Command', () => { + // overwrite spawn so we get piped stdout, not inherited + // @ts-ignore + coreChildProcess.spawn = jest.fn((...args) => execa(...args)); + + it('should diff packages from the first commit from DiffCommand class', async () => { + const cwd = await initFixture('basic'); + const [pkg1] = await getPackages(cwd); + const rootReadme = path.join(cwd, 'README.md'); + + await pkg1.set('changed', 1).serialize(); + await fs.outputFile(rootReadme, 'change outside packages glob'); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + + // @ts-ignore + const { stdout } = await new DiffCommand(createArgv(cwd, '')); + expect(stdout).toMatchSnapshot(); + }); + + it('should diff packages from the first commit from factory', async () => { + const cwd = await initFixture('basic'); + const [pkg1] = await getPackages(cwd); + const rootReadme = path.join(cwd, 'README.md'); + + await pkg1.set('changed', 1).serialize(); + await fs.outputFile(rootReadme, 'change outside packages glob'); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + + // @ts-ignore + const { stdout } = await factory(createArgv(cwd, '')); + expect(stdout).toMatchSnapshot(); + }); + + it('should diff packages from the first commit', async () => { + const cwd = await initFixture('basic'); + const [pkg1] = await getPackages(cwd); + const rootReadme = path.join(cwd, 'README.md'); + + await pkg1.set('changed', 1).serialize(); + await fs.outputFile(rootReadme, 'change outside packages glob'); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + + const { stdout } = await lernaDiff(cwd)(); + expect(stdout).toMatchSnapshot(); + }); + + it('should diff packages from the most recent tag', async () => { + const cwd = await initFixture('basic'); + const [pkg1] = await getPackages(cwd); + + await pkg1.set('changed', 1).serialize(); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + await gitTag(cwd, 'v1.0.1'); + + await pkg1.set('sinceLastTag', true).serialize(); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + + const { stdout } = await lernaDiff(cwd)(); + expect(stdout).toMatchSnapshot(); + }); + + it('should diff a specific package', async () => { + const cwd = await initFixture('basic'); + const [pkg1, pkg2] = await getPackages(cwd); + + await pkg1.set('changed', 1).serialize(); + await pkg2.set('changed', 1).serialize(); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + + const { stdout } = await lernaDiff(cwd)('package-2'); + expect(stdout).toMatchSnapshot(); + }); + + it('passes diff exclude globs configured with --ignored-changes', async () => { + const cwd = await initFixture('basic'); + const [pkg1] = await getPackages(cwd); + + await pkg1.set('changed', 1).serialize(); + await fs.outputFile(path.join(pkg1.location, 'README.md'), 'ignored change'); + await gitAdd(cwd, '-A'); + await gitCommit(cwd, 'changed'); + + const { stdout } = await lernaDiff(cwd)('--ignore-changes', '**/README.md'); + expect(stdout).toMatchSnapshot(); + }); + + it("should error when attempting to diff a package that doesn't exist", async () => { + const cwd = await initFixture('basic'); + const command = lernaDiff(cwd)('missing'); + + await expect(command).rejects.toThrow("Cannot diff, the package 'missing' does not exist."); + }); + + it('should error when running in a repository without commits', async () => { + const cwd = await initFixture('basic'); + + await fs.remove(path.join(cwd, '.git')); + await gitInit(cwd); + + const command = lernaDiff(cwd)('package-1'); + await expect(command).rejects.toThrow('Cannot diff, there are no commits in this repository yet.'); + }); + + it('should error when git diff exits non-zero', async () => { + const cwd = await initFixture('basic'); + + coreChildProcess.spawn.mockImplementationOnce(() => { + const nonZero = new Error('An actual non-zero, not git diff pager SIGPIPE'); + (nonZero as any).exitCode = 1; + + throw nonZero; + }); + + const command = lernaDiff(cwd)('package-1'); + await expect(command).rejects.toThrow('An actual non-zero, not git diff pager SIGPIPE'); + }); +}); diff --git a/packages/diff/src/diff-command.ts b/packages/diff/src/diff-command.ts new file mode 100644 index 00000000..d4dbca00 --- /dev/null +++ b/packages/diff/src/diff-command.ts @@ -0,0 +1,62 @@ +import { Command, CommandType, DiffCommandOption, Package, spawn, ValidationError } from '@lerna-lite/core'; + +import { getLastCommit } from './lib/get-last-commit'; +import { hasCommit } from './lib/has-commit'; + +export function factory(argv: DiffCommandOption) { + return new DiffCommand(argv); +} + +export class DiffCommand extends Command { + /** command name */ + name = 'diff' as CommandType; + + args: string[] = []; + + constructor(argv: DiffCommandOption) { + super(argv); + } + + async initialize() { + let targetPackage: Package | undefined = undefined; + const packageName = this.options.pkgName; + + if (packageName) { + targetPackage = this.packageGraph.get(packageName); + + if (!targetPackage) { + throw new ValidationError('ENOPKG', `Cannot diff, the package '${packageName}' does not exist.`); + } + } + + if (!hasCommit(this.execOpts)) { + throw new ValidationError('ENOCOMMITS', 'Cannot diff, there are no commits in this repository yet.'); + } + + const args = ['diff', getLastCommit(this.execOpts), '--color=auto']; + + if (targetPackage) { + args.push('--', targetPackage.location); + } else { + args.push('--', ...(await this.project.packageParentDirs)); + } + + if (this.options.ignoreChanges) { + this.options.ignoreChanges.forEach((ignorePattern) => { + // https://stackoverflow.com/a/21079437 + args.push(`:(exclude,glob)${ignorePattern}`); + }); + } + + this.args = args; + } + + execute() { + return spawn('git', this.args, this.execOpts).catch((err) => { + if (err.exitCode) { + // quitting the diff viewer is not an error + throw err; + } + }); + } +} diff --git a/packages/diff/src/index.ts b/packages/diff/src/index.ts new file mode 100644 index 00000000..271a7de4 --- /dev/null +++ b/packages/diff/src/index.ts @@ -0,0 +1 @@ +export * from './diff-command'; diff --git a/packages/diff/src/lib/get-last-commit.ts b/packages/diff/src/lib/get-last-commit.ts new file mode 100644 index 00000000..bd57d1e7 --- /dev/null +++ b/packages/diff/src/lib/get-last-commit.ts @@ -0,0 +1,34 @@ +import log from 'npmlog'; +import { ExecOpts, execSync } from '@lerna-lite/core'; + +/** + * @param {import("@lerna/child-process").ExecOpts} execOpts + */ +export function getLastCommit(execOpts: ExecOpts) { + if (hasTags(execOpts)) { + log.silly('git', 'getLastTagInBranch'); + + return execSync('git', ['describe', '--tags', '--abbrev=0'], execOpts); + } + + log.silly('git', 'getFirstCommit'); + return execSync('git', ['rev-list', '--max-parents=0', 'HEAD'], execOpts); +} + +/** + * @param {import("@lerna/child-process").ExecOpts} opts + */ +function hasTags(opts: ExecOpts) { + let result: boolean | string = false; + + try { + result = !!execSync('git', ['tag'], opts); + } catch (err: any) { + log.warn('ENOTAGS', 'No git tags were reachable from this branch!'); + log.verbose('hasTags error', err); + } + + log.verbose('hasTags', `${result}`); + + return result; +} diff --git a/packages/diff/src/lib/has-commit.ts b/packages/diff/src/lib/has-commit.ts new file mode 100644 index 00000000..c3124cd4 --- /dev/null +++ b/packages/diff/src/lib/has-commit.ts @@ -0,0 +1,20 @@ +import log from 'npmlog'; +import { ExecOpts, execSync } from '@lerna-lite/core'; + +/** + * @param {import("@lerna/child-process").ExecOpts} opts + */ +export function hasCommit(opts: ExecOpts) { + log.silly('git', 'hasCommit'); + let retVal; + + try { + execSync('git', ['log'], opts); + retVal = true; + } catch (e) { + retVal = false; + } + + log.verbose('hasCommit', retVal); + return retVal; +} diff --git a/packages/diff/tsconfig.bundle.json b/packages/diff/tsconfig.bundle.json new file mode 100644 index 00000000..9775156f --- /dev/null +++ b/packages/diff/tsconfig.bundle.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.bundle.json", + "compilerOptions": { + "typeRoots": ["../typings", "../../node_modules/@types"], + "outDir": "dist" + }, + "include": ["../typings", "**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1a3394b..f1f709d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ importers: '@lerna-lite/changed': workspace:* '@lerna-lite/cli': workspace:* '@lerna-lite/core': workspace:* + '@lerna-lite/diff': workspace:* '@lerna-lite/exec': workspace:* '@lerna-lite/info': workspace:* '@lerna-lite/list': workspace:* @@ -72,6 +73,7 @@ importers: '@lerna-lite/changed': link:packages/changed '@lerna-lite/cli': link:packages/cli '@lerna-lite/core': link:packages/core + '@lerna-lite/diff': link:packages/diff '@lerna-lite/exec': link:packages/exec '@lerna-lite/info': link:packages/info '@lerna-lite/list': link:packages/list @@ -317,6 +319,12 @@ importers: '@types/write-json-file': 3.2.1 '@types/write-pkg': 4.0.0 + packages/diff: + specifiers: + '@lerna-lite/core': workspace:* + dependencies: + '@lerna-lite/core': link:../core + packages/exec: specifiers: '@lerna-lite/core': workspace:*