├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── bin ├── cmd.js └── usage.txt ├── index.js ├── lib ├── format-pretty.js ├── format-tap.js ├── rule.js ├── rules │ ├── co-authored-by-is-trailer.js │ ├── fixes-url.js │ ├── line-after-title.js │ ├── line-length.js │ ├── metadata-end.js │ ├── pr-url.js │ ├── reviewers.js │ ├── subsystem.js │ ├── title-format.js │ └── title-length.js ├── tap.js ├── utils.js └── validator.js ├── package.json └── test ├── cli-test.js ├── fixtures ├── commit.json └── pr.json ├── rule-test.js ├── rules ├── co-authored-by-is-trailer.js ├── fixes-url.js ├── line-after-title.js ├── line-length.js ├── pr-url.js ├── reviewers.js ├── subsystem.js └── title-format.js ├── utils-test.js └── validator.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | test: 14 | name: Test on Node.js 15 | uses: pkgjs/action/.github/workflows/node-test.yaml@v0 16 | with: 17 | # We need to fetch some specific commits that we are using in our tests. We also need `--deepen=2` for CodCov. 18 | post-checkout-steps: | 19 | - run: git fetch --deepen=2 origin 2b98d02b52a0abe98054eccb351e1e5c71c81bb0 69435db261650dfc74ede6dca89acbe97ba30081 20 | shell: bash 21 | post-install-steps: | 22 | - run: npm run build --if-present 23 | shell: bash 24 | test-command: npm run test-ci 25 | post-test-steps: | 26 | - name: Upload coverage report to Codecov 27 | run: bash <(curl -s https://codecov.io/bash) 28 | shell: bash 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | .nyc_output/ 3 | /node_modules 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Evan Lucas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # core-validate-commit 2 | 3 | [![Build Status](https://github.com/nodejs/core-validate-commit/actions/workflows/node.js.yml/badge.svg)](https://github.com/nodejs/core-validate-commit/actions/workflows/node.js.yml) 4 | [![codecov](https://img.shields.io/codecov/c/github/nodejs/core-validate-commit.svg?style=flat-square)](https://codecov.io/gh/nodejs/core-validate-commit) 5 | 6 | Validate the commit message for a particular commit in node core 7 | 8 | ## Install 9 | 10 | ```bash 11 | $ npm install [-g] core-validate-commit 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```bash 17 | # for a single commit 18 | $ core-validate-commit 19 | 20 | # validate since 21 | $ git rev-list ..HEAD | xargs core-validate-commit 22 | 23 | # list all rules 24 | $ core-validate-commit --list 25 | fixes-url enforce format of Fixes URLs 26 | line-after-title enforce a blank newline after the commit title 27 | line-length enforce max length of lines in commit body 28 | metadata-end enforce that metadata is at the end of commit messages 29 | pr-url enforce PR-URL 30 | reviewers enforce having reviewers 31 | subsystem enforce subsystem validity 32 | title-format enforce commit title format 33 | title-length enforce max length of commit title 34 | ``` 35 | 36 | To see a list of valid subsystems: 37 | ```bash 38 | $ core-validate-commit --list-subsystem 39 | ``` 40 | 41 | Valid subsystems are also defined in [lib/rules/subsystem.js](./lib/rules/subsystem.js). 42 | 43 | ## Test 44 | 45 | ```bash 46 | $ npm test 47 | ``` 48 | 49 | ## Author 50 | 51 | Evan Lucas 52 | 53 | ## License 54 | 55 | MIT (See `LICENSE` for more info) 56 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { exec } from 'node:child_process' 4 | import fs from 'node:fs' 5 | import http from 'node:http' 6 | import https from 'node:https' 7 | import path from 'node:path' 8 | import nopt from 'nopt' 9 | import pretty from '../lib/format-pretty.js' 10 | import formatTap from '../lib/format-tap.js' 11 | import Validator from '../lib/validator.js' 12 | import Tap from '../lib/tap.js' 13 | import * as utils from '../lib/utils.js' 14 | import subsystem from '../lib/rules/subsystem.js' 15 | 16 | const knownOpts = { 17 | help: Boolean, 18 | version: Boolean, 19 | 'validate-metadata': Boolean, 20 | tap: Boolean, 21 | out: path, 22 | list: Boolean, 23 | 'list-subsystems': Boolean 24 | } 25 | const shortHand = { 26 | h: ['--help'], 27 | v: ['--version'], 28 | V: ['--validate-metadata'], 29 | t: ['--tap'], 30 | o: ['--out'], 31 | l: ['--list'], 32 | ls: ['--list-subsystems'] 33 | } 34 | 35 | const parsed = nopt(knownOpts, shortHand) 36 | 37 | if (parsed.help) { 38 | const usagePath = path.join(new URL(import.meta.url).pathname, '../usage.txt') 39 | const help = await fs.promises.readFile(usagePath, 'utf8') 40 | console.log(help) 41 | process.exit(0) 42 | } 43 | 44 | if (parsed.version) { 45 | const pkgJsonPath = path.join(new URL(import.meta.url).pathname, '../../package.json') 46 | const pkgJson = await fs.promises.readFile(pkgJsonPath, 'utf8') 47 | const { version } = JSON.parse(pkgJson) 48 | console.log(`core-validate-commit v${version}`) 49 | process.exit(0) 50 | } 51 | 52 | const args = parsed.argv.remain 53 | if (!parsed.help && !args.length) { args.push('HEAD') } 54 | 55 | function load (sha, cb) { 56 | try { 57 | const parsed = new URL(sha) 58 | return loadPatch(parsed, cb) 59 | } catch (_) { 60 | exec(`git show --quiet --format=medium ${sha}`, (err, stdout, stderr) => { 61 | if (err) return cb(err) 62 | cb(null, stdout.trim()) 63 | }) 64 | } 65 | } 66 | 67 | function loadPatch (uri, cb) { 68 | let h = http 69 | if (~uri.protocol.indexOf('https')) { 70 | h = https 71 | } 72 | const headers = { 73 | 'user-agent': 'core-validate-commit' 74 | } 75 | h.get(uri, { headers }, (res) => { 76 | let buf = '' 77 | res.on('data', (chunk) => { 78 | buf += chunk 79 | }) 80 | 81 | res.on('end', () => { 82 | try { 83 | const out = JSON.parse(buf) 84 | cb(null, out) 85 | } catch (err) { 86 | cb(err) 87 | } 88 | }) 89 | }).on('error', cb) 90 | } 91 | 92 | const v = new Validator(parsed) 93 | 94 | if (parsed['list-subsystems']) { 95 | utils.describeSubsystem(subsystem.defaults.subsystems.sort()) 96 | process.exit(0) 97 | } 98 | 99 | if (parsed.list) { 100 | const ruleNames = Array.from(v.rules.keys()) 101 | const max = ruleNames.reduce((m, item) => { 102 | if (item.length > m) m = item.length 103 | return m 104 | }, 0) 105 | 106 | for (const rule of v.rules.values()) { 107 | utils.describeRule(rule, max) 108 | } 109 | process.exit(0) 110 | } 111 | 112 | if (parsed.tap) { 113 | const tap = new Tap() 114 | tap.pipe(process.stdout) 115 | if (parsed.out) tap.pipe(fs.createWriteStream(parsed.out)) 116 | let count = 0 117 | const total = args.length 118 | 119 | v.on('commit', (c) => { 120 | count++ 121 | const test = tap.test(c.commit.sha) 122 | formatTap(test, c.commit, c.messages, v) 123 | if (count === total) { 124 | setImmediate(() => { 125 | tap.end() 126 | if (tap.status === 'fail') { process.exitCode = 1 } 127 | }) 128 | } 129 | }) 130 | 131 | tapRun() 132 | } else { 133 | v.on('commit', (c) => { 134 | pretty(c.commit, c.messages, v) 135 | commitRun() 136 | }) 137 | 138 | commitRun() 139 | } 140 | 141 | function tapRun () { 142 | if (!args.length) return 143 | const sha = args.shift() 144 | load(sha, (err, data) => { 145 | if (err) throw err 146 | v.lint(data) 147 | tapRun() 148 | }) 149 | } 150 | 151 | function commitRun () { 152 | if (!args.length) { 153 | process.exitCode = v.errors 154 | return 155 | } 156 | const sha = args.shift() 157 | load(sha, (err, data) => { 158 | if (err) throw err 159 | v.lint(data) 160 | }) 161 | } 162 | -------------------------------------------------------------------------------- /bin/usage.txt: -------------------------------------------------------------------------------- 1 | core-validate-commit - Validate the commit message for a particular commit in node core 2 | 3 | usage: core-validate-commit [options] [sha[, sha]] 4 | 5 | options: 6 | -h, --help show help and usage 7 | -v, --version show version 8 | -V, --validate-metadata validate PR-URL and reviewers (on by default) 9 | -t, --tap output in tap format 10 | -l, --list list rules and their descriptions 11 | -ls --list-subsystems list the available subsystems 12 | 13 | examples: 14 | Validate a single sha: 15 | 16 | $ core-validate-commit 64c87e2cf4bdac6cdea302d5a5ead36c56f81c65 17 | 18 | Validate from a specific sha to HEAD: 19 | 20 | $ git rev-list 287bdab..HEAD | xargs core-validate-commit 21 | 22 | Passing a url to Github's PR commit list API: 23 | 24 | $ core-validate-commit https://api.github.com/repos/nodejs/node/pulls/7957/commits 25 | 26 | Passing a url to a specific sha on github: 27 | 28 | $ core-validate-commit https://api.github.com/repos/nodejs/node/git/commits/9e9d499b8be8ffc6050db25129b042507d7b4b02 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Validator from './lib/validator.js' 2 | 3 | export default Validator 4 | -------------------------------------------------------------------------------- /lib/format-pretty.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import * as utils from './utils.js' 3 | 4 | const MAX_LINE_COL_LEN = 6 5 | 6 | export default function formatPretty (context, msgs, validator, opts) { 7 | opts = Object.assign({ 8 | detailed: false 9 | }, opts) 10 | 11 | if (!msgs.length) { 12 | console.log(' ', utils.header(context.sha, 'pass')) 13 | return 14 | } 15 | 16 | let level = 'pass' 17 | for (const msg of msgs) { 18 | if (msg.level === 'fail') level = 'fail' 19 | } 20 | 21 | msgs.sort((a, b) => { 22 | if (a.line === b.line) { 23 | return a.column < b.column 24 | ? -1 25 | : a.column > b.column 26 | ? 1 27 | : 0 28 | } 29 | 30 | return a.line < b.line 31 | ? -1 32 | : a.line > b.line 33 | ? 1 34 | : 0 35 | }) 36 | 37 | console.log(' ', utils.header(context.sha, level)) 38 | 39 | for (const msg of msgs) { 40 | const ruleId = msg.id 41 | const rule = validator.rules.get(ruleId) 42 | if (!rule) { 43 | throw new Error(`Invalid rule: "${ruleId}"`) 44 | } 45 | 46 | switch (ruleId) { 47 | case 'title-length': 48 | case 'line-length': 49 | console.log(formatLength(msg, opts)) 50 | break 51 | default: 52 | console.log(formatMessage(msg)) 53 | break 54 | } 55 | } 56 | } 57 | 58 | function formatLength (msg, opts) { 59 | const out = formatMessage(msg) 60 | const str = msg.string 61 | const l = str.length 62 | if (!opts.detailed) return out 63 | const col = msg.column || 0 64 | const diff = str.slice(0, col) + chalk.red(str.slice(col, l)) 65 | return `${out} 66 | ${diff}` 67 | } 68 | 69 | function formatMessage (msg) { 70 | const l = msg.line || 0 71 | const col = msg.column || 0 72 | const pad = utils.rightPad(`${l}:${col}`, MAX_LINE_COL_LEN) 73 | const line = chalk.grey(pad) 74 | const id = formatId(msg.id) 75 | const m = msg.message 76 | const icon = msg.level === 'fail' 77 | ? utils.X 78 | : msg.level === 'warn' 79 | ? utils.WARN 80 | : utils.CHECK 81 | return ` ${icon} ${line} ${utils.rightPad(m, 40)} ${id}` 82 | } 83 | 84 | function formatId (id) { 85 | return chalk.red(id) 86 | } 87 | -------------------------------------------------------------------------------- /lib/format-tap.js: -------------------------------------------------------------------------------- 1 | export default function formatTap (t, context, msgs, validator) { 2 | for (const m of msgs) { 3 | switch (m.level) { 4 | case 'pass': { 5 | const a = m.string ? ` [${m.string}]` : '' 6 | t.pass(`${m.id}: ${m.message}${a}`) 7 | break 8 | } 9 | case 'skip': 10 | t.skip(`${m.id}: ${m.message}`) 11 | break 12 | case 'fail': 13 | onFail(context, m, validator, t) 14 | break 15 | } 16 | } 17 | } 18 | 19 | function onFail (context, m, validator, t) { 20 | switch (m.id) { 21 | case 'line-length': 22 | case 'title-length': 23 | lengthFail(context, m, validator, t) 24 | break 25 | case 'subsystem': 26 | subsystemFail(context, m, validator, t) 27 | break 28 | default: 29 | defaultFail(context, m, validator, t) 30 | break 31 | } 32 | } 33 | 34 | function lengthFail (context, m, validator, t) { 35 | const body = m.id === 'title-length' 36 | ? context.title 37 | : context.body 38 | t.fail(`${m.id}: ${m.message}`, { 39 | found: m.string.length, 40 | compare: '<=', 41 | wanted: m.maxLength, 42 | at: { 43 | line: m.line || 0, 44 | column: m.column || 0, 45 | body 46 | } 47 | }) 48 | } 49 | 50 | function subsystemFail (context, m, validator, t) { 51 | t.fail(`${m.id}: ${m.message} (${m.string})`, { 52 | found: m.string, 53 | compare: 'indexOf() !== -1', 54 | wanted: m.wanted || '', 55 | at: { 56 | line: m.line || 0, 57 | column: m.column || 0, 58 | body: m.title 59 | } 60 | }) 61 | } 62 | 63 | function defaultFail (context, m, validator, t) { 64 | t.fail(`${m.id}: ${m.message} (${m.string})`, { 65 | found: m.string, 66 | compare: Array.isArray(m.wanted) ? 'indexOf() !== -1' : '===', 67 | wanted: m.wanted || '', 68 | at: { 69 | line: m.line || 0, 70 | column: m.column || 0, 71 | body: context.body 72 | } 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /lib/rule.js: -------------------------------------------------------------------------------- 1 | export default class Rule { 2 | constructor (opts) { 3 | opts = Object.assign({ 4 | options: {}, 5 | defaults: {}, 6 | meta: {} 7 | }, opts) 8 | 9 | if (!opts.id) { 10 | throw new Error('Rule must have an id') 11 | } 12 | 13 | if (typeof opts.validate !== 'function') { 14 | throw new TypeError('Rule must have validate function') 15 | } 16 | 17 | this.id = opts.id 18 | this.disabled = opts.disabled === true 19 | this.meta = opts.meta 20 | this.defaults = Object.assign({}, opts.defaults) 21 | this.options = Object.assign({}, opts.defaults, opts.options) 22 | this._validate = opts.validate 23 | } 24 | 25 | validate (commit) { 26 | this._validate(commit, this) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/rules/co-authored-by-is-trailer.js: -------------------------------------------------------------------------------- 1 | const id = 'co-authored-by-is-trailer' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce that "Co-authored-by:" lines are trailers', 7 | recommended: true 8 | }, 9 | defaults: {}, 10 | options: {}, 11 | validate: (context, rule) => { 12 | const parsed = context.toJSON() 13 | const lines = parsed.body.map((line, i) => [line, i]) 14 | const re = /^\s*Co-authored-by:/gi 15 | const coauthors = lines.filter(([line]) => re.test(line)) 16 | if (coauthors.length !== 0) { 17 | const firstCoauthor = coauthors[0] 18 | const emptyLines = lines.filter(([text]) => text.trim().length === 0) 19 | // There must be at least one empty line, and the last empty line must be 20 | // above the first Co-authored-by line. 21 | const isTrailer = (emptyLines.length !== 0) && 22 | emptyLines.pop()[1] < firstCoauthor[1] 23 | if (isTrailer) { 24 | context.report({ 25 | id, 26 | message: 'Co-authored-by is a trailer', 27 | string: '', 28 | level: 'pass' 29 | }) 30 | } else { 31 | context.report({ 32 | id, 33 | message: 'Co-authored-by must be a trailer', 34 | string: firstCoauthor[0], 35 | line: firstCoauthor[1], 36 | column: 0, 37 | level: 'fail' 38 | }) 39 | } 40 | } else { 41 | context.report({ 42 | id, 43 | message: 'no Co-authored-by metadata', 44 | string: '', 45 | level: 'pass' 46 | }) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/rules/fixes-url.js: -------------------------------------------------------------------------------- 1 | const id = 'fixes-url' 2 | const github = new RegExp('^https://github\\.com/[\\w-]+/[\\w-]+/' + 3 | '(issues|pull)/\\d+(#issuecomment-\\d+|#discussion_r\\d+)?/?$' 4 | ) 5 | 6 | export default { 7 | id, 8 | meta: { 9 | description: 'enforce format of Fixes URLs', 10 | recommended: true 11 | }, 12 | defaults: {}, 13 | options: {}, 14 | validate: (context, rule) => { 15 | const parsed = context.toJSON() 16 | if (!Array.isArray(parsed.fixes) || !parsed.fixes.length) { 17 | context.report({ 18 | id, 19 | message: 'skipping fixes-url', 20 | string: '', 21 | level: 'skip' 22 | }) 23 | return 24 | } 25 | // Allow GitHub issues with optional comment. 26 | // GitHub pull requests must reference a comment or discussion. 27 | for (const url of parsed.fixes) { 28 | const match = github.exec(url) 29 | if (url[0] === '#') { 30 | // See nodejs/node#2aa376914b621018c5784104b82c13e78ee51307 31 | // for an example 32 | const { line, column } = findLineAndColumn(context.body, url) 33 | context.report({ 34 | id, 35 | message: 'Fixes must be a URL, not an issue number.', 36 | string: url, 37 | line, 38 | column, 39 | level: 'fail' 40 | }) 41 | } else if (match) { 42 | if (match[1] === 'pull' && match[2] === undefined) { 43 | const { line, column } = findLineAndColumn(context.body, url) 44 | context.report({ 45 | id, 46 | message: 'Pull request URL must reference a comment or discussion.', 47 | string: url, 48 | line, 49 | column, 50 | level: 'fail' 51 | }) 52 | } else { 53 | const { line, column } = findLineAndColumn(context.body, url) 54 | context.report({ 55 | id, 56 | message: 'Valid fixes URL.', 57 | string: url, 58 | line, 59 | column, 60 | level: 'pass' 61 | }) 62 | } 63 | } else { 64 | const { line, column } = findLineAndColumn(context.body, url) 65 | context.report({ 66 | id, 67 | message: 'Fixes must be a GitHub URL.', 68 | string: url, 69 | line, 70 | column, 71 | level: 'fail' 72 | }) 73 | } 74 | } 75 | } 76 | } 77 | 78 | function findLineAndColumn (body, str) { 79 | for (let i = 0; i < body.length; i++) { 80 | const l = body[i] 81 | if (~l.indexOf('Fixes')) { 82 | const idx = l.indexOf(str) 83 | if (idx !== -1) { 84 | return { 85 | line: i, 86 | column: idx 87 | } 88 | } 89 | } 90 | } 91 | 92 | return { 93 | line: -1, 94 | column: -1 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/rules/line-after-title.js: -------------------------------------------------------------------------------- 1 | const id = 'line-after-title' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce a blank newline after the commit title', 7 | recommended: true 8 | }, 9 | defaults: {}, 10 | options: {}, 11 | validate: (context, rule) => { 12 | // all commits should have a body and a blank line after the title 13 | if (context.body[0]) { 14 | context.report({ 15 | id, 16 | message: 'blank line expected after title', 17 | string: context.body.length ? context.body[0] : '', 18 | line: 1, 19 | column: 0, 20 | level: 'fail' 21 | }) 22 | return 23 | } 24 | 25 | context.report({ 26 | id, 27 | message: 'blank line after title', 28 | string: '', 29 | level: 'pass' 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/rules/line-length.js: -------------------------------------------------------------------------------- 1 | const id = 'line-length' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce max length of lines in commit body', 7 | recommended: true 8 | }, 9 | defaults: { 10 | length: 72 11 | }, 12 | options: { 13 | length: 72 14 | }, 15 | validate: (context, rule) => { 16 | const len = rule.options.length 17 | const parsed = context.toJSON() 18 | // release commits include the notable changes from the changelog 19 | // in the commit message 20 | if (parsed.release) { 21 | context.report({ 22 | id, 23 | message: 'skipping line-length for release commit', 24 | string: '', 25 | level: 'skip' 26 | }) 27 | return 28 | } 29 | let failed = false 30 | for (let i = 0; i < parsed.body.length; i++) { 31 | const line = parsed.body[i] 32 | // Skip quoted lines, e.g. for original commit messages of V8 backports. 33 | if (line.startsWith(' ')) { continue } 34 | // Skip lines with URLs. 35 | if (/https?:\/\//.test(line)) { continue } 36 | if (line.length > len) { 37 | failed = true 38 | context.report({ 39 | id, 40 | message: `Line should be <= ${len} columns.`, 41 | string: line, 42 | maxLength: len, 43 | line: i, 44 | column: len, 45 | level: 'fail' 46 | }) 47 | } 48 | } 49 | 50 | if (!failed) { 51 | context.report({ 52 | id, 53 | message: 'line-lengths are valid', 54 | string: '', 55 | level: 'pass' 56 | }) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/rules/metadata-end.js: -------------------------------------------------------------------------------- 1 | const id = 'metadata-end' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce that metadata is at the end of commit messages', 7 | recommended: true 8 | }, 9 | defaults: {}, 10 | options: {}, 11 | validate: (context, rule) => { 12 | const parsed = context.toJSON() 13 | const body = parsed.body 14 | const end = parsed.metadata.end 15 | if (end < body.length) { 16 | const extra = body.slice(end + 1) 17 | let lineNum = end + 1 18 | for (let i = 0; i < extra.length; i++) { 19 | if (extra[i]) { 20 | lineNum += i 21 | break 22 | } 23 | } 24 | 25 | if (lineNum !== end + 1) { 26 | context.report({ 27 | id, 28 | message: 'commit metadata at end of message', 29 | string: body[lineNum], 30 | line: lineNum, 31 | column: 0, 32 | level: 'fail' 33 | }) 34 | return 35 | } 36 | } 37 | 38 | context.report({ 39 | id, 40 | message: 'metadata is at end of message', 41 | string: '', 42 | level: 'pass' 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/rules/pr-url.js: -------------------------------------------------------------------------------- 1 | const id = 'pr-url' 2 | const prUrl = /^https:\/\/github\.com\/[\w-]+\/[\w-]+\/pull\/\d+\/?$/ 3 | 4 | export default { 5 | id, 6 | meta: { 7 | description: 'enforce PR-URL', 8 | recommended: true 9 | }, 10 | defaults: {}, 11 | options: {}, 12 | validate: (context, rule) => { 13 | if (!context.prUrl) { 14 | context.report({ 15 | id, 16 | message: 'Commit must have a PR-URL.', 17 | string: context.prUrl, 18 | line: 0, 19 | column: 0, 20 | level: 'fail' 21 | }) 22 | return 23 | } 24 | let line = -1 25 | let column = -1 26 | for (let i = 0; i < context.body.length; i++) { 27 | const l = context.body[i] 28 | if (~l.indexOf('PR-URL') && ~l.indexOf(context.prUrl)) { 29 | line = i 30 | column = l.indexOf(context.prUrl) 31 | } 32 | } 33 | if (context.prUrl[0] === '#') { 34 | // see nodejs/node#7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 35 | // for an example 36 | context.report({ 37 | id, 38 | message: 'PR-URL must be a URL, not a pull request number.', 39 | string: context.prUrl, 40 | line, 41 | column, 42 | level: 'fail' 43 | }) 44 | } else if (!prUrl.test(context.prUrl)) { 45 | context.report({ 46 | id, 47 | message: 'PR-URL must be a GitHub pull request URL.', 48 | string: context.prUrl, 49 | line, 50 | column, 51 | level: 'fail' 52 | }) 53 | } else { 54 | context.report({ 55 | id, 56 | message: 'PR-URL is valid.', 57 | string: context.prUrl, 58 | line, 59 | column, 60 | level: 'pass' 61 | }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/rules/reviewers.js: -------------------------------------------------------------------------------- 1 | const id = 'reviewers' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce having reviewers', 7 | recommended: true 8 | }, 9 | defaults: {}, 10 | options: {}, 11 | validate: (context, rule) => { 12 | const parsed = context.toJSON() 13 | // release commits generally won't have any reviewers 14 | if (parsed.release) { 15 | context.report({ 16 | id, 17 | message: 'skipping reviewers for release commit', 18 | string: '', 19 | level: 'skip' 20 | }) 21 | return 22 | } 23 | 24 | if (!Array.isArray(parsed.reviewers) || !parsed.reviewers.length) { 25 | // See nodejs/node#5aac4c42da104c30d8f701f1042d61c2f06b7e6c 26 | // for an example 27 | return context.report({ 28 | id, 29 | message: 'Commit must have at least 1 reviewer.', 30 | string: null, 31 | line: 0, 32 | column: 0, 33 | level: 'fail' 34 | }) 35 | } 36 | 37 | // TODO(evanlucas) verify that each reviewer is a collaborator 38 | // This will probably be easier to do once we move gitlint-parser-node 39 | // over to using an array of objects with parsed reviewers vs an array 40 | // of strings 41 | 42 | context.report({ 43 | id, 44 | message: 'reviewers are valid', 45 | string: '', 46 | level: 'pass' 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/rules/subsystem.js: -------------------------------------------------------------------------------- 1 | const id = 'subsystem' 2 | 3 | const validSubsystems = [ 4 | 'benchmark', 5 | 'build', 6 | 'bootstrap', 7 | 'cli', 8 | 'deps', 9 | 'doc', 10 | 'errors', 11 | 'etw', 12 | 'esm', 13 | 'gyp', 14 | 'inspector', 15 | 'lib', 16 | 'loader', 17 | 'meta', 18 | 'msi', 19 | 'node', 20 | 'node-api', 21 | 'perfctr', 22 | 'permission', 23 | 'policy', 24 | 'sea', 25 | 'src', 26 | 'test', 27 | 'tools', 28 | 'typings', 29 | 'wasm', 30 | 'watch', 31 | 'win', 32 | 33 | // core libs 34 | 'assert', 35 | 'async_hooks', 36 | 'buffer', 37 | 'child_process', 38 | 'cluster', 39 | 'console', 40 | 'constants', 41 | 'crypto', 42 | 'debugger', 43 | 'dgram', 44 | 'diagnostics_channel', 45 | 'dns', 46 | 'domain', 47 | 'events', 48 | 'fs', 49 | 'http', 50 | 'http2', 51 | 'https', 52 | 'inspector', 53 | 'module', 54 | 'net', 55 | 'os', 56 | 'path', 57 | 'perf_hooks', 58 | 'process', 59 | 'punycode', 60 | 'querystring', 61 | 'quic', 62 | 'readline', 63 | 'repl', 64 | 'report', 65 | 'sqlite', 66 | 'stream', 67 | 'string_decoder', 68 | 'sys', 69 | 'test_runner', 70 | 'timers', 71 | 'tls', 72 | 'trace_events', 73 | 'tty', 74 | 'url', 75 | 'util', 76 | 'v8', 77 | 'vm', 78 | 'wasi', 79 | 'worker', 80 | 'zlib' 81 | ] 82 | 83 | export default { 84 | id, 85 | meta: { 86 | description: 'enforce subsystem validity', 87 | recommended: true 88 | }, 89 | defaults: { 90 | subsystems: validSubsystems 91 | }, 92 | options: { 93 | subsystems: validSubsystems 94 | }, 95 | validate: (context, rule) => { 96 | const subs = rule.options.subsystems 97 | const parsed = context.toJSON() 98 | if (!parsed.subsystems.length) { 99 | if (!parsed.release && !parsed.working) { 100 | // Missing subsystem 101 | context.report({ 102 | id, 103 | message: 'Missing subsystem.', 104 | string: parsed.title, 105 | line: 0, 106 | column: 0, 107 | level: 'fail', 108 | wanted: subs 109 | }) 110 | } else { 111 | context.report({ 112 | id, 113 | message: 'Release commits do not have subsystems', 114 | string: '', 115 | level: 'skip' 116 | }) 117 | } 118 | } else { 119 | let failed = false 120 | for (const sub of parsed.subsystems) { 121 | if (!~subs.indexOf(sub)) { 122 | failed = true 123 | // invalid subsystem 124 | const column = parsed.title.indexOf(sub) 125 | context.report({ 126 | id, 127 | message: `Invalid subsystem: "${sub}"`, 128 | string: parsed.title, 129 | line: 0, 130 | column, 131 | level: 'fail', 132 | wanted: subs 133 | }) 134 | } 135 | } 136 | 137 | if (!failed) { 138 | context.report({ 139 | id, 140 | message: 'valid subsystems', 141 | string: parsed.subsystems.join(','), 142 | level: 'pass' 143 | }) 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/rules/title-format.js: -------------------------------------------------------------------------------- 1 | const id = 'title-format' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce commit title format', 7 | recommended: true 8 | }, 9 | validate: (context, rule) => { 10 | const isRevertCommit = /^Revert ".*"$/.test(context.title) 11 | const revertPrefixLength = 'Revert "'.length 12 | const titleWithoutRevertPrefix = isRevertCommit 13 | ? context.title.substr(revertPrefixLength, 14 | context.title.length - revertPrefixLength - 1) 15 | : context.title 16 | let pass = true 17 | if (/[.?!]$/.test(titleWithoutRevertPrefix)) { 18 | context.report({ 19 | id, 20 | message: 'Do not use punctuation at end of title.', 21 | string: context.title, 22 | line: 0, 23 | column: context.title.length, 24 | level: 'fail' 25 | }) 26 | pass = false 27 | } 28 | 29 | { 30 | const result = /^([^:]+:)[^ ]/.exec(titleWithoutRevertPrefix) 31 | if (result) { 32 | context.report({ 33 | id, 34 | message: 'Add a space after subsystem(s).', 35 | string: context.title, 36 | line: 0, 37 | column: result[1].length, 38 | level: 'fail' 39 | }) 40 | pass = false 41 | } 42 | } 43 | 44 | { 45 | const result = /\s\s/.exec(titleWithoutRevertPrefix) 46 | if (result) { 47 | context.report({ 48 | id, 49 | message: 'Do not use consecutive spaces in title.', 50 | string: context.title, 51 | line: 0, 52 | column: result.index + 1, 53 | level: 'fail' 54 | }) 55 | pass = false 56 | } 57 | } 58 | 59 | const isV8 = titleWithoutRevertPrefix.startsWith('deps: V8:') 60 | if (!isV8) { 61 | const result = /^([^:]+?): [A-Z]/.exec(titleWithoutRevertPrefix) 62 | if (result) { 63 | context.report({ 64 | id, 65 | message: 'First word after subsystem(s) in title should be lowercase.', 66 | string: context.title, 67 | line: 0, 68 | column: result[1].length + 3, 69 | level: 'fail' 70 | }) 71 | pass = false 72 | } 73 | } 74 | 75 | if (pass) { 76 | context.report({ 77 | id, 78 | message: 'Title is formatted correctly.', 79 | string: '', 80 | level: 'pass' 81 | }) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/rules/title-length.js: -------------------------------------------------------------------------------- 1 | const id = 'title-length' 2 | 3 | export default { 4 | id, 5 | meta: { 6 | description: 'enforce max length of commit title', 7 | recommended: true 8 | }, 9 | defaults: { 10 | length: 50, 11 | max_length: 72 12 | }, 13 | options: { 14 | length: 50, 15 | max_length: 72 16 | }, 17 | validate: (context, rule) => { 18 | const isRevertCommit = /^Revert ".*"$/.test(context.title) 19 | const max = rule.options.max_length + (isRevertCommit ? 'Revert ""'.length : 0) 20 | if (context.title.length > max) { 21 | context.report({ 22 | id, 23 | message: `Title must be <= ${max} columns.`, 24 | string: context.title, 25 | maxLength: max, 26 | line: 0, 27 | column: max, 28 | level: 'fail' 29 | }) 30 | return 31 | } 32 | 33 | const len = rule.options.length + (isRevertCommit ? 'Revert ""'.length : 0) 34 | if (context.title.length > len) { 35 | context.report({ 36 | id, 37 | message: `Title should be <= ${len} columns.`, 38 | string: context.title, 39 | maxLength: len, 40 | line: 0, 41 | column: len, 42 | level: 'warn' 43 | }) 44 | return 45 | } 46 | 47 | context.report({ 48 | id, 49 | message: `Title is <= ${len} columns.`, 50 | string: '', 51 | level: 'pass' 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/tap.js: -------------------------------------------------------------------------------- 1 | import util from 'node:util' 2 | import { Readable } from 'node:stream' 3 | 4 | class Test { 5 | constructor (tap, name) { 6 | this.tap = tap 7 | this.name = name 8 | this._count = 0 9 | this._passes = 0 10 | this._failures = 0 11 | this._skips = 0 12 | this.begin() 13 | } 14 | 15 | pass (msg) { 16 | this.tap.write(`ok ${++this._count} ${msg}`) 17 | this._passes++ 18 | this.tap.pass() 19 | } 20 | 21 | fail (msg, extra) { 22 | this.tap.write(`not ok ${++this._count} ${msg}`) 23 | if (extra) { 24 | this.tap.write(' ---') 25 | if (typeof extra === 'object') { 26 | extra = util.inspect(extra) 27 | } 28 | 29 | if (typeof extra === 'string') { 30 | extra.split(/\n/).forEach((item) => { 31 | this.tap.write(` ${item}`) 32 | }) 33 | } 34 | this.tap.write(' ...') 35 | } 36 | this._failures++ 37 | this.tap.fail() 38 | } 39 | 40 | skip (msg) { 41 | this.tap.write(`ok ${++this._count} ${msg} # SKIP`) 42 | this._skips++ 43 | this.tap.skip() 44 | } 45 | 46 | begin () { 47 | this.tap.write(`# ${this.name}`) 48 | } 49 | } 50 | 51 | export default class Tap extends Readable { 52 | constructor () { 53 | super() 54 | this._wroteVersion = false 55 | this._count = 0 56 | this._passes = 0 57 | this._failures = 0 58 | this._skips = 0 59 | } 60 | 61 | get status () { 62 | if (this._failures) { 63 | return 'fail' 64 | } 65 | 66 | return 'pass' 67 | } 68 | 69 | writeVersion () { 70 | if (!this._wroteVersion) { 71 | this.write('TAP version 13') 72 | this._wroteVersion = true 73 | } 74 | } 75 | 76 | pass () { 77 | this._passes++ 78 | this._count++ 79 | } 80 | 81 | fail () { 82 | this._failures++ 83 | this._count++ 84 | } 85 | 86 | skip () { 87 | this._skips++ 88 | this._count++ 89 | } 90 | 91 | write (str = '') { 92 | this.push(`${str}\n`) 93 | } 94 | 95 | test (name) { 96 | const t = new Test(this, name) 97 | return t 98 | } 99 | 100 | _read () {} 101 | 102 | end () { 103 | this.write() 104 | this.write(`0..${this._count}`) 105 | this.write(`# tests ${this._count}`) 106 | if (this._passes) { 107 | this.write(`# pass ${this._passes}`) 108 | } 109 | 110 | if (this._failures) { 111 | this.write(`# fail ${this._failures}`) 112 | this.write('# Please review the commit message guidelines:') 113 | this.write('# https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines') 114 | } 115 | 116 | this.push(null) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export const CHECK = chalk.green('✔') 4 | export const X = chalk.red('✖') 5 | export const WARN = chalk.yellow('⚠') 6 | 7 | export function rightPad (str, max) { 8 | const diff = max - str.length + 1 9 | if (diff > 0) { 10 | return `${str}${' '.repeat(diff)}` 11 | } 12 | return str 13 | } 14 | 15 | export function leftPad (str, max) { 16 | const diff = max - str.length + 1 17 | if (diff > 0) { 18 | return `${' '.repeat(diff)}${str}` 19 | } 20 | return str 21 | } 22 | 23 | export function header (sha, status) { 24 | switch (status) { 25 | case 'skip': 26 | case 'pass': { 27 | const suffix = status === 'skip' ? ' # SKIPPED' : '' 28 | return `${CHECK} ${chalk.underline(sha)}${suffix}` 29 | } 30 | case 'fail': 31 | return `${X} ${chalk.underline(sha)}` 32 | } 33 | } 34 | 35 | export function describeRule (rule, max = 20) { 36 | if (rule.meta && rule.meta.description) { 37 | const desc = rule.meta.description 38 | const title = leftPad(rule.id, max) 39 | console.log(' %s %s', chalk.red(title), chalk.dim(desc)) 40 | } 41 | } 42 | 43 | export function describeSubsystem (subsystems, max = 20) { 44 | if (subsystems) { 45 | for (let sub = 0; sub < subsystems.length; sub = sub + 3) { 46 | console.log('%s %s %s', 47 | chalk.green(leftPad(subsystems[sub] || '', max)), 48 | chalk.green(leftPad(subsystems[sub + 1] || '', max)), 49 | chalk.green(leftPad(subsystems[sub + 2] || '', max)) 50 | ) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/validator.js: -------------------------------------------------------------------------------- 1 | import EE from 'node:events' 2 | import Parser from 'gitlint-parser-node' 3 | import BaseRule from './rule.js' 4 | 5 | // Rules 6 | import coAuthoredByIsTrailer from './rules/co-authored-by-is-trailer.js' 7 | import fixesUrl from './rules/fixes-url.js' 8 | import lineAfterTitle from './rules/line-after-title.js' 9 | import lineLength from './rules/line-length.js' 10 | import metadataEnd from './rules/metadata-end.js' 11 | import prUrl from './rules/pr-url.js' 12 | import reviewers from './rules/reviewers.js' 13 | import subsystem from './rules/subsystem.js' 14 | import titleFormat from './rules/title-format.js' 15 | import titleLength from './rules/title-length.js' 16 | 17 | const RULES = { 18 | 'co-authored-by-is-trailer': coAuthoredByIsTrailer, 19 | 'fixes-url': fixesUrl, 20 | 'line-after-title': lineAfterTitle, 21 | 'line-length': lineLength, 22 | 'metadata-end': metadataEnd, 23 | 'pr-url': prUrl, 24 | reviewers, 25 | subsystem, 26 | 'title-format': titleFormat, 27 | 'title-length': titleLength 28 | } 29 | 30 | export default class ValidateCommit extends EE { 31 | constructor (options) { 32 | super() 33 | 34 | this.opts = Object.assign({ 35 | 'validate-metadata': true 36 | }, options) 37 | 38 | this.messages = new Map() 39 | this.errors = 0 40 | 41 | this.rules = new Map() 42 | this.loadBaseRules() 43 | } 44 | 45 | loadBaseRules () { 46 | const keys = Object.keys(RULES) 47 | for (const key of keys) { 48 | this.rules.set(key, new BaseRule(RULES[key])) 49 | } 50 | if (!this.opts['validate-metadata']) { 51 | this.disableRule('pr-url') 52 | this.disableRule('reviewers') 53 | this.disableRule('metadata-end') 54 | } 55 | } 56 | 57 | disableRule (id) { 58 | if (!this.rules.has(id)) { 59 | throw new TypeError(`Invalid rule: "${id}"`) 60 | } 61 | 62 | this.rules.get(id).disabled = true 63 | } 64 | 65 | lint (str) { 66 | if (Array.isArray(str)) { 67 | for (const item of str) { 68 | this.lint(item) 69 | } 70 | } else { 71 | const commit = new Parser(str, this) 72 | for (const rule of this.rules.values()) { 73 | if (rule.disabled) continue 74 | rule.validate(commit) 75 | } 76 | 77 | setImmediate(() => { 78 | this.emit('commit', { 79 | commit, 80 | messages: this.messages.get(commit.sha) || [] 81 | }) 82 | }) 83 | } 84 | } 85 | 86 | report (opts) { 87 | const commit = opts.commit 88 | const sha = commit.sha 89 | if (!sha) { 90 | throw new Error('Invalid report. Missing commit sha') 91 | } 92 | 93 | if (opts.data.level === 'fail') { this.errors++ } 94 | const ar = this.messages.get(sha) || [] 95 | ar.push(opts.data) 96 | this.messages.set(sha, ar) 97 | setImmediate(() => { 98 | this.emit('message', opts) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core-validate-commit", 3 | "version": "4.1.0", 4 | "description": "Validate the commit message for a particular commit in node core", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "pretest": "standard && check-pkg", 9 | "test": "c8 tap -j4 --no-coverage test/**/*.js test/*.js", 10 | "test-ci": "npm run test && c8 report --reporter=lcov" 11 | }, 12 | "dependencies": { 13 | "chalk": "^5.2.0", 14 | "gitlint-parser-node": "^1.1.0", 15 | "nopt": "^7.0.0" 16 | }, 17 | "devDependencies": { 18 | "c8": "^7.13.0", 19 | "check-pkg": "^2.1.1", 20 | "standard": "^17.0.0", 21 | "tap": "^16.3.4" 22 | }, 23 | "files": [ 24 | "lib/", 25 | "bin/", 26 | "index.js" 27 | ], 28 | "license": "MIT", 29 | "bin": { 30 | "core-validate-commit": "./bin/cmd.js" 31 | }, 32 | "engines": { 33 | "node": "^18.18.0 || >=20.10.0" 34 | }, 35 | "author": "Evan Lucas ", 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/nodejs/core-validate-commit" 39 | }, 40 | "homepage": "https://github.com/nodejs/core-validate-commit", 41 | "bugs": { 42 | "url": "https://github.com/nodejs/core-validate-commit/issues" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/cli-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import { readFileSync } from 'node:fs' 3 | import { spawn } from 'node:child_process' 4 | import subsystems from '../lib/rules/subsystem.js' 5 | 6 | test('Test cli flags', (t) => { 7 | t.test('test list-subsystems', (tt) => { 8 | const ls = spawn('./bin/cmd.js', ['--list-subsystems'], { 9 | env: { ...process.env, FORCE_COLOR: 0 } 10 | }) 11 | let compiledData = '' 12 | ls.stdout.on('data', (data) => { 13 | compiledData += data 14 | }) 15 | 16 | ls.stderr.on('data', (data) => { 17 | tt.fail('This should not happen') 18 | }) 19 | 20 | ls.on('close', (code) => { 21 | // Get the list of subsytems as an Array. 22 | // Need to match words that also have the "-" in them 23 | const subsystemsFromOutput = compiledData.match(/[\w'-]+/g) 24 | const defaultSubsystems = subsystems.defaults.subsystems 25 | 26 | tt.equal(subsystemsFromOutput.length, 27 | defaultSubsystems.length, 28 | 'Should have the same length') 29 | 30 | // Loop through the output list and compare with the real list 31 | // to make sure they are all there 32 | const missing = [] 33 | subsystemsFromOutput.forEach((sub) => { 34 | if (!defaultSubsystems.find((x) => { return x === sub })) { 35 | missing.push(sub) 36 | } 37 | }) 38 | 39 | tt.equal(missing.length, 0, 'Should have no missing subsystems') 40 | tt.end() 41 | }) 42 | }) 43 | 44 | t.test('test help output', (tt) => { 45 | const usage = readFileSync('bin/usage.txt', { encoding: 'utf8' }) 46 | const ls = spawn('./bin/cmd.js', ['--help']) 47 | let compiledData = '' 48 | ls.stdout.on('data', (data) => { 49 | compiledData += data 50 | }) 51 | 52 | ls.stderr.on('data', (data) => { 53 | tt.fail('This should not happen') 54 | }) 55 | 56 | ls.on('close', (code) => { 57 | tt.equal(compiledData.trim(), 58 | usage.trim(), 59 | '--help output is as expected') 60 | tt.end() 61 | }) 62 | }) 63 | 64 | t.test('test sha', (tt) => { 65 | const ls = spawn('./bin/cmd.js', ['--no-validate-metadata', '2b98d02b52']) 66 | let compiledData = '' 67 | ls.stdout.on('data', (data) => { 68 | compiledData += data 69 | }) 70 | 71 | ls.stderr.on('data', (data) => { 72 | tt.fail('This should not happen') 73 | }) 74 | 75 | ls.on('close', (code) => { 76 | tt.match(compiledData.trim(), 77 | /2b98d02b52/, 78 | 'output is as expected') 79 | tt.end() 80 | }) 81 | }) 82 | 83 | t.test('test tap output', (tt) => { 84 | // Use a commit from this repository that does not follow the guidelines. 85 | const ls = spawn('./bin/cmd.js', ['--no-validate-metadata', '--tap', '69435db261']) 86 | let compiledData = '' 87 | ls.stdout.on('data', (data) => { 88 | compiledData += data 89 | }) 90 | 91 | ls.stderr.on('data', (data) => { 92 | tt.fail(`Unexpected stderr output ${data.toString()}`) 93 | }) 94 | 95 | ls.on('close', (code) => { 96 | const output = compiledData.trim() 97 | tt.match(output, 98 | /# 69435db261/, 99 | 'TAP output contains the sha of the commit being linted') 100 | tt.match(output, 101 | /not ok \d+ subsystem: Invalid subsystem: "chore" \(chore: update tested node release lines \(#94\)\)/, 102 | 'TAP output contains failure for subsystem') 103 | tt.match(output, 104 | /# fail\s+\d+/, 105 | 'TAP output contains total failures') 106 | tt.match(output, 107 | /# Please review the commit message guidelines:\s# https:\/\/github.com\/nodejs\/node\/blob\/HEAD\/doc\/contributing\/pull-requests.md#commit-message-guidelines/, 108 | 'TAP output contains pointer to commit message guidelines') 109 | tt.equal(code, 1, 'CLI exits with non-zero code on failure') 110 | tt.end() 111 | }) 112 | }) 113 | 114 | t.test('test url', (tt) => { 115 | const ls = spawn('./bin/cmd.js', ['--no-validate-metadata', 'https://api.github.com/repos/nodejs/core-validate-commit/commits/2b98d02b52']) 116 | let compiledData = '' 117 | ls.stdout.on('data', (data) => { 118 | compiledData += data 119 | }) 120 | 121 | ls.stderr.on('data', (data) => { 122 | tt.fail('This should not happen') 123 | }) 124 | 125 | ls.on('close', (code) => { 126 | tt.match(compiledData.trim(), 127 | /2b98d02b52/, 128 | 'output is as expected') 129 | tt.end() 130 | }) 131 | }) 132 | 133 | t.test('test version flag', (tt) => { 134 | const ls = spawn('./bin/cmd.js', ['--version']) 135 | let compiledData = '' 136 | ls.stdout.on('data', (data) => { 137 | compiledData += data 138 | }) 139 | 140 | ls.stderr.on('data', (data) => { 141 | tt.fail('This should not happen') 142 | }) 143 | 144 | ls.on('close', async (code) => { 145 | const pkgJsonPath = new URL('../package.json', import.meta.url) 146 | const pkgJson = readFileSync(pkgJsonPath, { encoding: 'utf8' }) 147 | const { version } = JSON.parse(pkgJson) 148 | tt.equal(compiledData.trim(), 149 | `core-validate-commit v${version}`, 150 | 'output is equal') 151 | tt.end() 152 | }) 153 | }) 154 | 155 | t.end() 156 | }) 157 | -------------------------------------------------------------------------------- /test/fixtures/commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha": "e7c077c610afa371430180fbd447bfef60ebc5ea", 3 | "url": "https://api.github.com/repos/nodejs/node/git/commits/e7c077c610afa371430180fbd447bfef60ebc5ea", 4 | "html_url": "https://github.com/nodejs/node/commit/e7c077c610afa371430180fbd447bfef60ebc5ea", 5 | "author": { 6 | "name": "Calvin Metcalf", 7 | "email": "cmetcalf@appgeo.com", 8 | "date": "2016-04-12T19:42:23Z" 9 | }, 10 | "committer": { 11 | "name": "James M Snell", 12 | "email": "jasnell@gmail.com", 13 | "date": "2016-04-20T20:28:35Z" 14 | }, 15 | "tree": { 16 | "sha": "d3f20ccfaa7b0919a7c5a472e344b7de8829b30c", 17 | "url": "https://api.github.com/repos/nodejs/node/git/trees/d3f20ccfaa7b0919a7c5a472e344b7de8829b30c" 18 | }, 19 | "message": "stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ", 20 | "parents": [ 21 | { 22 | "sha": "ec2822adaad76b126b5cccdeaa1addf2376c9aa6", 23 | "url": "https://api.github.com/repos/nodejs/node/git/commits/ec2822adaad76b126b5cccdeaa1addf2376c9aa6", 24 | "html_url": "https://github.com/nodejs/node/commit/ec2822adaad76b126b5cccdeaa1addf2376c9aa6" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/pr.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sha": "e7c077c610afa371430180fbd447bfef60ebc5ea", 4 | "commit": { 5 | "author": { 6 | "name": "Calvin Metcalf", 7 | "email": "cmetcalf@appgeo.com", 8 | "date": "2016-04-12T19:42:23Z" 9 | }, 10 | "committer": { 11 | "name": "Calvin Metcalf", 12 | "email": "cmetcalf@appgeo.com", 13 | "date": "2016-04-13T16:33:55Z" 14 | }, 15 | "message": "stream: make null an invalid chunk to write in object mode\n\nthis harmonizes behavior between readable, writable, and transform\nstreams so that they all handle nulls in object mode the same way by\nconsidering them invalid chunks.\n\nPR-URL: https://github.com/nodejs/node/pull/6170\nReviewed-By: James M Snell \nReviewed-By: Matteo Collina ", 16 | "tree": { 17 | "sha": "e4f9381fdd77d1fd38fe27a80dc43486ac732d48", 18 | "url": "https://api.github.com/repos/nodejs/node/git/trees/e4f9381fdd77d1fd38fe27a80dc43486ac732d48" 19 | }, 20 | "url": "https://api.github.com/repos/nodejs/node/git/commits/401ff75945d39b28b26c4e54863f312b19c0a2dd", 21 | "comment_count": 0 22 | }, 23 | "url": "https://api.github.com/repos/nodejs/node/commits/401ff75945d39b28b26c4e54863f312b19c0a2dd", 24 | "html_url": "https://github.com/nodejs/node/commit/401ff75945d39b28b26c4e54863f312b19c0a2dd", 25 | "comments_url": "https://api.github.com/repos/nodejs/node/commits/401ff75945d39b28b26c4e54863f312b19c0a2dd/comments", 26 | "author": { 27 | "login": "calvinmetcalf", 28 | "id": 1128607, 29 | "avatar_url": "https://avatars.githubusercontent.com/u/1128607?v=3", 30 | "gravatar_id": "", 31 | "url": "https://api.github.com/users/calvinmetcalf", 32 | "html_url": "https://github.com/calvinmetcalf", 33 | "followers_url": "https://api.github.com/users/calvinmetcalf/followers", 34 | "following_url": "https://api.github.com/users/calvinmetcalf/following{/other_user}", 35 | "gists_url": "https://api.github.com/users/calvinmetcalf/gists{/gist_id}", 36 | "starred_url": "https://api.github.com/users/calvinmetcalf/starred{/owner}{/repo}", 37 | "subscriptions_url": "https://api.github.com/users/calvinmetcalf/subscriptions", 38 | "organizations_url": "https://api.github.com/users/calvinmetcalf/orgs", 39 | "repos_url": "https://api.github.com/users/calvinmetcalf/repos", 40 | "events_url": "https://api.github.com/users/calvinmetcalf/events{/privacy}", 41 | "received_events_url": "https://api.github.com/users/calvinmetcalf/received_events", 42 | "type": "User", 43 | "site_admin": false 44 | }, 45 | "committer": { 46 | "login": "calvinmetcalf", 47 | "id": 1128607, 48 | "avatar_url": "https://avatars.githubusercontent.com/u/1128607?v=3", 49 | "gravatar_id": "", 50 | "url": "https://api.github.com/users/calvinmetcalf", 51 | "html_url": "https://github.com/calvinmetcalf", 52 | "followers_url": "https://api.github.com/users/calvinmetcalf/followers", 53 | "following_url": "https://api.github.com/users/calvinmetcalf/following{/other_user}", 54 | "gists_url": "https://api.github.com/users/calvinmetcalf/gists{/gist_id}", 55 | "starred_url": "https://api.github.com/users/calvinmetcalf/starred{/owner}{/repo}", 56 | "subscriptions_url": "https://api.github.com/users/calvinmetcalf/subscriptions", 57 | "organizations_url": "https://api.github.com/users/calvinmetcalf/orgs", 58 | "repos_url": "https://api.github.com/users/calvinmetcalf/repos", 59 | "events_url": "https://api.github.com/users/calvinmetcalf/events{/privacy}", 60 | "received_events_url": "https://api.github.com/users/calvinmetcalf/received_events", 61 | "type": "User", 62 | "site_admin": false 63 | }, 64 | "parents": [ 65 | { 66 | "sha": "aba035fb27b14fe561c45540818be6a2bbb9dc9e", 67 | "url": "https://api.github.com/repos/nodejs/node/commits/aba035fb27b14fe561c45540818be6a2bbb9dc9e", 68 | "html_url": "https://github.com/nodejs/node/commit/aba035fb27b14fe561c45540818be6a2bbb9dc9e" 69 | } 70 | ] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /test/rule-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import BaseRule from '../lib/rule.js' 3 | 4 | test('Base Rule Test', (t) => { 5 | t.test('No id param', (tt) => { 6 | tt.throws(() => { 7 | const Rule = new BaseRule() 8 | Rule() 9 | }, 'Rule must have an id') 10 | 11 | tt.end() 12 | }) 13 | 14 | t.test('No validate function', (tt) => { 15 | tt.throws(() => { 16 | const Rule = new BaseRule({ id: 'test-rule' }) 17 | Rule() 18 | }, 'Rule must have validate function') 19 | 20 | tt.end() 21 | }) 22 | 23 | t.end() 24 | }) 25 | -------------------------------------------------------------------------------- /test/rules/co-authored-by-is-trailer.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/co-authored-by-is-trailer.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | 6 | test('rule: co-authored-by-is-trailer', (t) => { 7 | t.test('no co-authors', (tt) => { 8 | tt.plan(4) 9 | const v = new Validator() 10 | const context = new Commit({ 11 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 12 | author: { 13 | name: 'Foo', 14 | email: 'foo@example.com', 15 | date: '2016-04-12T19:42:23Z' 16 | }, 17 | message: 'test: fix something\n' + 18 | '\n' + 19 | 'fhqwhgads' 20 | }, v) 21 | 22 | context.report = (opts) => { 23 | tt.pass('called report') 24 | tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') 25 | tt.equal(opts.message, 'no Co-authored-by metadata', 'message') 26 | tt.equal(opts.level, 'pass', 'level') 27 | } 28 | 29 | Rule.validate(context) 30 | }) 31 | 32 | t.test('no empty lines above', (tt) => { 33 | tt.plan(7) 34 | const v = new Validator() 35 | const context = new Commit({ 36 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 37 | author: { 38 | name: 'Foo', 39 | email: 'foo@example.com', 40 | date: '2016-04-12T19:42:23Z' 41 | }, 42 | message: 'test: fix something\n' + 43 | 'Co-authored-by: Someone ' 44 | }, v) 45 | 46 | context.report = (opts) => { 47 | tt.pass('called report') 48 | tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') 49 | tt.equal(opts.message, 'Co-authored-by must be a trailer', 'message') 50 | tt.equal(opts.string, 'Co-authored-by: Someone ', 'string') 51 | tt.equal(opts.line, 0, 'line') 52 | tt.equal(opts.column, 0, 'column') 53 | tt.equal(opts.level, 'fail', 'level') 54 | } 55 | 56 | Rule.validate(context) 57 | }) 58 | 59 | t.test('not trailer', (tt) => { 60 | tt.plan(7) 61 | const v = new Validator() 62 | const context = new Commit({ 63 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 64 | author: { 65 | name: 'Foo', 66 | email: 'foo@example.com', 67 | date: '2016-04-12T19:42:23Z' 68 | }, 69 | message: 'test: fix something\n' + 70 | '\n' + 71 | 'Some description.\n' + 72 | '\n' + 73 | 'Co-authored-by: Someone \n' + 74 | '\n' + 75 | 'Reviewed-By: Bar ' 76 | }, v) 77 | 78 | context.report = (opts) => { 79 | tt.pass('called report') 80 | tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') 81 | tt.equal(opts.message, 'Co-authored-by must be a trailer', 'message') 82 | tt.equal(opts.string, 'Co-authored-by: Someone ', 'string') 83 | tt.equal(opts.line, 3, 'line') 84 | tt.equal(opts.column, 0, 'column') 85 | tt.equal(opts.level, 'fail', 'level') 86 | } 87 | 88 | Rule.validate(context) 89 | }) 90 | 91 | t.test('not all are trailers', (tt) => { 92 | tt.plan(7) 93 | const v = new Validator() 94 | const context = new Commit({ 95 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 96 | author: { 97 | name: 'Foo', 98 | email: 'foo@example.com', 99 | date: '2016-04-12T19:42:23Z' 100 | }, 101 | message: 'test: fix something\n' + 102 | '\n' + 103 | 'Some description.\n' + 104 | '\n' + 105 | 'Co-authored-by: Someone \n' + 106 | '\n' + 107 | 'Co-authored-by: Someone Else \n' + 108 | 'Reviewed-By: Bar ' 109 | }, v) 110 | 111 | context.report = (opts) => { 112 | tt.pass('called report') 113 | tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') 114 | tt.equal(opts.message, 'Co-authored-by must be a trailer', 'message') 115 | tt.equal(opts.string, 'Co-authored-by: Someone ', 'string') 116 | tt.equal(opts.line, 3, 'line') 117 | tt.equal(opts.column, 0, 'column') 118 | tt.equal(opts.level, 'fail', 'level') 119 | } 120 | 121 | Rule.validate(context) 122 | }) 123 | 124 | t.test('is trailer', (tt) => { 125 | tt.plan(4) 126 | const v = new Validator() 127 | const context = new Commit({ 128 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 129 | author: { 130 | name: 'Foo', 131 | email: 'foo@example.com', 132 | date: '2016-04-12T19:42:23Z' 133 | }, 134 | message: 'test: fix something\n' + 135 | '\n' + 136 | 'Some description.\n' + 137 | '\n' + 138 | 'More description.\n' + 139 | '\n' + 140 | 'Co-authored-by: Someone \n' + 141 | 'Reviewed-By: Bar ' 142 | }, v) 143 | 144 | context.report = (opts) => { 145 | tt.pass('called report') 146 | tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') 147 | tt.equal(opts.message, 'Co-authored-by is a trailer', 'message') 148 | tt.equal(opts.level, 'pass', 'level') 149 | } 150 | 151 | Rule.validate(context) 152 | }) 153 | 154 | t.end() 155 | }) 156 | -------------------------------------------------------------------------------- /test/rules/fixes-url.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/fixes-url.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | 6 | const INVALID_PRURL = 'Pull request URL must reference a comment or discussion.' 7 | const NOT_AN_ISSUE_NUMBER = 'Fixes must be a URL, not an issue number.' 8 | const NOT_A_GITHUB_URL = 'Fixes must be a GitHub URL.' 9 | const VALID_FIXES_URL = 'Valid fixes URL.' 10 | 11 | const makeCommit = (msg) => { 12 | return new Commit({ 13 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 14 | author: { 15 | name: 'Evan Lucas', 16 | email: 'evanlucas@me.com', 17 | date: '2016-04-12T19:42:23Z' 18 | }, 19 | message: msg 20 | }, new Validator()) 21 | } 22 | 23 | test('rule: fixes-url', (t) => { 24 | const valid = [ 25 | ['GitHub issue URL', 26 | 'https://github.com/nodejs/node/issues/1234'], 27 | ['GitHub issue URL with trailing slash', 28 | 'https://github.com/nodejs/node/issues/1234/'], 29 | ['GitHub issue URL containing hyphen', 30 | 'https://github.com/nodejs/node-report/issues/1234'], 31 | ['GitHub issue URL containing hyphen with comment', 32 | 'https://github.com/nodejs/node-report/issues/1234#issuecomment-1234'], 33 | ['GitHub issue URL with comment', 34 | 'https://github.com/nodejs/node/issues/1234#issuecomment-1234'], 35 | ['GitHub PR URL containing hyphen with comment', 36 | 'https://github.com/nodejs/node-report/pull/1234#issuecomment-1234'], 37 | ['GitHub PR URL containing hyphen with discussion comment', 38 | 'https://github.com/nodejs/node-report/pull/1234#discussion_r1234'], 39 | ['GitHub PR URL with comment', 40 | 'https://github.com/nodejs/node/pull/1234#issuecomment-1234'], 41 | ['GitHub PR URL with discussion comment', 42 | 'https://github.com/nodejs/node/pull/1234#discussion_r1234'] 43 | ] 44 | 45 | for (const [name, url] of valid) { 46 | t.test(name, (tt) => { 47 | tt.plan(7) 48 | const context = makeCommit(`test: fix something 49 | 50 | Fixes: ${url}` 51 | ) 52 | 53 | context.report = (opts) => { 54 | tt.pass('called report') 55 | tt.equal(opts.id, 'fixes-url', 'id') 56 | tt.equal(opts.message, VALID_FIXES_URL, 'message') 57 | tt.equal(opts.string, url, 'string') 58 | tt.equal(opts.line, 1, 'line') 59 | tt.equal(opts.column, 7, 'column') 60 | tt.equal(opts.level, 'pass', 'level') 61 | } 62 | 63 | Rule.validate(context) 64 | }) 65 | } 66 | 67 | const invalid = [ 68 | ['issue number', NOT_AN_ISSUE_NUMBER, 69 | '#1234'], 70 | ['GitHub PR URL', INVALID_PRURL, 71 | 'https://github.com/nodejs/node/pull/1234'], 72 | ['GitHub PR URL containing hyphen', INVALID_PRURL, 73 | 'https://github.com/nodejs/node-report/pull/1234'], 74 | ['non-GitHub URL', NOT_A_GITHUB_URL, 75 | 'https://nodejs.org'], 76 | ['not a URL or issue number', NOT_A_GITHUB_URL, 77 | 'fhqwhgads'] 78 | ] 79 | for (const [name, expected, url] of invalid) { 80 | t.test(name, (tt) => { 81 | tt.plan(7) 82 | const context = makeCommit(`test: fix something 83 | 84 | Fixes: ${url}` 85 | ) 86 | 87 | context.report = (opts) => { 88 | tt.pass('called report') 89 | tt.equal(opts.id, 'fixes-url', 'id') 90 | tt.equal(opts.message, expected, 'message') 91 | tt.equal(opts.string, url, 'string') 92 | tt.equal(opts.line, 1, 'line') 93 | tt.equal(opts.column, 7, 'column') 94 | tt.equal(opts.level, 'fail', 'level') 95 | } 96 | 97 | Rule.validate(context) 98 | }) 99 | } 100 | 101 | t.end() 102 | }) 103 | -------------------------------------------------------------------------------- /test/rules/line-after-title.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/line-after-title.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | 6 | test('rule: line-after-title', (t) => { 7 | t.test('no blank line', (tt) => { 8 | tt.plan(7) 9 | const v = new Validator() 10 | const context = new Commit({ 11 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 12 | author: { 13 | name: 'Evan Lucas', 14 | email: 'evanlucas@me.com', 15 | date: '2016-04-12T19:42:23Z' 16 | }, 17 | message: 'test: fix something\nfhqwhgads' 18 | }, v) 19 | 20 | context.report = (opts) => { 21 | tt.pass('called report') 22 | tt.equal(opts.id, 'line-after-title', 'id') 23 | tt.equal(opts.message, 'blank line expected after title', 'message') 24 | tt.equal(opts.string, 'fhqwhgads', 'string') 25 | tt.equal(opts.line, 1, 'line') 26 | tt.equal(opts.column, 0, 'column') 27 | tt.equal(opts.level, 'fail', 'level') 28 | } 29 | 30 | Rule.validate(context) 31 | }) 32 | 33 | t.test('blank line', (tt) => { 34 | tt.plan(4) 35 | const v = new Validator() 36 | const context = new Commit({ 37 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 38 | author: { 39 | name: 'Evan Lucas', 40 | email: 'evanlucas@me.com', 41 | date: '2016-04-12T19:42:23Z' 42 | }, 43 | message: 'test: fix something\n\nfhqwhgads' 44 | }, v) 45 | 46 | context.report = (opts) => { 47 | tt.pass('called report') 48 | tt.equal(opts.id, 'line-after-title', 'id') 49 | tt.equal(opts.message, 'blank line after title', 'message') 50 | tt.equal(opts.level, 'pass', 'level') 51 | } 52 | 53 | Rule.validate(context) 54 | }) 55 | 56 | t.test('just one line', (tt) => { 57 | tt.plan(4) 58 | const v = new Validator() 59 | const context = new Commit({ 60 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 61 | author: { 62 | name: 'Evan Lucas', 63 | email: 'evanlucas@me.com', 64 | date: '2016-04-12T19:42:23Z' 65 | }, 66 | message: 'test: fix something' 67 | }, v) 68 | 69 | context.report = (opts) => { 70 | tt.pass('called report') 71 | tt.equal(opts.id, 'line-after-title', 'id') 72 | tt.equal(opts.message, 'blank line after title', 'message') 73 | tt.equal(opts.level, 'pass', 'level') 74 | } 75 | 76 | Rule.validate(context) 77 | }) 78 | 79 | t.end() 80 | }) 81 | -------------------------------------------------------------------------------- /test/rules/line-length.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/line-length.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | 6 | test('rule: line-length', (t) => { 7 | t.test('line too long', (tt) => { 8 | tt.plan(7) 9 | const v = new Validator() 10 | const context = new Commit({ 11 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 12 | author: { 13 | name: 'Evan Lucas', 14 | email: 'evanlucas@me.com', 15 | date: '2016-04-12T19:42:23Z' 16 | }, 17 | message: `test: fix something 18 | 19 | ${'aaa'.repeat(30)}` 20 | }, v) 21 | 22 | context.report = (opts) => { 23 | tt.pass('called report') 24 | tt.equal(opts.id, 'line-length', 'id') 25 | tt.equal(opts.message, 'Line should be <= 72 columns.', 'message') 26 | tt.equal(opts.string, 'aaa'.repeat(30), 'string') 27 | tt.equal(opts.line, 1, 'line') 28 | tt.equal(opts.column, 72, 'column') 29 | tt.equal(opts.level, 'fail', 'level') 30 | } 31 | 32 | Rule.validate(context, { 33 | options: { 34 | length: 72 35 | } 36 | }) 37 | }) 38 | 39 | t.test('release commit', (tt) => { 40 | const v = new Validator() 41 | const context = new Commit({ 42 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 43 | author: { 44 | name: 'Evan Lucas', 45 | email: 'evanlucas@me.com', 46 | date: '2016-04-12T19:42:23Z' 47 | }, 48 | message: `2016-01-01, Version 1.0.0 49 | 50 | ${'aaa'.repeat(30)}` 51 | }, v) 52 | 53 | context.report = (opts) => { 54 | tt.pass('called report') 55 | tt.equal(opts.id, 'line-length', 'id') 56 | tt.equal(opts.string, '', 'string') 57 | tt.equal(opts.level, 'skip', 'level') 58 | } 59 | 60 | Rule.validate(context, { 61 | options: { 62 | length: 72 63 | } 64 | }) 65 | tt.end() 66 | }) 67 | 68 | t.test('quoted lines', (tt) => { 69 | const v = new Validator() 70 | const context = new Commit({ 71 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 72 | author: { 73 | name: 'Evan Lucas', 74 | email: 'evanlucas@me.com', 75 | date: '2016-04-12T19:42:23Z' 76 | }, 77 | message: `src: make foo mor foo-ey 78 | 79 | Here’s the original code: 80 | 81 | ${'aaa'.repeat(30)} 82 | 83 | That was the original code. 84 | ` 85 | }, v) 86 | 87 | context.report = (opts) => { 88 | tt.pass('called report') 89 | tt.equal(opts.id, 'line-length', 'id') 90 | tt.equal(opts.string, '', 'string') 91 | tt.equal(opts.level, 'pass', 'level') 92 | } 93 | 94 | Rule.validate(context, { 95 | options: { 96 | length: 72 97 | } 98 | }) 99 | tt.end() 100 | }) 101 | 102 | t.test('URLs', (tt) => { 103 | const v = new Validator() 104 | const context = new Commit({ 105 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 106 | author: { 107 | name: 'Evan Lucas', 108 | email: 'evanlucas@me.com', 109 | date: '2016-04-12T19:42:23Z' 110 | }, 111 | message: `src: make foo mor foo-ey 112 | 113 | https://${'very-'.repeat(80)}-long-url.org/ 114 | ` 115 | }, v) 116 | 117 | context.report = (opts) => { 118 | tt.pass('called report') 119 | tt.equal(opts.id, 'line-length', 'id') 120 | tt.equal(opts.string, '', 'string') 121 | tt.equal(opts.level, 'pass', 'level') 122 | } 123 | 124 | Rule.validate(context, { 125 | options: { 126 | length: 72 127 | } 128 | }) 129 | tt.end() 130 | }) 131 | 132 | t.end() 133 | }) 134 | -------------------------------------------------------------------------------- /test/rules/pr-url.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/pr-url.js' 3 | const MISSING_PR_URL = 'Commit must have a PR-URL.' 4 | const INVALID_PR_URL = 'PR-URL must be a GitHub pull request URL.' 5 | const NUMERIC_PR_URL = 'PR-URL must be a URL, not a pull request number.' 6 | const VALID_PR_URL = 'PR-URL is valid.' 7 | 8 | test('rule: pr-url', (t) => { 9 | t.test('missing', (tt) => { 10 | tt.plan(7) 11 | const context = { 12 | prUrl: null, 13 | report: (opts) => { 14 | tt.pass('called report') 15 | tt.equal(opts.id, 'pr-url', 'id') 16 | tt.equal(opts.message, MISSING_PR_URL, 'message') 17 | tt.equal(opts.string, null, 'string') 18 | tt.equal(opts.line, 0, 'line') 19 | tt.equal(opts.column, 0, 'column') 20 | tt.equal(opts.level, 'fail', 'level') 21 | } 22 | } 23 | 24 | Rule.validate(context) 25 | }) 26 | 27 | t.test('invalid numeric', (tt) => { 28 | tt.plan(7) 29 | const context = { 30 | prUrl: '#1234', 31 | body: [ 32 | '', 33 | 'PR-URL: #1234' 34 | ], 35 | report: (opts) => { 36 | tt.pass('called report') 37 | tt.equal(opts.id, 'pr-url', 'id') 38 | tt.equal(opts.message, NUMERIC_PR_URL, 'message') 39 | tt.equal(opts.string, '#1234', 'string') 40 | tt.equal(opts.line, 1, 'line') 41 | tt.equal(opts.column, 8, 'column') 42 | tt.equal(opts.level, 'fail', 'level') 43 | } 44 | } 45 | 46 | Rule.validate(context) 47 | }) 48 | 49 | t.test('invalid', (tt) => { 50 | tt.plan(7) 51 | const url = 'https://github.com/nodejs/node/issues/1234' 52 | const context = { 53 | prUrl: url, 54 | body: [ 55 | '', 56 | `PR-URL: ${url}` 57 | ], 58 | report: (opts) => { 59 | tt.pass('called report') 60 | tt.equal(opts.id, 'pr-url', 'id') 61 | tt.equal(opts.message, INVALID_PR_URL, 'message') 62 | tt.equal(opts.string, url, 'string') 63 | tt.equal(opts.line, 1, 'line') 64 | tt.equal(opts.column, 8, 'column') 65 | tt.equal(opts.level, 'fail', 'level') 66 | } 67 | } 68 | 69 | Rule.validate(context) 70 | }) 71 | 72 | t.test('valid', (tt) => { 73 | tt.plan(7) 74 | const url = 'https://github.com/nodejs/node/pull/1234' 75 | const context = { 76 | prUrl: url, 77 | body: [ 78 | '', 79 | `PR-URL: ${url}` 80 | ], 81 | report: (opts) => { 82 | tt.pass('called report') 83 | tt.equal(opts.id, 'pr-url', 'id') 84 | tt.equal(opts.message, VALID_PR_URL, 'message') 85 | tt.equal(opts.string, url, 'string') 86 | tt.equal(opts.line, 1, 'line') 87 | tt.equal(opts.column, 8, 'column') 88 | tt.equal(opts.level, 'pass', 'level') 89 | } 90 | } 91 | 92 | Rule.validate(context) 93 | }) 94 | 95 | t.test('valid URL containing hyphen', (tt) => { 96 | tt.plan(7) 97 | const url = 'https://github.com/nodejs/node-report/pull/1234' 98 | const context = { 99 | prUrl: url, 100 | body: [ 101 | '', 102 | `PR-URL: ${url}` 103 | ], 104 | report: (opts) => { 105 | tt.pass('called report') 106 | tt.equal(opts.id, 'pr-url', 'id') 107 | tt.equal(opts.message, VALID_PR_URL, 'message') 108 | tt.equal(opts.string, url, 'string') 109 | tt.equal(opts.line, 1, 'line') 110 | tt.equal(opts.column, 8, 'column') 111 | tt.equal(opts.level, 'pass', 'level') 112 | } 113 | } 114 | 115 | Rule.validate(context) 116 | }) 117 | 118 | t.test('valid URL with trailing slash', (tt) => { 119 | tt.plan(7) 120 | const url = 'https://github.com/nodejs/node-report/pull/1234/' 121 | const context = { 122 | prUrl: url, 123 | body: [ 124 | '', 125 | `PR-URL: ${url}` 126 | ], 127 | report: (opts) => { 128 | tt.pass('called report') 129 | tt.equal(opts.id, 'pr-url', 'id') 130 | tt.equal(opts.message, VALID_PR_URL, 'message') 131 | tt.equal(opts.string, url, 'string') 132 | tt.equal(opts.line, 1, 'line') 133 | tt.equal(opts.column, 8, 'column') 134 | tt.equal(opts.level, 'pass', 'level') 135 | } 136 | } 137 | 138 | Rule.validate(context) 139 | }) 140 | 141 | t.end() 142 | }) 143 | -------------------------------------------------------------------------------- /test/rules/reviewers.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/reviewers.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | const MSG = 'Commit must have at least 1 reviewer.' 6 | 7 | test('rule: reviewers', (t) => { 8 | t.test('missing', (tt) => { 9 | tt.plan(7) 10 | const v = new Validator() 11 | const context = new Commit({ 12 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 13 | author: { 14 | name: 'Evan Lucas', 15 | email: 'evanlucas@me.com', 16 | date: '2016-04-12T19:42:23Z' 17 | }, 18 | message: `test: fix something 19 | 20 | This is a test` 21 | }, v) 22 | 23 | context.report = (opts) => { 24 | tt.pass('called report') 25 | tt.equal(opts.id, 'reviewers', 'id') 26 | tt.equal(opts.message, MSG, 'message') 27 | tt.equal(opts.string, null, 'string') 28 | tt.equal(opts.line, 0, 'line') 29 | tt.equal(opts.column, 0, 'column') 30 | tt.equal(opts.level, 'fail', 'level') 31 | } 32 | 33 | Rule.validate(context) 34 | }) 35 | 36 | t.test('skip for release commit', (tt) => { 37 | tt.plan(2) 38 | const v = new Validator() 39 | const context = new Commit({ 40 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 41 | author: { 42 | name: 'Evan Lucas', 43 | email: 'evanlucas@me.com', 44 | date: '2016-04-12T19:42:23Z' 45 | }, 46 | message: `2016-04-12, Version x.y.z 47 | 48 | This is a test` 49 | }, v) 50 | 51 | context.report = (opts) => { 52 | tt.pass('called report') 53 | tt.strictSame(opts, { 54 | id: 'reviewers', 55 | message: 'skipping reviewers for release commit', 56 | string: '', 57 | level: 'skip' 58 | }) 59 | } 60 | 61 | Rule.validate(context) 62 | }) 63 | 64 | t.end() 65 | }) 66 | -------------------------------------------------------------------------------- /test/rules/subsystem.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/subsystem.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | 6 | test('rule: subsystem', (t) => { 7 | t.test('invalid', (tt) => { 8 | tt.plan(7) 9 | const v = new Validator() 10 | const context = new Commit({ 11 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 12 | author: { 13 | name: 'Evan Lucas', 14 | email: 'evanlucas@me.com', 15 | date: '2016-04-12T19:42:23Z' 16 | }, 17 | message: 'fhqwhgads: come on' 18 | }, v) 19 | 20 | context.report = (opts) => { 21 | tt.pass('called report') 22 | tt.equal(opts.id, 'subsystem', 'id') 23 | tt.equal(opts.message, 'Invalid subsystem: "fhqwhgads"', 'message') 24 | tt.equal(opts.string, 'fhqwhgads: come on', 'string') 25 | tt.equal(opts.line, 0, 'line') 26 | tt.equal(opts.column, 0, 'column') 27 | tt.equal(opts.level, 'fail', 'level') 28 | tt.end() 29 | } 30 | 31 | Rule.validate(context, { options: { subsystems: Rule.defaults.subsystems } }) 32 | }) 33 | 34 | t.test('skip for release commit', (tt) => { 35 | tt.plan(2) 36 | 37 | const v = new Validator() 38 | const context = new Commit({ 39 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 40 | author: { 41 | name: 'Evan Lucas', 42 | email: 'evanlucas@me.com', 43 | date: '2016-04-12T19:42:23Z' 44 | }, 45 | message: '2016-04-12, Version x.y.z' 46 | }, v) 47 | 48 | context.report = (opts) => { 49 | tt.pass('called report') 50 | tt.strictSame(opts, { 51 | id: 'subsystem', 52 | message: 'Release commits do not have subsystems', 53 | string: '', 54 | level: 'skip' 55 | }) 56 | tt.end() 57 | } 58 | 59 | Rule.validate(context, { options: { subsystems: Rule.defaults.subsystems } }) 60 | }) 61 | 62 | t.test('valid', (tt) => { 63 | tt.plan(2) 64 | 65 | const v = new Validator() 66 | const context = new Commit({ 67 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 68 | author: { 69 | name: 'Evan Lucas', 70 | email: 'evanlucas@me.com', 71 | date: '2016-04-12T19:42:23Z' 72 | }, 73 | message: 'quic: come on, fhqwhgads' 74 | }, v) 75 | 76 | context.report = (opts) => { 77 | tt.pass('called report') 78 | tt.strictSame(opts, { 79 | id: 'subsystem', 80 | message: 'valid subsystems', 81 | string: 'quic', 82 | level: 'pass' 83 | }) 84 | tt.end() 85 | } 86 | 87 | Rule.validate(context, { options: { subsystems: Rule.defaults.subsystems } }) 88 | }) 89 | t.end() 90 | }) 91 | -------------------------------------------------------------------------------- /test/rules/title-format.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Rule from '../../lib/rules/title-format.js' 3 | import Commit from 'gitlint-parser-node' 4 | import Validator from '../../index.js' 5 | 6 | function makeCommit (title) { 7 | const v = new Validator() 8 | return new Commit({ 9 | sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', 10 | author: { 11 | name: 'Evan Lucas', 12 | email: 'evanlucas@me.com', 13 | date: '2016-04-12T19:42:23Z' 14 | }, 15 | message: title 16 | }, v) 17 | } 18 | 19 | test('rule: title-format', (t) => { 20 | t.test('space after subsystem', (tt) => { 21 | tt.plan(2) 22 | const context = makeCommit('test:missing space') 23 | 24 | context.report = (opts) => { 25 | tt.pass('called report') 26 | tt.strictSame(opts, { 27 | id: 'title-format', 28 | message: 'Add a space after subsystem(s).', 29 | string: 'test:missing space', 30 | line: 0, 31 | column: 5, 32 | level: 'fail' 33 | }) 34 | } 35 | 36 | Rule.validate(context) 37 | tt.end() 38 | }) 39 | 40 | t.test('space after subsystem, colon in message', (tt) => { 41 | tt.plan(2) 42 | const context = makeCommit('test: missing:space') 43 | 44 | context.report = (opts) => { 45 | tt.pass('called report') 46 | tt.strictSame(opts, { 47 | id: 'title-format', 48 | message: 'Title is formatted correctly.', 49 | string: '', 50 | level: 'pass' 51 | }) 52 | } 53 | 54 | Rule.validate(context) 55 | tt.end() 56 | }) 57 | 58 | t.test('consecutive spaces', (tt) => { 59 | tt.plan(2) 60 | const context = makeCommit('test: with two spaces') 61 | 62 | context.report = (opts) => { 63 | tt.pass('called report') 64 | tt.strictSame(opts, { 65 | id: 'title-format', 66 | message: 'Do not use consecutive spaces in title.', 67 | string: 'test: with two spaces', 68 | line: 0, 69 | column: 11, 70 | level: 'fail' 71 | }) 72 | } 73 | 74 | Rule.validate(context) 75 | tt.end() 76 | }) 77 | 78 | t.test('first word after subsystem should be in lowercase', (tt) => { 79 | tt.plan(2) 80 | const context = makeCommit('test: Some message') 81 | 82 | context.report = (opts) => { 83 | tt.pass('called report') 84 | tt.strictSame(opts, { 85 | id: 'title-format', 86 | message: 'First word after subsystem(s) in title should be lowercase.', 87 | string: 'test: Some message', 88 | line: 0, 89 | column: 7, 90 | level: 'fail' 91 | }) 92 | } 93 | 94 | Rule.validate(context) 95 | tt.end() 96 | }) 97 | 98 | t.test('colon in message followed by uppercase word', (tt) => { 99 | tt.plan(2) 100 | const context = makeCommit('test: some message: Message') 101 | 102 | context.report = (opts) => { 103 | tt.pass('called report') 104 | tt.strictSame(opts, { 105 | id: 'title-format', 106 | message: 'Title is formatted correctly.', 107 | string: '', 108 | level: 'pass' 109 | }) 110 | } 111 | 112 | Rule.validate(context) 113 | tt.end() 114 | }) 115 | 116 | t.test('Skip case checks for V8 updates ', (tt) => { 117 | tt.plan(2) 118 | const context = makeCommit('deps: V8: cherry-pick e0a109c') 119 | 120 | context.report = (opts) => { 121 | tt.pass('called report') 122 | tt.strictSame(opts, { 123 | id: 'title-format', 124 | message: 'Title is formatted correctly.', 125 | string: '', 126 | level: 'pass' 127 | }) 128 | } 129 | 130 | Rule.validate(context) 131 | tt.end() 132 | }) 133 | 134 | t.end() 135 | }) 136 | -------------------------------------------------------------------------------- /test/utils-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import * as utils from '../lib/utils.js' 3 | 4 | // We aren't testing the chalk library, so strip off the colors/styles it adds 5 | const stripAnsiRegex = 6 | /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g // eslint-disable-line no-control-regex 7 | 8 | const originalConsoleLog = console.log 9 | 10 | test('test utility functions', (t) => { 11 | t.test('test rightPad function - with padding', (tt) => { 12 | const padded = utils.rightPad('string', 10) 13 | tt.equal(padded.length, 11, 'should have extra padding') 14 | tt.equal(padded, 'string ', 'should have padding on the right') 15 | 16 | tt.end() 17 | }) 18 | 19 | t.test('test rightPad function - withou padding', (tt) => { 20 | const padded = utils.rightPad('string', 5) 21 | tt.equal(padded.length, 6, 'should have the same length') 22 | tt.equal(padded, 'string', 'should have no padding on the right') 23 | 24 | tt.end() 25 | }) 26 | 27 | t.test('test leftPad function - with padding', (tt) => { 28 | const padded = utils.leftPad('string', 10) 29 | tt.equal(padded.length, 11, 'should have extra padding') 30 | tt.equal(padded, ' string', 'should have padding on the left') 31 | 32 | tt.end() 33 | }) 34 | 35 | t.test('test leftPad function - withou padding', (tt) => { 36 | const padded = utils.leftPad('string', 5) 37 | tt.equal(padded.length, 6, 'should have the same length') 38 | tt.equal(padded, 'string', 'should have no padding on the left') 39 | 40 | tt.end() 41 | }) 42 | 43 | t.test('test headers function - skip', (tt) => { 44 | const header = utils.header('abc123', 'skip') 45 | tt.equal(header.replace(stripAnsiRegex, ''), 46 | '✔ abc123 # SKIPPED', 47 | 'should be equal') 48 | tt.end() 49 | }) 50 | 51 | t.test('test headers function - pass', (tt) => { 52 | const header = utils.header('abc123', 'pass') 53 | tt.equal(header.replace(stripAnsiRegex, ''), 54 | '✔ abc123', 55 | 'should be equal') 56 | tt.end() 57 | }) 58 | 59 | t.test('test headers function - pass', (tt) => { 60 | const header = utils.header('abc123', 'pass') 61 | tt.equal(header.replace(stripAnsiRegex, ''), 62 | '✔ abc123', 63 | 'should be equal') 64 | tt.end() 65 | }) 66 | 67 | t.test('test headers function - fail', (tt) => { 68 | const header = utils.header('abc123', 'fail') 69 | tt.equal(header.replace(stripAnsiRegex, ''), 70 | '✖ abc123', 71 | 'should be equal') 72 | tt.end() 73 | }) 74 | 75 | t.test('test describeRule function', (tt) => { 76 | function logger () { 77 | const args = [...arguments] 78 | tt.equal(args[1].replace(stripAnsiRegex, ''), 79 | ' rule-id', 'has a title with padding') 80 | tt.equal(args[2].replace(stripAnsiRegex, ''), 81 | 'a description', 'has a description') 82 | } 83 | 84 | // overrite the console.log 85 | console.log = logger 86 | utils.describeRule({ id: 'rule-id', meta: { description: 'a description' } }) 87 | // put it back 88 | console.log = originalConsoleLog 89 | tt.end() 90 | }) 91 | 92 | t.test('test describeRule function - no meta data description', (tt) => { 93 | function logger () { 94 | tt.fails('should not reach here') 95 | } 96 | 97 | // overrite the console.log 98 | console.log = logger 99 | utils.describeRule({ id: 'rule-id', meta: {} }) 100 | tt.pass('no return value') 101 | 102 | // put it back 103 | console.log = originalConsoleLog 104 | tt.end() 105 | }) 106 | 107 | t.test('test describeSubsystem function - no subsystems', (tt) => { 108 | function logger () { 109 | tt.fails('should not reach here') 110 | } 111 | 112 | // overrite the console.log 113 | console.log = logger 114 | utils.describeSubsystem() 115 | tt.pass('no return value') 116 | // put it back 117 | console.log = originalConsoleLog 118 | tt.end() 119 | }) 120 | 121 | t.end() 122 | }) 123 | -------------------------------------------------------------------------------- /test/validator.js: -------------------------------------------------------------------------------- 1 | import { test } from 'tap' 2 | import Validator from '../lib/validator.js' 3 | import { readFileSync } from 'node:fs' 4 | 5 | // Note, these are not necessarily all real commit messages 6 | const str = `commit e7c077c610afa371430180fbd447bfef60ebc5ea 7 | Author: Calvin Metcalf 8 | AuthorDate: Tue Apr 12 15:42:23 2016 -0400 9 | Commit: James M Snell 10 | CommitDate: Wed Apr 20 13:28:35 2016 -0700 11 | 12 | stream: make null an invalid chunk to write in object mode 13 | 14 | this harmonizes behavior between readable, writable, and transform 15 | streams so that they all handle nulls in object mode the same way by 16 | considering them invalid chunks. 17 | 18 | PR-URL: https://github.com/nodejs/node/pull/6170 19 | Reviewed-By: James M Snell 20 | Reviewed-By: Matteo Collina 21 | ` 22 | 23 | const str2 = `commit b6475b9a9d0da0971eec7eb5559dff4d18a0e721 24 | Author: Evan Lucas 25 | Date: Tue Mar 29 08:09:37 2016 -0500 26 | 27 | Revert "tty: do not read from the console stream upon creation" 28 | 29 | This reverts commit 461138929498f31bd35bea61aa4375a2f56cceb7. 30 | 31 | The offending commit broke certain usages of piping from stdin. 32 | 33 | Fixes: https://github.com/nodejs/node/issues/5927 34 | PR-URL: https://github.com/nodejs/node/pull/5947 35 | Reviewed-By: Matteo Collina 36 | Reviewed-By: Alexis Campailla 37 | Reviewed-By: Colin Ihrig 38 | ` 39 | 40 | /* eslint-disable */ 41 | const str3 = `commit 75487f0db80e70a3e27fabfe323a33258dfbbea8 42 | Author: Michaël Zasso 43 | Date: Fri Apr 15 13:32:36 2016 +0200 44 | 45 | module: fix resolution of filename with trailing slash - make this tile too long 46 | 47 | A recent optimization of module loading performance [1] forgot to check that 48 | extensions were set in a certain code path. 49 | 50 | [1] https://github.com/nodejs/node/pull/5172/commits/ae18bbef48d87d9c641df85369f62cfd5ed8c250 51 | 52 | Fixes: https://github.com/nodejs/node/issues/6214 53 | PR-URL: https://github.com/nodejs/node/pull/6215 54 | Reviewed-By: James M Snell 55 | Reviewed-By: Brian White ` 56 | /* eslint-enable */ 57 | 58 | const str4 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 59 | Author: Wyatt Preul 60 | Date: Thu Mar 3 10:10:46 2016 -0600 61 | 62 | check memoryUsage properties 63 | The properties on memoryUsage were not checked before, 64 | this commit checks them. 65 | 66 | PR-URL: #5546 67 | Reviewed-By: Colin Ihrig ` 68 | 69 | const str5 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 70 | Author: Wyatt Preul 71 | Date: Thu Mar 3 10:10:46 2016 -0600 72 | 73 | test: check memoryUsage properties 74 | 75 | The properties on memoryUsage were not checked before, 76 | this commit checks them.` 77 | 78 | /* eslint-disable */ 79 | const str6 = { 80 | "sha": "c5545f2c63fe30b0cfcdafab18c26df8286881d0", 81 | "url": "https://api.github.com/repos/nodejs/node/git/commits/c5545f2c63fe30b0cfcdafab18c26df8286881d0", 82 | "html_url": "https://github.com/nodejs/node/commit/c5545f2c63fe30b0cfcdafab18c26df8286881d0", 83 | "author": { 84 | "name": "Anna Henningsen", 85 | "email": "anna@addaleax.net", 86 | "date": "2016-09-13T10:57:49Z" 87 | }, 88 | "committer": { 89 | "name": "Anna Henningsen", 90 | "email": "anna@addaleax.net", 91 | "date": "2016-09-19T12:50:57Z" 92 | }, 93 | "tree": { 94 | "sha": "b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59", 95 | "url": "https://api.github.com/repos/nodejs/node/git/trees/b505c0ffa0555730e9f4cdb391d1ebeb48bb2f59" 96 | }, 97 | "message": "fs: fix handling of `uv_stat_t` fields\n\n`FChown` and `Chown` test that the `uid` and `gid` parameters\nthey receive are unsigned integers, but `Stat()` and `FStat()`\nwould return the corresponding fields of `uv_stat_t` as signed\nintegers. Applications which pass those these values directly\nto `Chown` may fail\n(e.g. for `nobody` on OS X, who has an `uid` of `-2`, see e.g.\nhttps://github.com/nodejs/node-v0.x-archive/issues/5890).\n\nThis patch changes the `Integer::New()` call for `uid` and `gid`\nto `Integer::NewFromUnsigned()`.\n\nAll other fields are kept as they are, for performance, but\nstrictly speaking the respective sizes of those\nfields aren’t specified, either.\n\nRef: https://github.com/npm/npm/issues/13918\nPR-URL: https://github.com/nodejs/node/pull/8515\nReviewed-By: Ben Noordhuis \nReviewed-By: Sakthipriyan Vairamani \nReviewed-By: James M Snell \n\nundo accidental change to other fields of uv_fs_stat", 98 | "parents": [ 99 | { 100 | "sha": "4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", 101 | "url": "https://api.github.com/repos/nodejs/node/git/commits/4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4", 102 | "html_url": "https://github.com/nodejs/node/commit/4e76bffc0c7076a5901179e70c7b8a8f9fcd22e4" 103 | } 104 | ] 105 | } 106 | /* eslint-enable */ 107 | 108 | const str7 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 109 | Author: Wyatt Preul 110 | Date: Thu Mar 3 10:10:46 2016 -0600 111 | 112 | test: check memoryUsage properties. 113 | ` 114 | 115 | const str8 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 116 | Author: Wyatt Preul 117 | Date: Thu Mar 3 10:10:46 2016 -0600 118 | 119 | test: Check memoryUsage properties 120 | ` 121 | 122 | const str9 = `commit 7d3a7ea0d7df9b6f11df723dec370f49f4f87e99 123 | Author: Wyatt Preul 124 | Date: Thu Mar 3 10:10:46 2016 -0600 125 | 126 | test: Check memoryUsage properties. 127 | ` 128 | 129 | const str10 = `commit b04fe688d5859f707cf1a5e0206967268118bf7a 130 | Author: Darshan Sen 131 | Date: Sun May 1 21:10:21 2022 +0530 132 | 133 | Revert "bootstrap: delay the instantiation of maps in per-context scripts" 134 | 135 | The linked issue, https://bugs.chromium.org/p/v8/issues/detail?id=6593, 136 | is marked as "Fixed", so I think we can revert this now. 137 | 138 | This reverts commit 08a9c4a996964aca909cd75fa8ecafd652c54885. 139 | 140 | Signed-off-by: Darshan Sen 141 | PR-URL: https://github.com/nodejs/node/pull/42934 142 | Refs: https://bugs.chromium.org/p/v8/issues/detail?id=9187 143 | Reviewed-By: Joyee Cheung 144 | Reviewed-By: Antoine du Hamel 145 | ` 146 | 147 | const str11 = `commit b04fe688d5859f707cf1a5e0206967268118bf7a 148 | Author: Darshan Sen 149 | Date: Sun May 1 21:10:21 2022 +0530 150 | 151 | Revert "bootstrap: delay the instantiation of all maps in the per-context scripts" 152 | 153 | The linked issue, https://bugs.chromium.org/p/v8/issues/detail?id=6593, 154 | is marked as "Fixed", so I think we can revert this now. 155 | 156 | This reverts commit 08a9c4a996964aca909cd75fa8ecafd652c54885. 157 | 158 | Signed-off-by: Darshan Sen 159 | PR-URL: https://github.com/nodejs/node/pull/42934 160 | Refs: https://bugs.chromium.org/p/v8/issues/detail?id=9187 161 | Reviewed-By: Joyee Cheung 162 | Reviewed-By: Antoine du Hamel 163 | ` 164 | 165 | const str12 = `commit cbb404503c9df13aaeb3dd8b345cb3f34c8c07e4 166 | Author: Michaël Zasso 167 | Date: Sat Oct 22 10:22:43 2022 +0200 168 | 169 | Revert "deps: V8: forward declaration of \`Rtl*FunctionTable\`" 170 | 171 | This reverts commit 01bc8e6fd81314e76c7fb0d09e5310f609e48bee. 172 | ` 173 | 174 | test('Validator - misc', (t) => { 175 | const v = new Validator() 176 | 177 | t.throws(() => { 178 | v.disableRule('biscuits') 179 | }, /Invalid rule: "biscuits"/) 180 | 181 | v.disableRule('line-length') 182 | t.equal(v.rules.get('line-length').disabled, true, 'disabled') 183 | v.rules.get('line-length').disabled = false 184 | 185 | t.end() 186 | }) 187 | 188 | test('Validator - real commits', (t) => { 189 | t.test('basic', (tt) => { 190 | const commit = JSON.parse(readFileSync(new URL('fixtures/commit.json', import.meta.url), { encoding: 'utf8' })) 191 | const pr = JSON.parse(readFileSync(new URL('fixtures/pr.json', import.meta.url), { encoding: 'utf8' })) 192 | tt.plan(21) 193 | const v = new Validator() 194 | // run against the output of git show --quiet 195 | // run against the output of github's get commit api request 196 | // run against the output of github's list commits for pr api request 197 | v.lint(str) 198 | v.lint(commit) 199 | v.lint(pr) 200 | v.on('commit', (data) => { 201 | const c = data.commit 202 | tt.equal(c.sha, 'e7c077c610afa371430180fbd447bfef60ebc5ea', 'sha') 203 | tt.same(c.subsystems, ['stream'], 'subsystems') 204 | tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/6170', 'pr') 205 | const msgs = data.messages 206 | const failed = msgs.filter((item) => { 207 | return item.level === 'fail' 208 | }) 209 | tt.equal(failed.length, 0, 'failed.length') 210 | const warned = msgs.filter((item) => { 211 | return item.level === 'warn' 212 | }) 213 | tt.equal(warned.length, 3, 'warned.length') 214 | tt.equal(warned[0].level, 'warn') 215 | tt.equal(warned[0].id, 'title-length') 216 | }) 217 | }) 218 | 219 | t.test('basic revert', (tt) => { 220 | const v = new Validator() 221 | v.lint(str2) 222 | v.on('commit', (data) => { 223 | const c = data.commit.toJSON() 224 | tt.equal(c.sha, 'b6475b9a9d0da0971eec7eb5559dff4d18a0e721', 'sha') 225 | tt.equal(c.date, 'Tue Mar 29 08:09:37 2016 -0500', 'date') 226 | tt.same(c.subsystems, ['tty'], 'subsystems') 227 | tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/5947', 'pr') 228 | tt.equal(c.revert, true, 'revert') 229 | const msgs = data.messages 230 | const filtered = msgs.filter((item) => { 231 | return item.level === 'warn' 232 | }) 233 | tt.equal(filtered.length, 1, 'messages.length') 234 | tt.equal(filtered[0].level, 'warn') 235 | tt.equal(filtered[0].id, 'title-length') 236 | tt.end() 237 | }) 238 | }) 239 | 240 | t.test('more basic', (tt) => { 241 | const v = new Validator() 242 | v.lint(str3) 243 | v.on('commit', (data) => { 244 | const c = data.commit.toJSON() 245 | tt.equal(c.sha, '75487f0db80e70a3e27fabfe323a33258dfbbea8', 'sha') 246 | tt.equal(c.date, 'Fri Apr 15 13:32:36 2016 +0200', 'date') 247 | tt.same(c.subsystems, ['module'], 'subsystems') 248 | tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/6215', 'pr') 249 | tt.equal(c.revert, false, 'revert') 250 | const msgs = data.messages 251 | const filtered = msgs.filter((item) => { 252 | return item.level === 'fail' 253 | }) 254 | tt.equal(filtered.length, 2, 'messages.length') 255 | const ids = filtered.map((item) => { 256 | return item.id 257 | }) 258 | const exp = ['line-length', 'title-length'] 259 | tt.same(ids.sort(), exp.sort(), 'message ids') 260 | tt.end() 261 | }) 262 | }) 263 | 264 | t.test('accept revert commit titles that are elongated by git', (tt) => { 265 | const v = new Validator() 266 | v.lint(str10) 267 | v.on('commit', (data) => { 268 | const c = data.commit.toJSON() 269 | tt.equal(c.sha, 'b04fe688d5859f707cf1a5e0206967268118bf7a', 'sha') 270 | tt.equal(c.date, 'Sun May 1 21:10:21 2022 +0530', 'date') 271 | tt.same(c.subsystems, ['bootstrap'], 'subsystems') 272 | tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/42934', 'pr') 273 | tt.equal(c.revert, true, 'revert') 274 | const msgs = data.messages 275 | const filtered = msgs.filter((item) => { 276 | return item.level === 'fail' 277 | }) 278 | tt.equal(filtered.length, 0, 'messages.length') 279 | tt.end() 280 | }) 281 | }) 282 | 283 | t.test('reject revert commit titles whose original titles are really long', (tt) => { 284 | const v = new Validator() 285 | v.lint(str11) 286 | v.on('commit', (data) => { 287 | const c = data.commit.toJSON() 288 | tt.equal(c.sha, 'b04fe688d5859f707cf1a5e0206967268118bf7a', 'sha') 289 | tt.equal(c.date, 'Sun May 1 21:10:21 2022 +0530', 'date') 290 | tt.same(c.subsystems, ['bootstrap'], 'subsystems') 291 | tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/42934', 'pr') 292 | tt.equal(c.revert, true, 'revert') 293 | const msgs = data.messages 294 | const filtered = msgs.filter((item) => { 295 | return item.level === 'fail' 296 | }) 297 | tt.equal(filtered.length, 1, 'messages.length') 298 | const ids = filtered.map((item) => { 299 | return item.id 300 | }) 301 | const exp = ['title-length'] 302 | tt.same(ids.sort(), exp.sort(), 'message ids') 303 | tt.end() 304 | }) 305 | }) 306 | 307 | t.test('accept deps: V8 as the subsystem for revert commits', (tt) => { 308 | const v = new Validator({ 309 | 'validate-metadata': false 310 | }) 311 | v.lint(str12) 312 | v.on('commit', (data) => { 313 | const c = data.commit.toJSON() 314 | tt.equal(c.sha, 'cbb404503c9df13aaeb3dd8b345cb3f34c8c07e4', 'sha') 315 | tt.equal(c.date, 'Sat Oct 22 10:22:43 2022 +0200', 'date') 316 | tt.same(c.subsystems, ['deps'], 'subsystems') 317 | tt.equal(c.revert, true, 'revert') 318 | const msgs = data.messages 319 | const filtered = msgs.filter((item) => { 320 | return item.level === 'fail' 321 | }) 322 | tt.equal(filtered.length, 0, 'messages.length') 323 | tt.end() 324 | }) 325 | }) 326 | 327 | t.test('invalid pr-url, missing subsystem', (tt) => { 328 | const v = new Validator() 329 | v.lint(str4) 330 | v.on('commit', (data) => { 331 | const c = data.commit.toJSON() 332 | tt.equal(c.sha, '7d3a7ea0d7df9b6f11df723dec370f49f4f87e99', 'sha') 333 | tt.equal(c.date, 'Thu Mar 3 10:10:46 2016 -0600', 'date') 334 | tt.same(c.subsystems, [], 'subsystems') 335 | tt.equal(c.prUrl, '#5546', 'pr') 336 | tt.equal(c.revert, false, 'revert') 337 | const msgs = data.messages 338 | msgs.sort((a, b) => { 339 | return a.id < b.id 340 | ? -1 341 | : a.id > b.id 342 | ? 1 343 | : 0 344 | }) 345 | const filtered = msgs.filter((item) => { 346 | return item.level === 'fail' 347 | }) 348 | tt.equal(filtered.length, 3, 'messages.length') 349 | tt.equal(filtered[0].id, 'line-after-title', 'message id') 350 | tt.equal(filtered[1].id, 'pr-url', 'message id') 351 | tt.equal(filtered[1].string, '#5546', 'message string') 352 | tt.equal(filtered[2].id, 'subsystem', 'message id') 353 | tt.equal(filtered[2].line, 0, 'line') 354 | tt.equal(filtered[2].column, 0, 'column') 355 | tt.end() 356 | }) 357 | }) 358 | 359 | t.test('invalid pr-url, missing subsystem no meta', (tt) => { 360 | const v = new Validator({ 361 | 'validate-metadata': false 362 | }) 363 | v.lint(str5) 364 | v.on('commit', (data) => { 365 | const c = data.commit.toJSON() 366 | tt.equal(c.sha, '7d3a7ea0d7df9b6f11df723dec370f49f4f87e99', 'sha') 367 | tt.equal(c.date, 'Thu Mar 3 10:10:46 2016 -0600', 'date') 368 | tt.same(c.subsystems, ['test'], 'subsystems') 369 | tt.equal(c.prUrl, null, 'pr') 370 | tt.equal(c.revert, false, 'revert') 371 | const msgs = data.messages 372 | const filtered = msgs.filter((item) => { 373 | return item.level === 'fail' 374 | }) 375 | tt.equal(filtered.length, 0, 'messages.length') 376 | tt.end() 377 | }) 378 | }) 379 | 380 | t.test('non empty lines after metadata', (tt) => { 381 | const v = new Validator() 382 | v.lint(str6) 383 | v.on('commit', (data) => { 384 | const c = data.commit.toJSON() 385 | tt.equal(c.sha, 'c5545f2c63fe30b0cfcdafab18c26df8286881d0', 'sha') 386 | tt.equal(c.date, '2016-09-13T10:57:49Z', 'date') 387 | tt.same(c.subsystems, ['fs'], 'subsystems') 388 | tt.equal(c.prUrl, 'https://github.com/nodejs/node/pull/8515', 'pr') 389 | tt.equal(c.revert, false, 'revert') 390 | const msgs = data.messages 391 | const filtered = msgs.filter((item) => { 392 | return item.level === 'fail' 393 | }) 394 | tt.equal(filtered.length, 1, 'messages.length') 395 | const item = filtered[0] 396 | tt.equal(item.id, 'metadata-end', 'id') 397 | tt.equal(item.message, 'commit metadata at end of message', 'message') 398 | tt.equal(item.line, 22, 'line') 399 | tt.equal(item.column, 0, 'column') 400 | tt.end() 401 | }) 402 | }) 403 | 404 | t.test('trailing punctuation in title line', (tt) => { 405 | const v = new Validator({ 406 | 'validate-metadata': false 407 | }) 408 | v.lint(str7) 409 | v.on('commit', (data) => { 410 | const msgs = data.messages 411 | const filtered = msgs.filter((item) => { 412 | return item.level === 'fail' 413 | }) 414 | tt.equal(filtered.length, 1, 'messages.length') 415 | tt.equal(filtered[0].message, 416 | 'Do not use punctuation at end of title.', 417 | 'message') 418 | tt.end() 419 | }) 420 | }) 421 | 422 | t.test('first word is lowercase in title line', (tt) => { 423 | const v = new Validator({ 424 | 'validate-metadata': false 425 | }) 426 | v.lint(str8) 427 | v.on('commit', (data) => { 428 | const msgs = data.messages 429 | const filtered = msgs.filter((item) => { 430 | return item.level === 'fail' 431 | }) 432 | tt.equal(filtered.length, 1, 'messages.length') 433 | tt.equal(filtered[0].message, 434 | 'First word after subsystem(s) in title should be lowercase.', 435 | 'message') 436 | tt.equal(filtered[0].column, 7, 'column') 437 | tt.end() 438 | }) 439 | }) 440 | 441 | t.test('more than one formatting error in title line', (tt) => { 442 | const v = new Validator({ 443 | 'validate-metadata': false 444 | }) 445 | v.lint(str9) 446 | v.on('commit', (data) => { 447 | const msgs = data.messages 448 | const filtered = msgs.filter((item) => { 449 | return item.level === 'fail' 450 | }) 451 | tt.equal(filtered.length, 2, 'messages.length') 452 | tt.end() 453 | }) 454 | }) 455 | 456 | t.end() 457 | }) 458 | --------------------------------------------------------------------------------