Skip to content

Commit e0f76e0

Browse files
committed
feat(rules/git-regex-tag-names): add new rule
This rule enables Git tag naming enforcement with JavaScript regex. Signed-off-by: Rifa Achrinza <[email protected]>
1 parent 6e00cef commit e0f76e0

File tree

8 files changed

+410
-1
lines changed

8 files changed

+410
-1
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ vendor/
77
.bundle
88
out
99
.vscode/
10+
*~
11+
.#*
12+
\#*#

docs/rules.md

+14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Below is a complete list of rules that Repolinter can run, along with their conf
2020
- [`git-grep-commits`](#git-grep-commits)
2121
- [`git-grep-log`](#git-grep-log)
2222
- [`git-list-tree`](#git-list-tree)
23+
- [`git-regex-tag-names`](#git-regex-tag-names)
2324
- [`git-working-tree`](#git-working-tree)
2425
- [`json-schema-passes`](#json-schema-passes)
2526
- [`large-file`](#large-file)
@@ -189,6 +190,19 @@ Check for blacklisted filepaths in Git.
189190
| `denylist` | **Yes** | `string[]` | | A list of patterns to search against all paths in the git history. |
190191
| `ignoreCase` | No | `boolean` | `false` | Set to true to make `denylist` case insensitive. |
191192

193+
194+
### `git-regex-tag-names`
195+
196+
Check for permitted or denied Git tag names using JavaScript regular expressions.
197+
198+
| Input | Required | Type | Default | Description |
199+
|--------------|----------|------------|---------|------------------------------------------------------------------|
200+
| `allowlist` | **Yes*** | `string[]` | | A list of permitted patterns to search against all git tag names |
201+
| `denylist` | **Yes*** | `string[]` | | A list of denied patterns to search against all git tag names |
202+
| `ignoreCase` | No | `boolean` | `false` | Set to true to make `denylist` case insensitive. |
203+
204+
*`allowlist` and `denylist` cannot be both used within the same rule.
205+
192206
### `git-working-tree`
193207

194208
Checks whether the directory is managed with Git.

lib/git_helper.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ function gitAllCommits(targetDir) {
99
return spawnSync('git', args).stdout.toString().split('\n')
1010
}
1111

12+
/**
13+
* @param targetDir
14+
* @ignore
15+
*/
16+
function gitAllTagNames(targetDir) {
17+
const args = ['-C', targetDir, 'tag', '-l']
18+
const tagNames = spawnSync('git', args).stdout.toString().split('\n')
19+
tagNames.pop()
20+
return tagNames
21+
}
22+
1223
module.exports = {
13-
gitAllCommits
24+
gitAllCommits,
25+
gitAllTagNames
1426
}

rules/git-regex-tag-names-config.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "https://raw.githubusercontent.com/todogroup/repolinter/master/rules/git-regex-tag-names-config.json",
4+
"type": "object",
5+
"oneOf": [
6+
{
7+
"properties": {
8+
"allowlist": {
9+
"type": "array",
10+
"items": { "type": "string" }
11+
},
12+
"ignoreCase": {
13+
"type": "boolean",
14+
"default": false
15+
}
16+
},
17+
"required": ["allowlist"]
18+
},
19+
{
20+
"properties": {
21+
"denylist": {
22+
"type": "array",
23+
"items": { "type": "string" }
24+
},
25+
"ignoreCase": {
26+
"type": "boolean",
27+
"default": false
28+
}
29+
},
30+
"required": ["denylist"]
31+
}
32+
]
33+
}

rules/git-regex-tag-names.js

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2024 TODO Group. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
const Result = require('../lib/result')
5+
// eslint-disable-next-line no-unused-vars
6+
const FileSystem = require('../lib/file_system')
7+
const GitHelper = require('../lib/git_helper')
8+
9+
/**
10+
* @param {string} flags
11+
* @returns {regexMatchFactory~regexFn}
12+
* @ignore
13+
*/
14+
function regexMatchFactory(flags) {
15+
/**
16+
* @param {string} value
17+
* @param {string} pattern
18+
* @returns {object}
19+
* @ignore
20+
*/
21+
const regexFn = function (value, pattern) {
22+
return value.match(new RegExp(pattern, flags))
23+
}
24+
return regexFn
25+
}
26+
27+
/**
28+
* @param {string[]} tagNames
29+
* @param {object} options The rule configuration
30+
* @param {string[]=} options.allowlist
31+
* @param {string[]=} options.denylist
32+
* @param {boolean=} options.ignoreCase
33+
* @returns {Result}
34+
* @ignore
35+
*/
36+
function validateAgainstAllowlist(tagNames, options) {
37+
const targets = []
38+
const allowlist = options.allowlist
39+
console.log(options.ignoreCase)
40+
const regexMatch = regexMatchFactory(options.ignoreCase ? 'i' : '')
41+
42+
for (const tagName of tagNames) {
43+
let matched = false
44+
for (const allowRegex of allowlist) {
45+
if (regexMatch(tagName, allowRegex) !== null) {
46+
matched = true
47+
break // tag name passed at least one allowlist entry.
48+
}
49+
}
50+
if (!matched) {
51+
// Tag name did not pass any allowlist entries
52+
const message = [
53+
`The tag name for tag "${tagName}" does not match any regex in allowlist.\n`,
54+
`\tAllowlist: ${allowlist.join(', ')}`
55+
].join('\n')
56+
57+
targets.push({
58+
passed: false,
59+
message,
60+
path: tagName
61+
})
62+
}
63+
}
64+
65+
if (targets.length <= 0) {
66+
const message = [
67+
`Tag names comply with regex allowlist.\n`,
68+
`\tAllowlist: ${allowlist.join(', ')}`
69+
].join('\n')
70+
return new Result(message, [], true)
71+
}
72+
return new Result('', targets, false)
73+
}
74+
75+
/**
76+
* @param {string[]} tagNames
77+
* @param {object} options The rule configuration
78+
* @param {string[]=} options.allowlist
79+
* @param {string[]=} options.denylist
80+
* @param {boolean=} options.ignoreCase
81+
* @returns {Result}
82+
* @ignore
83+
*/
84+
function validateAgainstDenylist(tagNames, options) {
85+
const targets = []
86+
const denylist = options.denylist
87+
const regexMatch = regexMatchFactory(options.ignoreCase ? 'i' : '')
88+
89+
for (const tagName of tagNames) {
90+
for (const denyRegex of denylist) {
91+
if (regexMatch(tagName, denyRegex) !== null) {
92+
// Tag name matches a denylist entry
93+
const message = [
94+
`The tag name for tag "${tagName}" matched a regex in denylist.\n`,
95+
`\tDenylist: ${denylist.join(', ')}`
96+
].join('\n')
97+
98+
targets.push({
99+
passed: false,
100+
message,
101+
path: tagName
102+
})
103+
}
104+
}
105+
}
106+
if (targets.length <= 0) {
107+
const message = [
108+
`No denylisted regex found in any tag names.\n`,
109+
`\tDenylist: ${denylist.join(', ')}`
110+
].join('\n')
111+
return new Result(message, [], true)
112+
}
113+
return new Result('', targets, false)
114+
}
115+
116+
/**
117+
*
118+
* @param {FileSystem} fs A filesystem object configured with filter paths and target directories
119+
* @param {object} options The rule configuration
120+
* @param {string[]=} options.allowlist
121+
* @param {string[]=} options.denylist
122+
* @param {boolean=} options.ignoreCase
123+
* @returns {Result} The lint rule result
124+
* @ignore
125+
*/
126+
function gitRegexTagNames(fs, options) {
127+
if (options.allowlist && options.denylist) {
128+
throw new Error('"allowlist" and "denylist" cannot be both set.')
129+
} else if (!options.allowlist && !options.denylist) {
130+
throw new Error('missing "allowlist" or "denylist".')
131+
}
132+
const tagNames = GitHelper.gitAllTagNames(fs.targetDir)
133+
134+
// Allowlist
135+
if (options.allowlist) {
136+
return validateAgainstAllowlist(tagNames, options)
137+
} else if (options.denylist) {
138+
return validateAgainstDenylist(tagNames, options)
139+
}
140+
}
141+
142+
module.exports = gitRegexTagNames

rules/rules.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = {
1717
'git-grep-commits': require('./git-grep-commits'),
1818
'git-grep-log': require('./git-grep-log'),
1919
'git-list-tree': require('./git-list-tree'),
20+
'git-regex-tag-names': require('./git-regex-tag-names'),
2021
'git-working-tree': require('./git-working-tree'),
2122
'large-file': require('./large-file'),
2223
'license-detectable-by-licensee': require('./license-detectable-by-licensee'),

rulesets/schema.json

+12
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@
214214
}
215215
}
216216
},
217+
{
218+
"if": {
219+
"properties": { "type": { "const": "git-regex-tag-names" } }
220+
},
221+
"then": {
222+
"properties": {
223+
"options": {
224+
"$ref": "../rules/git-regex-tag-names-config.json"
225+
}
226+
}
227+
}
228+
},
217229
{
218230
"if": {
219231
"properties": { "type": { "const": "git-working-tree" } }

0 commit comments

Comments
 (0)