Skip to content

Commit

Permalink
Merge pull request #11 from atlassian/jonelson/autocomplete-prompting
Browse files Browse the repository at this point in the history
Autocomplete prompting
  • Loading branch information
jpnelson authored Feb 24, 2017
2 parents d7039b5 + 780e150 commit 6facb31
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 48 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
"author": "Joshua Nelson <jonelson@atlassian.com>, Joscha Feth <jfeth@atlassian.com>",
"license": "MIT",
"dependencies": {
"cz-conventional-changelog": "^1.2.0",
"@semantic-release/commit-analyzer": "^2.0.0",
"chalk": "^1.1.3",
"cz-customizable": "^4.0.0",
"inquirer-autocomplete-prompt": "^0.7.0",
"promise": "^7.1.1",
"shelljs": "0.7.0"
},
"peerDependencies": {
Expand All @@ -27,6 +31,8 @@
"babel-preset-es2015": "6.6.0",
"babel-register": "^6.18.0",
"commitizen": "^2.9.5",
"cz-conventional-changelog": "^1.2.0",
"inquirer": "^3.0.4",
"lerna": "^2.0.0-beta.31",
"mocha": "^3.2.0",
"semantic-release": "^4.3.5"
Expand Down
16 changes: 16 additions & 0 deletions src/autocomplete-questions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function autoCompleteSource(options) {
return (answersSoFar, input) => {
return new Promise((resolve) => {
const matches = options.filter(({ name }) => (!input || name.toLowerCase().indexOf(input.toLowerCase()) === 0));
resolve(
matches
);
});
};
}

export default function (questions) {
return questions.map(question => Object.assign(question, question.type === 'autocomplete' ? {
source: autoCompleteSource(question.choices),
} : {}));
}
102 changes: 66 additions & 36 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import conventionalChangelog from 'cz-conventional-changelog';

import PackageUtilities from 'lerna/lib/PackageUtilities';
import Repository from 'lerna/lib/Repository';

import shell from 'shelljs';
import path from 'path';
import commitAnalyzer from '@semantic-release/commit-analyzer';
import chalk from 'chalk';
import buildCommit from 'cz-customizable/buildCommit';
import autocomplete from 'inquirer-autocomplete-prompt';
import Repository from 'lerna/lib/Repository';
import PackageUtilities from 'lerna/lib/PackageUtilities';

import makeDefaultQuestions from './make-default-questions';
import autocompleteQuestions from './autocomplete-questions';

function getAllPackages () {
return PackageUtilities.getPackages(new Repository());
Expand All @@ -29,44 +33,70 @@ function getChangedPackages () {
});
}

