From ae2a59b58a16e850aba468244bf3c8da116f08b7 Mon Sep 17 00:00:00 2001 From: Pierre-Denis Vanduynslager Date: Fri, 25 Aug 2017 02:02:08 -0400 Subject: [PATCH] =?UTF-8?q?feat(changelog-writer):=20Add=20commit=20groups?= =?UTF-8?q?=20sorting=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 8 ++-- src/aliases.js | 13 ++++++ src/lib/commit-groups-compare.js | 23 +++++++++++ src/preset.js | 5 ++- src/types.js | 18 +++++++++ test/aliases.test.js | 48 ++++++++++++++++++++++ test/commit-groups-compare.test.js | 58 +++++++++++++++++++++++++++ test/helpers/changelog.js | 6 ++- test/helpers/commit-groups-compare.js | 12 ++++++ test/preset.test.js | 32 ++++++++++----- test/types.test.js | 14 ++++++- 12 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 src/aliases.js create mode 100644 src/lib/commit-groups-compare.js create mode 100644 test/aliases.test.js create mode 100644 test/commit-groups-compare.test.js create mode 100644 test/helpers/commit-groups-compare.js diff --git a/.gitignore b/.gitignore index 1ce3d24..6761844 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ $RECYCLE.BIN/ /release-rules.* /lib /types.* +/aliases.* diff --git a/package.json b/package.json index be93655..fe4fa0c 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "lib/*.js", "preset.js", "release-rules.js", - "types.js" + "types.js", + "aliases.js" ], "homepage": "https://github.com/vanduynslagerp/conventional-changelog-metahub#readme", "keywords": [ @@ -82,7 +83,8 @@ "lib/*.js", "preset.js", "release-rules.js", - "types.js" + "types.js", + "aliases.js" ] }, "publishConfig": { @@ -108,7 +110,7 @@ "cm": "git-cz", "codecov": "codecov", "commitmsg": "commitlint -e", - "compile": "rimraf preset.* release-rules.* types.* lib && babel src --source-maps --out-dir .", + "compile": "rimraf preset.* release-rules.* types.* aliases.* lib && babel src --source-maps --out-dir .", "lint": "eslint src test package.json", "precommit": "npm run compile && npm run lint", "prepublishOnly": "npm run compile", diff --git a/src/aliases.js b/src/aliases.js new file mode 100644 index 0000000..68f6f58 --- /dev/null +++ b/src/aliases.js @@ -0,0 +1,13 @@ +import {merge, transform, each} from 'lodash'; +import {types} from './types'; + +/** + * @return {Object} Object with each alias as a key and the alias value merge with it's `type` as value. + */ +export default transform(types, (aliases, value, type) => { + if (value.aliases) { + each(value.aliases, (aliasValue, alias) => { + aliases[alias] = merge({type}, aliasValue); + }); + } +}); diff --git a/src/lib/commit-groups-compare.js b/src/lib/commit-groups-compare.js new file mode 100644 index 0000000..a370f09 --- /dev/null +++ b/src/lib/commit-groups-compare.js @@ -0,0 +1,23 @@ +import {findKey, pick} from 'lodash'; +import {types, typesOrder} from '../types'; +import aliases from '../aliases'; + +/** + * Comparison function to sort Commit Groups. + * + * @param {Object} group1 commit group object with a `title` attribute. + * @param {Object} group2 commit group object with a `title` attribute. + * @return {integer} -1 if `group1` should be displayed before `group2`, 1 for the opposite and 0 if they are equals. + */ +module.exports = (group1, group2) => { + const type1 = typesOrder.indexOf(findKey(types, pick(group1, 'title')) || findKey(aliases, pick(group1, 'title'))); + const type2 = typesOrder.indexOf(findKey(types, pick(group2, 'title')) || findKey(aliases, pick(group2, 'title'))); + + if (type1 !== -1 && type2 === -1) return -1; + if (type1 === -1 && type2 !== -1) return 1; + if (type1 < type2) return -1; + if (type1 > type2) return 1; + if (group1.title < group2.title) return -1; + if (group1.title > group2.title) return 1; + return 0; +}; diff --git a/src/preset.js b/src/preset.js index d238ef4..453f169 100644 --- a/src/preset.js +++ b/src/preset.js @@ -1,8 +1,11 @@ import {merge} from 'lodash'; import conventionalChangelogAngular from 'conventional-changelog-angular'; +const commitGroupsSort = require('./lib/commit-groups-compare'); const transform = require('./lib/commit-transform'); /** * @type {Promise} preset with `parserOpts` and `writerOpts`. */ -module.exports = conventionalChangelogAngular.then(preset => merge(preset, {writerOpts: {transform}})); +module.exports = conventionalChangelogAngular.then(preset => + merge(preset, {writerOpts: {transform, commitGroupsSort}}) +); diff --git a/src/types.js b/src/types.js index ed41ac5..b5f3024 100644 --- a/src/types.js +++ b/src/types.js @@ -1,6 +1,24 @@ module.exports = { maxSubjectLength: 72, bodyLineLength: 100, + typesOrder: [ + 'feat', + 'fix', + 'perf', + 'build', + 'refactor', + 'docs', + 'test', + 'ci', + 'chore', + 'style', + 'revert', + 'initial', + 'dependencies', + 'peerDependencies', + 'devDependencies', + 'metadata', + ], types: { feat: { description: 'A new feature', diff --git a/test/aliases.test.js b/test/aliases.test.js new file mode 100644 index 0000000..aa424ff --- /dev/null +++ b/test/aliases.test.js @@ -0,0 +1,48 @@ +import test from 'ava'; +import emojiRegex from 'emoji-regex/text'; +import {length} from 'stringz'; +import {types, typesOrder} from '../types'; +import aliases from '../aliases'; + +/** + * AVA macro that verifies that each object in `object` has the property `prop` of type `type`. + * + * @method hasProperty + * @param {Object} t AVA assertion librarie + * @param {string} object object to verify + * @param {string} prop property to verify + * @param {string} type type to verify + */ +function hasProperty(t, object, prop, type) { + for (const obj in object) { + if (Object.prototype.hasOwnProperty.call(object, obj)) { + t.true(Object.prototype.hasOwnProperty.call(object[obj], prop)); + if (type === 'string') { + t.true(typeof object[obj][prop] === type); + } else if (type === 'emoji') { + t.true(emojiRegex({exact: true}).test(object[obj][prop])); + t.is(length(object[obj][prop]), 1); + } + } + } +} + +test('Each alias has the property title', hasProperty, aliases, 'title', 'string'); +test('Each type has the property description', hasProperty, aliases, 'description', 'string'); +test('Each type has the property emoji', hasProperty, aliases, 'emoji', 'emoji'); + +test('Each alias`s type property has a value that exists in types', t => { + for (const alias in aliases) { + if (Object.prototype.hasOwnProperty.call(aliases, alias)) { + t.true(Object.prototype.hasOwnProperty.call(types, aliases[alias].type)); + } + } +}); + +test('Each alias exists in typesOrder', t => { + for (const type in types) { + if (Object.prototype.hasOwnProperty.call(types, type)) { + t.true(typesOrder.indexOf(type) !== -1); + } + } +}); diff --git a/test/commit-groups-compare.test.js b/test/commit-groups-compare.test.js new file mode 100644 index 0000000..eeefa17 --- /dev/null +++ b/test/commit-groups-compare.test.js @@ -0,0 +1,58 @@ +import test from 'ava'; +import commitGroupsCompare from './helpers/commit-groups-compare'; + +test('Return ordered commit groups', t => { + const commitGroups = [ + {title: 'Metadata'}, + {title: 'Documentation'}, + {title: 'Bug Fixes'}, + {title: 'Initial'}, + {title: 'Features'}, + ]; + const compare = commitGroupsCompare({ + typesOrder: ['feat', 'fix', 'docs', 'initial', 'metadata'], + types: { + feat: {title: 'Features', aliases: {initial: {title: 'Initial'}}}, + fix: {title: 'Bug Fixes', aliases: {metadata: {title: 'Metadata'}}}, + docs: {title: 'Documentation'}, + }, + }); + + t.deepEqual(commitGroups.sort(compare), [ + {title: 'Features'}, + {title: 'Bug Fixes'}, + {title: 'Documentation'}, + {title: 'Initial'}, + {title: 'Metadata'}, + ]); +}); + +test('Return alphabeticaly ordered commit groups not in "typesOrder" at the end of the list', t => { + const commitGroups = [ + {title: 'b-Test'}, + {title: 'z-Test'}, + {title: 'Bug Fixes'}, + {title: 'z-Test'}, + {title: 'a-Test'}, + {title: 'Features'}, + ]; + const compare = commitGroupsCompare({ + typesOrder: ['feat', 'fix'], + types: { + feat: {title: 'Features'}, + fix: {title: 'Bug Fixes'}, + atest: {title: 'a-Test'}, + ztest: {title: 'z-Test'}, + btest: {title: 'b-Test'}, + }, + }); + + t.deepEqual(commitGroups.sort(compare), [ + {title: 'Features'}, + {title: 'Bug Fixes'}, + {title: 'a-Test'}, + {title: 'b-Test'}, + {title: 'z-Test'}, + {title: 'z-Test'}, + ]); +}); diff --git a/test/helpers/changelog.js b/test/helpers/changelog.js index 55c61a5..68fe632 100644 --- a/test/helpers/changelog.js +++ b/test/helpers/changelog.js @@ -29,7 +29,11 @@ export default async function changelog(messages, types, config = {}) { merge( { config: proxyquire('../../preset', { - './lib/commit-transform': proxyquire('../../lib/commit-transform', {'../types': {types}}), + './lib/commit-transform': proxyquire('../../lib/commit-transform', {'../types': types}), + './lib/commit-groups-compare': proxyquire('../../lib/commit-groups-compare', { + '../types': types, + '../aliases': proxyquire('../../aliases', types), + }), }), }, config diff --git a/test/helpers/commit-groups-compare.js b/test/helpers/commit-groups-compare.js new file mode 100644 index 0000000..da98ca2 --- /dev/null +++ b/test/helpers/commit-groups-compare.js @@ -0,0 +1,12 @@ +import proxyquire from 'proxyquire'; + +/** + * Return the `commit-groups-compare` function, replacing `types` with parameter. + * + * @method commitGroupsCompare + * @param {Object} types commit types to test with. + * @return {Object} the commit groups compare function. + */ +export default function commitGroupsCompare(types) { + return proxyquire('../../lib/commit-groups-compare', {'../types': types}); +} diff --git a/test/preset.test.js b/test/preset.test.js index c2b83bd..a5d8367 100644 --- a/test/preset.test.js +++ b/test/preset.test.js @@ -6,8 +6,7 @@ const COMMIT_HASH_LENGTH = 7; test.serial('Include only commits with "changelog" set to "true"', async t => { const log = await changelog(['fix(scope1): First fix', 'feat(scope2): Second feature'], { - feat: {title: 'Feature title', changelog: true}, - fix: {title: 'Fix title'}, + types: {feat: {title: 'Feature title', changelog: true}, fix: {title: 'Fix title'}}, }); t.regex(log, /### Feature title/); @@ -23,7 +22,7 @@ test.serial('Include commits with breaking changes even if "changelog" is not se 'fix(scope1): First fix \n\n BREAKING CHANGE: afgshytrsd', 'feat(scope2): Second feature \n\n BREAKING CHANGE: afgshytrsd', ], - {feat: {title: 'Feature title', changelog: false}, fix: {title: 'Fix title'}} + {types: {feat: {title: 'Feature title', changelog: false}, fix: {title: 'Fix title'}}} ); t.regex(log, /### Breaking changes/); @@ -34,7 +33,7 @@ test.serial('Include commits with breaking changes even if "changelog" is not se }); test.serial('Do not include "scope" if set to "*"', async t => { - const log = await changelog(['fix(*): First fix'], {fix: {title: 'Fix title', changelog: true}}); + const log = await changelog(['fix(*): First fix'], {types: {fix: {title: 'Fix title', changelog: true}}}); t.regex(log, /### Fix title/); t.regex(log, /\* First fix/); @@ -43,7 +42,7 @@ test.serial('Do not include "scope" if set to "*"', async t => { test.serial('Create commit link', async t => { const log = await changelog( ['fix: First fix'], - {fix: {title: 'Fix title', changelog: true}}, + {types: {fix: {title: 'Fix title', changelog: true}}}, {pkg: {path: path.join(__dirname, '/fixtures/_package.json')}} ); const [, hash, url] = /\(\[(.*?)\]\((.*?)\)\)/.exec(log); @@ -55,7 +54,7 @@ test.serial('Create commit link', async t => { test.serial('Create reference link', async t => { const log = await changelog( ['fix: First fix\n\ncloses #123, fix #456'], - {fix: {title: 'Fix title', changelog: true}}, + {types: {fix: {title: 'Fix title', changelog: true}}}, {pkg: {path: path.join(__dirname, '/fixtures/_package.json')}} ); @@ -68,7 +67,7 @@ test.serial('Create reference link', async t => { test.serial('Create reference link if referenced in subject', async t => { const log = await changelog( ['fix: First fix closes #123 fix #456'], - {fix: {title: 'Fix title', changelog: true}}, + {types: {fix: {title: 'Fix title', changelog: true}}}, {pkg: {path: path.join(__dirname, '/fixtures/_package.json')}} ); @@ -81,7 +80,7 @@ test.serial('Create reference link if referenced in subject', async t => { test.serial('Do not duplicate reference link', async t => { const log = await changelog( ['fix: First fix closes #123 fix #456\n\nfix closes #123'], - {fix: {title: 'Fix title', changelog: true}}, + {types: {fix: {title: 'Fix title', changelog: true}}}, {pkg: {path: path.join(__dirname, '/fixtures/_package.json')}} ); @@ -94,7 +93,7 @@ test.serial('Do not duplicate reference link', async t => { test.serial('Create mention link', async t => { const log = await changelog( ['fix: Subject, @username @username2'], - {fix: {title: 'Fix title', changelog: true}}, + {types: {fix: {title: 'Fix title', changelog: true}}}, {pkg: {path: path.join(__dirname, '/fixtures/_package.json')}} ); @@ -103,3 +102,18 @@ test.serial('Create mention link', async t => { /\* Subject, \[@username\]\(https:\/\/github.com\/username\) \[@username2\]\(https:\/\/github.com\/username2\)/ ); }); + +test.serial('Print commit group in order', async t => { + const log = await changelog( + ['docs(scope1): Some doc update', 'fix(scope1): First fix', 'feat(scope2): Second feature'], + { + types: { + feat: {title: 'Feature title', changelog: true}, + fix: {title: 'Fix title', changelog: true}, + docs: {title: 'Documentation title', changelog: true}, + }, + } + ); + + t.regex(log, /[\S\s]*### Feature title[\S\s]*### Fix title[\S\s]*### Documentation title/); +}); diff --git a/test/types.test.js b/test/types.test.js index 4749fca..76a9092 100644 --- a/test/types.test.js +++ b/test/types.test.js @@ -1,7 +1,7 @@ import test from 'ava'; import emojiRegex from 'emoji-regex/text'; import {length} from 'stringz'; -import {types} from '../types'; +import {types, typesOrder} from '../types'; /** * AVA macro that verifies that each object in `object` has the property `prop` of type `type`. @@ -55,3 +55,15 @@ test('Each type has the property description', hasProperty, types, 'description' test('Each type has the property emoji', hasProperty, types, 'emoji', 'emoji'); test('Each type has the property changelog', hasProperty, types, 'changelog', 'boolean'); test('Each type has the property release', hasValidRelease, types); + +test('../types has the property typesOrder', t => { + t.truthy(typesOrder); +}); + +test('Each type exists in typesOrder', t => { + for (const type in types) { + if (Object.prototype.hasOwnProperty.call(types, type)) { + t.true(typesOrder.indexOf(type) !== -1); + } + } +});