Skip to content

Commit

Permalink
feat(changelog-writer): Add commit groups sorting ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg committed Aug 25, 2017
1 parent 100f17a commit ae2a59b
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,4 @@ $RECYCLE.BIN/
/release-rules.*
/lib
/types.*
/aliases.*
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -82,7 +83,8 @@
"lib/*.js",
"preset.js",
"release-rules.js",
"types.js"
"types.js",
"aliases.js"
]
},
"publishConfig": {
Expand All @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions src/aliases.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
23 changes: 23 additions & 0 deletions src/lib/commit-groups-compare.js
Original file line number Diff line number Diff line change
@@ -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;
};
5 changes: 4 additions & 1 deletion src/preset.js
Original file line number Diff line number Diff line change
@@ -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<Object>} preset with `parserOpts` and `writerOpts`.
*/
module.exports = conventionalChangelogAngular.then(preset => merge(preset, {writerOpts: {transform}}));
module.exports = conventionalChangelogAngular.then(preset =>
merge(preset, {writerOpts: {transform, commitGroupsSort}})
);
18 changes: 18 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
48 changes: 48 additions & 0 deletions test/aliases.test.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
});
58 changes: 58 additions & 0 deletions test/commit-groups-compare.test.js
Original file line number Diff line number Diff line change
@@ -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'},
]);
});
6 changes: 5 additions & 1 deletion test/helpers/changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions test/helpers/commit-groups-compare.js
Original file line number Diff line number Diff line change
@@ -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});
}
32 changes: 23 additions & 9 deletions test/preset.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand All @@ -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/);
Expand All @@ -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/);
Expand All @@ -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);
Expand All @@ -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')}}
);

Expand All @@ -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')}}
);

Expand All @@ -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')}}
);

Expand All @@ -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')}}
);

Expand All @@ -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/);
});
14 changes: 13 additions & 1 deletion test/types.test.js
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down Expand Up @@ -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);
}
}
});

0 comments on commit ae2a59b

Please sign in to comment.