Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions lib/gitlint-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { spawnSync } from 'node:child_process'
import Base from 'gitlint-parser-base'

const revertRE = /Revert "(.*)"$/
const workingRE = /Working on v([\d]+)\.([\d]+).([\d]+)$/
const releaseRE = /([\d]{4})-([\d]{2})-([\d]{2}),? Version/
const reviewedByRE = /^Reviewed-By: (.*)$/
const fixesRE = /^Fixes: (.*)$/
const prUrlRE = /^PR-URL: (.*)$/
const refsRE = /^Refs?: (.*)$/

export default class Parser extends Base {
constructor (str, validator) {
super(str, validator)
this.subsystems = []
this.fixes = []
this.prUrl = null
this.refs = []
this.reviewers = []
this._metaStart = 0
this._metaEnd = 0
this._parse()
}

_setMetaStart (n) {
if (this._metaStart) return
this._metaStart = n
}

_setMetaEnd (n) {
if (n < this._metaEnd) return
this._metaEnd = n
}

_parseTrailers (body) {
const interpretTrailers = commitMessage => spawnSync('git', [
'interpret-trailers', '--only-trailers', '--only-input', '--no-divider'
], {
encoding: 'utf-8',
input: `${commitMessage}\n`
}).stdout

let originalTrailers
try {
originalTrailers = interpretTrailers(body.join('\n')).trim()
} catch (err) {
console.warn('git is not available, trailers detection might be a bit ' +
'off which is acceptable in most cases', err)
return body
}
const trailerFreeBody = body.slice(1) // clone, and remove the first empty line
const stillInTrailers = () => {
const result = interpretTrailers(trailerFreeBody.join('\n'))
return result.length && originalTrailers.startsWith(result.trim())
}
for (let i = trailerFreeBody.length - 1; stillInTrailers(); i--) {
// Remove last line until git no longer detects any trailers
trailerFreeBody.pop()
}
this._metaStart = trailerFreeBody.length + 1
for (let i = trailerFreeBody.length - 1; trailerFreeBody[i] === ''; i--) {
// Remove additional empty line(s)
trailerFreeBody.pop()
}
this._metaEnd = body.length - 1
this.trailerFreeBody = trailerFreeBody
return (this.trailers = originalTrailers.split('\n'))
}

_parse () {
const revert = this.isRevert()
if (!revert) {
this.subsystems = getSubsystems(this.title || '')
} else {
const matches = this.title.match(revertRE)
if (matches) {
const title = matches[1]
this.subsystems = getSubsystems(title)
}
}

const trailers = this._parseTrailers(this.body)

for (let i = 0; i < trailers.length; i++) {
const line = trailers[i]
const reviewedBy = reviewedByRE.exec(line)
if (reviewedBy) {
this._setMetaStart(i)
this._setMetaEnd(i)
this.reviewers.push(reviewedBy[1])
continue
}

const fixes = fixesRE.exec(line)
if (fixes) {
this._setMetaStart(i)
this._setMetaEnd(i)
this.fixes.push(fixes[1])
continue
}

const prUrl = prUrlRE.exec(line)
if (prUrl) {
this._setMetaStart(i)
this._setMetaEnd(i)
this.prUrl = prUrl[1]
continue
}

const refs = refsRE.exec(line)
if (refs) {
this._setMetaStart(i)
this._setMetaEnd(i)
this.refs.push(refs[1])
continue
}

if (this._metaStart && !this._metaEnd) { this._setMetaEnd(i) }
}
}

isRevert () {
return revertRE.test(this.title)
}

isWorkingCommit () {
return workingRE.test(this.title)
}

isReleaseCommit () {
return releaseRE.test(this.title)
}

toJSON () {
return {
sha: this.sha,
title: this.title,
subsystems: this.subsystems,
author: this.author,
date: this.date,
fixes: this.fixes,
refs: this.refs,
prUrl: this.prUrl,
reviewers: this.reviewers,
body: this.body,
trailers: this.trailers,
trailerFreeBody: this.trailerFreeBody,
revert: this.isRevert(),
release: this.isReleaseCommit(),
working: this.isWorkingCommit(),
metadata: {
start: this._metaStart,
end: this._metaEnd
}
}
}
}

function getSubsystems (str) {
str = str || ''
const colon = str.indexOf(':')
if (colon === -1) {
return []
}

const subStr = str.slice(0, colon)
const subs = subStr.split(',')
return subs.map((item) => {
return item.trim()
})
}
33 changes: 23 additions & 10 deletions lib/rules/line-length.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export default {
recommended: true
},
defaults: {
length: 72
length: 72,
trailerLength: 120
},
options: {
length: 72
length: 72,
trailerLength: 120
},
validate: (context, rule) => {
const len = rule.options.length
Expand All @@ -27,19 +29,14 @@ export default {
return
}
let failed = false
for (let i = 0; i < parsed.body.length; i++) {
const line = parsed.body[i]
const body = parsed.trailerFreeBody ?? parsed.body
for (let i = 0; i < body.length; i++) {
const line = body[i]

// Skip quoted lines, e.g. for original commit messages of V8 backports.
if (line.startsWith(' ')) { continue }
// Skip lines with URLs.
if (/https?:\/\//.test(line)) { continue }
// Skip co-authorship.
if (/^co-authored-by:/i.test(line)) { continue }
// Skip DCO sign-offs.
if (/^signed-off-by:/i.test(line)) { continue }
// Skip agentic assistants.
if (/^assisted-by:/i.test(line)) { continue }

if (line.length > len) {
failed = true
Expand All @@ -54,6 +51,22 @@ export default {
})
}
}
for (let i = 0; i < (parsed.trailers?.length ?? 0); i++) {
const line = parsed.trailers[i]
const len = rule.options.trailerLength
if (line.length > len) {
failed = true
context.report({
id,
message: `Trailer should be <= ${len} columns.`,
string: line,
maxLength: len,
line: i + parsed.body.length - parsed.trailers.length,
column: len,
level: 'fail'
})
}
}

if (!failed) {
context.report({
Expand Down
10 changes: 6 additions & 4 deletions lib/rules/signed-off-by.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ export default {
return
}

const body = parsed.trailers ?? parsed.body

// Backport commits (identified by a Backport-PR-URL trailer) are
// cherry-picks of existing commits into release branches. The
// original commit was already validated.
if (parsed.body.some((line) => backportPattern.test(line))) {
if (body.some((line) => backportPattern.test(line))) {
context.report({
id,
message: 'skipping sign-off for backport commit',
Expand All @@ -80,9 +82,9 @@ export default {
return
}

const signoffs = parsed.body
.map((line, i) => [line, i])
.filter(([line]) => signoffPattern.test(line))
const signoffs = body
.filter(line => signoffPattern.test(line))
.map((line, i) => [line, i + parsed.body.length - body.length])

// Bot-authored commits don't need a sign-off.
// If they have one, warn; otherwise pass.
Expand Down
2 changes: 1 addition & 1 deletion lib/validator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import EE from 'node:events'
import Parser from 'gitlint-parser-node'
import Parser from './gitlint-parser.js'
import BaseRule from './rule.js'

// Rules
Expand Down
Loading
Loading