diff --git a/buildCommit.js b/buildCommit.js index 4996251..0ae096f 100644 --- a/buildCommit.js +++ b/buildCommit.js @@ -25,6 +25,16 @@ module.exports = function buildCommit(answers, config) { return subject.trim(); } + function addTicketNumber(ticketNumber, config) { + if (!ticketNumber) { + return ''; + } + if (config.ticketNumberPrefix) { + return config.ticketNumberPrefix + ticketNumber.trim() + ' '; + } + return ticketNumber.trim() + ' '; + } + function escapeSpecialChars(result) { var specialChars = ['\`']; @@ -38,7 +48,7 @@ module.exports = function buildCommit(answers, config) { } // Hard limit this line - var head = (answers.type + addScope(answers.scope) + addSubject(answers.subject)).slice(0, maxLineWidth); + var head = (answers.type + addScope(answers.scope) + addTicketNumber(answers.ticketNumber, config) + addSubject(answers.subject)).slice(0, maxLineWidth); // Wrap these lines at 100 characters var body = wrap(answers.body, wrapOptions) || ''; diff --git a/cz-config-EXAMPLE.js b/cz-config-EXAMPLE.js index 3de9ce1..e404b67 100644 --- a/cz-config-EXAMPLE.js +++ b/cz-config-EXAMPLE.js @@ -22,6 +22,11 @@ module.exports = { {name: 'changeMe'} ], + allowTicketNumber: true, + isTicketNumberRequired: true, + ticketNumberPrefix: 'TICKET-', + ticketNumberRegExp: '\\d{1,5}', + // it needs to match the value for field type. Eg.: 'fix' /* scopeOverrides: { @@ -55,4 +60,5 @@ module.exports = { // limit subject length subjectLimit: 100 + }; diff --git a/questions.js b/questions.js index 6fda416..46d8aca 100644 --- a/questions.js +++ b/questions.js @@ -1,6 +1,8 @@ 'use strict'; +var fs = require('fs'); +var path = require('path'); var buildCommit = require('./buildCommit'); var log = require('winston'); @@ -9,6 +11,23 @@ var isNotWip = function(answers) { return answers.type.toLowerCase() !== 'wip'; }; +var cwd = fs.realpathSync(process.cwd()); +var packageData = require(path.join(cwd, 'package.json')); + +function isValidateTicketNo(value, config) { + if (!value) { + return config.isTicketNumberRequired ? false : true; + } + if (!config.ticketNumberRegExp) { + return true; + } + var reg = new RegExp(config.ticketNumberRegExp); + if (value.replace(reg, '') !== '') { + return false; + } + return true; +} + module.exports = { getQuestions: function(config, cz) { @@ -21,6 +40,13 @@ module.exports = { messages.type = messages.type || 'Select the type of change that you\'re committing:'; messages.scope = messages.scope || '\nDenote the SCOPE of this change (optional):'; messages.customScope = messages.customScope || 'Denote the SCOPE of this change:'; + if (!messages.ticketNumber) { + if (config.ticketNumberRegExp) { + messages.ticketNumber = messages.ticketNumberPattern || 'Enter the ticket number following this pattern (' + config.ticketNumberRegExp + ')\n'; + } else { + messages.ticketNumber = 'Enter the ticket number:\n'; + } + } messages.subject = messages.subject || 'Write a SHORT, IMPERATIVE tense description of the change:\n'; messages.body = messages.body || 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n'; messages.breaking = messages.breaking || 'List any BREAKING CHANGES (optional):\n'; @@ -77,6 +103,17 @@ module.exports = { return answers.scope === 'custom'; } }, + { + type: 'input', + name: 'ticketNumber', + message: messages.ticketNumber, + when: function() { + return !!config.allowTicketNumber; // no ticket numbers allowed unless specifed + }, + validate: function (value) { + return isValidateTicketNo(value, config); + } + }, { type: 'input', name: 'subject', diff --git a/spec/czCustomizableSpec.js b/spec/czCustomizableSpec.js index 0695826..05c08a1 100644 --- a/spec/czCustomizableSpec.js +++ b/spec/czCustomizableSpec.js @@ -264,4 +264,54 @@ describe('cz-customizable', function() { expect(commit).toHaveBeenCalledWith('feat(myScope): create a new cool feature\n\nBREAKING CHANGE:\nbreaking\n\nFIXES: my footer'); }); + it('should call commit() function with ticket number', function() { + var answers = { + confirmCommit: 'yes', + type: 'feat', + scope: 'myScope', + subject: 'create a new cool feature', + ticketNumber: 'TICKET-1234' + }; + + var mockCz = getMockedCz(answers); + module.prompter(mockCz, commit); + expect(commit).toHaveBeenCalledWith('feat(myScope): TICKET-1234 create a new cool feature'); + }); + + it('should call commit() function with ticket number and prefix', function() { + + module.__set__({ + // it mocks winston logging tool + log: { + info: function() {} + }, + + readConfigFile: function() { + return { + types: [{value: 'feat', name: 'feat: my feat'}], + scopes: [{name: 'myScope'}], + scopeOverrides: { + fix: [{name: 'fixOverride'}] + }, + allowCustomScopes: true, + allowBreakingChanges: ['feat'], + breakingPrefix: 'WARNING:', + ticketNumberPrefix: 'TICKET-' + }; + } + }); + + var answers = { + confirmCommit: 'yes', + type: 'feat', + scope: 'myScope', + subject: 'create a new cool feature', + ticketNumber: '1234' + }; + + var mockCz = getMockedCz(answers); + module.prompter(mockCz, commit); + expect(commit).toHaveBeenCalledWith('feat(myScope): TICKET-1234 create a new cool feature'); + }); + }); diff --git a/spec/questionsSpec.js b/spec/questionsSpec.js index 00314c8..5508fda 100644 --- a/spec/questionsSpec.js +++ b/spec/questionsSpec.js @@ -26,6 +26,10 @@ describe('cz-customizable', function() { }, allowCustomScopes: true, allowBreakingChanges: ['feat'], + allowTicketNumber: true, + isTicketNumberRequired: true, + ticketNumberPrefix: 'TICKET-', + ticketNumberRegExp: '\\d{1,5}', subjectLimit: 20 }; @@ -48,33 +52,39 @@ describe('cz-customizable', function() { expect(getQuestion(3).when({scope: false})).toEqual(false); expect(getQuestion(3).when({scope: 'scope'})).toEqual(false); - // question 4 - SUBJECT - expect(getQuestion(4).name).toEqual('subject'); + // question 4 - TICKET_NUMBER + expect(getQuestion(4).name).toEqual('ticketNumber'); expect(getQuestion(4).type).toEqual('input'); - expect(getQuestion(4).message).toMatch(/IMPERATIVE tense description/); - expect(getQuestion(4).validate('good subject')).toEqual(true); - expect(getQuestion(4).validate('bad subject that exceed limit')).toEqual('Exceed limit: 20'); - expect(getQuestion(4).filter('Subject')).toEqual('subject'); + expect(getQuestion(4).message.indexOf('Enter the ticket number following this pattern')).toEqual(0); + expect(getQuestion(4).validate()).toEqual(false); //mandatory question - // question 5 - BODY - expect(getQuestion(5).name).toEqual('body'); + // question 5 - SUBJECT + expect(getQuestion(5).name).toEqual('subject'); expect(getQuestion(5).type).toEqual('input'); - - // question 6 - BREAKING CHANGE - expect(getQuestion(6).name).toEqual('breaking'); + expect(getQuestion(5).message).toMatch(/IMPERATIVE tense description/); + expect(getQuestion(5).filter('Subject')).toEqual('subject'); + expect(getQuestion(5).validate('bad subject that exceed limit')).toEqual('Exceed limit: 20'); + expect(getQuestion(5).validate('good subject')).toEqual(true); + + // question 6 - BODY + expect(getQuestion(6).name).toEqual('body'); expect(getQuestion(6).type).toEqual('input'); - expect(getQuestion(6).when({type: 'feat'})).toEqual(true); - expect(getQuestion(6).when({type: 'fix'})).toEqual(false); - // question 7 - FOOTER - expect(getQuestion(7).name).toEqual('footer'); + // question 7 - BREAKING CHANGE + expect(getQuestion(7).name).toEqual('breaking'); expect(getQuestion(7).type).toEqual('input'); - expect(getQuestion(7).when({type: 'fix'})).toEqual(true); - expect(getQuestion(7).when({type: 'WIP'})).toEqual(false); + expect(getQuestion(7).when({type: 'feat'})).toEqual(true); + expect(getQuestion(7).when({type: 'fix'})).toEqual(false); + + // question 8 - FOOTER + expect(getQuestion(8).name).toEqual('footer'); + expect(getQuestion(8).type).toEqual('input'); + expect(getQuestion(8).when({type: 'fix'})).toEqual(true); + expect(getQuestion(8).when({type: 'WIP'})).toEqual(false); - //question 8, last one, CONFIRM COMMIT OR NOT - expect(getQuestion(8).name).toEqual('confirmCommit'); - expect(getQuestion(8).type).toEqual('expand'); + //question 9, last one, CONFIRM COMMIT OR NOT + expect(getQuestion(9).name).toEqual('confirmCommit'); + expect(getQuestion(9).type).toEqual('expand'); var answers = { @@ -83,15 +93,15 @@ describe('cz-customizable', function() { scope: 'myScope', subject: 'create a new cool feature' }; - expect(getQuestion(8).message(answers)).toMatch('Are you sure you want to proceed with the commit above?'); + expect(getQuestion(9).message(answers)).toMatch('Are you sure you want to proceed with the commit above?'); }); it('default length limit of subject should be 100', function() { config = { types: [{value: 'feat', name: 'feat: my feat'}] }; - expect(getQuestion(4).validate('good subject')).toEqual(true); - expect(getQuestion(4).validate('bad subject that exceed limit bad subject that exceed limitbad subject that exceed limit test test test')).toEqual('Exceed limit: 100'); + expect(getQuestion(5).validate('good subject')).toEqual(true); + expect(getQuestion(5).validate('bad subject that exceed limit bad subject that exceed limitbad subject that exceed limit test test test')).toEqual('Exceed limit: 100'); }); @@ -103,13 +113,13 @@ describe('cz-customizable', function() { scopes: [{name: 'myScope'}], allowBreakingChanges: ['fix'] }; - expect(getQuestion(6).name).toEqual('breaking'); + expect(getQuestion(7).name).toEqual('breaking'); var answers = { type: 'feat' }; - expect(getQuestion(6).when(answers)).toEqual(false); // not allowed + expect(getQuestion(7).when(answers)).toEqual(false); // not allowed }); it('should allow BREAKING CHANGE question when config property "allowBreakingChanges" specifies array of types and answer is one of those', function() { @@ -118,13 +128,13 @@ describe('cz-customizable', function() { scopes: [{name: 'myScope'}], allowBreakingChanges: ['fix', 'feat'] }; - expect(getQuestion(6).name).toEqual('breaking'); + expect(getQuestion(7).name).toEqual('breaking'); var answers = { type: 'feat' }; - expect(getQuestion(6).when(answers)).toEqual(true); // allowed + expect(getQuestion(7).when(answers)).toEqual(true); // allowed }); }); @@ -153,5 +163,84 @@ describe('cz-customizable', function() { }); }); + describe('no TicketNumber question', function() { + + it('should use scope override', function() { + config = { + types: [{value: 'feat', name: 'feat: my feat'}], + allowTicketNumber: false + }; + + // question 4 with + expect(getQuestion(4).name).toEqual('ticketNumber'); + expect(getQuestion(4).when()).toEqual(false); + }); + }); + + describe('TicketNumber', function() { + + it('disable TicketNumber question', function() { + config = { + types: [{value: 'feat', name: 'feat: my feat'}], + allowTicketNumber: false + }; + + // question 4 with + expect(getQuestion(4).name).toEqual('ticketNumber'); + expect(getQuestion(4).when()).toEqual(false); + }); + + it('custom message defined', function() { + config = { + types: [{value: 'feat', name: 'feat: my feat'}], + allowTicketNumber: true, + messages: { + ticketNumber: 'ticket number' + } + }; + + // question 4 with + expect(getQuestion(4).name).toEqual('ticketNumber'); + expect(getQuestion(4).message).toEqual('ticket number'); + }); + + describe('validation', function() { + it('invalid because empty and required', function() { + config = { + isTicketNumberRequired: true + }; + expect(getQuestion(4).validate('')).toEqual(false); + }); + it('empty but valid because optional', function() { + config = { + isTicketNumberRequired: false + }; + expect(getQuestion(4).validate('')).toEqual(true); + }); + it('valid because there is no regexp defined', function() { + config = { + isTicketNumberRequired: true, + ticketNumberRegExp: undefined + }; + expect(getQuestion(4).validate('21234')).toEqual(true); + }); + it('invalid because regexp don\'t match', function() { + config = { + isTicketNumberRequired: true, + ticketNumberRegExp: '\\d{1,5}' + }; + expect(getQuestion(4).validate('sddsa')).toEqual(false); + }); + it('valid because regexp match', function() { + config = { + isTicketNumberRequired: true, + ticketNumberRegExp: '\\d{1,5}' + }; + expect(getQuestion(4).validate('12345')).toEqual(true); + }); + }); + + }); + });