Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rules/git-regex-tag-names): add new rule #332

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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"]
}
]
}
142 changes: 142 additions & 0 deletions rules/git-regex-tag-names.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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) {
throw new Error('"allowlist" and "denylist" cannot be both set.')
} else if (!options.allowlist && !options.denylist) {
throw new Error('missing "allowlist" or "denylist".')
}
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