diff --git a/bin/cmd.js b/bin/cmd.js index be5660e..e827dd6 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -20,6 +20,7 @@ const knownOpts = { help: Boolean , tap: Boolean , out: path , list: Boolean +, 'check-autosquashable': Boolean } const shortHand = { h: ['--help'] , v: ['--version'] @@ -27,6 +28,7 @@ const shortHand = { h: ['--help'] , t: ['--tap'] , o: ['--out'] , l: ['--list'] +, a: ['--check-autosquashable'] } const parsed = nopt(knownOpts, shortHand) diff --git a/bin/usage.txt b/bin/usage.txt index 9e68603..7a3dd1d 100644 --- a/bin/usage.txt +++ b/bin/usage.txt @@ -8,6 +8,7 @@ core-validate-commit - Validate the commit message for a particular commit in no -V, --validate-metadata validate PR-URL and reviewers (on by default) -t, --tap output in tap format -l, --list list rules and their descriptions + -a, --check-autosquashable check autosquashable commits (on by default) examples: Validate a single sha: diff --git a/lib/index.js b/lib/index.js index c9327f2..b57d202 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,6 +11,7 @@ module.exports = class ValidateCommit extends EE { this.opts = Object.assign({ 'validate-metadata': true + , 'check-autosquashable': true }, options) this.messages = new Map() @@ -47,9 +48,15 @@ module.exports = class ValidateCommit extends EE { } } else { const commit = new Parser(str, this) - for (const rule of this.rules.values()) { - if (rule.disabled) continue - rule.validate(commit) + if (!this.opts['check-autosquashable'] && + (commit.title.startsWith('fixup!') || + commit.title.startsWith('squash!'))) { + this.rules.get('skip-autosquashable').validate(commit) + } else { + for (const rule of this.rules.values()) { + if (rule.disabled) continue + rule.validate(commit) + } } setImmediate(() => { diff --git a/lib/rules/skip-autosquashable.js b/lib/rules/skip-autosquashable.js new file mode 100644 index 0000000..4598f01 --- /dev/null +++ b/lib/rules/skip-autosquashable.js @@ -0,0 +1,44 @@ +'use strict' + +const id = 'skip-autosquashable' + +module.exports = { + id: id +, meta: { + description: 'skip autosquashable fixup! and squash! commits' + , recommended: true + } +, disabled: true +, defaults: { } +, options: { } +, validate: (context, rule) => { + if (context.title.startsWith('fixup!')) { + context.report({ + id: id + , message: 'Skipping fixup! commit.' + , string: context.title + , level: 'warn' + }) + return + } + + if (context.title.startsWith('squash!')) { + context.report({ + id: id + , message: 'Skipping squash! commit.' + , string: context.title + , level: 'warn' + }) + return + } + + context.report({ + id: id + , message: 'Commit is neither fixup! nor squash!.' + , string: context.title + , line: 0 + , column: 0 + , level: 'fail' + }) + } +} diff --git a/test/fixtures/autosquashable-pr.json b/test/fixtures/autosquashable-pr.json new file mode 100644 index 0000000..7a8916f --- /dev/null +++ b/test/fixtures/autosquashable-pr.json @@ -0,0 +1,239 @@ +[ + { + "sha": "79f946e88e79f5063bbe775380a418bd8ab0b881", + "node_id": "MDY6Q29tbWl0MTU1ODE5NzI2Ojc5Zjk0NmU4OGU3OWY1MDYzYmJlNzc1MzgwYTQxOGJkOGFiMGI4ODE=", + "commit": { + "author": { + "name": "Richard Lau", + "email": "riclau@uk.ibm.com", + "date": "2018-11-09T16:24:33Z" + }, + "committer": { + "name": "Richard Lau", + "email": "riclau@uk.ibm.com", + "date": "2018-11-09T18:19:29Z" + }, + "message": "lib: skip fixup! and squash! commits", + "tree": { + "sha": "0117c48bf2beff44676b0fa8c4e652860d67e833", + "url": "https://api.github.com/repos/nodejs/core-validate-commit/git/trees/0117c48bf2beff44676b0fa8c4e652860d67e833" + }, + "url": "https://api.github.com/repos/nodejs/core-validate-commit/git/commits/79f946e88e79f5063bbe775380a418bd8ab0b881", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/79f946e88e79f5063bbe775380a418bd8ab0b881", + "html_url": "https://github.com/nodejs/core-validate-commit/commit/79f946e88e79f5063bbe775380a418bd8ab0b881", + "comments_url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/79f946e88e79f5063bbe775380a418bd8ab0b881/comments", + "author": { + "login": "richardlau", + "id": 5445507, + "node_id": "MDQ6VXNlcjU0NDU1MDc=", + "avatar_url": "https://avatars0.githubusercontent.com/u/5445507?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/richardlau", + "html_url": "https://github.com/richardlau", + "followers_url": "https://api.github.com/users/richardlau/followers", + "following_url": "https://api.github.com/users/richardlau/following{/other_user}", + "gists_url": "https://api.github.com/users/richardlau/gists{/gist_id}", + "starred_url": "https://api.github.com/users/richardlau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/richardlau/subscriptions", + "organizations_url": "https://api.github.com/users/richardlau/orgs", + "repos_url": "https://api.github.com/users/richardlau/repos", + "events_url": "https://api.github.com/users/richardlau/events{/privacy}", + "received_events_url": "https://api.github.com/users/richardlau/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "richardlau", + "id": 5445507, + "node_id": "MDQ6VXNlcjU0NDU1MDc=", + "avatar_url": "https://avatars0.githubusercontent.com/u/5445507?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/richardlau", + "html_url": "https://github.com/richardlau", + "followers_url": "https://api.github.com/users/richardlau/followers", + "following_url": "https://api.github.com/users/richardlau/following{/other_user}", + "gists_url": "https://api.github.com/users/richardlau/gists{/gist_id}", + "starred_url": "https://api.github.com/users/richardlau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/richardlau/subscriptions", + "organizations_url": "https://api.github.com/users/richardlau/orgs", + "repos_url": "https://api.github.com/users/richardlau/repos", + "events_url": "https://api.github.com/users/richardlau/events{/privacy}", + "received_events_url": "https://api.github.com/users/richardlau/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "4e64157b6a87267fd4b9f0598159eeb968e04e21", + "url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/4e64157b6a87267fd4b9f0598159eeb968e04e21", + "html_url": "https://github.com/nodejs/core-validate-commit/commit/4e64157b6a87267fd4b9f0598159eeb968e04e21" + } + ] + }, + { + "sha": "d07d94439be43a65d6e57ec2ee1d8c93676e8d55", + "node_id": "MDY6Q29tbWl0MTU1ODE5NzI2OmQwN2Q5NDQzOWJlNDNhNjVkNmU1N2VjMmVlMWQ4YzkzNjc2ZThkNTU=", + "commit": { + "author": { + "name": "Richard Lau", + "email": "riclau@uk.ibm.com", + "date": "2018-11-10T00:44:48Z" + }, + "committer": { + "name": "Richard Lau", + "email": "riclau@uk.ibm.com", + "date": "2018-11-10T00:44:48Z" + }, + "message": "squash! lib: skip fixup! and squash! commits", + "tree": { + "sha": "b37fb650a27feaddb950d153a7085fec2347ec7d", + "url": "https://api.github.com/repos/nodejs/core-validate-commit/git/trees/b37fb650a27feaddb950d153a7085fec2347ec7d" + }, + "url": "https://api.github.com/repos/nodejs/core-validate-commit/git/commits/d07d94439be43a65d6e57ec2ee1d8c93676e8d55", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/d07d94439be43a65d6e57ec2ee1d8c93676e8d55", + "html_url": "https://github.com/nodejs/core-validate-commit/commit/d07d94439be43a65d6e57ec2ee1d8c93676e8d55", + "comments_url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/d07d94439be43a65d6e57ec2ee1d8c93676e8d55/comments", + "author": { + "login": "richardlau", + "id": 5445507, + "node_id": "MDQ6VXNlcjU0NDU1MDc=", + "avatar_url": "https://avatars0.githubusercontent.com/u/5445507?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/richardlau", + "html_url": "https://github.com/richardlau", + "followers_url": "https://api.github.com/users/richardlau/followers", + "following_url": "https://api.github.com/users/richardlau/following{/other_user}", + "gists_url": "https://api.github.com/users/richardlau/gists{/gist_id}", + "starred_url": "https://api.github.com/users/richardlau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/richardlau/subscriptions", + "organizations_url": "https://api.github.com/users/richardlau/orgs", + "repos_url": "https://api.github.com/users/richardlau/repos", + "events_url": "https://api.github.com/users/richardlau/events{/privacy}", + "received_events_url": "https://api.github.com/users/richardlau/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "richardlau", + "id": 5445507, + "node_id": "MDQ6VXNlcjU0NDU1MDc=", + "avatar_url": "https://avatars0.githubusercontent.com/u/5445507?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/richardlau", + "html_url": "https://github.com/richardlau", + "followers_url": "https://api.github.com/users/richardlau/followers", + "following_url": "https://api.github.com/users/richardlau/following{/other_user}", + "gists_url": "https://api.github.com/users/richardlau/gists{/gist_id}", + "starred_url": "https://api.github.com/users/richardlau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/richardlau/subscriptions", + "organizations_url": "https://api.github.com/users/richardlau/orgs", + "repos_url": "https://api.github.com/users/richardlau/repos", + "events_url": "https://api.github.com/users/richardlau/events{/privacy}", + "received_events_url": "https://api.github.com/users/richardlau/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "79f946e88e79f5063bbe775380a418bd8ab0b881", + "url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/79f946e88e79f5063bbe775380a418bd8ab0b881", + "html_url": "https://github.com/nodejs/core-validate-commit/commit/79f946e88e79f5063bbe775380a418bd8ab0b881" + } + ] + }, + { + "sha": "25e5398728b59ab0fdf141d1676a68969204c60a", + "node_id": "MDY6Q29tbWl0MTU1ODE5NzI2OjI1ZTUzOTg3MjhiNTlhYjBmZGYxNDFkMTY3NmE2ODk2OTIwNGM2MGE=", + "commit": { + "author": { + "name": "Richard Lau", + "email": "riclau@uk.ibm.com", + "date": "2018-11-10T01:04:41Z" + }, + "committer": { + "name": "Richard Lau", + "email": "riclau@uk.ibm.com", + "date": "2018-11-10T01:04:41Z" + }, + "message": "fixup! lib: skip fixup! and squash! commits", + "tree": { + "sha": "c4f1faf780d28fce67c7d1ec0a7e0617f43bc79a", + "url": "https://api.github.com/repos/nodejs/core-validate-commit/git/trees/c4f1faf780d28fce67c7d1ec0a7e0617f43bc79a" + }, + "url": "https://api.github.com/repos/nodejs/core-validate-commit/git/commits/25e5398728b59ab0fdf141d1676a68969204c60a", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/25e5398728b59ab0fdf141d1676a68969204c60a", + "html_url": "https://github.com/nodejs/core-validate-commit/commit/25e5398728b59ab0fdf141d1676a68969204c60a", + "comments_url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/25e5398728b59ab0fdf141d1676a68969204c60a/comments", + "author": { + "login": "richardlau", + "id": 5445507, + "node_id": "MDQ6VXNlcjU0NDU1MDc=", + "avatar_url": "https://avatars0.githubusercontent.com/u/5445507?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/richardlau", + "html_url": "https://github.com/richardlau", + "followers_url": "https://api.github.com/users/richardlau/followers", + "following_url": "https://api.github.com/users/richardlau/following{/other_user}", + "gists_url": "https://api.github.com/users/richardlau/gists{/gist_id}", + "starred_url": "https://api.github.com/users/richardlau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/richardlau/subscriptions", + "organizations_url": "https://api.github.com/users/richardlau/orgs", + "repos_url": "https://api.github.com/users/richardlau/repos", + "events_url": "https://api.github.com/users/richardlau/events{/privacy}", + "received_events_url": "https://api.github.com/users/richardlau/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "richardlau", + "id": 5445507, + "node_id": "MDQ6VXNlcjU0NDU1MDc=", + "avatar_url": "https://avatars0.githubusercontent.com/u/5445507?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/richardlau", + "html_url": "https://github.com/richardlau", + "followers_url": "https://api.github.com/users/richardlau/followers", + "following_url": "https://api.github.com/users/richardlau/following{/other_user}", + "gists_url": "https://api.github.com/users/richardlau/gists{/gist_id}", + "starred_url": "https://api.github.com/users/richardlau/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/richardlau/subscriptions", + "organizations_url": "https://api.github.com/users/richardlau/orgs", + "repos_url": "https://api.github.com/users/richardlau/repos", + "events_url": "https://api.github.com/users/richardlau/events{/privacy}", + "received_events_url": "https://api.github.com/users/richardlau/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "d07d94439be43a65d6e57ec2ee1d8c93676e8d55", + "url": "https://api.github.com/repos/nodejs/core-validate-commit/commits/d07d94439be43a65d6e57ec2ee1d8c93676e8d55", + "html_url": "https://github.com/nodejs/core-validate-commit/commit/d07d94439be43a65d6e57ec2ee1d8c93676e8d55" + } + ] + } +] diff --git a/test/validator.js b/test/validator.js index b126bd9..f5cc50a 100644 --- a/test/validator.js +++ b/test/validator.js @@ -127,6 +127,27 @@ Date: Thu Mar 3 10:10:46 2016 -0600 test: Check memoryUsage properties. ` +const str10 = `commit 9af419a04237c3d158e71ecde9e04bbc2e77de1f +Author: Richard Lau +Date: Fri Nov 9 11:24:33 2018 -0500 + + skip fixup! and squash! commits +` + +const str11 = `commit 1fd0e4375ea3ecb62d847827365d95e63bfe03f2 +Author: Richard Lau +Date: Fri Nov 9 11:26:11 2018 -0500 + + fixup! skip fixup! and squash! commits +` + +const str12 = `commit 1fd0e4375ea3ecb62d847827365d95e63bfe03f2 +Author: Richard Lau +Date: Fri Nov 9 11:26:11 2018 -0500 + + squash! skip fixup! and squash! commits +` + test('Validator - misc', (t) => { const v = new Validator() @@ -344,5 +365,172 @@ test('Validator - real commits', (t) => { }) }) + t.test('title contains but does not start with fixup! and squash!', (tt) => { + const v = new Validator({ + 'validate-metadata': false + , 'check-autosquashable': false + }) + v.lint(str10) + v.on('commit', (data) => { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 1, 'messages.length') + tt.equal(filtered[0].message, 'Missing subsystem.') + tt.end() + }) + }) + + t.test('fixup! commits are not skipped by default', (tt) => { + const v = new Validator({ + 'validate-metadata': false + }) + v.lint(str11) + v.on('commit', (data) => { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 1, 'messages.length') + tt.equal(filtered[0].message, 'Missing subsystem.') + tt.end() + }) + }) + + t.test('squash! commits are not skipped by default', (tt) => { + const v = new Validator({ + 'validate-metadata': false + }) + v.lint(str12) + v.on('commit', (data) => { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 1, 'messages.length') + tt.equal(filtered[0].message, 'Missing subsystem.') + tt.end() + }) + }) + + t.test('fixup! commits are skipped', (tt) => { + const v = new Validator({ + 'validate-metadata': false + , 'check-autosquashable': false + }) + v.lint(str11) + v.on('commit', (data) => { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 0, 'reported failures') + tt.equal(msgs.length, 1, 'messages.length') + tt.equal(msgs[0].message, 'Skipping fixup! commit.') + tt.end() + }) + }) + + t.test('squash! commits are skipped', (tt) => { + const v = new Validator({ + 'validate-metadata': false + , 'check-autosquashable': false + }) + v.lint(str12) + v.on('commit', (data) => { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 0, 'reported failures') + tt.equal(msgs.length, 1, 'messages.length') + tt.equal(msgs[0].message, 'Skipping squash! commit.') + tt.end() + }) + }) + + t.test('autosquashable commits are not skipped by default', (tt) => { + const v = new Validator({ + 'validate-metadata': false + }) + v.lint(require('./fixtures/autosquashable-pr')) + v.on('commit', (data) => { + switch (data.commit.sha) { + case '79f946e88e79f5063bbe775380a418bd8ab0b881': { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 0, 'reported failures') + break + } + case 'd07d94439be43a65d6e57ec2ee1d8c93676e8d55': { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 1, 'reported failures') + tt.equal(filtered[0].message, 'Invalid subsystem: "squash! lib"') + break + } + case '25e5398728b59ab0fdf141d1676a68969204c60a': { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 1, 'reported failures') + tt.equal(filtered[0].message, 'Invalid subsystem: "fixup! lib"') + tt.end() + break + } + default: + tt.fail(`unexpected commit sha ${data.commit.sha}`) + } + }) + }) + + t.test('autosquashable commits are skipped', (tt) => { + const v = new Validator({ + 'validate-metadata': false + , 'check-autosquashable': false + }) + v.lint(require('./fixtures/autosquashable-pr')) + v.on('commit', (data) => { + switch (data.commit.sha) { + case '79f946e88e79f5063bbe775380a418bd8ab0b881': { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 0, 'reported failures') + break + } + case 'd07d94439be43a65d6e57ec2ee1d8c93676e8d55': { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 0, 'reported failures') + tt.equal(msgs.length, 1, 'messages.length') + tt.equal(msgs[0].message, 'Skipping squash! commit.') + break + } + case '25e5398728b59ab0fdf141d1676a68969204c60a': { + const msgs = data.messages + const filtered = msgs.filter((item) => { + return item.level === 'fail' + }) + tt.equal(filtered.length, 0, 'reported failures') + tt.equal(msgs.length, 1, 'messages.length') + tt.equal(msgs[0].message, 'Skipping fixup! commit.') + tt.end() + break + } + default: + tt.fail(`unexpected commit sha ${data.commit.sha}`) + } + }) + }) t.end() })