From eb590e6b232d18ce68f9b4da09f41a779a281127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Fri, 14 Jan 2022 21:10:21 +0100 Subject: [PATCH] rules: add co-authored-by-is-trailer Refs: https://github.com/nodejs/node-core-utils/issues/602 --- lib/rules/co-authored-by-is-trailer.js | 51 ++++++++ test/rules/co-authored-by-is-trailer.js | 157 ++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 lib/rules/co-authored-by-is-trailer.js create mode 100644 test/rules/co-authored-by-is-trailer.js diff --git a/lib/rules/co-authored-by-is-trailer.js b/lib/rules/co-authored-by-is-trailer.js new file mode 100644 index 0000000..cf31af8 --- /dev/null +++ b/lib/rules/co-authored-by-is-trailer.js @@ -0,0 +1,51 @@ +'use strict' + +const id = 'co-authored-by-is-trailer' + +module.exports = { + id: id, + meta: { + description: 'enforce that "Co-authored-by:" lines are trailers', + recommended: true + }, + defaults: {}, + options: {}, + validate: (context, rule) => { + const parsed = context.toJSON() + const lines = parsed.body.map((line, i) => [line, i]) + const re = /^\s*Co-authored-by:/gi + const coauthors = lines.filter(([line]) => re.test(line)) + if (coauthors.length !== 0) { + const firstCoauthor = coauthors[0] + const emptyLines = lines.filter(([text]) => text.trim().length === 0) + // There must be at least one empty line, and the last empty line must be + // above the first Co-authored-by line. + const isTrailer = (emptyLines.length !== 0) && + emptyLines.pop()[1] < firstCoauthor[1] + if (isTrailer) { + context.report({ + id: id, + message: 'Co-authored-by is a trailer', + string: '', + level: 'pass' + }) + } else { + context.report({ + id: id, + message: 'Co-authored-by must be a trailer', + string: firstCoauthor[0], + line: firstCoauthor[1], + column: 0, + level: 'fail' + }) + } + } else { + context.report({ + id: id, + message: 'no Co-authored-by metadata', + string: '', + level: 'pass' + }) + } + } +} diff --git a/test/rules/co-authored-by-is-trailer.js b/test/rules/co-authored-by-is-trailer.js new file mode 100644 index 0000000..21b3986 --- /dev/null +++ b/test/rules/co-authored-by-is-trailer.js @@ -0,0 +1,157 @@ +'use strict' + +const test = require('tap').test +const Rule = require('../../lib/rules/co-authored-by-is-trailer') +const Commit = require('gitlint-parser-node') +const Validator = require('../../') + +test('rule: co-authored-by-is-trailer', (t) => { + t.test('no co-authors', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'fhqwhgads' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') + tt.equal(opts.message, 'no Co-authored-by metadata', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.test('no empty lines above', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + 'Co-authored-by: Someone ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') + tt.equal(opts.message, 'Co-authored-by must be a trailer', 'message') + tt.equal(opts.string, 'Co-authored-by: Someone ', 'string') + tt.equal(opts.line, 0, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('not trailer', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Co-authored-by: Someone \n' + + '\n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') + tt.equal(opts.message, 'Co-authored-by must be a trailer', 'message') + tt.equal(opts.string, 'Co-authored-by: Someone ', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('not all are trailers', (tt) => { + tt.plan(7) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'Co-authored-by: Someone \n' + + '\n' + + 'Co-authored-by: Someone Else \n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') + tt.equal(opts.message, 'Co-authored-by must be a trailer', 'message') + tt.equal(opts.string, 'Co-authored-by: Someone ', 'string') + tt.equal(opts.line, 3, 'line') + tt.equal(opts.column, 0, 'column') + tt.equal(opts.level, 'fail', 'level') + } + + Rule.validate(context) + }) + + t.test('is trailer', (tt) => { + tt.plan(4) + const v = new Validator() + const context = new Commit({ + sha: 'e7c077c610afa371430180fbd447bfef60ebc5ea', + author: { + name: 'Foo', + email: 'foo@example.com', + date: '2016-04-12T19:42:23Z' + }, + message: 'test: fix something\n' + + '\n' + + 'Some description.\n' + + '\n' + + 'More description.\n' + + '\n' + + 'Co-authored-by: Someone \n' + + 'Reviewed-By: Bar ' + }, v) + + context.report = (opts) => { + tt.pass('called report') + tt.equal(opts.id, 'co-authored-by-is-trailer', 'id') + tt.equal(opts.message, 'Co-authored-by is a trailer', 'message') + tt.equal(opts.level, 'pass', 'level') + } + + Rule.validate(context) + }) + + t.end() +})