Skip to content

Commit

Permalink
feat(cli): generate changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
Robin Joseph committed Dec 13, 2015
1 parent 2a1734b commit c164cff
Show file tree
Hide file tree
Showing 23 changed files with 910 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage/
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
extends: "eslint-config-lob"
}
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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!
File renamed without changes.
93 changes: 93 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
15 changes: 15 additions & 0 deletions bin/generate
Original file line number Diff line number Diff line change
@@ -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);
});
13 changes: 13 additions & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -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');
58 changes: 58 additions & 0 deletions lib/git.js
Original file line number Diff line number Diff line change
@@ -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<Object>>} 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;
});
};
29 changes: 29 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -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<String>} 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);
});
};
72 changes: 72 additions & 0 deletions lib/package.js
Original file line number Diff line number Diff line change
@@ -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<Object>} 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<String|Null>} 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<String>} - 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('.');
});
};
Loading

0 comments on commit c164cff

Please sign in to comment.