diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..404abb2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +coverage/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8e267dd --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + extends: "eslint-config-lob" +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..65e3ba2 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +test/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..383e4c4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: node_js +sudo: false +node_js: + - '0.10' + - '0.12' + - '4' + - '5' +after_script: + - npm run lint + - npm run enforce + - npm run coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0ac00db --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +We encourage any form of contribution, whether that be issues, comments, or pull requests. If you are going to be submitting a PR, there are a few things we would appreciate that you do to keep the codebase clean: + +* **Write tests.** We enforce 100% code coverage on this repo so any new code that gets written should have accompanying tests. +* **Follow the linter.** We use our [ESLint configuration](https://github.com/lob/eslint-config-lob), and we run `npm run lint` in our Travis builds. +* **Ask questions if you aren't sure.** If you have any questions while implementing a fix or feature, feel free to create an issue and ask us. We're happy to help! diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index 5d8fba6..87313b1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,96 @@ # Generate Changelog +[![Build Status](https://travis-ci.org/lob/generate-changelog.svg)](https://travis-ci.org/lob/generate-changelog) +[![Coverage Status](https://coveralls.io/repos/lob/generate-changelog/badge.svg?branch=master&service=github)](https://coveralls.io/github/lob/generate-changelog?branch=master) + Generate a changelog from git commits. This is meant to be used so that for every patch, minor, or major version, you update the changelog, run `npm version`, and then the git tag refers to the commit that updated both the changelog and version. + +## Installation + +You can either install this module globally to be used for all of your repos on your local machine, or you can install it as a dev dependency to be referenced in your npm scripts. + +```bash +$ npm i generate-changelog -g # install it globally +# OR +$ npm i generate-changelog -D # install it as a dev dependency +``` + +## Usage + +To use this module, your commit messages have to be in this format: + +``` +type(category): description +``` + +Where `type` is one of the following: + +* `chore` +* `docs` +* `feat` +* `fix` +* `refactor` +* `style` +* `test` + +And `category` can be anything of your choice. + +You can either run this module as a CLI app that prints to stdout (recommended): + +```bash +$ changelog -h + + + Usage: generate [options] + + Generate a changelog from git commits. + + Options: + + -h, --help output usage information + -V, --version output the version number + -p, --patch create a patch changelog + -m, --minor create a minor changelog + -M, --major create a major changelog + -u, --repo-url [url] specify the repo URL for commit links + +``` + +Or you can write a script that calls the `generate` function: + +```js +var Changelog = require('generate-changelog'); +var File = require('fs'); + +return Changelog.generate({ patch: true, repoUrl: 'https://github.com/lob/generate-changelog' }) +.then(function (changelog) { + File.writeFileSync('./CHANGELOG.md', changelog); +}); +``` + +### Recommended + +The way that I would recommend using this module would be the way it's being used in this module: as npm scripts. You should install it as a dev dependency and then add the following to the `scripts` object in your `package.json`: + +```json + "changelog:major": "./bin/generate -M >> CHANGELOG.md && git commit -am 'updated CHANGELOG.md' && npm version major && git push origin && git push origin --tags", + "changelog:minor": "./bin/generate -m >> CHANGELOG.md && git commit -am 'updated CHANGELOG.md' && npm version minor && git push origin && git push origin --tags", + "changelog:patch": "./bin/generate -p >> CHANGELOG.md && git commit -am 'updated CHANGELOG.md' && npm version patch && git push origin && git push origin --tags", +``` + +## Testing + +To run the test suite, just clone the repository and run the following: + +```bash +$ npm i +$ npm test +``` + +## Contributing + +To contribute, please see the [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## License + +This project is released under the MIT license, which can be found in [`LICENSE.txt`](LICENSE.txt). diff --git a/bin/generate b/bin/generate new file mode 100755 index 0000000..0e5cc96 --- /dev/null +++ b/bin/generate @@ -0,0 +1,15 @@ +#!/usr/bin/env node +'use strict'; + +var CLI = require('../lib/cli'); +var Changelog = require('../lib'); + +CLI.parse(process.argv); + +return Changelog.generate(CLI) +.then(function (changelog) { + console.log(changelog); +}) +.catch(function (err) { + console.log(err); +}); diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..f65cccf --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,13 @@ +'use strict'; + +var CLI = require('commander'); + +var Package = require('../package'); + +module.exports = CLI + .description('Generate a changelog from git commits.') + .version(Package.version) + .option('-p, --patch', 'create a patch changelog') + .option('-m, --minor', 'create a minor changelog') + .option('-M, --major', 'create a major changelog') + .option('-u, --repo-url [url]', 'specify the repo URL for commit links'); diff --git a/lib/git.js b/lib/git.js new file mode 100644 index 0000000..44f540e --- /dev/null +++ b/lib/git.js @@ -0,0 +1,58 @@ +'use strict'; + +var Bluebird = require('bluebird'); +var CP = Bluebird.promisifyAll(require('child_process')); + +var SEPARATOR = '===END==='; +var COMMIT_PATTERN = /^(\w*)(\(([\w\$\.\-\* ]*)\))?\: (.*)$/; +var FORMAT = '%H%n%s%n%b%n' + SEPARATOR; + +/** + * Get all commits from the last tag (or the first commit if no tags). + * @returns {Promise>} array of parsed commit objects + */ +exports.getCommits = function () { + return CP.execAsync('git describe --tags --abbrev=0') + .catch(function () { + return ''; + }) + .then(function (tag) { + tag = tag.toString().trim(); + var revisions = tag ? tag + '..HEAD' : ''; + + return CP.execAsync('git log -E --format=' + FORMAT + ' ' + revisions); + }) + .catch(function () { + throw new Error('no commits found'); + }) + .then(function (commits) { + return commits.split('\n' + SEPARATOR + '\n'); + }) + .map(function (raw) { + if (!raw) { + return null; + } + + var lines = raw.split('\n'); + var commit = {}; + + commit.hash = lines.shift(); + commit.subject = lines.shift(); + commit.body = lines.join('\n'); + + var parsed = commit.subject.match(COMMIT_PATTERN); + + if (!parsed || !parsed[1] || !parsed[4]) { + return null; + } + + commit.type = parsed[1]; + commit.category = parsed[3]; + commit.subject = parsed[4]; + + return commit; + }) + .filter(function (commit) { + return commit !== null; + }); +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..7a01483 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,29 @@ +'use strict'; + +var Bluebird = require('bluebird'); + +var Git = require('./git'); +var Package = require('./package'); +var Writer = require('./writer'); + +/** + * Generate the changelog. + * @param {Object} options - generation options + * @param {Boolean} options.patch - whether it should be a patch changelog + * @param {Boolean} options.minor - whether it should be a minor changelog + * @param {Boolean} options.major - whether it should be a major changelog + * @param {String} options.repoUrl - repo URL that will be used when linking commits + * @returns {Promise} the \n separated changelog string + */ +exports.generate = function (options) { + return Bluebird.all([ + Package.extractRepoUrl(), + Package.calculateNewVersion(options), + Git.getCommits() + ]) + .spread(function (repoUrl, version, commits) { + options.repoUrl = options.repoUrl || repoUrl; + + return Writer.markdown(version, commits, options); + }); +}; diff --git a/lib/package.js b/lib/package.js new file mode 100644 index 0000000..62a5f04 --- /dev/null +++ b/lib/package.js @@ -0,0 +1,72 @@ +'use strict'; + +var Bluebird = require('bluebird'); +var File = Bluebird.promisifyAll(require('fs')); +var ParseGitHubUrl = require('github-url-from-git'); + +/** + * Get the package.json object located in the current directory. + * @returns {Promise} package.json object + */ +exports.getUserPackage = function () { + var userPackagePath = process.cwd() + '/package.json'; + + return File.statAsync(userPackagePath) + .then(function () { + return require(userPackagePath); + }) + .catch(function () { + throw new Error('valid package.json not found'); + }); +}; + +/** + * Grabs the repository URL if it exists in the package.json. + * @returns {Promise} the repository URL or null if it doesn't exist + */ +exports.extractRepoUrl = function () { + return exports.getUserPackage() + .then(function (userPackage) { + var url = userPackage.repository && userPackage.repository.url; + + if (typeof url !== 'string') { + return null; + } + + if (url.indexOf('github') === -1) { + return url; + } else { + return ParseGitHubUrl(url); + } + }); +}; + +/** + * Calculate the new semver version depending on the options. + * @param {Object} options - calculation options + * @param {Boolean} options.patch - whether it should be a patch version + * @param {Boolean} options.minor - whether it should be a minor version + * @param {Boolean} options.major - whether it should be a major version + * @returns {Promise} - new version + */ +exports.calculateNewVersion = function (options) { + return exports.getUserPackage() + .then(function (userPackage) { + var split = userPackage.version.split('.'); + + if (options.major) { + split[0] = (parseInt(split[0]) + 1).toString(); + split[1] = '0'; + split[2] = '0'; + } else if (options.minor) { + split[1] = (parseInt(split[1]) + 1).toString(); + split[2] = '0'; + } else if (options.patch) { + split[2] = (parseInt(split[2]) + 1).toString(); + } else { + throw new Error('patch, minor, or major needs to be set'); + } + + return split.join('.'); + }); +}; diff --git a/lib/writer.js b/lib/writer.js new file mode 100644 index 0000000..f40ba9d --- /dev/null +++ b/lib/writer.js @@ -0,0 +1,94 @@ +'use strict'; + +var Bluebird = require('bluebird'); + +var DEFAULT_TYPE = 'Other Changes'; +var TYPES = { + chore: 'Chores', + docs: 'Documentation Changes', + feat: 'New Features', + fix: 'Bug Fixes', + refactor: 'Refactors', + style: 'Code Style Changes', + test: 'Tests' +}; + +/** + * Generate the markdown for the changelog. + * @param {String} version - the new version affiliated to this changelog + * @param {Array} commits - array of parsed commit objects + * @param {Object} options - generation options + * @param {Boolean} options.patch - whether it should be a patch changelog + * @param {Boolean} options.minor - whether it should be a minor changelog + * @param {Boolean} options.major - whether it should be a major changelog + * @param {String} options.repoUrl - repo URL that will be used when linking commits + * @returns {Promise} the \n separated changelog string + */ +exports.markdown = function (version, commits, options) { + var content = []; + var now = new Date(); + var date = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate(); + var heading; + + if (options.major) { + heading = '##'; + } else if (options.minor) { + heading = '###'; + } else { + heading = '####'; + } + + heading += ' ' + version + ' (' + date + ')'; + + content.push(heading); + content.push(''); + + return Bluebird.resolve(commits) + .bind({ types: {} }) + .each(function (commit) { + var type = commit.type; + var category = commit.category; + + this.types[type] = this.types[type] || {}; + this.types[type][category] = this.types[type][category] || []; + + this.types[type][category].push(commit); + }) + .then(function () { + return Object.keys(this.types).sort(); + }) + .each(function (type) { + var types = this.types; + + content.push('##### ' + (TYPES[type] || DEFAULT_TYPE)); + content.push(''); + + Object.keys(this.types[type]).forEach(function (category) { + var prefix = '*'; + var nested = types[type][category].length > 1; + var categoryHeading = '* **' + category + ':**'; + + if (nested) { + content.push(categoryHeading); + prefix = ' *'; + } else { + prefix = categoryHeading; + } + + types[type][category].forEach(function (commit) { + var hash = commit.hash.substring(0, 8); + + if (options.repoUrl) { + hash = '[' + hash + '](' + options.repoUrl + '/commit/' + hash + ')'; + } + + content.push(prefix + ' ' + commit.subject + ' (' + hash + ')'); + }); + }); + + content.push(''); + }) + .then(function () { + return content.join('\n'); + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c5a1cb --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "generate-changelog", + "version": "0.0.1", + "description": "Generate a changelog from git commits.", + "bin": { + "generate-changelog": "./bin/generate", + "changelog": "./bin/generate" + }, + "main": "./lib/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "changelog:major": "./bin/generate -M >> CHANGELOG.md && git commit -am 'updated CHANGELOG.md' && npm version major && git push origin && git push origin --tags", + "changelog:minor": "./bin/generate -m >> CHANGELOG.md && git commit -am 'updated CHANGELOG.md' && npm version minor && git push origin && git push origin --tags", + "changelog:patch": "./bin/generate -p >> CHANGELOG.md && git commit -am 'updated CHANGELOG.md' && npm version patch && git push origin && git push origin --tags", + "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", + "enforce": "istanbul check-coverage --statement 100 --branch 100 --function 100 --lines 100", + "lint": "eslint .", + "test": "NODE_ENV=test istanbul cover _mocha -- test --recursive --timeout 30000", + "test-no-cover": "NODE_ENV=test mocha test --recursive --timeout 30000" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lob/generate-changelog.git" + }, + "keywords": [ + "changelog", + "cli", + "npm", + "version", + "git", + "semver" + ], + "author": "Lob ", + "license": "MIT", + "bugs": { + "url": "https://github.com/lob/generate-changelog/issues" + }, + "homepage": "https://github.com/lob/generate-changelog#readme", + "dependencies": { + "bluebird": "^3.0.6", + "commander": "^2.9.0", + "github-url-from-git": "^1.4.0" + }, + "devDependencies": { + "chai": "^3.4.1", + "chai-as-promised": "^5.1.0", + "coveralls": "^2.11.6", + "eslint": "^1.10.3", + "eslint-config-lob": "^1.0.1", + "istanbul": "^0.4.1", + "mocha": "^2.3.4", + "sinon": "^1.17.2" + }, + "engine": { + "node": ">= 0.10.x" + } +} diff --git a/test/data/git/invalid-commits.txt b/test/data/git/invalid-commits.txt new file mode 100644 index 0000000..d27c1c2 --- /dev/null +++ b/test/data/git/invalid-commits.txt @@ -0,0 +1,8 @@ +ada45fed0bf5a9bbdcf32e1c46176c4cbdcef329 +Merge pull request #1 from lob/testing +malformed commit +===END=== +a2c164a718e45d7d3c5e37eec2008a3ebf371e93 +malformed commit + +===END=== diff --git a/test/data/git/valid-commits.txt b/test/data/git/valid-commits.txt new file mode 100644 index 0000000..06c91a0 --- /dev/null +++ b/test/data/git/valid-commits.txt @@ -0,0 +1,8 @@ +ada45fed0bf5a9bbdcf32e1c46176c4cbdcef329 +Merge pull request #1 from lob/testing +feat(testing): did some testing +===END=== +a2c164a718e45d7d3c5e37eec2008a3ebf371e93 +feat(testing): did some testing + +===END=== diff --git a/test/data/package/invalid/package.json b/test/data/package/invalid/package.json new file mode 100644 index 0000000..39a1b34 --- /dev/null +++ b/test/data/package/invalid/package.json @@ -0,0 +1 @@ +invalid json diff --git a/test/data/package/valid/package.json b/test/data/package/valid/package.json new file mode 100644 index 0000000..d6ab62c --- /dev/null +++ b/test/data/package/valid/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "version": "1.0.0", + "main": "index.js", + "license": "MIT" +} diff --git a/test/git.test.js b/test/git.test.js new file mode 100644 index 0000000..87b3c5d --- /dev/null +++ b/test/git.test.js @@ -0,0 +1,88 @@ +'use strict'; + +var Bluebird = require('bluebird'); +var CP = require('child_process'); +var Expect = require('chai').expect; +var File = require('fs'); +var Sinon = require('sinon'); + +var Git = require('../lib/git'); + +var VALID_COMMITS = File.readFileSync(__dirname + '/data/git/valid-commits.txt', 'utf-8'); +var INVALID_COMMITS = File.readFileSync(__dirname + '/data/git/invalid-commits.txt', 'utf-8'); + +describe('git', function () { + + describe('getCommits', function () { + + it('passes in revisions if there is a previous tag', function () { + Sinon.stub(CP, 'execAsync') + .onFirstCall().returns(Bluebird.resolve('v1.2.3')) + .onSecondCall().returns(Bluebird.resolve(VALID_COMMITS)); + + return Git.getCommits() + .then(function () { + CP.execAsync.secondCall.calledWithMatch(/HEAD/); + CP.execAsync.restore(); + }); + }); + + it('does not pass in revisions if there are no previous tags', function () { + Sinon.stub(CP, 'execAsync') + .onFirstCall().returns(Bluebird.reject()) + .onSecondCall().returns(Bluebird.resolve(VALID_COMMITS)); + + return Git.getCommits() + .then(function () { + CP.execAsync.secondCall.notCalledWithMatch(/HEAD/); + CP.execAsync.restore(); + }); + }); + + it('errs if there are no commits yet', function () { + Sinon.stub(CP, 'execAsync') + .onFirstCall().returns(Bluebird.resolve('v1.2.3')) + .onSecondCall().returns(Bluebird.reject()); + + return Git.getCommits() + .bind({}) + .catch(function (err) { + this.err = err; + }) + .finally(function () { + Expect(this.err).to.be.instanceof(Error); + Expect(this.err.message).to.eql('no commits found'); + CP.execAsync.restore(); + }); + }); + + it('correctly parses type, category, and subject', function () { + Sinon.stub(CP, 'execAsync') + .onFirstCall().returns(Bluebird.resolve('v1.2.3')) + .onSecondCall().returns(Bluebird.resolve(VALID_COMMITS)); + + return Git.getCommits() + .then(function (commits) { + Expect(commits).to.have.length(1); + Expect(commits[0]).to.have.property('type'); + Expect(commits[0]).to.have.property('category'); + Expect(commits[0]).to.have.property('subject'); + CP.execAsync.restore(); + }); + }); + + it('skips malformed commits', function () { + Sinon.stub(CP, 'execAsync') + .onFirstCall().returns(Bluebird.resolve('v1.2.3')) + .onSecondCall().returns(Bluebird.resolve(INVALID_COMMITS)); + + return Git.getCommits() + .then(function (commits) { + Expect(commits).to.have.length(0); + CP.execAsync.restore(); + }); + }); + + }); + +}); diff --git a/test/package.test.js b/test/package.test.js new file mode 100644 index 0000000..e71d916 --- /dev/null +++ b/test/package.test.js @@ -0,0 +1,160 @@ +'use strict'; + +var Bluebird = require('bluebird'); +var Expect = require('chai').expect; +var Sinon = require('sinon'); + +var Package = require('../lib/package'); + +var CURRENT_DIRECTORY = process.cwd(); +var PACKAGE_DIRECTORY = '/test/data/package'; + +describe('package', function () { + + describe('getUserPackage', function () { + + afterEach(function () { + process.chdir(CURRENT_DIRECTORY); + }); + + it('pull the package.json from the current directory', function () { + process.chdir(CURRENT_DIRECTORY + PACKAGE_DIRECTORY + '/valid'); + + return Package.getUserPackage() + .then(function (userPackage) { + Expect(userPackage.name).to.eql('test'); + Expect(userPackage.version).to.eql('1.0.0'); + }); + }); + + it('errs if there is no package.json', function () { + process.chdir(CURRENT_DIRECTORY + PACKAGE_DIRECTORY); + + return Package.getUserPackage() + .bind({}) + .catch(function (err) { + this.err = err; + }) + .finally(function () { + Expect(this.err).to.be.instanceof(Error); + Expect(this.err.message).to.eql('valid package.json not found'); + }); + }); + + it('errs if the package.json is invalid', function () { + process.chdir(CURRENT_DIRECTORY + PACKAGE_DIRECTORY + '/invalid'); + + return Package.getUserPackage() + .bind({}) + .catch(function (err) { + this.err = err; + }) + .finally(function () { + Expect(this.err).to.be.instanceof(Error); + Expect(this.err.message).to.eql('valid package.json not found'); + }); + }); + + }); + + describe('extractRepoUrl', function () { + + it('returns null if there is no repo URL', function () { + var repo = null; + + Sinon.stub(Package, 'getUserPackage').returns(Bluebird.resolve({ repository: repo })); + + return Package.extractRepoUrl() + .then(function (url) { + Expect(url).to.be.null; + }) + .finally(function () { + Package.getUserPackage.restore(); + }); + }); + + it('returns the raw URL if it is not a GitHub URL', function () { + var repo = { url: 'https://bitbucket.org/lob/generate-changelog' }; + + Sinon.stub(Package, 'getUserPackage').returns(Bluebird.resolve({ repository: repo })); + + return Package.extractRepoUrl() + .then(function (url) { + Expect(url).to.eql(repo.url); + }) + .finally(function () { + Package.getUserPackage.restore(); + }); + }); + + it('correctly parses a GitHub URL', function () { + var parsedUrl = 'https://github.com/lob/generate-changelog'; + var repo = { url: 'git+https://github.com/lob/generate-changelog.git' }; + + Sinon.stub(Package, 'getUserPackage').returns(Bluebird.resolve({ repository: repo })); + + return Package.extractRepoUrl() + .then(function (url) { + Expect(url).to.eql(parsedUrl); + }) + .finally(function () { + Package.getUserPackage.restore(); + }); + }); + + }); + + describe('calculateNewVersion', function () { + + beforeEach(function () { + Sinon.stub(Package, 'getUserPackage').returns(Bluebird.resolve({ version: '1.2.3' })); + }); + + afterEach(function () { + Package.getUserPackage.restore(); + }); + + it('bumps the patch version if patch is true', function () { + var options = { patch: true }; + + return Package.calculateNewVersion(options) + .then(function (version) { + Expect(version).to.eql('1.2.4'); + }); + }); + + it('bumps the minor version if minor is true', function () { + var options = { minor: true }; + + return Package.calculateNewVersion(options) + .then(function (version) { + Expect(version).to.eql('1.3.0'); + }); + }); + + it('bumps the major version if major is true', function () { + var options = { major: true }; + + return Package.calculateNewVersion(options) + .then(function (version) { + Expect(version).to.eql('2.0.0'); + }); + }); + + it('errs if patch, minor, and major are all false', function () { + var options = {}; + + return Package.calculateNewVersion(options) + .bind({}) + .catch(function (err) { + this.err = err; + }) + .finally(function () { + Expect(this.err).to.be.instanceof(Error); + Expect(this.err.message).to.eql('patch, minor, or major needs to be set'); + }); + }); + + }); + +}); diff --git a/test/setup.test.js b/test/setup.test.js new file mode 100644 index 0000000..2cf67d4 --- /dev/null +++ b/test/setup.test.js @@ -0,0 +1,5 @@ +'use strict'; + +var Chai = require('chai'); + +Chai.use(require('chai-as-promised')); diff --git a/test/writer.test.js b/test/writer.test.js new file mode 100644 index 0000000..3ca1f70 --- /dev/null +++ b/test/writer.test.js @@ -0,0 +1,169 @@ +'use strict'; + +var Expect = require('chai').expect; + +var Writer = require('../lib/writer'); + +var VERSION = '1.2.3'; + +describe('writer', function () { + + describe('markdown', function () { + + it('makes heading h2 if major version', function () { + var options = { major: true }; + + return Writer.markdown(VERSION, [], options) + .then(function (changelog) { + var heading = changelog.split('\n')[0]; + + Expect(heading).to.contain('## ' + VERSION); + }); + }); + + it('makes heading h3 if minor version', function () { + var options = { minor: true }; + + return Writer.markdown(VERSION, [], options) + .then(function (changelog) { + var heading = changelog.split('\n')[0]; + + Expect(heading).to.contain('### ' + VERSION); + }); + }); + + it('makes heading h4 if minor version', function () { + var options = { patch: true }; + + return Writer.markdown(VERSION, [], options) + .then(function (changelog) { + var heading = changelog.split('\n')[0]; + + Expect(heading).to.contain('#### ' + VERSION); + }); + }); + + it('flushes out a commit type with its full name', function () { + var commits = [ + { type: 'feat', category: 'testing', subject: 'did some testing', hash: '1234567890' } + ]; + + return Writer.markdown(VERSION, commits, {}) + .then(function (changelog) { + return changelog.split('\n'); + }) + .filter(function (line) { + return line.indexOf('#####') > -1; + }) + .get(0) + .then(function (line) { + Expect(line).to.eql('##### New Features'); + }); + }); + + it('uses the default type name for uncommon types', function () { + var commits = [ + { type: 'uncommon', category: 'testing', subject: 'did some testing', hash: '1234567890' } + ]; + + return Writer.markdown(VERSION, commits, {}) + .then(function (changelog) { + return changelog.split('\n'); + }) + .filter(function (line) { + return line.indexOf('#####') > -1; + }) + .get(0) + .then(function (line) { + Expect(line).to.eql('##### Other Changes'); + }); + }); + + it('keeps a commit category on one line if there is only one commit in it', function () { + var category = 'testing'; + var subject = 'did some testing'; + var commits = [ + { type: 'feat', category: category, subject: subject, hash: '1234567890' } + ]; + + return Writer.markdown(VERSION, commits, {}) + .then(function (changelog) { + return changelog.split('\n'); + }) + .filter(function (line) { + return line.indexOf(category) > -1; + }) + .get(0) + .then(function (line) { + var regex = new RegExp('^\\* \\*\\*' + category + ':\\*\\* ' + subject); + + Expect(line).to.match(regex); + }); + }); + + it('breaks a commit category onto its own line if there are more than one commit in it', function () { + var category = 'testing'; + var commits = [ + { type: 'feat', category: category, subject: 'did some testing', hash: '1234567890' }, + { type: 'feat', category: category, subject: 'did some more testing', hash: '1234567890' } + ]; + + return Writer.markdown(VERSION, commits, {}) + .then(function (changelog) { + return changelog.split('\n'); + }) + .filter(function (line) { + return line.indexOf(category) > -1; + }) + .get(0) + .then(function (line) { + var regex = new RegExp('^\\* \\*\\*' + category + ':\\*\\*$'); + + Expect(line).to.match(regex); + }); + }); + + it('trims the commit hash to only 8 chars', function () { + var category = 'testing'; + var hash = '1234567890'; + var commits = [ + { type: 'feat', category: category, subject: 'did some testing', hash: hash } + ]; + + return Writer.markdown(VERSION, commits, {}) + .then(function (changelog) { + return changelog.split('\n'); + }) + .filter(function (line) { + return line.indexOf(category) > -1; + }) + .get(0) + .then(function (line) { + Expect(line).to.not.contain(hash); + Expect(line).to.contain(hash.substring(0, 8)); + }); + }); + + it('wraps the hash in a link if a repoUrl is provided', function () { + var category = 'testing'; + var url = 'https://github.com/lob/generate-changelog' + var commits = [ + { type: 'feat', category: category, subject: 'did some testing', hash: '1234567890' } + ]; + + return Writer.markdown(VERSION, commits, { repoUrl: url }) + .then(function (changelog) { + return changelog.split('\n'); + }) + .filter(function (line) { + return line.indexOf(category) > -1; + }) + .get(0) + .then(function (line) { + Expect(line).to.contain(url); + }); + }); + + }); + +});