├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── engine.js ├── engine.test.js ├── index.js ├── package-lock.json ├── package.json └── renovate.json /.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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | node: 15 | - 16 16 | - 18 17 | - 20 18 | - 22 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | 21 | steps: 22 | - name: 🛑 Cancel Previous Runs 23 | uses: styfle/cancel-workflow-action@0.12.1 24 | with: 25 | access_token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: ⬇️ Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: ⎔ Setup node ${{ matrix.node }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node }} 34 | cache: npm 35 | 36 | - name: 📥 Download deps 37 | run: npm ci 38 | 39 | - name: Test 40 | run: npm test 41 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "endOfLine": "lf", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cz-conventional-changelog 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/commitizen/cz-conventional-changelog.svg)](https://greenkeeper.io/) 4 | 5 | Status: 6 | [![npm version](https://img.shields.io/npm/v/cz-conventional-changelog.svg?style=flat-square)](https://www.npmjs.org/package/cz-conventional-changelog) 7 | [![npm downloads](https://img.shields.io/npm/dm/cz-conventional-changelog.svg?style=flat-square)](http://npm-stat.com/charts.html?package=cz-conventional-changelog&from=2015-08-01) 8 | [![Build Status](https://img.shields.io/travis/commitizen/cz-conventional-changelog.svg?style=flat-square)](https://travis-ci.org/commitizen/cz-conventional-changelog) 9 | 10 | Part of the [commitizen](https://github.com/commitizen/cz-cli) family. Prompts for [conventional changelog](https://github.com/conventional-changelog/conventional-changelog) standard. 11 | 12 | ## Configuration 13 | 14 | ### package.json 15 | 16 | Like commitizen, you specify the configuration of cz-conventional-changelog through the package.json's `config.commitizen` key. 17 | 18 | ```json5 19 | { 20 | // ... default values 21 | "config": { 22 | "commitizen": { 23 | "path": "./node_modules/cz-conventional-changelog", 24 | "disableScopeLowerCase": false, 25 | "disableSubjectLowerCase": false, 26 | "maxHeaderWidth": 100, 27 | "maxLineWidth": 100, 28 | "defaultType": "", 29 | "defaultScope": "", 30 | "defaultSubject": "", 31 | "defaultBody": "", 32 | "defaultIssues": "", 33 | "types": { 34 | ... 35 | "feat": { 36 | "description": "A new feature", 37 | "title": "Features" 38 | }, 39 | ... 40 | } 41 | } 42 | } 43 | // ... 44 | } 45 | ``` 46 | 47 | ### Environment variables 48 | 49 | The following environment variables can be used to override any default configuration or package.json based configuration. 50 | 51 | * CZ_TYPE = defaultType 52 | * CZ_SCOPE = defaultScope 53 | * CZ_SUBJECT = defaultSubject 54 | * CZ_BODY = defaultBody 55 | * CZ_MAX_HEADER_WIDTH = maxHeaderWidth 56 | * CZ_MAX_LINE_WIDTH = maxLineWidth 57 | 58 | ### Commitlint 59 | 60 | 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 100. This can be ovewritten by setting the 'maxHeaderWidth' configuration in package.json or the CZ_MAX_HEADER_WIDTH environment variable. 61 | -------------------------------------------------------------------------------- /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 chalk = require('chalk'); 7 | 8 | var filter = function(array) { 9 | return array.filter(function(x) { 10 | return x; 11 | }); 12 | }; 13 | 14 | var headerLength = function(answers) { 15 | return ( 16 | answers.type.length + 2 + (answers.scope ? answers.scope.length + 2 : 0) 17 | ); 18 | }; 19 | 20 | var maxSummaryLength = function(options, answers) { 21 | return options.maxHeaderWidth - headerLength(answers); 22 | }; 23 | 24 | var filterSubject = function(subject, disableSubjectLowerCase) { 25 | subject = subject.trim(); 26 | if (!disableSubjectLowerCase && subject.charAt(0).toLowerCase() !== subject.charAt(0)) { 27 | subject = 28 | subject.charAt(0).toLowerCase() + subject.slice(1, subject.length); 29 | } 30 | while (subject.endsWith('.')) { 31 | subject = subject.slice(0, subject.length - 1); 32 | } 33 | return subject; 34 | }; 35 | 36 | // This can be any kind of SystemJS compatible module. 37 | // We use Commonjs here, but ES6 or AMD would do just 38 | // fine. 39 | module.exports = function(options) { 40 | var types = options.types; 41 | 42 | var length = longest(Object.keys(types)).length + 1; 43 | var choices = map(types, function(type, key) { 44 | return { 45 | name: (key + ':').padEnd(length) + ' ' + type.description, 46 | value: key 47 | }; 48 | }); 49 | 50 | return { 51 | // When a user runs `git cz`, prompter will 52 | // be executed. We pass you cz, which currently 53 | // is just an instance of inquirer.js. Using 54 | // this you can ask questions and get answers. 55 | // 56 | // The commit callback should be executed when 57 | // you're ready to send back a commit template 58 | // to git. 59 | // 60 | // By default, we'll de-indent your commit 61 | // template and will keep empty lines. 62 | prompter: function(cz, commit) { 63 | // Let's ask some questions of the user 64 | // so that we can populate our commit 65 | // template. 66 | // 67 | // See inquirer.js docs for specifics. 68 | // You can also opt to use another input 69 | // collection library if you prefer. 70 | cz.prompt([ 71 | { 72 | type: 'list', 73 | name: 'type', 74 | message: "Select the type of change that you're committing:", 75 | choices: choices, 76 | default: options.defaultType 77 | }, 78 | { 79 | type: 'input', 80 | name: 'scope', 81 | message: 82 | 'What is the scope of this change (e.g. component or file name): (press enter to skip)', 83 | default: options.defaultScope, 84 | filter: function(value) { 85 | return options.disableScopeLowerCase 86 | ? value.trim() 87 | : value.trim().toLowerCase(); 88 | } 89 | }, 90 | { 91 | type: 'input', 92 | name: 'subject', 93 | message: function(answers) { 94 | return ( 95 | 'Write a short, imperative tense description of the change (max ' + 96 | maxSummaryLength(options, answers) + 97 | ' chars):\n' 98 | ); 99 | }, 100 | default: options.defaultSubject, 101 | validate: function(subject, answers) { 102 | var filteredSubject = filterSubject(subject, options.disableSubjectLowerCase); 103 | return filteredSubject.length == 0 104 | ? 'subject is required' 105 | : filteredSubject.length <= maxSummaryLength(options, answers) 106 | ? true 107 | : 'Subject length must be less than or equal to ' + 108 | maxSummaryLength(options, answers) + 109 | ' characters. Current length is ' + 110 | filteredSubject.length + 111 | ' characters.'; 112 | }, 113 | transformer: function(subject, answers) { 114 | var filteredSubject = filterSubject(subject, options.disableSubjectLowerCase); 115 | var color = 116 | filteredSubject.length <= maxSummaryLength(options, answers) 117 | ? chalk.green 118 | : chalk.red; 119 | return color('(' + filteredSubject.length + ') ' + subject); 120 | }, 121 | filter: function(subject) { 122 | return filterSubject(subject, options.disableSubjectLowerCase); 123 | } 124 | }, 125 | { 126 | type: 'input', 127 | name: 'body', 128 | message: 129 | 'Provide a longer description of the change: (press enter to skip)\n', 130 | default: options.defaultBody 131 | }, 132 | { 133 | type: 'confirm', 134 | name: 'isBreaking', 135 | message: 'Are there any breaking changes?', 136 | default: false 137 | }, 138 | { 139 | type: 'input', 140 | name: 'breakingBody', 141 | default: '-', 142 | message: 143 | 'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself:\n', 144 | when: function(answers) { 145 | return answers.isBreaking && !answers.body; 146 | }, 147 | validate: function(breakingBody, answers) { 148 | return ( 149 | breakingBody.trim().length > 0 || 150 | 'Body is required for BREAKING CHANGE' 151 | ); 152 | } 153 | }, 154 | { 155 | type: 'input', 156 | name: 'breaking', 157 | message: 'Describe the breaking changes:\n', 158 | when: function(answers) { 159 | return answers.isBreaking; 160 | } 161 | }, 162 | 163 | { 164 | type: 'confirm', 165 | name: 'isIssueAffected', 166 | message: 'Does this change affect any open issues?', 167 | default: options.defaultIssues ? true : false 168 | }, 169 | { 170 | type: 'input', 171 | name: 'issuesBody', 172 | default: '-', 173 | message: 174 | 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself:\n', 175 | when: function(answers) { 176 | return ( 177 | answers.isIssueAffected && !answers.body && !answers.breakingBody 178 | ); 179 | } 180 | }, 181 | { 182 | type: 'input', 183 | name: 'issues', 184 | message: 'Add issue references (e.g. "fix #123", "re #123".):\n', 185 | when: function(answers) { 186 | return answers.isIssueAffected; 187 | }, 188 | default: options.defaultIssues ? options.defaultIssues : undefined 189 | } 190 | ]).then(function(answers) { 191 | var wrapOptions = { 192 | trim: true, 193 | cut: false, 194 | newline: '\n', 195 | indent: '', 196 | width: options.maxLineWidth 197 | }; 198 | 199 | // parentheses are only needed when a scope is present 200 | var scope = answers.scope ? '(' + answers.scope + ')' : ''; 201 | 202 | // Hard limit this line in the validate 203 | var head = answers.type + scope + ': ' + answers.subject; 204 | 205 | // Wrap these lines at options.maxLineWidth characters 206 | var body = answers.body ? wrap(answers.body, wrapOptions) : false; 207 | 208 | // Apply breaking change prefix, removing it if already present 209 | var breaking = answers.breaking ? answers.breaking.trim() : ''; 210 | breaking = breaking 211 | ? 'BREAKING CHANGE: ' + breaking.replace(/^BREAKING CHANGE: /, '') 212 | : ''; 213 | breaking = breaking ? wrap(breaking, wrapOptions) : false; 214 | 215 | var issues = answers.issues ? wrap(answers.issues, wrapOptions) : false; 216 | 217 | commit(filter([head, body, breaking, issues]).join('\n\n')); 218 | }); 219 | } 220 | }; 221 | }; 222 | -------------------------------------------------------------------------------- /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('conventional-commit-types').types; 8 | 9 | var expect = chai.expect; 10 | chai.should(); 11 | 12 | var defaultOptions = { 13 | types, 14 | maxLineWidth: 100, 15 | maxHeaderWidth: 100 16 | }; 17 | 18 | var type = 'func'; 19 | var scope = 'everything'; 20 | var subject = 'testing123'; 21 | var longBody = 22 | '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' + 23 | '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' + 24 | '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'; 25 | var longBodySplit = 26 | longBody.slice(0, defaultOptions.maxLineWidth).trim() + 27 | '\n' + 28 | longBody 29 | .slice(defaultOptions.maxLineWidth, 2 * defaultOptions.maxLineWidth) 30 | .trim() + 31 | '\n' + 32 | longBody.slice(defaultOptions.maxLineWidth * 2, longBody.length).trim(); 33 | var body = 'A quick brown fox jumps over the dog'; 34 | var issues = 'a issues is not a person that kicks things'; 35 | var longIssues = 36 | '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' + 37 | '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' + 38 | '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'; 39 | var breakingChange = 'BREAKING CHANGE: '; 40 | var breaking = 'asdhdfkjhbakjdhjkashd adhfajkhs asdhkjdsh ahshd'; 41 | var longIssuesSplit = 42 | longIssues.slice(0, defaultOptions.maxLineWidth).trim() + 43 | '\n' + 44 | longIssues 45 | .slice(defaultOptions.maxLineWidth, defaultOptions.maxLineWidth * 2) 46 | .trim() + 47 | '\n' + 48 | longIssues.slice(defaultOptions.maxLineWidth * 2, longIssues.length).trim(); 49 | 50 | describe('commit message', function() { 51 | it('only header w/ out scope', function() { 52 | expect( 53 | commitMessage({ 54 | type, 55 | subject 56 | }) 57 | ).to.equal(`${type}: ${subject}`); 58 | }); 59 | it('only header w/ scope', function() { 60 | expect( 61 | commitMessage({ 62 | type, 63 | scope, 64 | subject 65 | }) 66 | ).to.equal(`${type}(${scope}): ${subject}`); 67 | }); 68 | it('header and body w/ out scope', function() { 69 | expect( 70 | commitMessage({ 71 | type, 72 | subject, 73 | body 74 | }) 75 | ).to.equal(`${type}: ${subject}\n\n${body}`); 76 | }); 77 | it('header and body w/ scope', function() { 78 | expect( 79 | commitMessage({ 80 | type, 81 | scope, 82 | subject, 83 | body 84 | }) 85 | ).to.equal(`${type}(${scope}): ${subject}\n\n${body}`); 86 | }); 87 | it('header and body w/ uppercase scope', function() { 88 | var upperCaseScope = scope.toLocaleUpperCase(); 89 | expect( 90 | commitMessage( 91 | { 92 | type, 93 | scope: upperCaseScope, 94 | subject, 95 | body 96 | }, 97 | { 98 | ...defaultOptions, 99 | disableScopeLowerCase: true 100 | } 101 | ) 102 | ).to.equal(`${type}(${upperCaseScope}): ${subject}\n\n${body}`); 103 | }); 104 | it('header and body w/ uppercase subject', function() { 105 | var upperCaseSubject = subject.toLocaleUpperCase(); 106 | expect( 107 | commitMessage( 108 | { 109 | type, 110 | scope, 111 | subject: upperCaseSubject, 112 | body 113 | }, 114 | { 115 | ...defaultOptions, 116 | disableSubjectLowerCase: true 117 | } 118 | ) 119 | ).to.equal(`${type}(${scope}): ${upperCaseSubject}\n\n${body}`); 120 | }); 121 | it('header, body and issues w/ out scope', function() { 122 | expect( 123 | commitMessage({ 124 | type, 125 | subject, 126 | body, 127 | issues 128 | }) 129 | ).to.equal(`${type}: ${subject}\n\n${body}\n\n${issues}`); 130 | }); 131 | it('header, body and issues w/ scope', function() { 132 | expect( 133 | commitMessage({ 134 | type, 135 | scope, 136 | subject, 137 | body, 138 | issues 139 | }) 140 | ).to.equal(`${type}(${scope}): ${subject}\n\n${body}\n\n${issues}`); 141 | }); 142 | it('header, body and long issues w/ out scope', function() { 143 | expect( 144 | commitMessage({ 145 | type, 146 | subject, 147 | body, 148 | issues: longIssues 149 | }) 150 | ).to.equal(`${type}: ${subject}\n\n${body}\n\n${longIssuesSplit}`); 151 | }); 152 | it('header, body and long issues w/ scope', function() { 153 | expect( 154 | commitMessage({ 155 | type, 156 | scope, 157 | subject, 158 | body, 159 | issues: longIssues 160 | }) 161 | ).to.equal( 162 | `${type}(${scope}): ${subject}\n\n${body}\n\n${longIssuesSplit}` 163 | ); 164 | }); 165 | it('header and long body w/ out scope', function() { 166 | expect( 167 | commitMessage({ 168 | type, 169 | subject, 170 | body: longBody 171 | }) 172 | ).to.equal(`${type}: ${subject}\n\n${longBodySplit}`); 173 | }); 174 | it('header and long body w/ scope', function() { 175 | expect( 176 | commitMessage({ 177 | type, 178 | scope, 179 | subject, 180 | body: longBody 181 | }) 182 | ).to.equal(`${type}(${scope}): ${subject}\n\n${longBodySplit}`); 183 | }); 184 | it('header, long body and issues w/ out scope', function() { 185 | expect( 186 | commitMessage({ 187 | type, 188 | subject, 189 | body: longBody, 190 | issues 191 | }) 192 | ).to.equal(`${type}: ${subject}\n\n${longBodySplit}\n\n${issues}`); 193 | }); 194 | it('header, long body and issues w/ scope', function() { 195 | expect( 196 | commitMessage({ 197 | type, 198 | scope, 199 | subject, 200 | body: longBody, 201 | issues 202 | }) 203 | ).to.equal( 204 | `${type}(${scope}): ${subject}\n\n${longBodySplit}\n\n${issues}` 205 | ); 206 | }); 207 | it('header, long body and long issues w/ out scope', function() { 208 | expect( 209 | commitMessage({ 210 | type, 211 | subject, 212 | body: longBody, 213 | issues: longIssues 214 | }) 215 | ).to.equal(`${type}: ${subject}\n\n${longBodySplit}\n\n${longIssuesSplit}`); 216 | }); 217 | it('header, long body and long issues w/ scope', function() { 218 | expect( 219 | commitMessage({ 220 | type, 221 | scope, 222 | subject, 223 | body: longBody, 224 | issues: longIssues 225 | }) 226 | ).to.equal( 227 | `${type}(${scope}): ${subject}\n\n${longBodySplit}\n\n${longIssuesSplit}` 228 | ); 229 | }); 230 | it('header, long body, breaking change, and long issues w/ scope', function() { 231 | expect( 232 | commitMessage({ 233 | type, 234 | scope, 235 | subject, 236 | body: longBody, 237 | breaking, 238 | issues: longIssues 239 | }) 240 | ).to.equal( 241 | `${type}(${scope}): ${subject}\n\n${longBodySplit}\n\n${breakingChange}${breaking}\n\n${longIssuesSplit}` 242 | ); 243 | }); 244 | it('header, long body, breaking change (with prefix entered), and long issues w/ scope', function() { 245 | expect( 246 | commitMessage({ 247 | type, 248 | scope, 249 | subject, 250 | body: longBody, 251 | breaking: `${breakingChange}${breaking}`, 252 | issues: longIssues 253 | }) 254 | ).to.equal( 255 | `${type}(${scope}): ${subject}\n\n${longBodySplit}\n\n${breakingChange}${breaking}\n\n${longIssuesSplit}` 256 | ); 257 | }); 258 | }); 259 | 260 | describe('validation', function() { 261 | it('subject exceeds max length', function() { 262 | expect(() => 263 | commitMessage({ 264 | type, 265 | scope, 266 | subject: longBody 267 | }) 268 | ).to.throw( 269 | 'length must be less than or equal to ' + 270 | `${defaultOptions.maxLineWidth - type.length - scope.length - 4}` 271 | ); 272 | }); 273 | it('empty subject', function() { 274 | expect(() => 275 | commitMessage({ 276 | type, 277 | scope, 278 | subject: '' 279 | }) 280 | ).to.throw('subject is required'); 281 | }); 282 | }); 283 | 284 | describe('defaults', function() { 285 | it('defaultType default', function() { 286 | expect(questionDefault('type')).to.be.undefined; 287 | }); 288 | it('defaultType options', function() { 289 | expect( 290 | questionDefault('type', customOptions({ defaultType: type })) 291 | ).to.equal(type); 292 | }); 293 | it('defaultScope default', function() { 294 | expect(questionDefault('scope')).to.be.undefined; 295 | }); 296 | it('defaultScope options', () => 297 | expect( 298 | questionDefault('scope', customOptions({ defaultScope: scope })) 299 | ).to.equal(scope)); 300 | 301 | it('defaultSubject default', () => 302 | expect(questionDefault('subject')).to.be.undefined); 303 | it('defaultSubject options', function() { 304 | expect( 305 | questionDefault( 306 | 'subject', 307 | customOptions({ 308 | defaultSubject: subject 309 | }) 310 | ) 311 | ).to.equal(subject); 312 | }); 313 | it('defaultBody default', function() { 314 | expect(questionDefault('body')).to.be.undefined; 315 | }); 316 | it('defaultBody options', function() { 317 | expect( 318 | questionDefault('body', customOptions({ defaultBody: body })) 319 | ).to.equal(body); 320 | }); 321 | it('defaultIssues default', function() { 322 | expect(questionDefault('issues')).to.be.undefined; 323 | }); 324 | it('defaultIssues options', function() { 325 | expect( 326 | questionDefault( 327 | 'issues', 328 | customOptions({ 329 | defaultIssues: issues 330 | }) 331 | ) 332 | ).to.equal(issues); 333 | }); 334 | it('disableScopeLowerCase default', function() { 335 | expect(questionDefault('disableScopeLowerCase')).to.be.undefined; 336 | }); 337 | it('disableSubjectLowerCase default', function() { 338 | expect(questionDefault('disableSubjectLowerCase')).to.be.undefined; 339 | }); 340 | }); 341 | 342 | describe('prompts', function() { 343 | it('commit subject prompt for commit w/ out scope', function() { 344 | expect(questionPrompt('subject', { type })).to.contain( 345 | `(max ${defaultOptions.maxHeaderWidth - type.length - 2} chars)` 346 | ); 347 | }); 348 | it('commit subject prompt for commit w/ scope', function() { 349 | expect(questionPrompt('subject', { type, scope })).to.contain( 350 | `(max ${defaultOptions.maxHeaderWidth - 351 | type.length - 352 | scope.length - 353 | 4} chars)` 354 | ); 355 | }); 356 | }); 357 | 358 | describe('transformation', function() { 359 | it('subject w/ character count', () => 360 | expect( 361 | questionTransformation('subject', { 362 | type, 363 | subject 364 | }) 365 | ).to.equal(chalk.green(`(${subject.length}) ${subject}`))); 366 | it('long subject w/ character count', () => 367 | expect( 368 | questionTransformation('subject', { 369 | type, 370 | subject: longBody 371 | }) 372 | ).to.equal(chalk.red(`(${longBody.length}) ${longBody}`))); 373 | }); 374 | 375 | describe('filter', function() { 376 | it('lowercase scope', () => 377 | expect(questionFilter('scope', 'HelloMatt')).to.equal('hellomatt')); 378 | it('lowerfirst subject trimmed and trailing dots striped', () => 379 | expect(questionFilter('subject', ' A subject... ')).to.equal( 380 | 'a subject' 381 | )); 382 | }); 383 | 384 | describe('when', function() { 385 | it('breaking by default', () => 386 | expect(questionWhen('breaking', {})).to.be.undefined); 387 | it('breaking when isBreaking', () => 388 | expect( 389 | questionWhen('breaking', { 390 | isBreaking: true 391 | }) 392 | ).to.be.true); 393 | it('issues by default', () => 394 | expect(questionWhen('issues', {})).to.be.undefined); 395 | it('issues when isIssueAffected', () => 396 | expect( 397 | questionWhen('issues', { 398 | isIssueAffected: true 399 | }) 400 | ).to.be.true); 401 | }); 402 | 403 | describe('commitlint config header-max-length', function() { 404 | //commitlint config parser only supports Node 6.0.0 and higher 405 | if (semver.gte(process.version, '6.0.0')) { 406 | function mockOptions(headerMaxLength) { 407 | var options = undefined; 408 | mock('./engine', function(opts) { 409 | options = opts; 410 | }); 411 | if (headerMaxLength) { 412 | mock('cosmiconfig', function() { 413 | return { 414 | load: function(cwd) { 415 | return { 416 | filepath: cwd + '/.commitlintrc.js', 417 | config: { 418 | rules: { 419 | 'header-max-length': [2, 'always', headerMaxLength] 420 | } 421 | } 422 | }; 423 | } 424 | }; 425 | }); 426 | } 427 | 428 | mock.reRequire('./index'); 429 | try { 430 | return mock 431 | .reRequire('@commitlint/load')() 432 | .then(function() { 433 | return options; 434 | }); 435 | } catch (err) { 436 | return Promise.resolve(options); 437 | } 438 | } 439 | 440 | afterEach(function() { 441 | delete require.cache[require.resolve('./index')]; 442 | delete require.cache[require.resolve('@commitlint/load')]; 443 | delete process.env.CZ_MAX_HEADER_WIDTH; 444 | mock.stopAll(); 445 | }); 446 | 447 | it('with no environment or commitizen config override', function() { 448 | return mockOptions(72).then(function(options) { 449 | expect(options).to.have.property('maxHeaderWidth', 72); 450 | }); 451 | }); 452 | 453 | it('with environment variable override', function() { 454 | process.env.CZ_MAX_HEADER_WIDTH = '105'; 455 | return mockOptions(72).then(function(options) { 456 | expect(options).to.have.property('maxHeaderWidth', 105); 457 | }); 458 | }); 459 | 460 | it('with commitizen config override', function() { 461 | mock('commitizen', { 462 | configLoader: { 463 | load: function() { 464 | return { 465 | maxHeaderWidth: 103 466 | }; 467 | } 468 | } 469 | }); 470 | return mockOptions(72).then(function(options) { 471 | expect(options).to.have.property('maxHeaderWidth', 103); 472 | }); 473 | }); 474 | } else { 475 | //Node 4 doesn't support commitlint so the config value should remain the same 476 | it('default value for Node 4', function() { 477 | return mockOptions(72).then(function(options) { 478 | expect(options).to.have.property('maxHeaderWidth', 100); 479 | }); 480 | }); 481 | } 482 | }); 483 | function commitMessage(answers, options) { 484 | options = options || defaultOptions; 485 | var result = null; 486 | engine(options).prompter( 487 | { 488 | prompt: function(questions) { 489 | return { 490 | then: function(finalizer) { 491 | processQuestions(questions, answers, options); 492 | finalizer(answers); 493 | } 494 | }; 495 | } 496 | }, 497 | function(message) { 498 | result = message; 499 | } 500 | ); 501 | return result; 502 | } 503 | 504 | function processQuestions(questions, answers, options) { 505 | for (var i in questions) { 506 | var question = questions[i]; 507 | var answer = answers[question.name]; 508 | var validation = 509 | answer === undefined || !question.validate 510 | ? true 511 | : question.validate(answer, answers); 512 | if (validation !== true) { 513 | throw new Error( 514 | validation || 515 | `Answer '${answer}' to question '${question.name}' was invalid` 516 | ); 517 | } 518 | if (question.filter && answer) { 519 | answers[question.name] = question.filter(answer); 520 | } 521 | } 522 | } 523 | 524 | function getQuestions(options) { 525 | options = options || defaultOptions; 526 | var result = null; 527 | engine(options).prompter({ 528 | prompt: function(questions) { 529 | result = questions; 530 | return { 531 | then: function() {} 532 | }; 533 | } 534 | }); 535 | return result; 536 | } 537 | 538 | function getQuestion(name, options) { 539 | options = options || defaultOptions; 540 | var questions = getQuestions(options); 541 | for (var i in questions) { 542 | if (questions[i].name === name) { 543 | return questions[i]; 544 | } 545 | } 546 | return false; 547 | } 548 | 549 | function questionPrompt(name, answers, options) { 550 | options = options || defaultOptions; 551 | var question = getQuestion(name, options); 552 | return question.message && typeof question.message === 'string' 553 | ? question.message 554 | : question.message(answers); 555 | } 556 | 557 | function questionTransformation(name, answers, options) { 558 | options = options || defaultOptions; 559 | var question = getQuestion(name, options); 560 | return ( 561 | question.transformer && 562 | question.transformer(answers[name], answers, options) 563 | ); 564 | } 565 | 566 | function questionFilter(name, answer, options) { 567 | options = options || defaultOptions; 568 | var question = getQuestion(name, options); 569 | return ( 570 | question.filter && 571 | question.filter(typeof answer === 'string' ? answer : answer[name]) 572 | ); 573 | } 574 | 575 | function questionDefault(name, options) { 576 | options = options || defaultOptions; 577 | var question = getQuestion(name, options); 578 | return question.default; 579 | } 580 | 581 | function questionWhen(name, answers, options) { 582 | options = options || defaultOptions; 583 | var question = getQuestion(name, options); 584 | return question.when(answers); 585 | } 586 | 587 | function customOptions(options) { 588 | Object.keys(defaultOptions).forEach(key => { 589 | if (options[key] === undefined) { 590 | options[key] = defaultOptions[key]; 591 | } 592 | }); 593 | return options; 594 | } 595 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'format cjs'; 2 | 3 | var engine = require('./engine'); 4 | var conventionalCommitTypes = require('conventional-commit-types'); 5 | var configLoader = require('commitizen').configLoader; 6 | 7 | var config = configLoader.load() || {}; 8 | var options = { 9 | types: config.types || conventionalCommitTypes.types, 10 | defaultType: process.env.CZ_TYPE || config.defaultType, 11 | defaultScope: process.env.CZ_SCOPE || config.defaultScope, 12 | defaultSubject: process.env.CZ_SUBJECT || config.defaultSubject, 13 | defaultBody: process.env.CZ_BODY || config.defaultBody, 14 | defaultIssues: process.env.CZ_ISSUES || config.defaultIssues, 15 | disableScopeLowerCase: 16 | process.env.DISABLE_SCOPE_LOWERCASE || config.disableScopeLowerCase, 17 | disableSubjectLowerCase: 18 | process.env.DISABLE_SUBJECT_LOWERCASE || config.disableSubjectLowerCase, 19 | maxHeaderWidth: 20 | (process.env.CZ_MAX_HEADER_WIDTH && 21 | parseInt(process.env.CZ_MAX_HEADER_WIDTH)) || 22 | config.maxHeaderWidth || 23 | 100, 24 | maxLineWidth: 25 | (process.env.CZ_MAX_LINE_WIDTH && 26 | parseInt(process.env.CZ_MAX_LINE_WIDTH)) || 27 | config.maxLineWidth || 28 | 100 29 | }; 30 | 31 | (function(options) { 32 | try { 33 | var commitlintLoad = require('@commitlint/load'); 34 | commitlintLoad().then(function(clConfig) { 35 | if (clConfig.rules) { 36 | var maxHeaderLengthRule = clConfig.rules['header-max-length']; 37 | if ( 38 | typeof maxHeaderLengthRule === 'object' && 39 | maxHeaderLengthRule.length >= 3 && 40 | !process.env.CZ_MAX_HEADER_WIDTH && 41 | !config.maxHeaderWidth 42 | ) { 43 | options.maxHeaderWidth = maxHeaderLengthRule[2]; 44 | } 45 | } 46 | }); 47 | } catch (err) {} 48 | })(options); 49 | 50 | module.exports = engine(options); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cz-conventional-changelog", 3 | "version": "0.0.2", 4 | "description": "Commitizen adapter following the conventional-changelog format.", 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/commitizen/cz-conventional-changelog", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/commitizen/cz-conventional-changelog.git" 16 | }, 17 | "engineStrict": true, 18 | "engines": { 19 | "node": ">= 16" 20 | }, 21 | "author": "Jim Cummins ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "chalk": "^4.1.2", 25 | "commitizen": "^4.3.1", 26 | "conventional-commit-types": "^3.0.0", 27 | "lodash.map": "^4.6.0", 28 | "longest": "^2.0.1", 29 | "word-wrap": "^1.2.5" 30 | }, 31 | "devDependencies": { 32 | "chai": "^4.5.0", 33 | "cosmiconfig": "^9.0.0", 34 | "mocha": "^10.7.3", 35 | "mock-require": "3.0.3", 36 | "prettier": "^1.19.1", 37 | "semantic-release": "^24.1.2", 38 | "semver": "^7.6.3" 39 | }, 40 | "optionalDependencies": { 41 | "@commitlint/load": "6.1.3" 42 | }, 43 | "config": { 44 | "commitizen": { 45 | "path": "./index.js" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>commitizen/commitizen-renovate-config" 4 | ] 5 | } 6 | --------------------------------------------------------------------------------