├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── appveyor.yml ├── hosts ├── bitbucket.json ├── github.json └── gitlab.json ├── index.js ├── lib └── merge-config.js ├── package.json ├── readme.md └── test ├── fixtures ├── _host-only.json ├── _malformation.json ├── _package.json ├── _short.json ├── _unknown-host.json └── _version-only.json └── test.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | tmp 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": null, 4 | "excludeFiles": ["node_modules/**"] 5 | } 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "eqnull": true, 6 | "immed": true, 7 | "latedef": true, 8 | "mocha" : true, 9 | "newcap": true, 10 | "noarg": true, 11 | "node": true, 12 | "sub": true, 13 | "undef": true, 14 | "unused": true 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '5' 5 | - '4' 6 | - '0.12' 7 | before_script: 8 | - git config --global user.name 'Travis-CI' 9 | - git config --global user.email 'dummy@example.org' 10 | after_script: NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec --timeout 30000 && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [1.5.0](https://github.com/conventional-changelog/conventional-changelog-core/compare/v1.4.0...v1.5.0) (2016-05-10) 3 | 4 | 5 | ### Features 6 | 7 | * **context:** fallback to repoUrl([da0b096](https://github.com/conventional-changelog/conventional-changelog-core/commit/da0b096)), closes [#7](https://github.com/conventional-changelog/conventional-changelog-core/issues/7) 8 | 9 | 10 | 11 | 12 | # [1.4.0](https://github.com/conventional-changelog/conventional-changelog-core/compare/v1.3.4...v1.4.0) (2016-05-08) 13 | 14 | 15 | ### Features 16 | 17 | * **debug:** make options.debug as default writeOpts.debug([eeb7e8f](https://github.com/conventional-changelog/conventional-changelog-core/commit/eeb7e8f)) 18 | 19 | 20 | 21 | 22 | ## [1.3.4](https://github.com/conventional-changelog/conventional-changelog-core/compare/v1.3.3...v1.3.4) (2016-05-07) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **mergeConfig:** respect issuePrefixes option ([4be052b](https://github.com/conventional-changelog/conventional-changelog-core/commit/4be052b)), closes [#6](https://github.com/conventional-changelog/conventional-changelog-core/issues/6) [#8](https://github.com/conventional-changelog/conventional-changelog-core/issues/8) 28 | 29 | 30 | 31 | 32 | ## [1.3.3](https://github.com/conventional-changelog/conventional-changelog-core/compare/v1.3.2...v1.3.3) (2016-04-19) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **unknownHost:** default context.repository ([eaa3b6f](https://github.com/conventional-changelog/conventional-changelog-core/commit/eaa3b6f)) 38 | 39 | 40 | 41 | 42 | ## [1.3.2](https://github.com/conventional-changelog/conventional-changelog-core/compare/v1.3.1...v1.3.2) (2016-04-17) 43 | 44 | 45 | 46 | 47 | 48 | ## [1.3.1](https://github.com/conventional-changelog/conventional-changelog-core/compare/v1.3.0...v1.3.1) (2016-04-09) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **defaults:** context tags ([2571038](https://github.com/conventional-changelog/conventional-changelog-core/commit/2571038)) 54 | 55 | 56 | 57 | 58 | # [1.3.0](https://github.com/stevemao/conventional-changelog-core/compare/v1.2.0...v1.3.0) (2016-02-13) 59 | 60 | 61 | ### Features 62 | 63 | * **debug:** add options.debug function ([aa56ae6](https://github.com/stevemao/conventional-changelog-core/commit/aa56ae6)) 64 | 65 | 66 | 67 | 68 | # [1.2.0](https://github.com/stevemao/conventional-changelog-core/compare/v1.1.0...v1.2.0) (2016-02-11) 69 | 70 | 71 | ### Features 72 | 73 | * **merge:** ignore merge commits ([8f788dc](https://github.com/stevemao/conventional-changelog-core/commit/8f788dc)) 74 | 75 | 76 | 77 | 78 | # [1.1.0](https://github.com/stevemao/conventional-changelog-core/compare/v1.0.2...v1.1.0) (2016-02-08) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * **default:** firstCommit and lastCommit should based on original unfiltered commits ([7fc49c9](https://github.com/stevemao/conventional-changelog-core/commit/7fc49c9)), closes [#2](https://github.com/stevemao/conventional-changelog-core/issues/2) 84 | 85 | 86 | 87 | 88 | ## [1.0.2](https://github.com/stevemao/conventional-changelog-core/compare/v1.0.1...v1.0.2) (2016-02-06) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * **currentTag:** if unreleased, currentTag should be last commit hash ([e3d25ae](https://github.com/stevemao/conventional-changelog-core/commit/e3d25ae)) 94 | 95 | 96 | 97 | 98 | ## [1.0.1](https://github.com/stevemao/conventional-changelog-core/compare/v1.0.0...v1.0.1) (2016-02-06) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * **unreleased:** now it can output unreleased commits ([87b7340](https://github.com/stevemao/conventional-changelog-core/commit/87b7340)) 104 | 105 | 106 | 107 | 108 | # [1.0.0](https://github.com/stevemao/conventional-changelog-core/compare/v0.0.2...v1.0.0) (2016-02-05) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * **oldNode:** git remote origin url feature is only available under node>=4 ([c69db53](https://github.com/stevemao/conventional-changelog-core/commit/c69db53)) 114 | 115 | ### Features 116 | 117 | * **pkg:** fallback to git remote origin url ([5b56952](https://github.com/stevemao/conventional-changelog-core/commit/5b56952)) 118 | * **unreleased:** option to output or not unreleased changelog ([9dfe8d8](https://github.com/stevemao/conventional-changelog-core/commit/9dfe8d8)), closes [ajoslin/conventional-changelog#120](https://github.com/ajoslin/conventional-changelog/issues/120) 119 | 120 | 121 | ### BREAKING CHANGES 122 | 123 | * unreleased: If `context.version` is the same as the version of the last release, by default the unreleased chagnelog will not output. 124 | 125 | 126 | 127 | 128 | ## [0.0.2](https://github.com/stevemao/conventional-changelog-core/compare/v0.0.1...v0.0.2) (2016-01-30) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * **error:** better error handling ([614ce1a](https://github.com/stevemao/conventional-changelog-core/commit/614ce1a)), closes [ajoslin/conventional-changelog#130](https://github.com/ajoslin/conventional-changelog/issues/130) 134 | 135 | 136 | 137 | 138 | ## 0.0.1 (2015-12-26) 139 | 140 | 141 | ### Features 142 | 143 | * **config:** change preset to config ([85fd9d9](https://github.com/stevemao/conventional-changelog-core/commit/85fd9d9)) 144 | * **init:** extract core from conventional-changelog ([4a4bca3](https://github.com/stevemao/conventional-changelog-core/commit/4a4bca3)) 145 | 146 | 147 | ### BREAKING CHANGES 148 | 149 | * config: `options.preset` is removed in favour of `options.config` 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '6' 4 | - nodejs_version: '5' 5 | - nodejs_version: '4' 6 | - nodejs_version: '0.12' 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - set CI=true 10 | - npm -g install npm@latest 11 | - set PATH=%APPDATA%\npm;%PATH% 12 | - npm install 13 | matrix: 14 | fast_finish: true 15 | build: off 16 | version: '{build}' 17 | test_script: 18 | - git config --global user.name "Appveyor-CI" 19 | - git config --global user.email "dummy@example.org" 20 | - node --version 21 | - npm --version 22 | - npm test 23 | -------------------------------------------------------------------------------- /hosts/bitbucket.json: -------------------------------------------------------------------------------- 1 | { 2 | "issue": "issue", 3 | "commit": "commits", 4 | "referenceActions": [ 5 | "close", 6 | "closes", 7 | "closed", 8 | "closing", 9 | "fix", 10 | "fixes", 11 | "fixed", 12 | "fixing", 13 | "resolve", 14 | "resolves", 15 | "resolved", 16 | "resolving" 17 | ], 18 | "issuePrefixes": [ 19 | "#" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /hosts/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "issue": "issues", 3 | "commit": "commit", 4 | "referenceActions": [ 5 | "close", 6 | "closes", 7 | "closed", 8 | "fix", 9 | "fixes", 10 | "fixed", 11 | "resolve", 12 | "resolves", 13 | "resolved" 14 | ], 15 | "issuePrefixes": [ 16 | "#", 17 | "gh-" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /hosts/gitlab.json: -------------------------------------------------------------------------------- 1 | { 2 | "issue": "issues", 3 | "commit": "commit", 4 | "referenceActions": [ 5 | "close", 6 | "closes", 7 | "closed", 8 | "closing", 9 | "fix", 10 | "fixes", 11 | "fixed", 12 | "fixing" 13 | ], 14 | "issuePrefixes": [ 15 | "#" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var gitRawCommits = require('git-raw-commits'); 3 | var conventionalCommitsParser = require('conventional-commits-parser'); 4 | var conventionalChangelogWriter = require('conventional-changelog-writer'); 5 | var stream = require('stream'); 6 | var through = require('through2'); 7 | var mergeConfig = require('./lib/merge-config'); 8 | 9 | function conventionalChangelog(options, context, gitRawCommitsOpts, parserOpts, writerOpts) { 10 | writerOpts = writerOpts || {}; 11 | 12 | var readable = new stream.Readable({ 13 | objectMode: writerOpts.includeDetails 14 | }); 15 | readable._read = function() {}; 16 | 17 | mergeConfig(options, context, gitRawCommitsOpts, parserOpts, writerOpts) 18 | .then(function(data) { 19 | options = data.options; 20 | context = data.context; 21 | gitRawCommitsOpts = data.gitRawCommitsOpts; 22 | parserOpts = data.parserOpts; 23 | writerOpts = data.writerOpts; 24 | 25 | gitRawCommits(gitRawCommitsOpts) 26 | .on('error', function(err) { 27 | err.message = 'Error in git-raw-commits: ' + err.message; 28 | setImmediate(readable.emit.bind(readable), 'error', err); 29 | }) 30 | .pipe(conventionalCommitsParser(parserOpts)) 31 | .on('error', function(err) { 32 | err.message = 'Error in conventional-commits-parser: ' + err.message; 33 | setImmediate(readable.emit.bind(readable), 'error', err); 34 | }) 35 | // it would be better if `gitRawCommits` could spit out better formatted data 36 | // so we don't need to transform here 37 | .pipe(through.obj(function(chunk, enc, cb) { 38 | try { 39 | options.transform.call(this, chunk, cb); 40 | } catch (err) { 41 | cb(err); 42 | } 43 | })) 44 | .on('error', function(err) { 45 | err.message = 'Error in options.transform: ' + err.message; 46 | setImmediate(readable.emit.bind(readable), 'error', err); 47 | }) 48 | .pipe(conventionalChangelogWriter(context, writerOpts)) 49 | .on('error', function(err) { 50 | err.message = 'Error in conventional-changelog-writer: ' + err.message; 51 | setImmediate(readable.emit.bind(readable), 'error', err); 52 | }) 53 | .pipe(through({ 54 | objectMode: writerOpts.includeDetails 55 | }, function(chunk, enc, cb) { 56 | try { 57 | readable.push(chunk); 58 | } catch (err) { 59 | setImmediate(function() { 60 | throw err; 61 | }); 62 | } 63 | 64 | cb(); 65 | }, function(cb) { 66 | readable.push(null); 67 | 68 | cb(); 69 | })); 70 | }) 71 | .catch(function(err) { 72 | setImmediate(readable.emit.bind(readable), 'error', err); 73 | }); 74 | 75 | return readable; 76 | } 77 | 78 | module.exports = conventionalChangelog; 79 | -------------------------------------------------------------------------------- /lib/merge-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var dateFormat = require('dateformat'); 3 | var getPkgRepo = require('get-pkg-repo'); 4 | var gitSemverTags = require('git-semver-tags'); 5 | var normalizePackageData = require('normalize-package-data'); 6 | var Q = require('q'); 7 | var gitRemoteOriginUrl; 8 | try { 9 | gitRemoteOriginUrl = require('git-remote-origin-url'); 10 | } catch (err) { 11 | gitRemoteOriginUrl = function() { 12 | return Q.reject(err); 13 | }; 14 | } 15 | var readPkg = require('read-pkg'); 16 | var readPkgUp = require('read-pkg-up'); 17 | var url = require('url'); 18 | var _ = require('lodash'); 19 | 20 | var rhosts = /github|bitbucket|gitlab/i; 21 | var rtag = /tag:\s*[v=]?(.+?)[,\)]/gi; 22 | 23 | function mergeConfig(options, context, gitRawCommitsOpts, parserOpts, writerOpts) { 24 | var configPromise; 25 | var pkgPromise; 26 | var semverTagsPromise; 27 | var gitRemoteOriginUrlPromise; 28 | 29 | context = context || {}; 30 | gitRawCommitsOpts = gitRawCommitsOpts || {}; 31 | 32 | options = _.merge({ 33 | pkg: { 34 | transform: function(pkg) { 35 | return pkg; 36 | } 37 | }, 38 | append: false, 39 | releaseCount: 1, 40 | debug: function() {}, 41 | transform: function(commit, cb) { 42 | if (_.isString(commit.gitTags)) { 43 | var match = rtag.exec(commit.gitTags); 44 | rtag.lastIndex = 0; 45 | 46 | if (match) { 47 | commit.version = match[1]; 48 | } 49 | } 50 | 51 | if (commit.committerDate) { 52 | commit.committerDate = dateFormat(commit.committerDate, 'yyyy-mm-dd', true); 53 | } 54 | 55 | cb(null, commit); 56 | } 57 | }, options); 58 | 59 | options.warn = options.warn || options.debug; 60 | 61 | if (options.config) { 62 | if (_.isFunction(options.config)) { 63 | configPromise = Q.nfcall(options.config); 64 | } else { 65 | configPromise = Q(options.config); // jshint ignore:line 66 | } 67 | } 68 | 69 | if (options.pkg) { 70 | if (options.pkg.path) { 71 | pkgPromise = Q(readPkg(options.pkg.path)); // jshint ignore:line 72 | } else { 73 | pkgPromise = Q(readPkgUp()); // jshint ignore:line 74 | } 75 | } 76 | 77 | semverTagsPromise = Q.nfcall(gitSemverTags); 78 | 79 | gitRemoteOriginUrlPromise = Q(gitRemoteOriginUrl()); // jshint ignore:line 80 | 81 | return Q.allSettled([configPromise, pkgPromise, semverTagsPromise, gitRemoteOriginUrlPromise]) 82 | .spread(function(configObj, pkgObj, tagsObj, gitRemoteOriginUrlObj) { 83 | var config; 84 | var pkg; 85 | var fromTag; 86 | var repo; 87 | 88 | var hostOpts; 89 | 90 | var gitSemverTags = []; 91 | 92 | if (configPromise) { 93 | if (configObj.state === 'fulfilled') { 94 | config = configObj.value; 95 | } else { 96 | options.warn('Error in config' + configObj.reason.toString()); 97 | config = {}; 98 | } 99 | } else { 100 | config = {}; 101 | } 102 | 103 | context = _.assign(context, config.context); 104 | 105 | if (options.pkg) { 106 | if (pkgObj.state === 'fulfilled') { 107 | if (options.pkg.path) { 108 | pkg = pkgObj.value; 109 | } else { 110 | pkg = pkgObj.value.pkg || {}; 111 | } 112 | 113 | pkg = options.pkg.transform(pkg); 114 | 115 | } else if (options.pkg.path) { 116 | options.warn(pkgObj.reason.toString()); 117 | } 118 | } 119 | 120 | if ((!pkg || !pkg.repository || !pkg.repository.url) && gitRemoteOriginUrlObj.state === 'fulfilled') { 121 | pkg = pkg || {}; 122 | pkg.repository = pkg.repository || {}; 123 | pkg.repository.url = gitRemoteOriginUrlObj.value; 124 | normalizePackageData(pkg); 125 | } 126 | 127 | if (pkg) { 128 | context.version = context.version || pkg.version; 129 | 130 | try { 131 | repo = getPkgRepo(pkg); 132 | } catch (err) { 133 | repo = {}; 134 | } 135 | 136 | if (repo.browse) { 137 | var browse = repo.browse(); 138 | var parsedBrowse = url.parse(browse); 139 | context.host = context.host || (repo.domain ? (parsedBrowse.protocol + (parsedBrowse.slashes ? '//' : '') + repo.domain) : null); 140 | context.owner = context.owner || repo.user || ''; 141 | context.repository = context.repository || repo.project; 142 | context.repoUrl = browse; 143 | } 144 | 145 | context.packageData = pkg; 146 | } 147 | 148 | if (tagsObj.state === 'fulfilled') { 149 | gitSemverTags = context.gitSemverTags = tagsObj.value; 150 | fromTag = gitSemverTags[options.releaseCount - 1]; 151 | 152 | var lastTag = gitSemverTags[0]; 153 | 154 | if (lastTag === context.version || lastTag === 'v' + context.version) { 155 | if (options.outputUnreleased) { 156 | context.version = 'Unreleased'; 157 | } else { 158 | options.outputUnreleased = false; 159 | } 160 | } 161 | } 162 | 163 | if (!_.isBoolean(options.outputUnreleased)) { 164 | options.outputUnreleased = true; 165 | } 166 | 167 | if (context.host && (!context.issue || !context.commit || !parserOpts || !parserOpts.referenceActions)) { 168 | var type; 169 | 170 | if (context.host) { 171 | var match = context.host.match(rhosts); 172 | if (match) { 173 | type = match[0]; 174 | } 175 | } else if (repo && repo.type) { 176 | type = repo.type; 177 | } 178 | 179 | if (type) { 180 | hostOpts = require('../hosts/' + type); 181 | 182 | context = _.assign({ 183 | issue: hostOpts.issue, 184 | commit: hostOpts.commit 185 | }, context); 186 | } else { 187 | options.warn('Host: "' + context.host + '" does not exist'); 188 | hostOpts = {}; 189 | } 190 | } else { 191 | hostOpts = {}; 192 | } 193 | 194 | gitRawCommitsOpts = _.assign({ 195 | format: '%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci', 196 | from: fromTag, 197 | merges: false, 198 | debug: options.debug 199 | }, 200 | config.gitRawCommitsOpts, 201 | gitRawCommitsOpts 202 | ); 203 | 204 | if (options.append) { 205 | gitRawCommitsOpts.reverse = gitRawCommitsOpts.reverse || true; 206 | } 207 | 208 | parserOpts = _.assign( 209 | {}, config.parserOpts, { 210 | warn: options.warn 211 | }, 212 | parserOpts); 213 | 214 | if (hostOpts.referenceActions && parserOpts) { 215 | parserOpts.referenceActions = hostOpts.referenceActions; 216 | } 217 | 218 | if (_.isEmpty(parserOpts.issuePrefixes) && hostOpts.issuePrefixes) { 219 | parserOpts.issuePrefixes = hostOpts.issuePrefixes; 220 | } 221 | 222 | writerOpts = _.assign({ 223 | finalizeContext: function(context, writerOpts, filteredCommits, keyCommit, originalCommits) { 224 | var firstCommit = originalCommits[0]; 225 | var lastCommit = originalCommits[originalCommits.length - 1]; 226 | var firstCommitHash = firstCommit ? firstCommit.hash : null; 227 | var lastCommitHash = lastCommit ? lastCommit.hash : null; 228 | 229 | if ((!context.currentTag || !context.previousTag) && keyCommit) { 230 | var match = /tag:\s*(.+?)[,\)]/gi.exec(keyCommit.gitTags); 231 | var currentTag = context.currentTag; 232 | context.currentTag = currentTag || match ? match[1] : null; 233 | var index = gitSemverTags.indexOf(context.currentTag); 234 | 235 | // if `keyCommit.gitTags` is not a semver 236 | if (index === -1) { 237 | context.currentTag = currentTag || null; 238 | } else { 239 | var previousTag = context.previousTag = gitSemverTags[index + 1]; 240 | 241 | if (!previousTag) { 242 | if (options.append) { 243 | context.previousTag = context.previousTag || firstCommitHash; 244 | } else { 245 | context.previousTag = context.previousTag || lastCommitHash; 246 | } 247 | } 248 | } 249 | } else { 250 | context.previousTag = context.previousTag || gitSemverTags[0]; 251 | 252 | if (context.version === 'Unreleased') { 253 | if (options.append) { 254 | context.currentTag = context.currentTag || lastCommitHash; 255 | } else { 256 | context.currentTag = context.currentTag || firstCommitHash; 257 | } 258 | } else { 259 | context.currentTag = context.currentTag || 'v' + context.version; 260 | } 261 | } 262 | 263 | if (!_.isBoolean(context.linkCompare) && context.previousTag && context.currentTag) { 264 | context.linkCompare = true; 265 | } 266 | 267 | return context; 268 | }, 269 | debug: options.debug 270 | }, 271 | config.writerOpts, { 272 | reverse: options.append, 273 | doFlush: options.outputUnreleased 274 | }, 275 | writerOpts 276 | ); 277 | 278 | return { 279 | options: options, 280 | context: context, 281 | gitRawCommitsOpts: gitRawCommitsOpts, 282 | parserOpts: parserOpts, 283 | writerOpts: writerOpts 284 | }; 285 | }); 286 | } 287 | 288 | module.exports = mergeConfig; 289 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conventional-changelog-core", 3 | "version": "1.5.0", 4 | "description": "conventional-changelog core", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/conventional-changelog/conventional-changelog-core.git" 8 | }, 9 | "keywords": [ 10 | "conventional-changelog", 11 | "conventional", 12 | "changelog", 13 | "log" 14 | ], 15 | "license": "MIT", 16 | "dependencies": { 17 | "conventional-changelog-writer": "^1.1.0", 18 | "conventional-commits-parser": "^1.0.0", 19 | "dateformat": "^1.0.12", 20 | "get-pkg-repo": "^1.0.0", 21 | "git-raw-commits": "^1.1.0", 22 | "git-remote-origin-url": "^2.0.0", 23 | "git-semver-tags": "^1.1.0", 24 | "lodash": "^4.0.0", 25 | "normalize-package-data": "^2.3.5", 26 | "q": "^1.4.1", 27 | "read-pkg": "^1.1.0", 28 | "read-pkg-up": "^1.0.1", 29 | "through2": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "better-than-before": "^1.0.0", 33 | "chai": "^3.4.1", 34 | "coveralls": "^2.11.6", 35 | "git-dummy-commit": "^1.1.0", 36 | "git-tails": "^1.0.0", 37 | "istanbul": "^0.4.1", 38 | "jscs": "^3.0.3", 39 | "jshint": "^2.9.1", 40 | "mocha": "*", 41 | "pinkie-promise": "^2.0.0", 42 | "semver": "^5.1.0", 43 | "shelljs": "^0.7.0" 44 | }, 45 | "scripts": { 46 | "coverage": "istanbul cover _mocha -- -R spec --timeout 30000 && rm -rf ./coverage", 47 | "lint": "jshint . --exclude node_modules && jscs .", 48 | "test": "mocha --timeout 30000 && npm run-script lint" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [![NPM version][npm-image]][npm-url] [![Build Status: Linux][travis-image]][travis-url] [![Build Status: Windows][appveyor-image]][appveyor-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage Status][coveralls-image]][coveralls-url] 2 | 3 | > [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog) core 4 | 5 | You are probably looking for the [cli](https://github.com/conventional-changelog/conventional-changelog-cli) module. Or use one of the plugins if you are already using the tool: [grunt](https://github.com/btford/grunt-conventional-changelog)/[gulp](https://github.com/conventional-changelog/gulp-conventional-changelog)/[atom](https://github.com/conventional-changelog/atom-conventional-changelog). 6 | 7 | 8 | ## Usage 9 | 10 | ```sh 11 | $ npm install --save conventional-changelog-core 12 | ``` 13 | 14 | ```js 15 | var conventionalChangelogCore = require('conventional-changelog-core'); 16 | 17 | conventionalChangelogCore() 18 | .pipe(process.stdout); // or any writable stream 19 | ``` 20 | 21 | 22 | ## API 23 | 24 | ### conventionalChangelogCore([options, [context, [gitRawCommitsOpts, [parserOpts, [writerOpts]]]]]) 25 | 26 | Returns a readable stream. 27 | 28 | *Note:* [`options.transform`](#transform-1), [`options.pkg.transform`](#transform) and [`writerOpts.transform`](https://github.com/conventional-changelog/conventional-changelog-writer#transform) are different. If you have a better naming suggestion, please send a PR. 29 | 30 | #### options 31 | 32 | ##### config 33 | 34 | Type: `promise`, `function` or `object` 35 | 36 | This should serve as default values for other arguments of `conventionalChangelogCore` so you don't need to rewrite the same or similar config across your projects. Any value in this could be overwritten. 37 | If this is a promise (recommended if async), it should resolve with the config. 38 | If this is a function, it expects a node style callback with the config object. 39 | If this is an object, it is the config object. The config object should include `context`, `gitRawCommitsOpts`, `parserOpts` and `writerOpts`. 40 | 41 | ##### pkg 42 | 43 | Type: `object` 44 | 45 | ###### path 46 | 47 | Type: `string` Default: [closest package.json](https://github.com/sindresorhus/read-pkg-up). 48 | 49 | The location of your "package.json". 50 | 51 | ###### transform 52 | 53 | Type: `function` Default: pass through. 54 | 55 | A function that takes `package.json` data as the argument and returns the modified data. Note this is performed before normalizing package.json data. Useful when you need to add a leading 'v' to your version or modify your repository url, etc. 56 | 57 | ##### append 58 | 59 | Type: `boolean` Default: `false` 60 | 61 | Should the log be appended to existing data. 62 | 63 | ##### releaseCount 64 | 65 | Type: `number` Default: `1` 66 | 67 | How many releases of changelog you want to generate. It counts from the upcoming release. Useful when you forgot to generate any previous changelog. Set to `0` to regenerate all. 68 | 69 | ##### debug 70 | 71 | Type: `function` Default: `function() {}` 72 | 73 | A debug function. EG: `console.debug.bind(console)` 74 | 75 | ##### warn 76 | 77 | Type: `function` Default: `options.debug` 78 | 79 | A warn function. EG: `grunt.verbose.writeln` 80 | 81 | ##### transform 82 | 83 | Type: `function` Default: get the version (without leading 'v') from tag and format date. 84 | 85 | ###### function(commit, cb) 86 | 87 | A transform function that applies after the parser and before the writer. 88 | 89 | This is the place to modify the parsed commits. 90 | 91 | ####### commit 92 | 93 | The commit from conventional-commits-parser. 94 | 95 | ####### cb 96 | 97 | Callback when you are done. 98 | 99 | ####### this 100 | 101 | `this` arg of through2. 102 | 103 | ##### outputUnreleased 104 | 105 | Type: `boolean` Default: `true` if a different version than last release is given. Otherwise `false`. 106 | 107 | If this value is `true` and `context.version` equals last release then `context.version` will be changed to `'Unreleased'`. 108 | 109 | #### context 110 | 111 | See the [conventional-changelog-writer](https://github.com/conventional-changelog/conventional-changelog-writer) docs. There are some defaults or changes: 112 | 113 | ##### host 114 | 115 | Default: normalized host found in `package.json`. 116 | 117 | ##### version 118 | 119 | Default: version found in `package.json`. 120 | 121 | ##### owner 122 | 123 | Default: extracted from normalized `package.json` `repository.url` field. 124 | 125 | ##### repository 126 | 127 | Default: extracted from normalized `package.json` `repository.url` field. 128 | 129 | ##### repoUrl 130 | 131 | Default: The whole normalized repository url in `package.json`. 132 | 133 | ##### gitSemverTags 134 | 135 | Type: `array` 136 | 137 | All git semver tags found in the repository. You can't overwrite this value. 138 | 139 | ##### previousTag 140 | 141 | Type: `string` Default: previous semver tag or the first commit hash if no previous tag. 142 | 143 | ##### currentTag 144 | 145 | Type: `string` Default: current semver tag or `'v'` + version if no current tag. 146 | 147 | ##### packageData 148 | 149 | Type: `object` 150 | 151 | Your `package.json` data. You can't overwrite this value. 152 | 153 | ##### linkCompare 154 | 155 | Type: `boolean` Default: `true` if `previousTag` and `currentTag` are truthy. 156 | 157 | Should link to the page that compares current tag with previous tag? 158 | 159 | #### gitRawCommitsOpts 160 | 161 | See the [git-raw-commits](https://github.com/conventional-changelog/git-raw-commits) docs. There are some defaults: 162 | 163 | ##### format 164 | 165 | Default: `'%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci'` 166 | 167 | ##### from 168 | 169 | Default: based on `options.releaseCount`. 170 | 171 | ##### reverse 172 | 173 | Default: `true` if `options.append` is truthy. 174 | 175 | ##### debug 176 | 177 | Type: `function` Default: `options.debug` 178 | 179 | #### parserOpts 180 | 181 | See the [conventional-commits-parser](https://github.com/conventional-changelog/conventional-commits-parser) docs. 182 | 183 | ##### warn 184 | 185 | Default: `options.warn` 186 | 187 | #### writerOpts 188 | 189 | See the [conventional-changelog-writer](https://github.com/conventional-changelog/conventional-changelog-writer) docs. There are some defaults: 190 | 191 | ##### finalizeContext 192 | 193 | Finalize context is used for generating above context. 194 | 195 | **NOTE:** If you overwrite this value the above context defaults will be gone. 196 | 197 | ##### debug 198 | 199 | Type: `function` Default: `options.debug` 200 | 201 | ##### reverse 202 | 203 | Default: `options.append` 204 | 205 | ##### doFlush 206 | 207 | Default: `options.outputUnreleased` 208 | 209 | 210 | ## Notes for parent modules 211 | 212 | This module has options `append` and `releaseCount`. However, it doesn't read your previous changelog. Reasons being: 213 | 214 | 1. The old logs is just to be appended or prepended to the newly generated logs, which is a very simple thing that could be done in the parent module. 215 | 2. We want it to be very flexible for the parent module. You could create a readable stream from the file or you could just read the file. 216 | 3. We want the duty of this module to be very minimum. 217 | 218 | So, when you build a parent module, you need to read the old logs and append or prepend to them based on `options.append`. However, if `options.releaseCount` is `0` you need to ignore any previous logs. Please see [conventional-github-releaser](https://github.com/conventional-changelog/conventional-github-releaser) as an example. 219 | 220 | Arguments passed to `conventionalChangelogCore` will be mutated. 221 | 222 | 223 | ## License 224 | 225 | MIT 226 | 227 | 228 | [npm-image]: https://badge.fury.io/js/conventional-changelog-core.svg 229 | [npm-url]: https://npmjs.org/package/conventional-changelog-core 230 | [travis-image]: https://travis-ci.org/conventional-changelog/conventional-changelog-core.svg?branch=master 231 | [travis-url]: https://travis-ci.org/conventional-changelog/conventional-changelog-core 232 | [appveyor-image]: https://ci.appveyor.com/api/projects/status/baoumm34w8c5o0hv/branch/master?svg=true 233 | [appveyor-url]: https://ci.appveyor.com/project/stevemao/conventional-changelog-core/branch/master 234 | [daviddm-image]: https://david-dm.org/conventional-changelog/conventional-changelog-core.svg?theme=shields.io 235 | [daviddm-url]: https://david-dm.org/conventional-changelog/conventional-changelog-core 236 | [coveralls-image]: https://coveralls.io/repos/conventional-changelog/conventional-changelog-core/badge.svg 237 | [coveralls-url]: https://coveralls.io/r/conventional-changelog/conventional-changelog-core 238 | -------------------------------------------------------------------------------- /test/fixtures/_host-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.17", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://unknown-host/.git" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/_malformation.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conventional-changelog-archived-repos/conventional-changelog-core/873a2fb3d08ec52a5178b7965700ed6acd17e32d/test/fixtures/_malformation.json -------------------------------------------------------------------------------- /test/fixtures/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.17", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/ajoslin/conventional-changelog.git" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/_short.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.17", 3 | "repository": "ajoslin/conventional-changelog" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/_unknown-host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.17", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://stash.local/scm/conventional-changelog/conventional-changelog.git" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/_version-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.17" 3 | } 4 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var conventionalChangelogCore = require('../'); 3 | var expect = require('chai').expect; 4 | var gitTails = require('git-tails').sync; 5 | var shell = require('shelljs'); 6 | var gitDummyCommit = require('git-dummy-commit'); 7 | var through = require('through2'); 8 | var Promise = require('pinkie-promise'); 9 | var semver = require('semver'); 10 | var betterThanBefore = require('better-than-before')(); 11 | var preparing = betterThanBefore.preparing; 12 | 13 | betterThanBefore.setups([ 14 | function() { // 1 15 | shell.config.silent = true; 16 | shell.rm('-rf', 'tmp'); 17 | shell.mkdir('tmp'); 18 | shell.cd('tmp'); 19 | shell.mkdir('git-templates'); 20 | shell.exec('git init --template=./git-templates'); 21 | gitDummyCommit('First commit'); 22 | }, 23 | function() { // 2 24 | shell.exec('git tag v0.1.0'); 25 | gitDummyCommit('Second commit'); 26 | gitDummyCommit('Third commit closes #1'); 27 | }, 28 | function() { // 3 29 | shell.exec('git checkout -b feature'); 30 | gitDummyCommit('This commit is from feature branch'); 31 | shell.exec('git checkout master'); 32 | gitDummyCommit('This commit is from master branch'); 33 | shell.exec('git merge feature -m"Merge branch \'feature\'"'); 34 | }, 35 | function() { // 4 36 | gitDummyCommit('Custom prefix closes @42'); 37 | }, 38 | function() { // 5 39 | gitDummyCommit('Custom prefix closes @42'); 40 | gitDummyCommit('Old prefix closes #71'); 41 | }, 42 | function() { // 6 43 | gitDummyCommit('some more features'); 44 | shell.exec('git tag v2.0.0'); 45 | }, 46 | function() { // 7 47 | gitDummyCommit('test8'); 48 | }, 49 | function() { // 8 50 | gitDummyCommit('test8'); 51 | }, 52 | function() { // 9 53 | gitDummyCommit(['test9', 'Release note: super release!']); 54 | }, 55 | function() { // 10 56 | shell.exec('git remote add origin https://github.com/user/repo.git'); 57 | }, 58 | function(context) { // 11 59 | shell.exec('git tag -d v0.1.0'); 60 | var tails = gitTails(); 61 | context.tail = tails[tails.length - 1].substring(0, 7); 62 | }, 63 | function(context) { // 12 64 | shell.exec('git tag not-semver'); 65 | gitDummyCommit(); 66 | 67 | var head = shell.exec('git rev-parse HEAD').stdout.trim(); 68 | gitDummyCommit('Revert \\"test9\\" This reverts commit ' + head + '.'); 69 | context.head = shell.exec('git rev-parse HEAD').stdout.substring(0, 7); 70 | }, 71 | function(context) { // 13 72 | var tail = context.tail; 73 | shell.exec('git tag v0.0.1 ' + tail); 74 | }, 75 | function() { // 14 76 | gitDummyCommit(); 77 | shell.exec('git tag v1.0.0'); 78 | }, 79 | function() { // 15 80 | gitDummyCommit(); 81 | gitDummyCommit('something unreleased yet :)'); 82 | } 83 | ]); 84 | 85 | betterThanBefore.tearsWithJoy(function() { 86 | shell.cd('../'); 87 | shell.rm('-rf', 'tmp'); 88 | }); 89 | 90 | describe('conventionalChangelogCore', function() { 91 | it('should work if there is no tag', function(done) { 92 | preparing(1); 93 | 94 | conventionalChangelogCore() 95 | .pipe(through(function(chunk) { 96 | expect(chunk.toString()).to.include('First commit'); 97 | 98 | done(); 99 | })); 100 | }); 101 | 102 | it('should generate the changelog for the upcoming release', function(done) { 103 | preparing(2); 104 | 105 | conventionalChangelogCore() 106 | .pipe(through(function(chunk) { 107 | chunk = chunk.toString(); 108 | 109 | expect(chunk).to.include('Second commit'); 110 | expect(chunk).to.include('Third commit'); 111 | 112 | expect(chunk).to.not.include('First commit'); 113 | 114 | done(); 115 | })); 116 | }); 117 | 118 | it('should generate the changelog of the last two releases', function(done) { 119 | preparing(2); 120 | var i = 0; 121 | 122 | conventionalChangelogCore({ 123 | releaseCount: 2 124 | }) 125 | .pipe(through(function(chunk, enc, cb) { 126 | chunk = chunk.toString(); 127 | 128 | if (i === 0) { 129 | expect(chunk).to.include('Second commit'); 130 | expect(chunk).to.include('Third commit'); 131 | } else if (i === 1) { 132 | expect(chunk).to.include('First commit'); 133 | } 134 | 135 | i++; 136 | cb(); 137 | }, function() { 138 | expect(i).to.equal(2); 139 | done(); 140 | })); 141 | }); 142 | 143 | it('should generate the changelog of the last two releases even if release count exceeds the limit', function(done) { 144 | preparing(2); 145 | var i = 0; 146 | 147 | conventionalChangelogCore({ 148 | releaseCount: 100 149 | }) 150 | .pipe(through(function(chunk, enc, cb) { 151 | chunk = chunk.toString(); 152 | 153 | if (i === 0) { 154 | expect(chunk).to.include('Second commit'); 155 | expect(chunk).to.include('Third commit'); 156 | } else if (i === 1) { 157 | expect(chunk).to.include('First commit'); 158 | } 159 | 160 | i++; 161 | cb(); 162 | }, function() { 163 | expect(i).to.equal(2); 164 | done(); 165 | })); 166 | }); 167 | 168 | it('should honour `gitRawCommitsOpts.from`', function(done) { 169 | preparing(2); 170 | 171 | conventionalChangelogCore({}, {}, { 172 | from: 'HEAD~2' 173 | }, {}, { 174 | commitsSort: null 175 | }) 176 | .pipe(through(function(chunk) { 177 | chunk = chunk.toString(); 178 | 179 | expect(chunk).to.include('Second commit'); 180 | expect(chunk).to.include('Third commit'); 181 | expect(chunk).to.match(/Third commit closes #1[\w\W]*?\* Second commit/); 182 | 183 | expect(chunk).to.not.include('First commit'); 184 | 185 | done(); 186 | })); 187 | }); 188 | 189 | it('should ignore merge commits by default', function(done) { 190 | preparing(3); 191 | 192 | conventionalChangelogCore() 193 | .pipe(through(function(chunk) { 194 | chunk = chunk.toString(); 195 | 196 | expect(chunk).to.include('This commit is from feature branch'); 197 | 198 | expect(chunk).to.not.include('Merge'); 199 | 200 | done(); 201 | })); 202 | }); 203 | 204 | it('should spit out some debug info', function(done) { 205 | preparing(3); 206 | 207 | var first = true; 208 | 209 | conventionalChangelogCore({ 210 | debug: function(cmd) { 211 | if (first) { 212 | first = false; 213 | expect(cmd).to.include('Your git-log command is:'); 214 | done(); 215 | } 216 | } 217 | }); 218 | }); 219 | 220 | it('should load package.json for data', function(done) { 221 | preparing(3); 222 | 223 | conventionalChangelogCore({ 224 | pkg: { 225 | path: __dirname + '/fixtures/_package.json' 226 | } 227 | }) 228 | .pipe(through(function(chunk) { 229 | chunk = chunk.toString(); 230 | 231 | expect(chunk).to.include('## 0.0.17'); 232 | expect(chunk).to.include('Second commit'); 233 | expect(chunk).to.include('closes [#1](https://github.com/ajoslin/conventional-changelog/issues/1)'); 234 | 235 | done(); 236 | })); 237 | }); 238 | 239 | it('should load package.json for data even if repository field is missing', function(done) { 240 | preparing(3); 241 | 242 | conventionalChangelogCore({ 243 | pkg: { 244 | path: __dirname + '/fixtures/_version-only.json' 245 | } 246 | }) 247 | .pipe(through(function(chunk) { 248 | chunk = chunk.toString(); 249 | 250 | expect(chunk).to.include('## 0.0.17'); 251 | expect(chunk).to.include('Second commit'); 252 | 253 | done(); 254 | })); 255 | }); 256 | 257 | it('should fallback to use repo url if repo is repository is null', function(done) { 258 | preparing(3); 259 | 260 | conventionalChangelogCore({ 261 | pkg: { 262 | path: __dirname + '/fixtures/_host-only.json' 263 | } 264 | }, { 265 | linkReferences: true 266 | }).pipe(through(function(chunk) { 267 | chunk = chunk.toString(); 268 | 269 | expect(chunk).to.include('](https://unknown-host/commits/'); 270 | expect(chunk).to.include('closes [#1](https://unknown-host/issues/1)'); 271 | 272 | done(); 273 | })); 274 | }); 275 | 276 | it('should fallback to use repo url if repo is repository is null', function(done) { 277 | preparing(3); 278 | 279 | conventionalChangelogCore({ 280 | pkg: { 281 | path: __dirname + '/fixtures/_unknown-host.json' 282 | } 283 | }, { 284 | linkReferences: true 285 | }).pipe(through(function(chunk) { 286 | chunk = chunk.toString(); 287 | 288 | expect(chunk).to.include('](https://stash.local/scm/conventional-changelog/conventional-changelog/commits/'); 289 | expect(chunk).to.include('closes [#1](https://stash.local/scm/conventional-changelog/conventional-changelog/issues/1)'); 290 | 291 | done(); 292 | })); 293 | }); 294 | 295 | it('should transform package.json data', function(done) { 296 | preparing(3); 297 | 298 | conventionalChangelogCore({ 299 | pkg: { 300 | path: __dirname + '/fixtures/_short.json', 301 | transform: function(pkg) { 302 | pkg.version = 'v' + pkg.version; 303 | pkg.repository = 'a/b'; 304 | return pkg; 305 | } 306 | } 307 | }) 308 | .pipe(through(function(chunk) { 309 | chunk = chunk.toString(); 310 | 311 | expect(chunk).to.include('## v0.0.17'); 312 | expect(chunk).to.include('Second commit'); 313 | expect(chunk).to.include('closes [#1](https://github.com/a/b/issues/1)'); 314 | 315 | done(); 316 | })); 317 | }); 318 | 319 | it('should work in append mode', function(done) { 320 | preparing(3); 321 | 322 | conventionalChangelogCore({ 323 | append: true, 324 | }) 325 | .pipe(through(function(chunk) { 326 | chunk = chunk.toString(); 327 | 328 | expect(chunk).to.match(/Second commit[\w\W]*?\* Third commit/); 329 | 330 | done(); 331 | })); 332 | }); 333 | 334 | it('should read package.json if only `context.version` is missing', function(done) { 335 | preparing(3); 336 | 337 | conventionalChangelogCore({ 338 | pkg: { 339 | path: __dirname + '/fixtures/_package.json' 340 | } 341 | }, { 342 | host: 'github', 343 | owner: 'a', 344 | repository: 'b' 345 | }).pipe(through(function(chunk) { 346 | chunk = chunk.toString(); 347 | 348 | expect(chunk).to.include('## 0.0.17'); 349 | expect(chunk).to.include('closes [#1](github/a/b/issues/1)'); 350 | 351 | done(); 352 | })); 353 | }); 354 | 355 | it('should read the closest package.json by default', function(done) { 356 | preparing(3); 357 | 358 | conventionalChangelogCore() 359 | .pipe(through(function(chunk) { 360 | expect(chunk.toString()).to.include('closes [#1](https://github.com/conventional-changelog/conventional-changelog-core/issues/1)'); 361 | 362 | done(); 363 | })); 364 | }); 365 | 366 | it('should ignore other prefixes if an `issuePrefixes` option is not provided', function(done) { 367 | preparing(4); 368 | 369 | conventionalChangelogCore({}, { 370 | host: 'github', 371 | owner: 'b', 372 | repository: 'a' 373 | }, {}, {}).pipe(through(function(chunk) { 374 | chunk = chunk.toString(); 375 | 376 | expect(chunk).to.include('](github/b/a/commit/'); 377 | expect(chunk).to.not.include('closes [#42](github/b/a/issues/42)'); 378 | 379 | done(); 380 | })); 381 | }); 382 | 383 | it('should use custom prefixes if an `issuePrefixes` option is provided', function(done) { 384 | preparing(5); 385 | 386 | conventionalChangelogCore({}, { 387 | host: 'github', 388 | owner: 'b', 389 | repository: 'a' 390 | }, {}, { 391 | issuePrefixes: ['@'] 392 | }).pipe(through(function(chunk) { 393 | chunk = chunk.toString(); 394 | 395 | expect(chunk).to.include('](github/b/a/commit/'); 396 | expect(chunk).to.include('closes [#42](github/b/a/issues/42)'); 397 | expect(chunk).to.not.include('closes [#71](github/b/a/issues/71)'); 398 | 399 | done(); 400 | })); 401 | }); 402 | 403 | it('should read host configs if only `parserOpts.referenceActions` is missing', function(done) { 404 | preparing(5); 405 | 406 | conventionalChangelogCore({}, { 407 | host: 'github', 408 | owner: 'b', 409 | repository: 'a', 410 | issue: 'issue', 411 | commit: 'commits' 412 | }, {}, {}).pipe(through(function(chunk) { 413 | chunk = chunk.toString(); 414 | 415 | expect(chunk).to.include('](github/b/a/commits/'); 416 | expect(chunk).to.include('closes [#1](github/b/a/issue/1)'); 417 | 418 | done(); 419 | })); 420 | }); 421 | 422 | it('should read github\'s host configs', function(done) { 423 | preparing(5); 424 | 425 | conventionalChangelogCore({}, { 426 | host: 'github', 427 | owner: 'b', 428 | repository: 'a' 429 | }, {}, {}).pipe(through(function(chunk) { 430 | chunk = chunk.toString(); 431 | 432 | expect(chunk).to.include('](github/b/a/commit/'); 433 | expect(chunk).to.include('closes [#1](github/b/a/issues/1)'); 434 | 435 | done(); 436 | })); 437 | }); 438 | 439 | it('should read bitbucket\'s host configs', function(done) { 440 | preparing(5); 441 | 442 | conventionalChangelogCore({}, { 443 | host: 'bitbucket', 444 | owner: 'b', 445 | repository: 'a' 446 | }, {}, {}).pipe(through(function(chunk) { 447 | chunk = chunk.toString(); 448 | 449 | expect(chunk).to.include('](bitbucket/b/a/commits/'); 450 | expect(chunk).to.include('closes [#1](bitbucket/b/a/issue/1)'); 451 | 452 | done(); 453 | })); 454 | }); 455 | 456 | it('should read gitlab\'s host configs', function(done) { 457 | preparing(5); 458 | 459 | conventionalChangelogCore({}, { 460 | host: 'gitlab', 461 | owner: 'b', 462 | repository: 'a' 463 | }, {}, {}).pipe(through(function(chunk) { 464 | chunk = chunk.toString(); 465 | 466 | expect(chunk).to.include('](gitlab/b/a/commit/'); 467 | expect(chunk).to.include('closes [#1](gitlab/b/a/issues/1)'); 468 | 469 | done(); 470 | })); 471 | }); 472 | 473 | it('should transform the commit', function(done) { 474 | preparing(5); 475 | 476 | conventionalChangelogCore({ 477 | transform: function(chunk, cb) { 478 | chunk.header = 'A tiny header'; 479 | cb(null, chunk); 480 | } 481 | }) 482 | .pipe(through(function(chunk) { 483 | chunk = chunk.toString(); 484 | 485 | expect(chunk).to.include('A tiny header'); 486 | expect(chunk).to.not.include('Third'); 487 | 488 | done(); 489 | })); 490 | }); 491 | 492 | it('should generate all log blocks', function(done) { 493 | preparing(5); 494 | var i = 0; 495 | 496 | conventionalChangelogCore({ 497 | releaseCount: 0 498 | }) 499 | .pipe(through(function(chunk, enc, cb) { 500 | chunk = chunk.toString(); 501 | 502 | if (i === 0) { 503 | expect(chunk).to.include('Second commit'); 504 | expect(chunk).to.include('Third commit closes #1'); 505 | } else { 506 | expect(chunk).to.include('First commit'); 507 | } 508 | 509 | i++; 510 | cb(); 511 | }, function() { 512 | expect(i).to.equal(2); 513 | done(); 514 | })); 515 | }); 516 | 517 | it('should work if there are two semver tags', function(done) { 518 | preparing(6); 519 | var i = 0; 520 | 521 | conventionalChangelogCore({ 522 | releaseCount: 0 523 | }) 524 | .pipe(through(function(chunk, enc, cb) { 525 | chunk = chunk.toString(); 526 | 527 | if (i === 1) { 528 | expect(chunk).to.include('# 2.0.0'); 529 | } else if (i === 2) { 530 | expect(chunk).to.include('# 0.1.0'); 531 | } 532 | 533 | i++; 534 | cb(); 535 | }, function() { 536 | expect(i).to.equal(3); 537 | done(); 538 | })); 539 | }); 540 | 541 | it('semverTags should be attached to the `context` object', function(done) { 542 | preparing(6); 543 | var i = 0; 544 | 545 | conventionalChangelogCore({ 546 | releaseCount: 0 547 | }, {}, {}, {}, { 548 | mainTemplate: '{{gitSemverTags}} or {{gitSemverTags.[0]}}' 549 | }) 550 | .pipe(through(function(chunk, enc, cb) { 551 | chunk = chunk.toString(); 552 | 553 | expect(chunk).to.equal('v2.0.0,v0.1.0 or v2.0.0'); 554 | 555 | i++; 556 | cb(); 557 | }, function() { 558 | expect(i).to.equal(3); 559 | done(); 560 | })); 561 | }); 562 | 563 | it('should not link compare', function(done) { 564 | preparing(6); 565 | var i = 0; 566 | 567 | conventionalChangelogCore({ 568 | releaseCount: 0, 569 | append: true 570 | }, { 571 | version: '3.0.0', 572 | linkCompare: false 573 | }, {}, {}, { 574 | mainTemplate: '{{#if linkCompare}}{{previousTag}}...{{currentTag}}{{else}}Not linked{{/if}}', 575 | transform: function() { 576 | return null; 577 | } 578 | }) 579 | .pipe(through(function(chunk, enc, cb) { 580 | chunk = chunk.toString(); 581 | 582 | expect(chunk).to.equal('Not linked'); 583 | 584 | i++; 585 | cb(); 586 | }, function() { 587 | expect(i).to.equal(3); 588 | done(); 589 | })); 590 | }); 591 | 592 | it('should warn if host is not found', function(done) { 593 | preparing(6); 594 | 595 | conventionalChangelogCore({ 596 | pkg: null, 597 | warn: function(warning) { 598 | expect(warning).to.equal('Host: "no" does not exist'); 599 | 600 | done(); 601 | } 602 | }, { 603 | host: 'no' 604 | }); 605 | }); 606 | 607 | it('should warn if package.json is not found', function(done) { 608 | preparing(6); 609 | 610 | conventionalChangelogCore({ 611 | pkg: { 612 | path: 'no' 613 | }, 614 | warn: function(warning) { 615 | expect(warning).to.include('Error'); 616 | 617 | done(); 618 | } 619 | }); 620 | }); 621 | 622 | it('should warn if package.json cannot be parsed', function(done) { 623 | preparing(6); 624 | 625 | conventionalChangelogCore({ 626 | pkg: { 627 | path: __dirname + '/fixtures/_malformation.json' 628 | }, 629 | warn: function(warning) { 630 | expect(warning).to.include('Error'); 631 | 632 | done(); 633 | } 634 | }); 635 | }); 636 | 637 | it('should error if anything throws', function(done) { 638 | preparing(6); 639 | 640 | conventionalChangelogCore({ 641 | pkg: { 642 | path: __dirname + '/fixtures/_malformation.json' 643 | }, 644 | warn: function() { 645 | undefined.a = 10; 646 | } 647 | }).on('error', function(err) { 648 | expect(err).to.be.ok; // jshint ignore:line 649 | done(); 650 | }); 651 | }); 652 | 653 | it('should error if there is an error in `options.pkg.transform`', function(done) { 654 | preparing(6); 655 | 656 | conventionalChangelogCore({ 657 | pkg: { 658 | path: __dirname + '/fixtures/_short.json', 659 | transform: function() { 660 | undefined.a = 10; 661 | } 662 | } 663 | }) 664 | .on('error', function(err) { 665 | expect(err.message).to.include('undefined'); 666 | 667 | done(); 668 | }); 669 | }); 670 | 671 | it('should error if it errors in git-raw-commits', function(done) { 672 | preparing(6); 673 | 674 | conventionalChangelogCore({}, {}, { 675 | unknowOptions: false 676 | }) 677 | .on('error', function(err) { 678 | expect(err.message).to.include('Error in git-raw-commits:'); 679 | 680 | done(); 681 | }); 682 | }); 683 | 684 | it('should error if it emits an error in `options.transform`', function(done) { 685 | preparing(7); 686 | 687 | conventionalChangelogCore({ 688 | transform: function(commit, cb) { 689 | cb(new Error('error')); 690 | } 691 | }) 692 | .on('error', function(err) { 693 | expect(err.message).to.include('Error in options.transform:'); 694 | 695 | done(); 696 | }); 697 | }); 698 | 699 | it('should error if there is an error in `options.transform`', function(done) { 700 | preparing(8); 701 | 702 | conventionalChangelogCore({ 703 | transform: function() { 704 | undefined.a = 10; 705 | } 706 | }) 707 | .on('error', function(err) { 708 | expect(err.message).to.include('Error in options.transform:'); 709 | 710 | done(); 711 | }); 712 | }); 713 | 714 | it('should error if it errors in conventional-changelog-writer', function(done) { 715 | preparing(8); 716 | 717 | conventionalChangelogCore({}, {}, {}, {}, { 718 | finalizeContext: function() { 719 | return undefined.a; 720 | } 721 | }) 722 | .on('error', function(err) { 723 | expect(err.message).to.include('Error in conventional-changelog-writer:'); 724 | 725 | done(); 726 | }); 727 | }); 728 | 729 | it('should be object mode if `writerOpts.includeDetails` is `true`', function(done) { 730 | preparing(8); 731 | 732 | conventionalChangelogCore({}, {}, {}, {}, { 733 | includeDetails: true 734 | }) 735 | .pipe(through.obj(function(chunk) { 736 | expect(chunk).to.be.an('object'); 737 | done(); 738 | })); 739 | }); 740 | 741 | it('should pass `parserOpts` to conventional-commits-parser', function(done) { 742 | preparing(9); 743 | 744 | conventionalChangelogCore({}, {}, {}, { 745 | noteKeywords: [ 746 | 'Release note' 747 | ] 748 | }) 749 | .pipe(through(function(chunk, enc, cb) { 750 | chunk = chunk.toString(); 751 | 752 | expect(chunk).to.include('* test9'); 753 | expect(chunk).to.include('### Release note\n\n* super release!'); 754 | 755 | cb(); 756 | }, function() { 757 | done(); 758 | })); 759 | }); 760 | 761 | it('should pass fallback to git remote origin url', function(done) { 762 | if (semver.major(process.version) < 4) { 763 | console.log('This feature is only available under node>=4'); 764 | done(); 765 | return; 766 | } 767 | 768 | preparing(10); 769 | 770 | conventionalChangelogCore({ 771 | pkg: { 772 | path: __dirname + '/fixtures/_version-only.json' 773 | }, 774 | }) 775 | .pipe(through(function(chunk, enc, cb) { 776 | chunk = chunk.toString(); 777 | 778 | expect(chunk).to.include('https://github.com/user/repo'); 779 | expect(chunk).to.not.include('.git'); 780 | 781 | cb(); 782 | }, function() { 783 | done(); 784 | })); 785 | }); 786 | 787 | describe('finalizeContext', function() { 788 | it('should make `context.previousTag` default to a previous semver version of generated log (prepend)', function(done) { 789 | var tail = preparing(11).tail; 790 | var i = 0; 791 | 792 | conventionalChangelogCore({ 793 | releaseCount: 0 794 | }, { 795 | version: '3.0.0' 796 | }, {}, {}, { 797 | mainTemplate: '{{previousTag}}...{{currentTag}}' 798 | }) 799 | .pipe(through(function(chunk, enc, cb) { 800 | chunk = chunk.toString(); 801 | 802 | if (i === 0) { 803 | expect(chunk).to.equal('v2.0.0...v3.0.0'); 804 | } else if (i === 1) { 805 | expect(chunk).to.equal(tail + '...v2.0.0'); 806 | } 807 | 808 | i++; 809 | cb(); 810 | }, function() { 811 | expect(i).to.equal(2); 812 | done(); 813 | })); 814 | }); 815 | 816 | it('should make `context.previousTag` default to a previous semver version of generated log (append)', function(done) { 817 | var tail = preparing(11).tail; 818 | var i = 0; 819 | 820 | conventionalChangelogCore({ 821 | releaseCount: 0, 822 | append: true 823 | }, { 824 | version: '3.0.0' 825 | }, {}, {}, { 826 | mainTemplate: '{{previousTag}}...{{currentTag}}' 827 | }) 828 | .pipe(through(function(chunk, enc, cb) { 829 | chunk = chunk.toString(); 830 | 831 | if (i === 0) { 832 | expect(chunk).to.equal(tail + '...v2.0.0'); 833 | } else if (i === 1) { 834 | expect(chunk).to.equal('v2.0.0...v3.0.0'); 835 | } 836 | 837 | i++; 838 | cb(); 839 | }, function() { 840 | expect(i).to.equal(2); 841 | done(); 842 | })); 843 | }); 844 | 845 | it('`context.previousTag` and `context.currentTag` should be `null` if `keyCommit.gitTags` is not a semver', function(done) { 846 | var tail = preparing(12).tail; 847 | var i = 0; 848 | 849 | conventionalChangelogCore({ 850 | releaseCount: 0, 851 | append: true 852 | }, { 853 | version: '3.0.0' 854 | }, {}, {}, { 855 | mainTemplate: '{{previousTag}}...{{currentTag}}', 856 | generateOn: 'version' 857 | }) 858 | .pipe(through(function(chunk, enc, cb) { 859 | chunk = chunk.toString(); 860 | 861 | if (i === 0) { 862 | expect(chunk).to.equal(tail + '...v2.0.0'); 863 | } else if (i === 1) { 864 | expect(chunk).to.equal('...'); 865 | } else { 866 | expect(chunk).to.equal('v2.0.0...v3.0.0'); 867 | } 868 | 869 | i++; 870 | cb(); 871 | }, function() { 872 | expect(i).to.equal(3); 873 | done(); 874 | })); 875 | }); 876 | 877 | it('should still work if first release has no commits (prepend)', function(done) { 878 | preparing(13); 879 | var i = 0; 880 | 881 | conventionalChangelogCore({ 882 | releaseCount: 0 883 | }, { 884 | version: '3.0.0' 885 | }, {}, {}, { 886 | mainTemplate: '{{previousTag}}...{{currentTag}}', 887 | transform: function() { 888 | return null; 889 | } 890 | }) 891 | .pipe(through(function(chunk, enc, cb) { 892 | chunk = chunk.toString(); 893 | 894 | if (i === 0) { 895 | expect(chunk).to.equal('v2.0.0...v3.0.0'); 896 | } else if (i === 1) { 897 | expect(chunk).to.equal('v0.0.1...v2.0.0'); 898 | } else if (i === 2) { 899 | expect(chunk).to.equal('...v0.0.1'); 900 | } 901 | 902 | i++; 903 | cb(); 904 | }, function() { 905 | expect(i).to.equal(3); 906 | done(); 907 | })); 908 | }); 909 | 910 | it('should still work if first release has no commits (append)', function(done) { 911 | preparing(13); 912 | var i = 0; 913 | 914 | conventionalChangelogCore({ 915 | releaseCount: 0, 916 | append: true 917 | }, { 918 | version: '3.0.0' 919 | }, {}, {}, { 920 | mainTemplate: '{{previousTag}}...{{currentTag}}', 921 | transform: function() { 922 | return null; 923 | } 924 | }) 925 | .pipe(through(function(chunk, enc, cb) { 926 | chunk = chunk.toString(); 927 | 928 | if (i === 0) { 929 | expect(chunk).to.equal('...v0.0.1'); 930 | } else if (i === 1) { 931 | expect(chunk).to.equal('v0.0.1...v2.0.0'); 932 | } else if (i === 2) { 933 | expect(chunk).to.equal('v2.0.0...v3.0.0'); 934 | } 935 | 936 | i++; 937 | cb(); 938 | }, function() { 939 | expect(i).to.equal(3); 940 | done(); 941 | })); 942 | }); 943 | 944 | it('should change `context.currentTag` to last commit hash if it is unreleased', function(done) { 945 | var head = preparing(13).head; 946 | var i = 0; 947 | 948 | conventionalChangelogCore({ 949 | outputUnreleased: true 950 | }, { 951 | version: '2.0.0' 952 | }, {}, {}, { 953 | mainTemplate: '{{previousTag}}...{{currentTag}}' 954 | }) 955 | .pipe(through(function(chunk, enc, cb) { 956 | chunk = chunk.toString(); 957 | 958 | expect(chunk).to.equal('v2.0.0...' + head); 959 | 960 | i++; 961 | cb(); 962 | }, function() { 963 | expect(i).to.equal(1); 964 | done(); 965 | })); 966 | }); 967 | 968 | it('should not link compare if previousTag is not truthy', function(done) { 969 | preparing(13); 970 | var i = 0; 971 | 972 | conventionalChangelogCore({ 973 | releaseCount: 0, 974 | append: true 975 | }, { 976 | version: '3.0.0' 977 | }, {}, {}, { 978 | mainTemplate: '{{#if linkCompare}}{{previousTag}}...{{currentTag}}{{else}}Not linked{{/if}}', 979 | transform: function() { 980 | return null; 981 | } 982 | }) 983 | .pipe(through(function(chunk, enc, cb) { 984 | chunk = chunk.toString(); 985 | 986 | if (i === 0) { 987 | expect(chunk).to.equal('Not linked'); 988 | } else if (i === 1) { 989 | expect(chunk).to.equal('v0.0.1...v2.0.0'); 990 | } else if (i === 2) { 991 | expect(chunk).to.equal('v2.0.0...v3.0.0'); 992 | } 993 | 994 | i++; 995 | cb(); 996 | }, function() { 997 | expect(i).to.equal(3); 998 | done(); 999 | })); 1000 | }); 1001 | }); 1002 | 1003 | describe('config', function() { 1004 | var config = { 1005 | context: { 1006 | version: 'v100.0.0' 1007 | } 1008 | }; 1009 | 1010 | var promise = new Promise(function(resolve) { 1011 | resolve(config); 1012 | }); 1013 | 1014 | var fn = function(cb) { 1015 | cb(null, config); 1016 | }; 1017 | 1018 | it('should load object config', function(done) { 1019 | conventionalChangelogCore({ 1020 | config: config, 1021 | pkg: { 1022 | path: __dirname + '/fixtures/_package.json' 1023 | } 1024 | }) 1025 | .pipe(through(function(chunk, enc, cb) { 1026 | chunk = chunk.toString(); 1027 | 1028 | expect(chunk).to.include('v100.0.0'); 1029 | 1030 | cb(); 1031 | }, function() { 1032 | done(); 1033 | })); 1034 | }); 1035 | 1036 | it('should load promise config', function(done) { 1037 | conventionalChangelogCore({ 1038 | config: promise 1039 | }) 1040 | .pipe(through(function(chunk, enc, cb) { 1041 | chunk = chunk.toString(); 1042 | 1043 | expect(chunk).to.include('v100.0.0'); 1044 | 1045 | cb(); 1046 | }, function() { 1047 | done(); 1048 | })); 1049 | }); 1050 | 1051 | it('should load function config', function(done) { 1052 | conventionalChangelogCore({ 1053 | config: fn 1054 | }) 1055 | .pipe(through(function(chunk, enc, cb) { 1056 | chunk = chunk.toString(); 1057 | 1058 | expect(chunk).to.include('v100.0.0'); 1059 | 1060 | cb(); 1061 | }, function() { 1062 | done(); 1063 | })); 1064 | }); 1065 | 1066 | it('should warn if config errors', function(done) { 1067 | conventionalChangelogCore({ 1068 | config: new Promise(function(solve, reject) { 1069 | reject('config error'); 1070 | }), 1071 | warn: function(warning) { 1072 | expect(warning).to.include('config error'); 1073 | 1074 | done(); 1075 | } 1076 | }); 1077 | }); 1078 | }); 1079 | 1080 | describe('unreleased', function() { 1081 | it('should not output unreleased', function(done) { 1082 | preparing(14); 1083 | 1084 | conventionalChangelogCore({}, { 1085 | version: '1.0.0' 1086 | }) 1087 | .pipe(through(function() { 1088 | done(new Error('should not output unreleased')); 1089 | }, function() { 1090 | done(); 1091 | })); 1092 | }); 1093 | 1094 | it('should output unreleased', function(done) { 1095 | preparing(15); 1096 | 1097 | conventionalChangelogCore({ 1098 | outputUnreleased: true 1099 | }, { 1100 | version: 'v1.0.0' 1101 | }) 1102 | .pipe(through(function(chunk, enc, cb) { 1103 | chunk = chunk.toString(); 1104 | 1105 | expect(chunk).to.include('something unreleased yet :)'); 1106 | expect(chunk).to.include('Unreleased'); 1107 | 1108 | cb(); 1109 | }, function() { 1110 | done(); 1111 | })); 1112 | }); 1113 | }); 1114 | }); 1115 | --------------------------------------------------------------------------------