├── .prettierrc ├── images └── demo.gif ├── .editorconfig ├── configurable.js ├── defaults.js ├── .gitignore ├── .github └── workflows │ └── release.yml ├── LICENSE ├── types.js ├── package.json ├── LimitedInputPrompt.js ├── index.js ├── engine.js ├── README.md └── engine.test.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "endOfLine": "lf", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalroute/cz-conventional-changelog-for-jira/HEAD/images/demo.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | indent_size = 2 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = 80 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /configurable.js: -------------------------------------------------------------------------------- 1 | var engine = require('./engine'); 2 | var defaults = require('./defaults'); 3 | 4 | module.exports = function(overridenOptions) { 5 | return engine({ ...defaults, ...overridenOptions }); 6 | }; 7 | -------------------------------------------------------------------------------- /defaults.js: -------------------------------------------------------------------------------- 1 | var conventionalCommitTypes = require('./types'); 2 | 3 | module.exports = { 4 | types: conventionalCommitTypes, 5 | jiraMode: true, 6 | skipScope: true, 7 | skipType: false, 8 | skipDescription: false, 9 | skipBreaking: false, 10 | customScope: false, 11 | maxHeaderWidth: 72, 12 | minHeaderWidth: 2, 13 | maxLineWidth: 100, 14 | jiraPrefix: 'DAZ', 15 | jiraOptional: false, 16 | jiraLocation: 'pre-description', 17 | jiraPrepend: '', 18 | jiraAppend: '', 19 | exclamationMark: false 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # IDE 30 | .idea 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - beta 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 16 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Run tests 23 | run: npm test 24 | - name: Release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | run: npx semantic-release 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2018 Commitizen Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | feat: { 3 | description: 'A new feature', 4 | title: 'Features' 5 | }, 6 | fix: { 7 | description: 'A bug fix', 8 | title: 'Bug Fixes' 9 | }, 10 | docs: { 11 | description: 'Documentation only changes', 12 | title: 'Documentation' 13 | }, 14 | refactor: { 15 | description: 16 | 'A code change that neither fixes a bug nor adds a feature (formatting, performance improvement, etc)', 17 | title: 'Code Refactoring' 18 | }, 19 | test: { 20 | description: 'Adding missing tests or correcting existing tests', 21 | title: 'Tests' 22 | }, 23 | build: { 24 | description: 25 | 'Changes that affect the build system or external dependencies (npm, webpack, typescript)', 26 | title: 'Builds' 27 | }, 28 | ci: { 29 | description: 30 | 'Changes to our CI configuration files and scripts (NOTE: Does not bump the version)', 31 | title: 'Continuous Integrations' 32 | }, 33 | chore: { 34 | description: "Other changes that don't modify src or test files", 35 | title: 'Chores' 36 | }, 37 | revert: { 38 | description: 'Reverts a previous commit', 39 | title: 'Reverts' 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@digitalroute/cz-conventional-changelog-for-jira", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Commitizen adapter following the conventional-changelog format and also asking for JIRA issue.", 5 | "main": "index.js", 6 | "scripts": { 7 | "commit": "git-cz", 8 | "test": "mocha *.test.js", 9 | "format": "prettier --write *.js", 10 | "semantic-release": "semantic-release" 11 | }, 12 | "homepage": "https://github.com/digitalroute/cz-conventional-changelog-for-jira", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/digitalroute/cz-conventional-changelog-for-jira.git" 16 | }, 17 | "engineStrict": true, 18 | "engines": { 19 | "node": ">= 10" 20 | }, 21 | "author": "Marcus Johansson ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "boxen": "^5.1.2", 25 | "chalk": "^2.4.1", 26 | "commitizen": "^4.2.6", 27 | "cz-conventional-changelog": "^3.3.0", 28 | "inquirer": "^8.2.4", 29 | "lodash.map": "^4.5.1", 30 | "longest": "^2.0.1", 31 | "right-pad": "^1.0.1", 32 | "word-wrap": "^1.0.3" 33 | }, 34 | "devDependencies": { 35 | "@types/chai": "^4.1.7", 36 | "@types/mocha": "^5.2.7", 37 | "chai": "^4.2.0", 38 | "cosmiconfig": "^5.2.1", 39 | "mocha": "^10.0.0", 40 | "mock-require": "^3.0.3", 41 | "prettier": "^1.15.3", 42 | "semantic-release": "^19.0.3", 43 | "semver": "^6.2.0" 44 | }, 45 | "optionalDependencies": { 46 | "@commitlint/load": ">6.1.1" 47 | }, 48 | "config": { 49 | "commitizen": { 50 | "path": "./index.js", 51 | "jiraMode": false, 52 | "skipScope": true 53 | } 54 | }, 55 | "publishConfig": { 56 | "access": "public" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LimitedInputPrompt.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const InputPrompt = require('inquirer/lib/prompts/input'); 3 | 4 | class LimitedInputPrompt extends InputPrompt { 5 | constructor(...args) { 6 | super(...args); 7 | 8 | if (!this.opt.maxLength) { 9 | this.throwParamError('maxLength'); 10 | } 11 | this.originalMessage = this.opt.message; 12 | this.spacer = new Array(this.opt.maxLength).fill('-').join(''); 13 | 14 | if (this.opt.leadingLabel) { 15 | if (typeof this.opt.leadingLabel === 'function') { 16 | this.leadingLabel = ' ' + this.opt.leadingLabel(this.answers); 17 | } else { 18 | this.leadingLabel = ' ' + this.opt.leadingLabel; 19 | } 20 | } else { 21 | this.leadingLabel = ''; 22 | } 23 | 24 | this.leadingLength = this.leadingLabel.length; 25 | } 26 | 27 | remainingChar() { 28 | return this.opt.maxLength - this.leadingLength - this.rl.line.length; 29 | } 30 | 31 | onKeypress() { 32 | if (this.rl.line.length > this.opt.maxLength - this.leadingLength) { 33 | this.rl.line = this.rl.line.slice( 34 | 0, 35 | this.opt.maxLength - this.leadingLength 36 | ); 37 | this.rl.cursor--; 38 | } 39 | 40 | this.render(); 41 | } 42 | 43 | getCharsLeftText() { 44 | const chars = this.remainingChar(); 45 | 46 | if (chars > 15) { 47 | return chalk.green(`${chars} chars left`); 48 | } else if (chars > 5) { 49 | return chalk.yellow(`${chars} chars left`); 50 | } else { 51 | return chalk.red(`${chars} chars left`); 52 | } 53 | } 54 | 55 | render(error) { 56 | let bottomContent = ''; 57 | let message = this.getQuestion(); 58 | let appendContent = ''; 59 | const isFinal = this.status === 'answered'; 60 | 61 | if (isFinal) { 62 | appendContent = this.answer; 63 | } else { 64 | appendContent = this.rl.line; 65 | } 66 | 67 | message = `${message} 68 | [${this.spacer}] ${this.getCharsLeftText()} 69 | ${this.leadingLabel} ${appendContent}`; 70 | 71 | if (error) { 72 | bottomContent = chalk.red('>> ') + error; 73 | } 74 | 75 | this.screen.render(message, bottomContent); 76 | } 77 | } 78 | 79 | module.exports = LimitedInputPrompt; 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'format cjs'; 2 | 3 | var engine = require('./engine'); 4 | var conventionalCommitTypes = require('./types'); 5 | var defaults = require('./defaults'); 6 | var configLoader = require('commitizen').configLoader; 7 | 8 | var config = configLoader.load(); 9 | 10 | function getEnvOrConfig(env, configVar, defaultValue) { 11 | const isEnvSet = Boolean(env); 12 | const isConfigSet = typeof configVar === 'boolean'; 13 | 14 | if (isEnvSet) return env === 'true'; 15 | if (isConfigSet) return configVar; 16 | return defaultValue; 17 | } 18 | 19 | const options = { 20 | types: conventionalCommitTypes, 21 | scopes: config.scopes, 22 | jiraMode: getEnvOrConfig( 23 | process.env.CZ_JIRA_MODE, 24 | config.jiraMode, 25 | defaults.jiraMode 26 | ), 27 | skipScope: getEnvOrConfig( 28 | process.env.CZ_SKIP_SCOPE, 29 | config.skipScope, 30 | defaults.skipScope 31 | ), 32 | skipType: getEnvOrConfig( 33 | process.env.CZ_SKIP_TYPE, 34 | config.skipType, 35 | defaults.skipType 36 | ), 37 | skipDescription: getEnvOrConfig( 38 | process.env.CZ_SKIP_DESCRIPTION, 39 | config.skipDescription, 40 | defaults.skipDescription 41 | ), 42 | skipBreaking: getEnvOrConfig( 43 | process.env.CZ_SKIP_BREAKING, 44 | config.skipBreaking, 45 | defaults.skipBreaking 46 | ), 47 | customScope: getEnvOrConfig( 48 | process.env.CZ_CUSTOM_SCOPE, 49 | config.customScope, 50 | defaults.customScope 51 | ), 52 | defaultType: process.env.CZ_TYPE || config.defaultType, 53 | defaultScope: process.env.CZ_SCOPE || config.defaultScope, 54 | defaultSubject: process.env.CZ_SUBJECT || config.defaultSubject, 55 | defaultBody: process.env.CZ_BODY || config.defaultBody, 56 | defaultIssues: process.env.CZ_ISSUES || config.defaultIssues, 57 | maxHeaderWidth: 58 | (process.env.CZ_MAX_HEADER_WIDTH && 59 | parseInt(process.env.CZ_MAX_HEADER_WIDTH)) || 60 | config.maxHeaderWidth || 61 | defaults.maxHeaderWidth, 62 | minHeaderWidth: 63 | (process.env.CZ_MIN_HEADER_WIDTH && 64 | parseInt(process.env.CZ_MIN_HEADER_WIDTH)) || 65 | config.minHeaderWidth || 66 | defaults.minHeaderWidth, 67 | maxLineWidth: 68 | (process.env.CZ_MAX_LINE_WIDTH && 69 | parseInt(process.env.CZ_MAX_LINE_WIDTH)) || 70 | config.maxLineWidth || 71 | defaults.maxLineWidth, 72 | jiraOptional: getEnvOrConfig( 73 | process.env.CZ_JIRA_OPTIONAL, 74 | config.jiraOptional, 75 | defaults.jiraOptional 76 | ), 77 | jiraPrefix: 78 | process.env.CZ_JIRA_PREFIX || config.jiraPrefix || defaults.jiraPrefix, 79 | jiraLocation: 80 | process.env.CZ_JIRA_LOCATION || 81 | config.jiraLocation || 82 | defaults.jiraLocation, 83 | jiraPrepend: 84 | process.env.CZ_JIRA_PREPEND || config.jiraPrepend || defaults.jiraPrepend, 85 | jiraAppend: 86 | process.env.CZ_JIRA_APPEND || config.jiraAppend || defaults.jiraAppend, 87 | exclamationMark: getEnvOrConfig( 88 | process.env.CZ_EXCLAMATION_MARK, 89 | config.exclamationMark, 90 | defaults.exclamationMark 91 | ) 92 | }; 93 | 94 | (function(options) { 95 | try { 96 | var commitlintLoad = require('@commitlint/load'); 97 | commitlintLoad().then(function(clConfig) { 98 | if (clConfig.rules) { 99 | var maxHeaderLengthRule = clConfig.rules['header-max-length']; 100 | if ( 101 | typeof maxHeaderLengthRule === 'object' && 102 | maxHeaderLengthRule.length >= 3 && 103 | !process.env.CZ_MAX_HEADER_WIDTH && 104 | !config.maxHeaderWidth 105 | ) { 106 | options.maxHeaderWidth = maxHeaderLengthRule[2]; 107 | } 108 | } 109 | }); 110 | } catch (err) {} 111 | })(options); 112 | 113 | module.exports = engine(options); 114 | -------------------------------------------------------------------------------- /engine.js: -------------------------------------------------------------------------------- 1 | 'format cjs'; 2 | 3 | var wrap = require('word-wrap'); 4 | var map = require('lodash.map'); 5 | var longest = require('longest'); 6 | var rightPad = require('right-pad'); 7 | var chalk = require('chalk'); 8 | const { execSync } = require('child_process'); 9 | const boxen = require('boxen'); 10 | 11 | var defaults = require('./defaults'); 12 | const LimitedInputPrompt = require('./LimitedInputPrompt'); 13 | var filter = function(array) { 14 | return array.filter(function(x) { 15 | return x; 16 | }); 17 | }; 18 | 19 | var filterSubject = function(subject) { 20 | subject = subject.trim(); 21 | while (subject.endsWith('.')) { 22 | subject = subject.slice(0, subject.length - 1); 23 | } 24 | return subject; 25 | }; 26 | 27 | // This can be any kind of SystemJS compatible module. 28 | // We use Commonjs here, but ES6 or AMD would do just 29 | // fine. 30 | module.exports = function(options) { 31 | var getFromOptionsOrDefaults = function(key) { 32 | return options[key] || defaults[key]; 33 | }; 34 | var getJiraIssueLocation = function( 35 | location, 36 | type = '', 37 | scope = '', 38 | jiraWithDecorators, 39 | subject 40 | ) { 41 | let headerPrefix = type + scope; 42 | if (headerPrefix !== '') { 43 | headerPrefix += ': '; 44 | } 45 | switch (location) { 46 | case 'pre-type': 47 | return jiraWithDecorators + headerPrefix + subject; 48 | break; 49 | case 'pre-description': 50 | return headerPrefix + jiraWithDecorators + subject; 51 | break; 52 | case 'post-description': 53 | return headerPrefix + subject + ' ' + jiraWithDecorators; 54 | break; 55 | case 'post-body': 56 | return headerPrefix + subject; 57 | break; 58 | default: 59 | return headerPrefix + jiraWithDecorators + subject; 60 | } 61 | }; 62 | 63 | // Generate Jira issue prepend and append decorators 64 | const decorateJiraIssue = function(jiraIssue, options) { 65 | const prepend = options.jiraPrepend || '' 66 | const append = options.jiraAppend || '' 67 | return jiraIssue ? `${prepend}${jiraIssue}${append} `: ''; 68 | } 69 | 70 | var types = getFromOptionsOrDefaults('types'); 71 | 72 | var length = longest(Object.keys(types)).length + 1; 73 | var choices = map(types, function(type, key) { 74 | return { 75 | name: rightPad(key + ':', length) + ' ' + type.description, 76 | value: key 77 | }; 78 | }); 79 | 80 | const minHeaderWidth = getFromOptionsOrDefaults('minHeaderWidth'); 81 | const maxHeaderWidth = getFromOptionsOrDefaults('maxHeaderWidth'); 82 | 83 | const branchName = execSync('git branch --show-current').toString().trim(); 84 | const jiraIssueRegex = /(?(? 0; 92 | const customScope = !options.skipScope && hasScopes && options.customScope; 93 | const scopes = customScope ? [...options.scopes, 'custom' ]: options.scopes; 94 | 95 | var getProvidedScope = function(answers) { 96 | return answers.scope === 'custom' ? answers.customScope : answers.scope; 97 | } 98 | 99 | return { 100 | // When a user runs `git cz`, prompter will 101 | // be executed. We pass you cz, which currently 102 | // is just an instance of inquirer.js. Using 103 | // this you can ask questions and get answers. 104 | // 105 | // The commit callback should be executed when 106 | // you're ready to send back a commit template 107 | // to git. 108 | // 109 | // By default, we'll de-indent your commit 110 | // template and will keep empty lines. 111 | prompter: function(cz, commit, testMode) { 112 | cz.registerPrompt('limitedInput', LimitedInputPrompt); 113 | 114 | // Let's ask some questions of the user 115 | // so that we can populate our commit 116 | // template. 117 | // 118 | // See inquirer.js docs for specifics. 119 | // You can also opt to use another input 120 | // collection library if you prefer. 121 | cz.prompt([ 122 | { 123 | type: 'list', 124 | name: 'type', 125 | when: !options.skipType, 126 | message: "Select the type of change that you're committing:", 127 | choices: choices, 128 | default: options.skipType ? '' : options.defaultType 129 | }, 130 | { 131 | type: 'input', 132 | name: 'jira', 133 | message: 134 | 'Enter JIRA issue (' + 135 | getFromOptionsOrDefaults('jiraPrefix') + 136 | '-12345)' + 137 | (options.jiraOptional ? ' (optional)' : '') + 138 | ':', 139 | when: options.jiraMode, 140 | default: jiraIssue || '', 141 | validate: function(jira) { 142 | return ( 143 | (options.jiraOptional && !jira) || 144 | /^(? scope === 'custom'), 168 | message: 'Type custom scope (press enter to skip)' 169 | }, 170 | { 171 | type: 'limitedInput', 172 | name: 'subject', 173 | message: 'Write a short, imperative tense description of the change:', 174 | default: options.defaultSubject, 175 | maxLength: maxHeaderWidth - (options.exclamationMark ? 1 : 0), 176 | leadingLabel: answers => { 177 | let scope = ''; 178 | const providedScope = getProvidedScope(answers); 179 | if (providedScope && providedScope !== 'none') { 180 | scope = `(${providedScope})`; 181 | } 182 | 183 | const jiraWithDecorators = decorateJiraIssue(answers.jira, options); 184 | return getJiraIssueLocation(options.jiraLocation, answers.type, scope, jiraWithDecorators, '').trim(); 185 | }, 186 | validate: input => 187 | input.length >= minHeaderWidth || 188 | `The subject must have at least ${minHeaderWidth} characters`, 189 | filter: function(subject) { 190 | return filterSubject(subject); 191 | } 192 | }, 193 | { 194 | type: 'input', 195 | name: 'body', 196 | when: !options.skipDescription, 197 | message: 198 | 'Provide a longer description of the change: (press enter to skip)\n', 199 | default: options.defaultBody 200 | }, 201 | { 202 | type: 'confirm', 203 | name: 'isBreaking', 204 | when: !options.skipBreaking, 205 | message: 'Are there any breaking changes?', 206 | default: false 207 | }, 208 | { 209 | type: 'confirm', 210 | name: 'isBreaking', 211 | message: 'You do know that this will bump the major version, are you sure?', 212 | default: false, 213 | when: function(answers) { 214 | return answers.isBreaking; 215 | } 216 | }, 217 | { 218 | type: 'input', 219 | name: 'breaking', 220 | message: 'Describe the breaking changes:\n', 221 | when: function(answers) { 222 | return answers.isBreaking; 223 | } 224 | }, 225 | 226 | { 227 | type: 'confirm', 228 | name: 'isIssueAffected', 229 | message: 'Does this change affect any open issues?', 230 | default: options.defaultIssues ? true : false, 231 | when: !options.jiraMode 232 | }, 233 | { 234 | type: 'input', 235 | name: 'issuesBody', 236 | default: '-', 237 | message: 238 | 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself:\n', 239 | when: function(answers) { 240 | return ( 241 | answers.isIssueAffected && !answers.body && !answers.breakingBody 242 | ); 243 | } 244 | }, 245 | { 246 | type: 'input', 247 | name: 'issues', 248 | message: 'Add issue references (e.g. "fix #123", "re #123".):\n', 249 | when: function(answers) { 250 | return answers.isIssueAffected; 251 | }, 252 | default: options.defaultIssues ? options.defaultIssues : undefined 253 | } 254 | ]).then(async function(answers) { 255 | var wrapOptions = { 256 | trim: true, 257 | cut: false, 258 | newline: '\n', 259 | indent: '', 260 | width: options.maxLineWidth 261 | }; 262 | 263 | // parentheses are only needed when a scope is present 264 | const providedScope = getProvidedScope(answers); 265 | var scope = providedScope ? '(' + providedScope + ')' : ''; 266 | 267 | const addExclamationMark = options.exclamationMark && answers.breaking; 268 | scope = addExclamationMark ? scope + '!' : scope; 269 | 270 | // Get Jira issue prepend and append decorators 271 | const jiraWithDecorators = decorateJiraIssue(answers.jira, options); 272 | 273 | // Hard limit this line in the validate 274 | const head = getJiraIssueLocation(options.jiraLocation, answers.type, scope, jiraWithDecorators, answers.subject); 275 | 276 | // Wrap these lines at options.maxLineWidth characters 277 | var body = answers.body ? wrap(answers.body, wrapOptions) : false; 278 | if (options.jiraMode && options.jiraLocation === 'post-body') { 279 | if (body === false) { 280 | body = ''; 281 | } else { 282 | body += "\n\n"; 283 | } 284 | body += jiraWithDecorators.trim(); 285 | } 286 | 287 | // Apply breaking change prefix, removing it if already present 288 | var breaking = answers.breaking ? answers.breaking.trim() : ''; 289 | breaking = breaking 290 | ? 'BREAKING CHANGE: ' + breaking.replace(/^BREAKING CHANGE: /, '') 291 | : ''; 292 | breaking = breaking ? wrap(breaking, wrapOptions) : false; 293 | 294 | var issues = answers.issues ? wrap(answers.issues, wrapOptions) : false; 295 | 296 | const fullCommit = filter([head, body, breaking, issues]).join('\n\n'); 297 | 298 | if (testMode) { 299 | return commit(fullCommit); 300 | } 301 | 302 | console.log(); 303 | console.log(chalk.underline('Commit preview:')); 304 | console.log(boxen(chalk.green(fullCommit), { padding: 1, margin: 1 })); 305 | 306 | const { doCommit } = await cz.prompt([ 307 | { 308 | type: 'confirm', 309 | name: 'doCommit', 310 | message: 'Are you sure that you want to commit?' 311 | } 312 | ]); 313 | 314 | if (doCommit) { 315 | commit(fullCommit); 316 | } 317 | }); 318 | } 319 | }; 320 | }; 321 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cz-conventional-changelog-for-jira 2 | 3 | Part of the [commitizen/cz-cli](https://github.com/commitizen/cz-cli) family. Prompts for [conventional changelog](https://github.com/conventional-changelog/conventional-changelog) standard and also prompts for a mandatory JIRA issue. 4 | 5 | [![npm version](https://img.shields.io/npm/v/@digitalroute/cz-conventional-changelog-for-jira.svg?style=flat-square)](https://www.npmjs.org/package/@digitalroute/cz-conventional-changelog-for-jira) 6 | [![npm downloads](https://img.shields.io/npm/dm/@digitalroute/cz-conventional-changelog-for-jira.svg?style=flat-square)](http://npm-stat.com/charts.html?package=@digitalroute/cz-conventional-changelog-for-jira) 7 | [![Build Status](https://img.shields.io/travis/digitalroute/cz-conventional-changelog-for-jira.svg?style=flat-square)](https://travis-ci.org/digitalroute/cz-conventional-changelog-for-jira) 8 | 9 | ## Features 10 | 11 | - Works seamlessly with semantic-release 🚀 12 | - Works seamlessly with Jira smart commits 13 | - Automatically detects the Jira issue from the branch name 14 | 15 | ## Quickstart 16 | 17 | ### Installation 18 | 19 | ```bash 20 | npm install commitizen @digitalroute/cz-conventional-changelog-for-jira 21 | ``` 22 | 23 | and then add the following to package.json: 24 | 25 | ```json 26 | { 27 | "scripts": { 28 | "commit": "git-cz" 29 | }, 30 | "config": { 31 | "commitizen": { 32 | "path": "./node_modules/@digitalroute/cz-conventional-changelog-for-jira" 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### Usage 39 | 40 | ![Gif of terminal when using cz-conventional-changelog-for-jira](https://raw.githubusercontent.com/digitalroute/cz-conventional-changelog-for-jira/master/images/demo.gif) 41 | 42 | ## Configuration 43 | 44 | Like commitizen, you can specify the configuration of cz-conventional-changelog-for-jira through the package.json's `config.commitizen` key, or with environment variables. 45 | 46 | | Environment variable | package.json | Default | Description | 47 | | -------------------- | --------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 48 | | CZ_JIRA_MODE | jiraMode | true | If this is set to true, CZ will ask for a Jira issue and put it in the commit head. If set to false CZ will ask for the issue in the end, and can be used for GitHub. | 49 | | CZ_MAX_HEADER_WIDTH | maxHeaderWidth | 72 | This limits how long a commit message head can be. | 50 | | CZ_MIN_HEADER_WIDTH | minHeaderWidth | 2 | This limits how short a commit message can be. | 51 | | CZ_MAX_LINE_WIDTH | maxLineWidth | 100 | Commit message bodies are automatically wrapped. This decides how long the lines will be. | 52 | | CZ_SKIP_SCOPE | skipScope | true | If scope should be used in commit messages. | 53 | | CZ_SKIP_TYPE | skipType | false | If type should be used in commit messages. | 54 | | CZ_SKIP_DESCRIPTION | skipDescription | false | If description should be used in commit messages. | 55 | | CZ_SKIP_BREAKING | skipBreaking | false | If breaking changes should be used in commit messages. | 56 | | | scopes | undefined | A list (JS Array) of scopes that will be available for selection. Note that adding this will change the scope field from Inquirer 'input' to 'list'. | 57 | | CZ_TYPE | defaultType | undefined | The default type. | 58 | | CZ_SCOPE | defaultScope | undefined | The default scope. | 59 | | CZ_CUSTOM_SCOPE | customScope | false | Whether user can provide custom scope in addition to predefined ones | 60 | | CZ_SUBJECT | defaultSubject | undefined | A default subject. | 61 | | CZ_BODY | defaultBody | undefined | A default body. | 62 | | CZ_ISSUES | defaultIssues | undefined | A default issue. | 63 | | CZ_JIRA_OPTIONAL | jiraOptional | false | If this is set to true, you can leave the JIRA field blank. | 64 | | CZ_JIRA_PREFIX | jiraPrefix | "DAZ" | If this is set it will be displayed as the default JIRA ticket prefix | 65 | | CZ_JIRA_LOCATION | jiraLocation | "pre-description" | Changes position of JIRA ID. Options: `pre-type`, `pre-description`, `post-description`, `post-body` | 66 | | CZ_JIRA_PREPEND | jiraPrepend | "" | Prepends JIRA ID with an optional decorator. e.g.: `[DAZ-1234` | 67 | | CZ_JIRA_APPEND | jiraAppend | "" | Appends JIRA ID with an optional decorator. e.g.: `DAZ-1234]` | 68 | | CZ_EXCLAMATION_MARK | exclamationMark | false | On breaking changes, adds an exclamation mark (!) after the scope, e.g.: `type(scope)!: break stuff`. When activated, reduces the effective allowed header length by 1. | 69 | 70 | ### Jira Location Options 71 | 72 | pre-type: 73 | 74 | ```text 75 | JIRA-1234 type(scope): commit subject 76 | ``` 77 | 78 | pre-description: 79 | 80 | ```text 81 | type(scope): JIRA-1234 commit subject 82 | ``` 83 | 84 | post-description: 85 | 86 | ```text 87 | type(scope): commit subject JIRA-1234 88 | ``` 89 | 90 | post-body: 91 | 92 | ```text 93 | type(scope): commit subject 94 | 95 | JIRA-1234 96 | ``` 97 | 98 | ```text 99 | type(scope): commit subject 100 | 101 | this is a commit body 102 | 103 | JIRA-1234 104 | ``` 105 | 106 | ## Dynamic Configuration 107 | 108 | Alternatively, if you want to create your own profile, you can use the _configurable_ approach. 109 | Here is an example: 110 | **./index.js** 111 | 112 | ```javascript 113 | const custom = require('@digitalroute/cz-conventional-changelog-for-jira/configurable'); 114 | // You can do this optionally if you want to extend the commit types 115 | const defaultTypes = require('@digitalroute/cz-conventional-changelog-for-jira/types'); 116 | 117 | module.exports = custom({ 118 | types: { 119 | ...defaultTypes, 120 | perf: { 121 | description: 'Improvements that will make your code perform better', 122 | title: 'Performance' 123 | } 124 | }, 125 | skipScope: false, 126 | scopes: ['myScope1', 'myScope2'], 127 | customScope: true 128 | }); 129 | ``` 130 | 131 | **./package.json** 132 | 133 | ```json 134 | { 135 | "config": { 136 | "commitizen": { 137 | "path": "./index.js" 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | This example would: 144 | 145 | - Display _"perf"_ as an extra commit type 146 | - Ask you to add a commit scope 147 | - Limit the scope selection to either `myScope` or `myScope2` 148 | 149 | List of all supported configurable options when using the _configurable_ approach: 150 | 151 | | Key | Default | Description | 152 | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 153 | | jiraMode | true | If this is set to true, CZ will ask for a Jira issue and put it in the commit head. If set to false CZ will ask for the issue in the end, and can be used for GitHub. | 154 | | maxHeaderWidth | 72 | This limits how long a commit message head can be. | 155 | | minHeaderWidth | 2 | This limits how short a commit message can be. | 156 | | maxLineWidth | 100 | Commit message bodies are automatically wrapped. This decides how long the lines will be. | 157 | | skipScope | true | If scope should be used in commit messages. | 158 | | defaultType | undefined | The default type. | 159 | | defaultScope | undefined | The default scope. | 160 | | defaultSubject | undefined | A default subject. | 161 | | defaultBody | undefined | A default body. | 162 | | defaultIssues | undefined | A default issue. | 163 | | jiraPrefix | 'DAZ' | The default JIRA ticket prefix that will be displayed. | 164 | | types | ./types.js | A list (JS Object) of supported commit types. | 165 | | scopes | undefined | A list (JS Array) of scopes that will be available for selection. Note that adding this will change the scope field from Inquirer 'input' to 'list'. | 166 | | customScope | false | If this is set to true, users are given an option to provide custom scope aside the ones predefined in 'scopes' array. In this case a new option named 'custom' appears in the list and once chosen a prompt appears to provide custom scope | 167 | | jiraOptional | false | If this is set to true, you can leave the JIRA field blank. | 168 | | jiraLocation | "pre-description" | Changes position of JIRA ID. Options: `pre-type`, `pre-description`, `post-description`, `post-body` | 169 | | jiraPrepend | "" | Prepends JIRA ID with an optional decorator. e.g.: `[DAZ-1234` | 170 | | jiraAppend | "" | Appends JIRA ID with an optional decorator. e.g.: `DAZ-1234]` | 171 | | exclamationMark | false | On breaking changes, adds an exclamation mark (!) after the scope, e.g.: `type(scope)!: break stuff`. When activated, reduces the effective allowed header length by 1. | 172 | 173 | ### Commitlint 174 | 175 | If using the [commitlint](https://github.com/conventional-changelog/commitlint) js library, the "maxHeaderWidth" configuration property will default to the configuration of the "header-max-length" rule instead of the hard coded value of 72. This can be ovewritten by setting the 'maxHeaderWidth' configuration in package.json or the CZ_MAX_HEADER_WIDTH environment variable. 176 | -------------------------------------------------------------------------------- /engine.test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var chalk = require('chalk'); 3 | var engine = require('./engine'); 4 | var mock = require('mock-require'); 5 | var semver = require('semver'); 6 | 7 | var types = require('./types'); 8 | var defaults = require('./defaults'); 9 | 10 | var expect = chai.expect; 11 | chai.should(); 12 | 13 | var defaultOptions = defaults; 14 | const skipTypeOptions = { 15 | ...defaultOptions, 16 | skipType: true 17 | }; 18 | 19 | var type = 'func'; 20 | var scope = 'everything'; 21 | var customScope = 'custom scope'; 22 | var jira = 'dAz-123'; 23 | var jiraUpperCase = 'DAZ-123'; 24 | var subject = 'testing123'; 25 | const shortBody = 'a'; 26 | var longBody = 27 | 'a a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a' + 28 | 'a a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a' + 29 | 'a a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a aa a'; 30 | var longBodySplit = 31 | longBody.slice(0, defaultOptions.maxLineWidth).trim() + 32 | '\n' + 33 | longBody 34 | .slice(defaultOptions.maxLineWidth, 2 * defaultOptions.maxLineWidth) 35 | .trim() + 36 | '\n' + 37 | longBody.slice(defaultOptions.maxLineWidth * 2, longBody.length).trim(); 38 | var body = 'A quick brown fox jumps over the dog'; 39 | var issues = 'a issues is not a person that kicks things'; 40 | var longIssues = 41 | 'b b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b' + 42 | 'b b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b' + 43 | 'b b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b bb b'; 44 | var breakingChange = 'BREAKING CHANGE: '; 45 | var breaking = 'asdhdfkjhbakjdhjkashd adhfajkhs asdhkjdsh ahshd'; 46 | var longIssuesSplit = 47 | longIssues.slice(0, defaultOptions.maxLineWidth).trim() + 48 | '\n' + 49 | longIssues 50 | .slice(defaultOptions.maxLineWidth, defaultOptions.maxLineWidth * 2) 51 | .trim() + 52 | '\n' + 53 | longIssues.slice(defaultOptions.maxLineWidth * 2, longIssues.length).trim(); 54 | 55 | describe('commit message', function() { 56 | it('only header w/ out scope and w/ out type', function() { 57 | expect( 58 | commitMessage( 59 | { 60 | jira, 61 | subject 62 | }, 63 | skipTypeOptions 64 | ) 65 | ).to.equal(`${jiraUpperCase} ${subject}`); 66 | }); 67 | it('only header w/ out scope and w/ type', function() { 68 | expect( 69 | commitMessage({ 70 | type, 71 | jira, 72 | subject 73 | }) 74 | ).to.equal(`${type}: ${jiraUpperCase} ${subject}`); 75 | }); 76 | it('only header w/ scope and w/ type', function() { 77 | expect( 78 | commitMessage({ 79 | type, 80 | scope, 81 | jira, 82 | subject 83 | }) 84 | ).to.equal(`${type}(${scope}): ${jiraUpperCase} ${subject}`); 85 | }); 86 | it('only header w/ scope and w/ out type', function() { 87 | expect( 88 | commitMessage( 89 | { 90 | scope, 91 | jira, 92 | subject 93 | }, 94 | skipTypeOptions 95 | ) 96 | ).to.equal(`(${scope}): ${jiraUpperCase} ${subject}`); 97 | }); 98 | it('header and body w/ out scope and w/ type', function() { 99 | expect( 100 | commitMessage({ 101 | type, 102 | jira, 103 | subject, 104 | body 105 | }) 106 | ).to.equal(`${type}: ${jiraUpperCase} ${subject}\n\n${body}`); 107 | }); 108 | it('header and body w/ out scope and w/ out type', function() { 109 | expect( 110 | commitMessage( 111 | { 112 | jira, 113 | subject, 114 | body 115 | }, 116 | skipTypeOptions 117 | ) 118 | ).to.equal(`${jiraUpperCase} ${subject}\n\n${body}`); 119 | }); 120 | it('header and body w/ scope and w/ type', function() { 121 | expect( 122 | commitMessage({ 123 | type, 124 | scope, 125 | jira, 126 | subject, 127 | body 128 | }) 129 | ).to.equal(`${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${body}`); 130 | }); 131 | it('header and body w/ scope and w/ out type', function() { 132 | expect( 133 | commitMessage( 134 | { 135 | scope, 136 | jira, 137 | subject, 138 | body 139 | }, 140 | skipTypeOptions 141 | ) 142 | ).to.equal(`(${scope}): ${jiraUpperCase} ${subject}\n\n${body}`); 143 | }); 144 | it('header and body w/ custom scope', function() { 145 | expect( 146 | commitMessage({ 147 | type, 148 | scope: 'custom', 149 | customScope, 150 | jira, 151 | subject, 152 | body 153 | }) 154 | ).to.equal( 155 | `${type}(${customScope}): ${jiraUpperCase} ${subject}\n\n${body}` 156 | ); 157 | }); 158 | it('header, body and issues w/ out scope and w/ out type', function() { 159 | expect( 160 | commitMessage( 161 | { 162 | jira, 163 | subject, 164 | body, 165 | issues 166 | }, 167 | skipTypeOptions 168 | ) 169 | ).to.equal(`${jiraUpperCase} ${subject}\n\n${body}\n\n${issues}`); 170 | }); 171 | it('header, body and issues w/ out scope and w/ type', function() { 172 | expect( 173 | commitMessage({ 174 | type, 175 | jira, 176 | subject, 177 | body, 178 | issues 179 | }) 180 | ).to.equal(`${type}: ${jiraUpperCase} ${subject}\n\n${body}\n\n${issues}`); 181 | }); 182 | it('header, body and issues w/ scope and w/ out type', function() { 183 | expect( 184 | commitMessage( 185 | { 186 | scope, 187 | jira, 188 | subject, 189 | body, 190 | issues 191 | }, 192 | skipTypeOptions 193 | ) 194 | ).to.equal( 195 | `(${scope}): ${jiraUpperCase} ${subject}\n\n${body}\n\n${issues}` 196 | ); 197 | }); 198 | it('header, body and issues w/ scope and w/ type', function() { 199 | expect( 200 | commitMessage({ 201 | type, 202 | scope, 203 | jira, 204 | subject, 205 | body, 206 | issues 207 | }) 208 | ).to.equal( 209 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${body}\n\n${issues}` 210 | ); 211 | }); 212 | it('header, body and long issues w/ out scope and w/ out type', function() { 213 | expect( 214 | commitMessage( 215 | { 216 | jira, 217 | subject, 218 | body, 219 | issues: longIssues 220 | }, 221 | skipTypeOptions 222 | ) 223 | ).to.equal(`${jiraUpperCase} ${subject}\n\n${body}\n\n${longIssuesSplit}`); 224 | }); 225 | it('header, body and long issues w/ out scope and w/ type', function() { 226 | expect( 227 | commitMessage({ 228 | type, 229 | jira, 230 | subject, 231 | body, 232 | issues: longIssues 233 | }) 234 | ).to.equal( 235 | `${type}: ${jiraUpperCase} ${subject}\n\n${body}\n\n${longIssuesSplit}` 236 | ); 237 | }); 238 | it('header, body and long issues w/ scope and w/ out type', function() { 239 | expect( 240 | commitMessage( 241 | { 242 | scope, 243 | jira, 244 | subject, 245 | body, 246 | issues: longIssues 247 | }, 248 | skipTypeOptions 249 | ) 250 | ).to.equal( 251 | `(${scope}): ${jiraUpperCase} ${subject}\n\n${body}\n\n${longIssuesSplit}` 252 | ); 253 | }); 254 | it('header, body and long issues w/ scope and w/ tyoe', function() { 255 | expect( 256 | commitMessage({ 257 | type, 258 | scope, 259 | jira, 260 | subject, 261 | body, 262 | issues: longIssues 263 | }) 264 | ).to.equal( 265 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${body}\n\n${longIssuesSplit}` 266 | ); 267 | }); 268 | it('header and long body w/ out scope and w/ out type', function() { 269 | expect( 270 | commitMessage( 271 | { 272 | jira, 273 | subject, 274 | body: longBody 275 | }, 276 | skipTypeOptions 277 | ) 278 | ).to.equal(`${jiraUpperCase} ${subject}\n\n${longBodySplit}`); 279 | }); 280 | it('header and long body w/ out scope and w/ type', function() { 281 | expect( 282 | commitMessage({ 283 | type, 284 | jira, 285 | subject, 286 | body: longBody 287 | }) 288 | ).to.equal(`${type}: ${jiraUpperCase} ${subject}\n\n${longBodySplit}`); 289 | }); 290 | it('header and long body w/ scope and w/ out type', function() { 291 | expect( 292 | commitMessage( 293 | { 294 | scope, 295 | jira, 296 | subject, 297 | body: longBody 298 | }, 299 | skipTypeOptions 300 | ) 301 | ).to.equal(`(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}`); 302 | }); 303 | it('header and long body w/ scope and w/ type', function() { 304 | expect( 305 | commitMessage({ 306 | type, 307 | scope, 308 | jira, 309 | subject, 310 | body: longBody 311 | }) 312 | ).to.equal( 313 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}` 314 | ); 315 | }); 316 | it('header, long body and issues w/ out scope and w/ out type', function() { 317 | expect( 318 | commitMessage( 319 | { 320 | jira, 321 | subject, 322 | body: longBody, 323 | issues 324 | }, 325 | skipTypeOptions 326 | ) 327 | ).to.equal(`${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${issues}`); 328 | }); 329 | it('header, long body and issues w/ out scope and w/ type', function() { 330 | expect( 331 | commitMessage({ 332 | type, 333 | jira, 334 | subject, 335 | body: longBody, 336 | issues 337 | }) 338 | ).to.equal( 339 | `${type}: ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${issues}` 340 | ); 341 | }); 342 | it('header, long body and issues w/ scope and w/ out type', function() { 343 | expect( 344 | commitMessage( 345 | { 346 | scope, 347 | jira, 348 | subject, 349 | body: longBody, 350 | issues 351 | }, 352 | skipTypeOptions 353 | ) 354 | ).to.equal( 355 | `(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${issues}` 356 | ); 357 | }); 358 | it('header, long body and issues w/ scope and w/ type', function() { 359 | expect( 360 | commitMessage({ 361 | type, 362 | scope, 363 | jira, 364 | subject, 365 | body: longBody, 366 | issues 367 | }) 368 | ).to.equal( 369 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${issues}` 370 | ); 371 | }); 372 | it('header, long body and long issues w/ out scope and w/ out type', function() { 373 | expect( 374 | commitMessage( 375 | { 376 | jira, 377 | subject, 378 | body: longBody, 379 | issues: longIssues 380 | }, 381 | skipTypeOptions 382 | ) 383 | ).to.equal( 384 | `${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${longIssuesSplit}` 385 | ); 386 | }); 387 | it('header, long body and long issues w/ out scope and w/ type', function() { 388 | expect( 389 | commitMessage({ 390 | type, 391 | jira, 392 | subject, 393 | body: longBody, 394 | issues: longIssues 395 | }) 396 | ).to.equal( 397 | `${type}: ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${longIssuesSplit}` 398 | ); 399 | }); 400 | it('header, long body and long issues w/ scope and w/ out type', function() { 401 | expect( 402 | commitMessage( 403 | { 404 | scope, 405 | jira, 406 | subject, 407 | body: longBody, 408 | issues: longIssues 409 | }, 410 | skipTypeOptions 411 | ) 412 | ).to.equal( 413 | `(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${longIssuesSplit}` 414 | ); 415 | }); 416 | it('header, long body and long issues w/ scope and w/ type', function() { 417 | expect( 418 | commitMessage({ 419 | type, 420 | scope, 421 | jira, 422 | subject, 423 | body: longBody, 424 | issues: longIssues 425 | }) 426 | ).to.equal( 427 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${longIssuesSplit}` 428 | ); 429 | }); 430 | it('header, long body, breaking change, and long issues w/ scope', function() { 431 | expect( 432 | commitMessage({ 433 | scope, 434 | jira, 435 | subject, 436 | body: longBody, 437 | breaking, 438 | issues: longIssues 439 | }) 440 | ).to.equal( 441 | `(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${breakingChange}${breaking}\n\n${longIssuesSplit}` 442 | ); 443 | }); 444 | it('header, long body, breaking change, and long issues w/ scope and w/ type', function() { 445 | expect( 446 | commitMessage({ 447 | type, 448 | scope, 449 | jira, 450 | subject, 451 | body: longBody, 452 | breaking, 453 | issues: longIssues 454 | }) 455 | ).to.equal( 456 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${breakingChange}${breaking}\n\n${longIssuesSplit}` 457 | ); 458 | }); 459 | it('header, long body, breaking change (with prefix entered), and long issues w/ scope and w/ out type', function() { 460 | expect( 461 | commitMessage( 462 | { 463 | scope, 464 | jira, 465 | subject, 466 | body: longBody, 467 | breaking: `${breakingChange}${breaking}`, 468 | issues: longIssues 469 | }, 470 | skipTypeOptions 471 | ) 472 | ).to.equal( 473 | `(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${breakingChange}${breaking}\n\n${longIssuesSplit}` 474 | ); 475 | }); 476 | it('header, long body, breaking change (with prefix entered), and long issues w/ scope and w/ type', function() { 477 | expect( 478 | commitMessage({ 479 | type, 480 | scope, 481 | jira, 482 | subject, 483 | body: longBody, 484 | breaking: `${breakingChange}${breaking}`, 485 | issues: longIssues 486 | }) 487 | ).to.equal( 488 | `${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${longBodySplit}\n\n${breakingChange}${breaking}\n\n${longIssuesSplit}` 489 | ); 490 | }); 491 | it('header, body, breaking change, and issues w/ scope and w/o type; exclamation mark enabled', function() { 492 | expect( 493 | commitMessage( 494 | { 495 | scope, 496 | jira, 497 | subject, 498 | body, 499 | breaking, 500 | issues 501 | }, 502 | { ...skipTypeOptions, exclamationMark: true } 503 | ) 504 | ).to.equal( 505 | `(${scope})!: ${jiraUpperCase} ${subject}\n\n${body}\n\n${breakingChange}${breaking}\n\n${issues}` 506 | ); 507 | }); 508 | it('header, body, breaking change, and issues w/ scope and w/ type; exclamation mark enabled', function() { 509 | expect( 510 | commitMessage( 511 | { 512 | type, 513 | scope, 514 | jira, 515 | subject, 516 | body, 517 | breaking, 518 | issues 519 | }, 520 | { ...defaultOptions, exclamationMark: true } 521 | ) 522 | ).to.equal( 523 | `${type}(${scope})!: ${jiraUpperCase} ${subject}\n\n${body}\n\n${breakingChange}${breaking}\n\n${issues}` 524 | ); 525 | }); 526 | it('header, body, breaking change, and issues w/o scope and w/o type; exclamation mark enabled', function() { 527 | expect( 528 | commitMessage( 529 | { 530 | jira, 531 | subject, 532 | body, 533 | breaking, 534 | issues 535 | }, 536 | { ...skipTypeOptions, exclamationMark: true } 537 | ) 538 | ).to.equal( 539 | `!: ${jiraUpperCase} ${subject}\n\n${body}\n\n${breakingChange}${breaking}\n\n${issues}` 540 | ); 541 | }); 542 | it('header, body, breaking change, and issues w/o scope and w/ type; exclamation mark enabled', function() { 543 | expect( 544 | commitMessage( 545 | { 546 | type, 547 | jira, 548 | subject, 549 | body, 550 | breaking, 551 | issues 552 | }, 553 | { ...defaultOptions, exclamationMark: true } 554 | ) 555 | ).to.equal( 556 | `${type}!: ${jiraUpperCase} ${subject}\n\n${body}\n\n${breakingChange}${breaking}\n\n${issues}` 557 | ); 558 | }); 559 | it('skip jira task when optional', function() { 560 | expect( 561 | commitMessage( 562 | { 563 | type, 564 | scope, 565 | jira: '', 566 | subject 567 | }, 568 | { jiraOptional: true } 569 | ) 570 | ).to.equal(`${type}(${scope}): ${subject}`); 571 | }); 572 | it('default jiraLocation when unknown', function() { 573 | expect( 574 | commitMessage( 575 | { 576 | type, 577 | scope, 578 | jira, 579 | subject, 580 | body 581 | }, 582 | { jiraLocation: 'unknown-location' } 583 | ) 584 | ).to.equal(`${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${body}`); 585 | }); 586 | it('pre-type jiraLocation', function() { 587 | expect( 588 | commitMessage( 589 | { 590 | type, 591 | scope, 592 | jira, 593 | subject, 594 | body 595 | }, 596 | { jiraLocation: 'pre-type' } 597 | ) 598 | ).to.equal(`${jiraUpperCase} ${type}(${scope}): ${subject}\n\n${body}`); 599 | }); 600 | it('pre-description jiraLocation', function() { 601 | expect( 602 | commitMessage( 603 | { 604 | type, 605 | scope, 606 | jira, 607 | subject, 608 | body 609 | }, 610 | { jiraLocation: 'pre-description' } 611 | ) 612 | ).to.equal(`${type}(${scope}): ${jiraUpperCase} ${subject}\n\n${body}`); 613 | }); 614 | it('post-description jiraLocation', function() { 615 | expect( 616 | commitMessage( 617 | { 618 | type, 619 | scope, 620 | jira, 621 | subject, 622 | body 623 | }, 624 | { jiraLocation: 'post-description' } 625 | ) 626 | ).to.equal(`${type}(${scope}): ${subject} ${jiraUpperCase} \n\n${body}`); 627 | }); 628 | it('post-body jiraLocation with body', function() { 629 | expect( 630 | commitMessage( 631 | { 632 | type, 633 | scope, 634 | jira, 635 | subject, 636 | body 637 | }, 638 | { ...defaultOptions, jiraLocation: 'post-body' } 639 | ) 640 | ).to.equal(`${type}(${scope}): ${subject}\n\n${body}\n\n${jiraUpperCase}`); 641 | }); 642 | it('post-body jiraLocation no body', function() { 643 | expect( 644 | commitMessage( 645 | { 646 | type, 647 | scope, 648 | jira, 649 | subject, 650 | body: false 651 | }, 652 | { ...defaultOptions, jiraLocation: 'post-body' } 653 | ) 654 | ).to.equal(`${type}(${scope}): ${subject}\n\n${jiraUpperCase}`); 655 | }); 656 | it('post-body jiraLocation with body and footer', function() { 657 | var footer = `${breakingChange}${breaking}`; 658 | expect( 659 | commitMessage( 660 | { 661 | type, 662 | scope, 663 | jira, 664 | subject, 665 | body, 666 | breaking 667 | }, 668 | { ...defaultOptions, jiraLocation: 'post-body' } 669 | ) 670 | ).to.equal( 671 | `${type}(${scope}): ${subject}\n\n${body}\n\n${jiraUpperCase}\n\n${breakingChange}${breaking}` 672 | ); 673 | }); 674 | it('jiraPrepend decorator', function() { 675 | expect( 676 | commitMessage( 677 | { 678 | type, 679 | scope, 680 | jira, 681 | subject, 682 | body 683 | }, 684 | { jiraPrepend: '-' } 685 | ) 686 | ).to.equal(`${type}(${scope}): -${jiraUpperCase} ${subject}\n\n${body}`); 687 | }); 688 | it('jiraAppend decorator', function() { 689 | expect( 690 | commitMessage( 691 | { 692 | type, 693 | scope, 694 | jira, 695 | subject, 696 | body 697 | }, 698 | { jiraAppend: '+' } 699 | ) 700 | ).to.equal(`${type}(${scope}): ${jiraUpperCase}+ ${subject}\n\n${body}`); 701 | }); 702 | it('jiraPrepend and jiraAppend decorators', function() { 703 | expect( 704 | commitMessage( 705 | { 706 | type, 707 | scope, 708 | jira, 709 | subject, 710 | body 711 | }, 712 | { 713 | jiraAppend: ']', 714 | jiraPrepend: '[' 715 | } 716 | ) 717 | ).to.equal(`${type}(${scope}): [${jiraUpperCase}] ${subject}\n\n${body}`); 718 | }); 719 | it('jiraLocation, jiraPrepend, jiraAppend decorators', function() { 720 | expect( 721 | commitMessage( 722 | { 723 | type, 724 | scope, 725 | jira, 726 | subject, 727 | body 728 | }, 729 | { 730 | jiraAppend: ']', 731 | jiraPrepend: '[', 732 | jiraLocation: 'pre-type' 733 | } 734 | ) 735 | ).to.equal(`[${jiraUpperCase}] ${type}(${scope}): ${subject}\n\n${body}`); 736 | }); 737 | }); 738 | 739 | describe('validation', function() { 740 | it('subject exceeds max length', function() { 741 | expect(() => 742 | commitMessage({ 743 | type, 744 | scope, 745 | jira, 746 | subject: shortBody 747 | }) 748 | ).to.throw(`The subject must have at least 2 characters`); 749 | }); 750 | it('empty subject', function() { 751 | expect(() => 752 | commitMessage({ 753 | type, 754 | scope, 755 | subject: '' 756 | }) 757 | ).to.throw(`The subject must have at least 2 characters`); 758 | }); 759 | it('empty jira if not optional', function() { 760 | expect(() => 761 | commitMessage( 762 | { 763 | type, 764 | scope, 765 | jira: '', 766 | subject 767 | }, 768 | { jiraOptional: false } 769 | ) 770 | ).to.throw(`Answer '' to question 'jira' was invalid`); 771 | }); 772 | }); 773 | 774 | describe('defaults', function() { 775 | it('defaultType default', function() { 776 | expect(questionDefault('type')).to.be.undefined; 777 | }); 778 | it('defaultType options', function() { 779 | expect( 780 | questionDefault('type', customOptions({ defaultType: type })) 781 | ).to.equal(type); 782 | }); 783 | it('defaultScope default', function() { 784 | expect(questionDefault('scope')).to.be.undefined; 785 | }); 786 | it('defaultScope options', () => 787 | expect( 788 | questionDefault('scope', customOptions({ defaultScope: scope })) 789 | ).to.equal(scope)); 790 | 791 | it('defaultSubject default', () => 792 | expect(questionDefault('subject')).to.be.undefined); 793 | it('defaultSubject options', function() { 794 | expect( 795 | questionDefault( 796 | 'subject', 797 | customOptions({ 798 | defaultSubject: subject 799 | }) 800 | ) 801 | ).to.equal(subject); 802 | }); 803 | it('defaultBody default', function() { 804 | expect(questionDefault('body')).to.be.undefined; 805 | }); 806 | it('defaultBody options', function() { 807 | expect( 808 | questionDefault('body', customOptions({ defaultBody: body })) 809 | ).to.equal(body); 810 | }); 811 | it('defaultIssues default', function() { 812 | expect(questionDefault('issues')).to.be.undefined; 813 | }); 814 | it('defaultIssues options', function() { 815 | expect( 816 | questionDefault( 817 | 'issues', 818 | customOptions({ 819 | defaultIssues: issues 820 | }) 821 | ) 822 | ).to.equal(issues); 823 | }); 824 | }); 825 | 826 | describe('filter', function() { 827 | it('lowercase scope', () => 828 | expect(questionFilter('scope', 'HelloMatt')).to.equal('hellomatt')); 829 | }); 830 | 831 | describe('when', function() { 832 | it('breaking by default', () => 833 | expect(questionWhen('breaking', {})).to.be.undefined); 834 | it('breaking when isBreaking', () => 835 | expect( 836 | questionWhen('breaking', { 837 | isBreaking: true 838 | }) 839 | ).to.be.true); 840 | it('issues by default', () => 841 | expect(questionWhen('issues', {})).to.be.undefined); 842 | it('issues when isIssueAffected', () => 843 | expect( 844 | questionWhen('issues', { 845 | isIssueAffected: true 846 | }) 847 | ).to.be.true); 848 | }); 849 | 850 | describe('commitlint config header-max-length', function() { 851 | //commitlint config parser only supports Node 6.0.0 and higher 852 | if (semver.gte(process.version, '6.0.0')) { 853 | function mockOptions(headerMaxLength) { 854 | var options = undefined; 855 | mock('./engine', function(opts) { 856 | options = opts; 857 | }); 858 | if (headerMaxLength) { 859 | mock('cosmiconfig', function() { 860 | return { 861 | load: function(cwd) { 862 | return { 863 | filepath: cwd + '/.commitlintrc.js', 864 | config: { 865 | rules: { 866 | 'header-max-length': [2, 'always', headerMaxLength] 867 | } 868 | } 869 | }; 870 | } 871 | }; 872 | }); 873 | } 874 | 875 | mock.reRequire('./index'); 876 | try { 877 | return mock 878 | .reRequire('@commitlint/load')() 879 | .then(function() { 880 | return options; 881 | }); 882 | } catch (err) { 883 | return Promise.resolve(options); 884 | } 885 | } 886 | 887 | afterEach(function() { 888 | delete require.cache[require.resolve('./index')]; 889 | delete require.cache[require.resolve('@commitlint/load')]; 890 | delete process.env.CZ_MAX_HEADER_WIDTH; 891 | mock.stopAll(); 892 | }); 893 | 894 | it('with no environment or commitizen config override', function() { 895 | return mockOptions(72).then(function(options) { 896 | expect(options).to.have.property('maxHeaderWidth', 72); 897 | }); 898 | }); 899 | 900 | it('with environment variable override', function() { 901 | process.env.CZ_MAX_HEADER_WIDTH = '105'; 902 | return mockOptions(72).then(function(options) { 903 | expect(options).to.have.property('maxHeaderWidth', 105); 904 | }); 905 | }); 906 | 907 | it('with commitizen config override', function() { 908 | mock('commitizen', { 909 | configLoader: { 910 | load: function() { 911 | return { 912 | maxHeaderWidth: 103 913 | }; 914 | } 915 | } 916 | }); 917 | return mockOptions(72).then(function(options) { 918 | expect(options).to.have.property('maxHeaderWidth', 103); 919 | }); 920 | }); 921 | } else { 922 | //Node 4 doesn't support commitlint so the config value should remain the same 923 | it('default value for Node 4', function() { 924 | return mockOptions(72).then(function(options) { 925 | expect(options).to.have.property('maxHeaderWidth', 100); 926 | }); 927 | }); 928 | } 929 | }); 930 | 931 | describe('questions', function() { 932 | it('default jira question', function() { 933 | expect(questionPrompt('jira')).to.be.eq('Enter JIRA issue (DAZ-12345):'); 934 | }); 935 | it('optional jira question', function() { 936 | expect(questionPrompt('jira', [], { jiraOptional: true })).to.be.eq( 937 | 'Enter JIRA issue (DAZ-12345) (optional):' 938 | ); 939 | }); 940 | it('scope with list', function() { 941 | expect( 942 | questionPrompt('scope', [], { scopes: ['scope1', 'scope2'] }) 943 | ).to.be.eq( 944 | 'What is the scope of this change (e.g. component or file name): (select from the list)' 945 | ); 946 | }); 947 | it('scope without list', function() { 948 | expect(questionPrompt('scope')).to.be.eq( 949 | 'What is the scope of this change (e.g. component or file name): (press enter to skip)' 950 | ); 951 | }); 952 | }); 953 | 954 | function commitMessage(answers, options) { 955 | options = options || defaultOptions; 956 | var result = null; 957 | engine(options).prompter( 958 | { 959 | prompt: function(questions) { 960 | return { 961 | then: function(finalizer) { 962 | processQuestions(questions, answers, options); 963 | finalizer(answers); 964 | } 965 | }; 966 | }, 967 | registerPrompt: () => {} 968 | }, 969 | function(message) { 970 | result = message; 971 | }, 972 | true 973 | ); 974 | return result; 975 | } 976 | 977 | function processQuestions(questions, answers, options) { 978 | for (var i in questions) { 979 | var question = questions[i]; 980 | 981 | var answer = answers[question.name]; 982 | var validation = 983 | answer === undefined || !question.validate 984 | ? true 985 | : question.validate(answer, answers); 986 | if (validation !== true) { 987 | throw new Error( 988 | validation || 989 | `Answer '${answer}' to question '${question.name}' was invalid` 990 | ); 991 | } 992 | if (question.filter && answer) { 993 | answers[question.name] = question.filter(answer); 994 | } 995 | } 996 | } 997 | 998 | function getQuestions(options) { 999 | options = options || defaultOptions; 1000 | var result = null; 1001 | engine(options).prompter({ 1002 | prompt: function(questions) { 1003 | result = questions; 1004 | return { 1005 | then: function() {} 1006 | }; 1007 | }, 1008 | registerPrompt: () => {} 1009 | }); 1010 | return result; 1011 | } 1012 | 1013 | function getQuestion(name, options) { 1014 | options = options || defaultOptions; 1015 | var questions = getQuestions(options); 1016 | for (var i in questions) { 1017 | if (questions[i].name === name) { 1018 | return questions[i]; 1019 | } 1020 | } 1021 | return false; 1022 | } 1023 | 1024 | function questionPrompt(name, answers, options) { 1025 | options = options || defaultOptions; 1026 | var question = getQuestion(name, options); 1027 | return question.message && typeof question.message === 'string' 1028 | ? question.message 1029 | : question.message(answers); 1030 | } 1031 | 1032 | function questionTransformation(name, answers, options) { 1033 | options = options || defaultOptions; 1034 | var question = getQuestion(name, options); 1035 | return ( 1036 | question.transformer && 1037 | question.transformer(answers[name], answers, options) 1038 | ); 1039 | } 1040 | 1041 | function questionFilter(name, answer, options) { 1042 | options = options || defaultOptions; 1043 | var question = getQuestion(name, options); 1044 | return ( 1045 | question.filter && 1046 | question.filter(typeof answer === 'string' ? answer : answer[name]) 1047 | ); 1048 | } 1049 | 1050 | function questionDefault(name, options) { 1051 | options = options || defaultOptions; 1052 | var question = getQuestion(name, options); 1053 | return question.default; 1054 | } 1055 | 1056 | function questionWhen(name, answers, options) { 1057 | options = options || defaultOptions; 1058 | var question = getQuestion(name, options); 1059 | return question.when(answers); 1060 | } 1061 | 1062 | function customOptions(options) { 1063 | Object.keys(defaultOptions).forEach(key => { 1064 | if (options[key] === undefined) { 1065 | options[key] = defaultOptions[key]; 1066 | } 1067 | }); 1068 | return options; 1069 | } 1070 | --------------------------------------------------------------------------------