|
| 1 | +import { spawnSync } from 'node:child_process' |
| 2 | +import Base from 'gitlint-parser-base' |
| 3 | + |
| 4 | +const revertRE = /Revert "(.*)"$/ |
| 5 | +const workingRE = /Working on v([\d]+)\.([\d]+).([\d]+)$/ |
| 6 | +const releaseRE = /([\d]{4})-([\d]{2})-([\d]{2}),? Version/ |
| 7 | +const reviewedByRE = /^Reviewed-By: (.*)$/ |
| 8 | +const fixesRE = /^Fixes: (.*)$/ |
| 9 | +const prUrlRE = /^PR-URL: (.*)$/ |
| 10 | +const refsRE = /^Refs?: (.*)$/ |
| 11 | + |
| 12 | +export default class Parser extends Base { |
| 13 | + constructor (str, validator) { |
| 14 | + super(str, validator) |
| 15 | + this.subsystems = [] |
| 16 | + this.fixes = [] |
| 17 | + this.prUrl = null |
| 18 | + this.refs = [] |
| 19 | + this.reviewers = [] |
| 20 | + this._metaStart = 0 |
| 21 | + this._metaEnd = 0 |
| 22 | + this._parse() |
| 23 | + } |
| 24 | + |
| 25 | + _setMetaStart (n) { |
| 26 | + if (this._metaStart) return |
| 27 | + this._metaStart = n |
| 28 | + } |
| 29 | + |
| 30 | + _setMetaEnd (n) { |
| 31 | + if (n < this._metaEnd) return |
| 32 | + this._metaEnd = n |
| 33 | + } |
| 34 | + |
| 35 | + _parseTrailers (body) { |
| 36 | + const interpretTrailers = commitMessage => spawnSync('git', [ |
| 37 | + 'interpret-trailers', '--only-trailers', '--only-input', '--no-divider' |
| 38 | + ], { |
| 39 | + encoding: 'utf-8', |
| 40 | + input: `'dummy subject\n\n${commitMessage.join('\n')}\n` |
| 41 | + }).stdout |
| 42 | + |
| 43 | + let originalTrailers |
| 44 | + try { |
| 45 | + originalTrailers = interpretTrailers(body).trim() |
| 46 | + } catch (err) { |
| 47 | + console.warn('git is not available, trailers detection might be a bit ' + |
| 48 | + 'off which is acceptable in most cases', err) |
| 49 | + return body |
| 50 | + } |
| 51 | + const trailerFreeBody = body.slice(1) // clone, and remove the first empty line |
| 52 | + const stillInTrailers = () => { |
| 53 | + const result = interpretTrailers(trailerFreeBody) |
| 54 | + return result.length && originalTrailers.startsWith(result.trim()) |
| 55 | + } |
| 56 | + for (let i = trailerFreeBody.length - 1; stillInTrailers(); i--) { |
| 57 | + // Remove last line until git no longer detects any trailers |
| 58 | + trailerFreeBody.pop() |
| 59 | + } |
| 60 | + this._metaStart = trailerFreeBody.length + 1 // the subject line needs to be counted |
| 61 | + for (let i = trailerFreeBody.length - 1; trailerFreeBody[i] === ''; i--) { |
| 62 | + // Remove additional empty line(s) |
| 63 | + trailerFreeBody.pop() |
| 64 | + } |
| 65 | + this._metaEnd = body.length - 1 |
| 66 | + this.trailerFreeBody = trailerFreeBody |
| 67 | + return (this.trailers = originalTrailers.split('\n')) |
| 68 | + } |
| 69 | + |
| 70 | + _parse () { |
| 71 | + const revert = this.isRevert() |
| 72 | + if (!revert) { |
| 73 | + this.subsystems = getSubsystems(this.title || '') |
| 74 | + } else { |
| 75 | + const matches = this.title.match(revertRE) |
| 76 | + if (matches) { |
| 77 | + const title = matches[1] |
| 78 | + this.subsystems = getSubsystems(title) |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + const trailers = this._parseTrailers(this.body) |
| 83 | + |
| 84 | + for (let i = 0; i < trailers.length; i++) { |
| 85 | + const line = trailers[i] |
| 86 | + const reviewedBy = reviewedByRE.exec(line) |
| 87 | + if (reviewedBy) { |
| 88 | + this._setMetaStart(i) |
| 89 | + this._setMetaEnd(i) |
| 90 | + this.reviewers.push(reviewedBy[1]) |
| 91 | + continue |
| 92 | + } |
| 93 | + |
| 94 | + const fixes = fixesRE.exec(line) |
| 95 | + if (fixes) { |
| 96 | + this._setMetaStart(i) |
| 97 | + this._setMetaEnd(i) |
| 98 | + this.fixes.push(fixes[1]) |
| 99 | + continue |
| 100 | + } |
| 101 | + |
| 102 | + const prUrl = prUrlRE.exec(line) |
| 103 | + if (prUrl) { |
| 104 | + this._setMetaStart(i) |
| 105 | + this._setMetaEnd(i) |
| 106 | + this.prUrl = prUrl[1] |
| 107 | + continue |
| 108 | + } |
| 109 | + |
| 110 | + const refs = refsRE.exec(line) |
| 111 | + if (refs) { |
| 112 | + this._setMetaStart(i) |
| 113 | + this._setMetaEnd(i) |
| 114 | + this.refs.push(refs[1]) |
| 115 | + continue |
| 116 | + } |
| 117 | + |
| 118 | + if (this._metaStart && !this._metaEnd) { this._setMetaEnd(i) } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + isRevert () { |
| 123 | + return revertRE.test(this.title) |
| 124 | + } |
| 125 | + |
| 126 | + isWorkingCommit () { |
| 127 | + return workingRE.test(this.title) |
| 128 | + } |
| 129 | + |
| 130 | + isReleaseCommit () { |
| 131 | + return releaseRE.test(this.title) |
| 132 | + } |
| 133 | + |
| 134 | + toJSON () { |
| 135 | + return { |
| 136 | + sha: this.sha, |
| 137 | + title: this.title, |
| 138 | + subsystems: this.subsystems, |
| 139 | + author: this.author, |
| 140 | + date: this.date, |
| 141 | + fixes: this.fixes, |
| 142 | + refs: this.refs, |
| 143 | + prUrl: this.prUrl, |
| 144 | + reviewers: this.reviewers, |
| 145 | + body: this.body, |
| 146 | + trailers: this.trailers, |
| 147 | + trailerFreeBody: this.trailerFreeBody, |
| 148 | + revert: this.isRevert(), |
| 149 | + release: this.isReleaseCommit(), |
| 150 | + working: this.isWorkingCommit(), |
| 151 | + metadata: { |
| 152 | + start: this._metaStart, |
| 153 | + end: this._metaEnd |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +function getSubsystems (str) { |
| 160 | + str = str || '' |
| 161 | + const colon = str.indexOf(':') |
| 162 | + if (colon === -1) { |
| 163 | + return [] |
| 164 | + } |
| 165 | + |
| 166 | + const subStr = str.slice(0, colon) |
| 167 | + const subs = subStr.split(',') |
| 168 | + return subs.map((item) => { |
| 169 | + return item.trim() |
| 170 | + }) |
| 171 | +} |
0 commit comments