Skip to content

Commit

Permalink
feat: lint commit messages
Browse files Browse the repository at this point in the history
Will lint between remotes/origin/master and HEAD.

To lint between different commits use:

```sh
$ aegir lint-commits  --from $COMMITISH --to $COMMITISH
```

This should be set up as a pre-push or post-commit hook as regular
linting runs pre-commit so the messages we want to examine won't
be in the history yet..
  • Loading branch information
achingbrain committed Jun 28, 2018
1 parent ec45fe9 commit a972f68
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 0 deletions.
23 changes: 23 additions & 0 deletions cmds/lint-commits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict'

module.exports = {
command: 'lint-commits',
desc: 'Lint commit messages',
builder: {
from: {
alias: 'f',
describe: 'The commit-ish to lint from',
default: 'remotes/origin/master'
},
to: {
alias: 't',
describe: 'The commit-ish to lint to',
default: 'HEAD'
}
},
handler (argv) {
const lintCommits = require('../src/lint-commits')
const onError = require('../src/error-handler')
lintCommits(argv).catch(onError)
}
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
],
"license": "MIT",
"dependencies": {
"@commitlint/config-conventional": "^7.0.1",
"@commitlint/lint": "^7.0.0",
"@commitlint/load": "^7.0.0",
"@commitlint/read": "^7.0.0",
"async": "^2.6.0",
"browserify-zlib": "~0.2.0",
"chalk": "^2.3.0",
Expand Down
64 changes: 64 additions & 0 deletions src/lint-commits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict'

const loadCommitLintConfig = require('@commitlint/load')
const readCommits = require('@commitlint/read')
const lintCommitMessage = require('@commitlint/lint')
const conventionalCommits = require('@commitlint/config-conventional')

function lintCommitMessages (opts = {}) {
const from = opts.from || 'remotes/origin/master'
const to = opts.to || 'HEAD'

return Promise.all([
loadCommitLintConfig({
rules: conventionalCommits.rules
}),
readCommits({
from,
to
})
])
.then(([ { rules, parserPreset }, commits ]) => {
return Promise.all(
commits.map(commit => {
return lintCommitMessage(commit, rules, parserPreset ? { parserOpts: parserPreset.parserOpts } : {})
})
)
})
.then(results => {
let valid = true

results.forEach(report => {
if (valid === true) {
valid = report.valid
}

const firstLine = `${report.input.trim().split('\n')[0]}`.trim()

if (!report.valid) {
console.log(`Commit message '${firstLine}' failed validation:`)
console.log('')

report.errors.forEach(error => {
console.log(' [ERROR]', `${error.name}:`, error.message)
})

report.warnings.forEach(warning => {
console.log(' [WARNING]', `${warning.name}:`, warning.message)
})

console.log('')
}
})

if (!valid) {
throw new Error(`Linting commits ${from}..${to} failed`)
}
})
}

function lintCommits (opts) {
return lintCommitMessages(opts)
}

module.exports = lintCommits
89 changes: 89 additions & 0 deletions test/lint-commits.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* eslint-env mocha */
'use strict'

const lintCommits = require('../src/lint-commits')
const expect = require('chai').expect
const path = require('path')
const os = require('os')
const fs = require('fs')
const series = require('async/series')
const child = require('child_process')

function commitFile (directory, name, contents, message, callback) {
series([
(cb) => fs.writeFile(path.join(directory, name), contents, cb),
(cb) => child.exec('git add -A', {
cwd: directory
}, cb),
(cb) => child.exec(`git commit -m "${message}"`, {
cwd: directory
}, cb)
], callback)
}

// Sets up two directories, an origin to represent a remote git repository
// and another to be the current project.
const setupProject = (commitMessage = 'chore: initial commit') => {
const tmpDir = path.join(os.tmpdir(), `test-${Math.random()}`)

return new Promise((resolve, reject) => {
series([
(cb) => fs.mkdir(tmpDir, cb),
(cb) => child.exec('git init', {
cwd: tmpDir
}, cb),
(cb) => child.exec('git config user.email "you@example.com"', {
cwd: tmpDir
}, cb),
(cb) => child.exec('git config user.name "test"', {
cwd: tmpDir
}, cb),
(cb) => commitFile(tmpDir, 'hello.txt', 'Amazing', 'chore: initial commit', cb),
(cb) => commitFile(tmpDir, 'goodbye.txt', 'Amazing', commitMessage, cb)
], (error) => {
if (error) {
return reject(error)
}

process.chdir(tmpDir)

resolve()
})
})
}

const commitMessageShouldPassLinting = (commitMessage) => {
return setupProject(commitMessage)
.then(() => lintCommits({
from: 'HEAD~1'
}))
}

const commitMessageShouldFailLinting = (commitMessage) => {
return setupProject(commitMessage)
.then(() => lintCommits({
from: 'HEAD~1'
}))
.then(() => {
throw new Error('Should have failed!')
})
.catch(error => {
expect(error.message).to.contain('Linting commits HEAD~1..HEAD failed')
})
}

describe('lint commit messages', () => {
const cwd = process.cwd()

after(() => {
process.chdir(cwd)
})

it('passes with good commit messages', function () {
return commitMessageShouldPassLinting('chore: such a chore')
})

it('fails with bad commit messages', function () {
return commitMessageShouldFailLinting('Ahahallolol!!!11shift+1')
})
})

0 comments on commit a972f68

Please sign in to comment.