diff --git a/packages/gitmoji-changelog-cli/package.json b/packages/gitmoji-changelog-cli/package.json index ae8d5f2..cd1fe47 100644 --- a/packages/gitmoji-changelog-cli/package.json +++ b/packages/gitmoji-changelog-cli/package.json @@ -33,7 +33,10 @@ "dependencies": { "@gitmoji-changelog/core": "^1.1.0", "@gitmoji-changelog/markdown": "^1.1.0", + "immutadot": "^1.0.0", + "inquirer": "^6.3.1", "libnpm": "^1.0.0", + "lodash": "^4.17.11", "semver-compare": "^1.0.0", "simple-git": "^1.113.0", "yargs": "^12.0.1" diff --git a/packages/gitmoji-changelog-cli/src/cli.js b/packages/gitmoji-changelog-cli/src/cli.js index b6a0919..3973131 100644 --- a/packages/gitmoji-changelog-cli/src/cli.js +++ b/packages/gitmoji-changelog-cli/src/cli.js @@ -4,7 +4,7 @@ const libnpm = require('libnpm') const semverCompare = require('semver-compare') const { generateChangelog, logger } = require('@gitmoji-changelog/core') const { buildMarkdownFile, getLatestVersion } = require('@gitmoji-changelog/markdown') - +const { executeInteractiveMode } = require('./interactiveMode') const pkg = require('../package.json') @@ -39,7 +39,7 @@ async function main(options = {}) { try { switch (options.format) { case 'json': { - const changelog = await generateChangelog(options) + const changelog = await getChangelog(options) logMetaData(changelog) @@ -50,7 +50,7 @@ async function main(options = {}) { const lastVersion = getLatestVersion(options.output) const newOptions = set(options, 'meta.lastVersion', lastVersion) - const changelog = await generateChangelog(newOptions) + const changelog = await getChangelog(newOptions) logMetaData(changelog) @@ -66,6 +66,16 @@ async function main(options = {}) { process.exit(0) } +async function getChangelog(options) { + let changelog = await generateChangelog(options) + + if (options.interactive) { + changelog = await executeInteractiveMode(changelog) + } + + return changelog +} + function logMetaData(changelog) { if (changelog.meta.package) { const { name, version } = changelog.meta.package diff --git a/packages/gitmoji-changelog-cli/src/index.js b/packages/gitmoji-changelog-cli/src/index.js index 84ed839..7ea7b20 100755 --- a/packages/gitmoji-changelog-cli/src/index.js +++ b/packages/gitmoji-changelog-cli/src/index.js @@ -50,6 +50,7 @@ yargs .option('output', { desc: 'output changelog file' }) .option('group-similar-commits', { desc: '[⚗️ - beta] try to group similar commits', default: false }) .option('author', { default: false, desc: 'add the author in changelog lines' }) + .option('interactive', { default: false, desc: 'select commits manually', alias: 'i' }) .help('help') .epilog(`For more information visit: ${homepage}`) diff --git a/packages/gitmoji-changelog-cli/src/interactiveMode.js b/packages/gitmoji-changelog-cli/src/interactiveMode.js new file mode 100644 index 0000000..dad1aa9 --- /dev/null +++ b/packages/gitmoji-changelog-cli/src/interactiveMode.js @@ -0,0 +1,69 @@ +const { get, isEmpty, cloneDeep } = require('lodash') +const inquirer = require('inquirer') +const { set } = require('immutadot') + +const interactiveMode = { + buildFormattedChoices, + getFilteredChangelog, + executeInteractiveMode, +} + +function buildFormattedChoices(changelog) { + const formattedChoices = [] + + changelog.changes + .forEach(change => { + change.groups + .forEach(group => { + formattedChoices.push(new inquirer.Separator(`${group.label} (version ${change.version})`)) + + group.commits.forEach(commit => { + formattedChoices.push({ + name: `${get(commit, 'emoji', '')} ${commit.message}`, + value: commit.hash, + checked: true, + }) + }) + }) + }) + + return formattedChoices +} + +function getFilteredChangelog(changelog, selectedCommitsHashes) { + const changes = changelog.changes.map(change => { + const groups = change.groups.map(group => { + const filteredCommits = group.commits.filter(commit => { + return selectedCommitsHashes.find(hash => commit.hash === hash) + }) + + return set(group, 'commits', filteredCommits) + }).filter(group => !isEmpty(group.commits)) + + return set(change, 'groups', groups) + }).filter(change => !isEmpty(change.groups)) + + return { + ...changelog, + changes, + } +} + +async function executeInteractiveMode(initialChangelog) { + const initialChangelogCopy = cloneDeep(initialChangelog) + const formattedChoices = interactiveMode.buildFormattedChoices(initialChangelogCopy) + const prompt = inquirer.createPromptModule() + const question = { + type: 'checkbox', + name: 'selectedCommitsHashes', + message: 'Select commits', + choices: formattedChoices, + pageSize: 10, + } + + const { selectedCommitsHashes } = await prompt(question) + + return interactiveMode.getFilteredChangelog(initialChangelogCopy, selectedCommitsHashes) +} + +module.exports = interactiveMode diff --git a/packages/gitmoji-changelog-cli/src/interactiveMode.spec.js b/packages/gitmoji-changelog-cli/src/interactiveMode.spec.js new file mode 100644 index 0000000..be08b84 --- /dev/null +++ b/packages/gitmoji-changelog-cli/src/interactiveMode.spec.js @@ -0,0 +1,205 @@ +const inquirer = require('inquirer') +const interactiveMode = require('./interactiveMode') + +describe('interactiveMode', () => { + const commitAddProcess = { + hash: '00c90b7844c3d030e967721eafea1b436ee51a6b', + author: 'Franck', + date: '2019-05-19T10:57:22+02:00', + subject: ':sparkles: Add interactive process', + emojiCode: 'sparkles', + emoji: '✨', + message: 'Add interactive process', + group: 'added', + siblings: [], + body: '', + } + + const commitAddOption = { + hash: '3092ffd56e35fff7e35e8a9fcb7fff53005eac8a', + author: 'Franck', + date: '2019-05-18T18:39:44+02:00', + subject: ':sparkles: Add interactive option', + emojiCode: 'sparkles', + emoji: '✨', + message: 'Add interactive option', + group: 'added', + siblings: [], + body: '', + } + + const commitUpgradeDeps = { + hash: 'b77199f96c8570b827dfcb11907d6f4edac98823', + author: 's n', + date: '2019-04-23T15:55:19+02:00', + subject: ':arrow_up: Update handlebar to 4.0.14 (#78)', + emojiCode: 'arrow_up', + emoji: '⬆️', + message: 'Update handlebar to 4.0.14 (#78)', + group: 'changed', + siblings: [], + body: '', + } + + const commitAddAuthor = { + hash: '979da30f5e52385b99bd4a58e1a946793bd1196d', + author: 'Benjamin Petetot', + date: '2018-10-30T09:33:52+01:00', + subject: ':sparkles: Add the author in changelog lines (#56)', + emojiCode: 'sparkles', + emoji: '✨', + message: 'Add the author in changelog lines (#56)', + group: 'added', + siblings: [], + body: '', + } + + const groupAddedVersionNext = { + group: 'added', + label: 'Added', + commits: [ + commitAddProcess, + commitAddOption, + ], + } + + const groupChangedVersionNext = { + group: 'changed', + label: 'Changed', + commits: [ + commitUpgradeDeps, + ], + } + + const groupAddedVersionOne = { + group: 'added', + label: 'Added', + commits: [ + commitAddAuthor, + ], + } + + const versionNext = { + version: 'next', + groups: [ + groupAddedVersionNext, + groupChangedVersionNext, + ], + } + + const versionOne = { + version: '1.1.0', + date: '2018-11-15', + groups: [ + groupAddedVersionOne, + ], + } + + const initialChangelog = { + changes: [ + versionNext, + versionOne, + ], + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('buildFormattedChoices', () => { + it('should return a list of formatted choices from the given changelog', () => { + const result = interactiveMode.buildFormattedChoices(initialChangelog) + + const expectedResult = [ + new inquirer.Separator(`${groupAddedVersionNext.label} (version ${versionNext.version})`), + { + name: `${commitAddProcess.emoji} ${commitAddProcess.message}`, + value: commitAddProcess.hash, + checked: true, + }, + { + name: `${commitAddOption.emoji} ${commitAddOption.message}`, + value: commitAddOption.hash, + checked: true, + }, + new inquirer.Separator(`${groupChangedVersionNext.label} (version ${versionNext.version})`), + { + name: `${commitUpgradeDeps.emoji} ${commitUpgradeDeps.message}`, + value: commitUpgradeDeps.hash, + checked: true, + }, + new inquirer.Separator(`${groupAddedVersionOne.label} (version ${versionOne.version})`), + { + name: `${commitAddAuthor.emoji} ${commitAddAuthor.message}`, + value: commitAddAuthor.hash, + checked: true, + }, + ] + + expect(result).toEqual(expectedResult) + }) + }) + + describe('getFilteredChangelog', () => { + it('should return a new filtered changelog from the given inital changelog and selected commits', () => { + const selectedCommitsHashes = [commitAddProcess.hash, commitAddAuthor.hash] + + const result = interactiveMode.getFilteredChangelog(initialChangelog, selectedCommitsHashes) + + const expectedResult = { + changes: [ + { + version: 'next', + groups: [{ + group: 'added', + label: 'Added', + commits: [ + commitAddProcess, + ], + }], + }, + { + version: '1.1.0', + date: '2018-11-15', + groups: [ + groupAddedVersionOne, + ], + }, + ], + } + + expect(result).toEqual(expectedResult) + }) + }) + + describe('executeInteractiveMode', () => { + it('should call buildFormattedChoices, createPromptModule and getFilteredChangelog with correct parameters', async () => { + const formattedChoices = [{ + name: `${commitAddProcess.emoji} ${commitAddProcess.message}`, + value: commitAddProcess.hash, + checked: true, + }] + const selectedCommitsHashes = [commitAddProcess.hash, commitAddOption.hash] + const prompt = jest.fn(() => Promise.resolve({ selectedCommitsHashes })) + + interactiveMode.buildFormattedChoices = jest.fn(() => formattedChoices) + interactiveMode.getFilteredChangelog = jest.fn() + inquirer.createPromptModule = jest.fn() + inquirer.createPromptModule.mockReturnValueOnce(prompt) + + await interactiveMode.executeInteractiveMode(initialChangelog) + + expect(interactiveMode.buildFormattedChoices).toHaveBeenNthCalledWith(1, initialChangelog) + expect(inquirer.createPromptModule).toHaveBeenCalledTimes(1) + expect(prompt).toHaveBeenNthCalledWith(1, { + type: 'checkbox', + name: 'selectedCommitsHashes', + message: 'Select commits', + choices: formattedChoices, + pageSize: 10, + }) + expect(interactiveMode.getFilteredChangelog) + .toHaveBeenNthCalledWith(1, initialChangelog, selectedCommitsHashes) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 39ed003..32ffbd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -126,6 +126,11 @@ ansi-escapes@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -136,6 +141,11 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -710,11 +720,25 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" @@ -1710,6 +1734,15 @@ external-editor@^2.0.4, external-editor@^2.1.0: iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -2375,7 +2408,7 @@ iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@~0.4.13: +iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -2488,6 +2521,25 @@ inquirer@^5.2.0: strip-ansi "^4.0.0" through "^2.3.6" +inquirer@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.3.1.tgz#7a413b5e7950811013a3db491c61d1f3b776e8e7" + integrity sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.11" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -5024,6 +5076,13 @@ rxjs@^5.5.2: dependencies: symbol-observable "1.0.1" +rxjs@^6.4.0: + version "6.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" + integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -5430,6 +5489,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -5684,6 +5750,11 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"