Skip to content

Commit

Permalink
feat(rules/git-regex-tag-names): add new rule
Browse files Browse the repository at this point in the history
This rule enables Git tag naming enforcement with JavaScript regex.

Signed-off-by: Rifa Achrinza <[email protected]>
  • Loading branch information
achrinza committed Nov 13, 2024
1 parent 6e00cef commit 19ad086
Show file tree
Hide file tree
Showing 8 changed files with 404 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ vendor/
.bundle
out
.vscode/
*~
.#*
\#*#
14 changes: 14 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Below is a complete list of rules that Repolinter can run, along with their conf
- [`git-grep-commits`](#git-grep-commits)
- [`git-grep-log`](#git-grep-log)
- [`git-list-tree`](#git-list-tree)
- [`git-regex-tag-names`](#git-regex-tag-names)
- [`git-working-tree`](#git-working-tree)
- [`json-schema-passes`](#json-schema-passes)
- [`large-file`](#large-file)
Expand Down Expand Up @@ -189,6 +190,19 @@ Check for blacklisted filepaths in Git.
| `denylist` | **Yes** | `string[]` | | A list of patterns to search against all paths in the git history. |
| `ignoreCase` | No | `boolean` | `false` | Set to true to make `denylist` case insensitive. |


### `git-regex-tag-names`

Check for permitted or denied Git tag names using JavaScript regular expressions.

| Input | Required | Type | Default | Description |
|--------------|----------|------------|---------|------------------------------------------------------------------|
| `allowlist` | **Yes*** | `string[]` | | A list of permitted patterns to search against all git tag names |
| `denylist` | **Yes*** | `string[]` | | A list of denied patterns to search against all git tag names |
| `ignoreCase` | No | `boolean` | `false` | Set to true to make `denylist` case insensitive. |

*`allowlist` and `denylist` cannot be both used within the same rule.

### `git-working-tree`

Checks whether the directory is managed with Git.
Expand Down
14 changes: 13 additions & 1 deletion lib/git_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ function gitAllCommits(targetDir) {
return spawnSync('git', args).stdout.toString().split('\n')
}

/**
* @param targetDir
* @ignore
*/
function gitAllTagNames(targetDir) {
const args = ['-C', targetDir, 'tag', '-l']
const tagNames = spawnSync('git', args).stdout.toString().split('\n')
tagNames.pop();
return tagNames;
}

module.exports = {
gitAllCommits
gitAllCommits,
gitAllTagNames
}
33 changes: 33 additions & 0 deletions rules/git-regex-tag-names-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://raw.githubusercontent.com/todogroup/repolinter/master/rules/git-regex-tag-names-config.json",
"type": "object",
"oneOf": [
{
"properties": {
"allowlist": {
"type": "array",
"items": { "type": "string" }
},
"ignoreCase": {
"type": "boolean",
"default": false
}
},
"required": ["allowlist"]
},
{
"properties": {
"denylist": {
"type": "array",
"items": { "type": "string" }
},
"ignoreCase": {
"type": "boolean",
"default": false
}
},
"required": ["denylist"]
}
]
}
141 changes: 141 additions & 0 deletions rules/git-regex-tag-names.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2024 TODO Group. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const Result = require('../lib/result')
// eslint-disable-next-line no-unused-vars
const FileSystem = require('../lib/file_system')
const GitHelper = require('../lib/git_helper')

/**
* @param {string} flags
* @returns {regexMatchFactory~regexFn}
* @ignore
*/
function regexMatchFactory(flags) {
/**
* @param {string} value
* @param {string} pattern
* @returns {object}
* @ignore
*/
const regexFn = function (value, pattern) {
return value.match(new RegExp(pattern, flags))
}
return regexFn
}

/**
* @param {string[]} tagNames
* @param {object} options The rule configuration
* @param {string[]=} options.allowlist
* @param {string[]=} options.denylist
* @param {boolean=} options.ignoreCase
* @returns {Result}
* @ignore
*/
function validateAgainstAllowlist(tagNames, options) {
const targets = []
const allowlist = options.allowlist
console.log(options.ignoreCase)
const regexMatch = regexMatchFactory(options.ignoreCase ? 'i' : '')

for (const tagName of tagNames) {
let matched = false
for (const allowRegex of allowlist) {
if (regexMatch(tagName, allowRegex) !== null) {
matched = true
break // tag name passed at least one allowlist entry.
}
}
if (!matched) {
// Tag name did not pass any allowlist entries
const message = [
`The tag name for tag "${tagName}" does not match any regex in allowlist.\n`,
`\tAllowlist: ${allowlist.join(', ')}`
].join('\n')

targets.push({
passed: false,
message,
path: tagName
})
}
}

if (targets.length <= 0) {
const message = [
`Tag names comply with regex allowlist.\n`,
`\tAllowlist: ${allowlist.join(', ')}`
].join('\n')
return new Result(message, [], true)
}
return new Result('', targets, false)
}

/**
* @param {string[]} tagNames
* @param {object} options The rule configuration
* @param {string[]=} options.allowlist
* @param {string[]=} options.denylist
* @param {boolean=} options.ignoreCase
* @returns {Result}
* @ignore
*/
function validateAgainstDenylist(tagNames, options) {
const targets = []
const denylist = options.denylist
const regexMatch = regexMatchFactory(options.ignoreCase ? 'i' : '')

for (const tagName of tagNames) {
for (const denyRegex of denylist) {
if (regexMatch(tagName, denyRegex) !== null) {
// Tag name matches a denylist entry
const message = [
`The tag name for tag "${tagName}" matched a regex in denylist.\n`,
`\tDenylist: ${denylist.join(', ')}`
].join('\n')

targets.push({
passed: false,
message,
path: tagName
})
}
}
}
if (targets.length <= 0) {
const message = [
`No denylisted regex found in any tag names.\n`,
`\tDenylist: ${denylist.join(', ')}`
].join('\n')
return new Result(message, [], true)
}
return new Result('', targets, false)
}

/**
*
* @param {FileSystem} fs A filesystem object configured with filter paths and target directories
* @param {object} options The rule configuration
* @param {string[]=} options.allowlist
* @param {string[]=} options.denylist
* @param {boolean=} options.ignoreCase
* @returns {Result} The lint rule result
* @ignore
*/
function gitRegexTagNames(fs, options) {
if (options.allowlist && options.denylist) {
const message = '"allowlist" and "denylist" cannot be both set.'
return new Result(message, [], false)
}
const tagNames = GitHelper.gitAllTagNames(fs.targetDir)

// Allowlist
if (options.allowlist) {
return validateAgainstAllowlist(tagNames, options)
} else if (options.denylist) {
return validateAgainstDenylist(tagNames, options)
}
}

module.exports = gitRegexTagNames
1 change: 1 addition & 0 deletions rules/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
'git-grep-commits': require('./git-grep-commits'),
'git-grep-log': require('./git-grep-log'),
'git-list-tree': require('./git-list-tree'),
'git-regex-tag-names': require('./git-regex-tag-names'),
'git-working-tree': require('./git-working-tree'),
'large-file': require('./large-file'),
'license-detectable-by-licensee': require('./license-detectable-by-licensee'),
Expand Down
12 changes: 12 additions & 0 deletions rulesets/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,18 @@
}
}
},
{
"if": {
"properties": { "type": { "const": "git-regex-tag-names" } }
},
"then": {
"properties": {
"options": {
"$ref": "../rules/git-regex-tag-names-config.json"
}
}
}
},
{
"if": {
"properties": { "type": { "const": "git-working-tree" } }
Expand Down
Loading

0 comments on commit 19ad086

Please sign in to comment.