module.exports = {
prompter: function(cz, commit) {

const allPackages = getAllPackages().map((pkg) => pkg.name);

conventionalChangelog.prompter(cz, (commitMessage) => {
const [messageHead, ...restOfMessageParts] = commitMessage.split('\n\n');
function makeAffectsLine (answers) {
const selectedPackages = answers.packages;
if (selectedPackages && selectedPackages.length) {
return `affects: ${selectedPackages.join(', ')}`;
}
}

cz.prompt({
type: 'checkbox',
name: 'packages',
'default': getChangedPackages(),
choices: allPackages,
message: `The packages that this commit has affected (${getChangedPackages().length} detected)\n`,
validate: function (input) {
const type = commitMessage.type;
const isRequired = ['feat', 'fix'].some((type) => messageHead.indexOf(type) === 0);
const isProvided = input.length > 0;
return isRequired ? (isProvided ? true : `Commit type "${type}" must affect at least one component`) : true;
}
}).then(function (packageAnswers) {
const messages = [
messageHead
];
function getCommitTypeMessage (type) {
if (!type) {
return 'This commit does not indicate any release'
}
return {
patch: '🛠 This commit indicates a patch release (0.0.X)',
minor: '✨ This commit indicates a minor release (0.X.0)',
major: '💥 This commit indicates a major release (X.0.0)',
}[type];
}

const selectedPackages = packageAnswers.packages;
if (selectedPackages && selectedPackages.length) {
messages.push('affects: ' + selectedPackages.join(', '));
}
function mergeQuestions(defaultQuestions, customQuestions) {
const questions = [];
defaultQuestions.forEach(question => {
const matchingCustomQuestions = customQuestions.filter(({ name: customQuestionName }) => (customQuestionName === question.name));
const customQuestion = matchingCustomQuestions.length > 0 && matchingCustomQuestions[0]
questions.push(customQuestion || question);
});
return questions;
}

messages.push(...restOfMessageParts);
function makePrompter(makeCustomQuestions = () => []) {
return function(cz, commit) {
const allPackages = getAllPackages().map((pkg) => pkg.name);
const changedPackages = getChangedPackages();

const modifiedCommitMessage = messages.join('\n\n');
const defaultQuestions = makeDefaultQuestions(allPackages, changedPackages);
const customQuestions = makeCustomQuestions(allPackages, changedPackages);
const questions = mergeQuestions(defaultQuestions, customQuestions);

console.log(modifiedCommitMessage);
console.log('\n\nLine 1 will be cropped at 100 characters. All other lines will be wrapped after 100 characters.\n');

commit(modifiedCommitMessage);
cz.registerPrompt('autocomplete', autocomplete);
cz.prompt(
autocompleteQuestions(questions)
).then((answers) => {
const affectsLine = makeAffectsLine(answers);
if (affectsLine) {
answers.body = `${affectsLine}\n` + answers.body;
}
const message = buildCommit(answers);
const type = commitAnalyzer({}, {
commits: [{
hash: '',
message,
}],
}, (err, type) => {
console.log(chalk.green(`\n${getCommitTypeMessage(type)}\n`));
console.log('\n\nCommit message:');
console.log(chalk.blue(`\n\n${message}\n`));
commit(message)
});
});
}
}

module.exports = {
prompter: makePrompter(),
makePrompter: makePrompter,
};
65 changes: 65 additions & 0 deletions src/make-default-questions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Promise from 'promise';

module.exports = (allPackages, changedPackages) => ([
{
type: 'autocomplete',
name: 'type',
message: 'Select the type of change that you\'re committing:',
choices: [
{value: 'feat', name: 'feat: ✨ A new feature (note: this will indicate a release)'},
{value: 'fix', name: 'fix: 🛠 A bug fix (note: this will indicate a release)'},
{value: 'docs', name: 'docs: Documentation only changes'},
{value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'},
{value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'},
{value: 'perf', name: 'perf: A code change that improves performance'},
{value: 'test', name: 'test: Adding missing tests'},
{value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'},
{value: 'revert', name: 'revert: Revert to a commit'},
{value: 'WIP', name: 'WIP: Work in progress'}
],
},
{
type: 'input',
name: 'scope',
message: 'Denote the scope of this change:',
},
{
type: 'input',
name: 'subject',
message: 'Write a short, imperative tense description of the change:\n',
filter: function(value) {
return value.charAt(0).toLowerCase() + value.slice(1);
},
validate: function(value) {
return !!value;
},
},
{
type: 'input',
name: 'body',
message: 'Provide a longer description of the change (optional). Use "|" to break new line:\n'
},
{
type: 'input',
name: 'breaking',
message: 'List any BREAKING CHANGES (optional):\n',
},
{
type: 'input',
name: 'footer',
message: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n',
},
{
type: 'checkbox',
name: 'packages',
'default': changedPackages,
choices: allPackages,
message: `The packages that this commit has affected (${changedPackages.length} detected)\n`,
// validate: function (input) {
// const type = commitMessage.type;
// const isRequired = ['feat', 'fix'].some((type) => messageHead.indexOf(type) === 0);
// const isProvided = input.length > 0;
// return isRequired ? (isProvided ? true : `Commit type "${type}" must affect at least one component`) : true;
// }
},
]);
55 changes: 44 additions & 11 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import shell from 'shelljs';
import PackageUtilities from 'lerna/lib/PackageUtilities';

import stub from './_stub';
import { prompter } from '../src/index';
import { prompter, makePrompter } from '../src/index';


const createMockCommitizenCli = (answers) => ({
Expand All @@ -21,32 +21,65 @@ const createMockCommitizenCli = (answers) => ({
return acc;
}, {})
);
}
},
registerPrompt: () => {},
});


describe('cz-lerna-changelog', () => {
stub(shell, 'exec', () => ({ stdout: '' }));
stub(PackageUtilities, 'getPackages', () => ([{
name: 'test-package',
location: 'packages/test-package'
}]));
it('should generate correct commit message from prompt answers', (done) => {
stub(shell, 'exec', () => ({ stdout: '' }));
stub(PackageUtilities, 'getPackages', () => ([{
name: 'test-package',
location: 'packages/test-package'
}]));

const answers = {
'Select the type of change that you\'re committing:': 'feat',
'Denote the scope of this change ($location, $browser, $compile, etc.):\n': 'Fake Scope',
'Denote the scope of this change:': 'Fake scope',
'Write a short, imperative tense description of the change:\n': 'Test commit',
'Provide a longer description of the change:\n': 'This commit is a fake one',
'List any breaking changes or issues closed by this change:\n': '',
'Provide a longer description of the change (optional). Use "|" to break new line:\n': 'This commit is a fake one',
'List any BREAKING CHANGES (optional):\n': '',
'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n': '',
'The packages that this commit has affected (0 detected)\n': ['test-package']
};

prompter(createMockCommitizenCli(answers), (commitMessage) => {
try {
assert.equal(
commitMessage.trim(),
'feat(Fake Scope): Test commit\n\naffects: test-package\n\nThis commit is a fake one'
'feat(Fake scope): Test commit\n\naffects: test-package\n\nThis commit is a fake one'
);
done();
} catch (e) {
done(e);
}
})
});
it('allows questions to be overriden', (done) => {
const answers = {
'Select the type of change that you\'re committing:': 'feat',
'***Custom question for scope:***': 'Fake scope',
'Write a short, imperative tense description of the change:\n': 'Test commit',
'Provide a longer description of the change (optional). Use "|" to break new line:\n': 'This commit is a fake one',
'List any BREAKING CHANGES (optional):\n': '',
'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n': '',
'The packages that this commit has affected (0 detected)\n': ['test-package']
};

const makeCustomQuestions = () => ([
{
type: 'input',
name: 'scope',
message: '***Custom question for scope:***',
},
])

makePrompter(makeCustomQuestions)(createMockCommitizenCli(answers), (commitMessage) => {
try {
assert.equal(
commitMessage.trim(),
'feat(Fake scope): Test commit\n\naffects: test-package\n\nThis commit is a fake one'
);
done();
} catch (e) {
Expand Down

0 comments on commit 6facb31

Please # to comment.