From 1b170158ce23f101dab3f572555f9d2ceaa74092 Mon Sep 17 00:00:00 2001 From: Cole Gleason Date: Thu, 16 Jan 2014 01:01:00 -0600 Subject: [PATCH 1/2] chore(changelog): add commit-msg hook to verify commit messages This adds a Git commit-msg hook which runs after a commit message is written. It verifies that is matches a certain spec as defined by the AngularJS document here: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# This will allow for autmoated generation of Changelog.md in the future. Migration notes: Please run `ln -sf ../../validate-commit-msg.js .git/hooks/commit-msg` to install the commit hook. --- validate-commit-msg.js | 107 ++++++++++++++++++++++++++++++++++++ validate-commit-msg.spec.js | 77 ++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100755 validate-commit-msg.js create mode 100644 validate-commit-msg.spec.js diff --git a/validate-commit-msg.js b/validate-commit-msg.js new file mode 100755 index 0000000000..6803320243 --- /dev/null +++ b/validate-commit-msg.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Git COMMIT-MSG hook for validating commit message + * From: https://github.com/angular/angular.js + * See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit + * + * Installation: + * >> cd + * >> ln -s ../../validate-commit-msg.js .git/hooks/commit-msg + */ +var fs = require('fs'); +var util = require('util'); + + +var MAX_LENGTH = 100; +var PATTERN = /^(?:fixup!\s*)?(\w*)(\(([\w\$\.\-\*/]*)\))?\: (.*)$/; +var IGNORED = /^WIP\:/; +var TYPES = { + feat: true, + fix: true, + docs: true, + style: true, + refactor: true, + perf: true, + test: true, + chore: true, + revert: true +}; + + +var error = function() { + // gitx does not display it + // http://gitx.lighthouseapp.com/projects/17830/tickets/294-feature-display-hook-error-message-when-hook-fails + // https://groups.google.com/group/gitx/browse_thread/thread/a03bcab60844b812 + console.error('INVALID COMMIT MSG: ' + util.format.apply(null, arguments)); +}; + + +var validateMessage = function(message) { + var isValid = true; + + if (IGNORED.test(message)) { + console.log('Commit message validation ignored.'); + return true; + } + + if (message.length > MAX_LENGTH) { + error('is longer than %d characters !', MAX_LENGTH); + isValid = false; + } + + var match = PATTERN.exec(message); + + if (!match) { + error('does not match "(): " ! was: ' + message); + return false; + } + + var type = match[1]; + var scope = match[3]; + var subject = match[4]; + + if (!TYPES.hasOwnProperty(type)) { + error('"%s" is not allowed type !', type); + return false; + } + + // Some more ideas, do want anything like this ? + // - allow only specific scopes (eg. fix(docs) should not be allowed ? + // - auto correct the type to lower case ? + // - auto correct first letter of the subject to lower case ? + // - auto add empty line after subject ? + // - auto remove empty () ? + // - auto correct typos in type ? + // - store incorrect messages, so that we can learn + + return isValid; +}; + + +var firstLineFromBuffer = function(buffer) { + return buffer.toString().split('\n').shift(); +}; + + + +// publish for testing +exports.validateMessage = validateMessage; + +// hacky start if not run by jasmine :-D +if (process.argv.join('').indexOf('jasmine-node') === -1) { + var commitMsgFile = process.argv[2]; + var incorrectLogFile = commitMsgFile.replace('COMMIT_EDITMSG', 'logs/incorrect-commit-msgs'); + + fs.readFile(commitMsgFile, function(err, buffer) { + var msg = firstLineFromBuffer(buffer); + + if (!validateMessage(msg)) { + fs.appendFile(incorrectLogFile, msg + '\n', function() { + process.exit(1); + }); + } else { + process.exit(0); + } + }); +} diff --git a/validate-commit-msg.spec.js b/validate-commit-msg.spec.js new file mode 100644 index 0000000000..cc4b4c3c5b --- /dev/null +++ b/validate-commit-msg.spec.js @@ -0,0 +1,77 @@ +describe('validate-commit-msg.js', function() { + var m = require('./validate-commit-msg'); + var errors = []; + var logs = []; + + var VALID = true; + var INVALID = false; + + beforeEach(function() { + errors.length = 0; + logs.length = 0; + + spyOn(console, 'error').andCallFake(function(msg) { + errors.push(msg.replace(/\x1B\[\d+m/g, '')); // uncolor + }); + + spyOn(console, 'log').andCallFake(function(msg) { + logs.push(msg.replace(/\x1B\[\d+m/g, '')); // uncolor + }); + }); + + describe('validateMessage', function() { + + it('should be valid', function() { + expect(m.validateMessage('fixup! fix($compile): something')).toBe(VALID); + expect(m.validateMessage('fix($compile): something')).toBe(VALID); + expect(m.validateMessage('feat($location): something')).toBe(VALID); + expect(m.validateMessage('docs($filter): something')).toBe(VALID); + expect(m.validateMessage('style($http): something')).toBe(VALID); + expect(m.validateMessage('refactor($httpBackend): something')).toBe(VALID); + expect(m.validateMessage('test($resource): something')).toBe(VALID); + expect(m.validateMessage('chore($controller): something')).toBe(VALID); + expect(m.validateMessage('chore(foo-bar): something')).toBe(VALID); + expect(m.validateMessage('chore(*): something')).toBe(VALID); + expect(m.validateMessage('chore(guide/location): something')).toBe(VALID); + expect(m.validateMessage('revert(foo): something')).toBe(VALID); + expect(errors).toEqual([]); + }); + + + it('should validate 100 characters length', function() { + var msg = "fix($compile): something super mega extra giga tera long, maybe even longer and longer and longer... "; + + expect(m.validateMessage(msg)).toBe(INVALID); + expect(errors).toEqual(['INVALID COMMIT MSG: is longer than 100 characters !']); + }); + + + it('should validate "(): " format', function() { + var msg = 'not correct format'; + + expect(m.validateMessage(msg)).toBe(INVALID); + expect(errors).toEqual(['INVALID COMMIT MSG: does not match "(): " ! was: not correct format']); + }); + + + it('should validate type', function() { + expect(m.validateMessage('weird($filter): something')).toBe(INVALID); + expect(errors).toEqual(['INVALID COMMIT MSG: "weird" is not allowed type !']); + }); + + + it('should allow empty scope', function() { + expect(m.validateMessage('fix: blablabla')).toBe(VALID); + }); + + + it('should allow dot in scope', function() { + expect(m.validateMessage('chore(mocks.$httpBackend): something')).toBe(VALID); + }); + + + it('should ignore msg prefixed with "WIP: "', function() { + expect(m.validateMessage('WIP: bullshit')).toBe(VALID); + }); + }); +}); From 564241cd387f56f4d931fc26c0b69fe59906d75c Mon Sep 17 00:00:00 2001 From: Cole Gleason Date: Thu, 16 Jan 2014 02:10:00 -0600 Subject: [PATCH 2/2] chore(changelog): add git-changelog config and sample CHANGELOG.md This is the setup for the Grunt task that generates a changelog. Note that it currently generates the changelog for the range changelog-start..HEAD where changelog-start is a tag. It will always use the most recent tag it can find. We may want to change this behavior. Also, the original repo has some bugs, so I'm using my fork until he accepts my PRs. Migration notes: To run: `grunt git_changelog` --- CHANGELOG.md | 7 +++++++ EXTENDEDCHANGELOG.md | 11 +++++++++++ Gruntfile.js | 19 +++++++++++++++++++ package.json | 3 ++- 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 EXTENDEDCHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..8da5993552 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +HabitRPG +# (2014-01-16) + + +## Breaking Changes + + diff --git a/EXTENDEDCHANGELOG.md b/EXTENDEDCHANGELOG.md new file mode 100644 index 0000000000..88cc4a2553 --- /dev/null +++ b/EXTENDEDCHANGELOG.md @@ -0,0 +1,11 @@ +HabitRPG +# (2014-01-16) + + +## Chore + +- **changelog:** add commit-msg hook to verify commit messages + +## Breaking Changes + + diff --git a/Gruntfile.js b/Gruntfile.js index 38d3543b77..884aa0daf8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,6 +4,24 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ + git_changelog: { + minimal: { + options: { + repo_url: 'https://github.com/habitrpg/habitrpg', + appName : 'HabitRPG', + branch_name: 'develop' + } + }, + extended: { + options: { + file: 'EXTENDEDCHANGELOG.md', + repo_url: 'https://github.com/habitrpg/habitrpg', + appName : 'HabitRPG', + branch_name: 'develop', + grep_commits: '^fix|^feat|^docs|^refactor|^chore|BREAKING' + } + } + }, karma: { unit: { @@ -124,5 +142,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-hashres'); grunt.loadNpmTasks('grunt-karma'); + grunt.loadNpmTasks('git-changelog'); }; diff --git a/package.json b/package.json index 00a4ab9d07..864bc1f085 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "mongoskin": "~0.6.1", "expect.js": "~0.2.0", "superagent": "~0.15.7", - "superagent-defaults": "~0.1.5" + "superagent-defaults": "~0.1.5", + "git-changelog": "colegleason/git-changelog" } }