diff --git a/engine.js b/engine.js index 24a42b7c..6b274d34 100644 --- a/engine.js +++ b/engine.js @@ -40,7 +40,7 @@ module.exports = function(options) { var types = options.types; var length = longest(Object.keys(types)).length + 1; - var choices = map(types, function(type, key) { + var typeChoices = map(types, function(type, key) { return { name: (key + ':').padEnd(length) + ' ' + type.description, value: key @@ -72,122 +72,156 @@ module.exports = function(options) { type: 'list', name: 'type', message: "Select the type of change that you're committing:", - choices: choices, + choices: typeChoices, default: options.defaultType - }, - { - type: 'input', - name: 'scope', - message: - 'What is the scope of this change (e.g. component or file name): (press enter to skip)', - default: options.defaultScope, - filter: function(value) { + } + ]) + .then(function(firstAnswers) { + var scopes = options.allowedScopes; + + var scopeChoices = scopes && scopes.length ? + [{ + name: 'Empty', + value: undefined + }].concat(map(scopes, function(scope) { + return { + name: scope, + value: scope + }; + })) + : null; + var filterScope = function(value) { + if (!value) { + return value; + } + return options.disableScopeLowerCase ? value.trim() : value.trim().toLowerCase(); } - }, - { - type: 'input', - name: 'subject', - message: function(answers) { - return ( - 'Write a short, imperative tense description of the change (max ' + - maxSummaryLength(options, answers) + - ' chars):\n' - ); - }, - default: options.defaultSubject, - validate: function(subject, answers) { - var filteredSubject = filterSubject(subject, options.disableSubjectLowerCase); - return filteredSubject.length == 0 - ? 'subject is required' - : filteredSubject.length <= maxSummaryLength(options, answers) - ? true - : 'Subject length must be less than or equal to ' + - maxSummaryLength(options, answers) + - ' characters. Current length is ' + - filteredSubject.length + - ' characters.'; - }, - transformer: function(subject, answers) { - var filteredSubject = filterSubject(subject, options.disableSubjectLowerCase); - var color = - filteredSubject.length <= maxSummaryLength(options, answers) - ? chalk.green - : chalk.red; - return color('(' + filteredSubject.length + ') ' + subject); - }, - filter: function(subject) { - return filterSubject(subject, options.disableSubjectLowerCase); - } - }, - { - type: 'input', - name: 'body', - message: - 'Provide a longer description of the change: (press enter to skip)\n', - default: options.defaultBody - }, - { - type: 'confirm', - name: 'isBreaking', - message: 'Are there any breaking changes?', - default: false - }, - { - type: 'input', - name: 'breakingBody', - default: '-', - message: - 'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself:\n', - when: function(answers) { - return answers.isBreaking && !answers.body; - }, - validate: function(breakingBody, answers) { - return ( - breakingBody.trim().length > 0 || - 'Body is required for BREAKING CHANGE' - ); - } - }, - { - type: 'input', - name: 'breaking', - message: 'Describe the breaking changes:\n', - when: function(answers) { - return answers.isBreaking; - } - }, - { - type: 'confirm', - name: 'isIssueAffected', - message: 'Does this change affect any open issues?', - default: options.defaultIssues ? true : false - }, - { - type: 'input', - name: 'issuesBody', - default: '-', - message: - 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself:\n', - when: function(answers) { - return ( - answers.isIssueAffected && !answers.body && !answers.breakingBody - ); - } - }, - { - type: 'input', - name: 'issues', - message: 'Add issue references (e.g. "fix #123", "re #123".):\n', - when: function(answers) { - return answers.isIssueAffected; - }, - default: options.defaultIssues ? options.defaultIssues : undefined - } - ]).then(function(answers) { + var scopePrompt = scopeChoices ? { + type: 'list', + name: 'type', + message: "Select the scope of change that you're committing:", + choices: scopeChoices, + default: options.defaultScope, + filter: filterScope + } : { + type: 'input', + name: 'scope', + message: + 'What is the scope of this change (e.g. component or file name): (press enter to skip)', + default: options.defaultScope, + filter: filterScope + }; + + return cz.prompt([ + scopePrompt, + { + type: 'input', + name: 'subject', + message: function(answers) { + return ( + 'Write a short, imperative tense description of the change (max ' + + maxSummaryLength(options, answers) + + ' chars):\n' + ); + }, + default: options.defaultSubject, + validate: function(subject, answers) { + var filteredSubject = filterSubject(subject, options.disableSubjectLowerCase); + return filteredSubject.length == 0 + ? 'subject is required' + : filteredSubject.length <= maxSummaryLength(options, answers) + ? true + : 'Subject length must be less than or equal to ' + + maxSummaryLength(options, answers) + + ' characters. Current length is ' + + filteredSubject.length + + ' characters.'; + }, + transformer: function(subject, answers) { + var filteredSubject = filterSubject(subject, options.disableSubjectLowerCase); + var color = + filteredSubject.length <= maxSummaryLength(options, answers) + ? chalk.green + : chalk.red; + return color('(' + filteredSubject.length + ') ' + subject); + }, + filter: function(subject) { + return filterSubject(subject, options.disableSubjectLowerCase); + } + }, + { + type: 'input', + name: 'body', + message: + 'Provide a longer description of the change: (press enter to skip)\n', + default: options.defaultBody + }, + { + type: 'confirm', + name: 'isBreaking', + message: 'Are there any breaking changes?', + default: false + }, + { + type: 'input', + name: 'breakingBody', + default: '-', + message: + 'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself:\n', + when: function(answers) { + return answers.isBreaking && !answers.body; + }, + validate: function(breakingBody, answers) { + return ( + breakingBody.trim().length > 0 || + 'Body is required for BREAKING CHANGE' + ); + } + }, + { + type: 'input', + name: 'breaking', + message: 'Describe the breaking changes:\n', + when: function(answers) { + return answers.isBreaking; + } + }, + + { + type: 'confirm', + name: 'isIssueAffected', + message: 'Does this change affect any open issues?', + default: options.defaultIssues ? true : false + }, + { + type: 'input', + name: 'issuesBody', + default: '-', + message: + 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself:\n', + when: function(answers) { + return ( + answers.isIssueAffected && !answers.body && !answers.breakingBody + ); + } + }, + { + type: 'input', + name: 'issues', + message: 'Add issue references (e.g. "fix #123", "re #123".):\n', + when: function(answers) { + return answers.isIssueAffected; + }, + default: options.defaultIssues ? options.defaultIssues : undefined + } + ]).then(function(restOfAnswers) { + return Object.assign(firstAnswers, restOfAnswers); + }); + }).then(function(answers) { var wrapOptions = { trim: true, cut: false, diff --git a/engine.test.js b/engine.test.js index aacd134f..ac427b73 100644 --- a/engine.test.js +++ b/engine.test.js @@ -480,6 +480,88 @@ describe('commitlint config header-max-length', function() { }); } }); + +describe('commitlint config scope-enum', function() { + //commitlint config parser only supports Node 6.0.0 and higher + if (semver.gte(process.version, '6.0.0')) { + function mockOptions(allowedScopes) { + var options = undefined; + mock('./engine', function(opts) { + options = opts; + }); + if (allowedScopes) { + mock('cosmiconfig', function() { + return { + load: function(cwd) { + return { + filepath: cwd + '/.commitlintrc.js', + config: { + rules: { + 'scope-enum': [2, 'always', allowedScopes] + } + } + }; + } + }; + }); + } + + mock.reRequire('./index'); + try { + return mock + .reRequire('@commitlint/load')() + .then(function() { + return options; + }); + } catch (err) { + return Promise.resolve(options); + } + } + + afterEach(function() { + delete require.cache[require.resolve('./index')]; + delete require.cache[require.resolve('@commitlint/load')]; + delete process.env.CZ_ALLOWED_SCOPES; + mock.stopAll(); + }); + + it('with no environment or commitizen config override', function() { + return mockOptions(['client', 'server']).then(function(options) { + expect(options).to.have.deep.property('allowedScopes', ['client', 'server']); + }); + }); + + it('with environment variable override', function() { + process.env.CZ_ALLOWED_SCOPES = 'other,scopes'; + return mockOptions(['client', 'server']).then(function(options) { + expect(options).to.have.deep.property('allowedScopes', ['other', 'scopes']); + }); + }); + + it('with commitizen config override', function() { + mock('commitizen', { + configLoader: { + load: function() { + return { + allowedScopes: ['other', 'scopes'] + }; + } + } + }); + return mockOptions(['client', 'server']).then(function(options) { + expect(options).to.have.deep.property('allowedScopes', ['other', 'scopes']); + }); + }); + } else { + //Node 4 doesn't support commitlint so the config value should remain the same + it('default value for Node 4', function() { + return mockOptions(['other', 'scopes']).then(function(options) { + expect(options).to.have.deep.property('allowedScopes', []); + }); + }); + } +}); + function commitMessage(answers, options) { options = options || defaultOptions; var result = null; @@ -490,6 +572,13 @@ function commitMessage(answers, options) { then: function(finalizer) { processQuestions(questions, answers, options); finalizer(answers); + + return { + then: function(finalizer) { + processQuestions(questions, answers, options); + finalizer(answers); + } + }; } }; } @@ -526,9 +615,15 @@ function getQuestions(options) { var result = null; engine(options).prompter({ prompt: function(questions) { - result = questions; + result = result ? result.concat(questions) : questions; return { - then: function() {} + then: function(secondRun) { + secondRun({}); + + return { + then: function() {} + }; + } }; } }); diff --git a/index.js b/index.js index 7a4586d2..7da8cbc0 100644 --- a/index.js +++ b/index.js @@ -25,7 +25,11 @@ var options = { (process.env.CZ_MAX_LINE_WIDTH && parseInt(process.env.CZ_MAX_LINE_WIDTH)) || config.maxLineWidth || - 100 + 100, + allowedScopes: + (process.env.CZ_ALLOWED_SCOPES && process.env.CZ_ALLOWED_SCOPES.split(',')) || + config.allowedScopes || + [] }; (function(options) { @@ -34,6 +38,8 @@ var options = { commitlintLoad().then(function(clConfig) { if (clConfig.rules) { var maxHeaderLengthRule = clConfig.rules['header-max-length']; + var scopeEnumRule = clConfig.rules['scope-enum']; + if ( typeof maxHeaderLengthRule === 'object' && maxHeaderLengthRule.length >= 3 && @@ -42,6 +48,15 @@ var options = { ) { options.maxHeaderWidth = maxHeaderLengthRule[2]; } + + if ( + typeof scopeEnumRule === 'object' && + scopeEnumRule.length >= 3 && + !process.env.CZ_ALLOWED_SCOPES && + !(config.allowedScopes && config.allowedScopes.length) + ) { + options.allowedScopes = scopeEnumRule[2]; + } } }); } catch (err) {}