mirror of
https://github.com/dorny/paths-filter.git
synced 2026-08-09 12:00:39 +02:00
feat: add 'some-with-excludes' predicate quantifier (#322)
This commit is contained in:
@@ -167,6 +167,41 @@ jobs:
|
||||
if: steps.filter.outputs.local_count != 1
|
||||
run: exit 1
|
||||
|
||||
test-predicate-quantifier-some-with-excludes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: modify working tree
|
||||
run: |
|
||||
mkdir -p mobile/.config
|
||||
echo "TEST" > mobile/main.kt
|
||||
echo "TEST" > mobile/README.md
|
||||
echo "TEST" > mobile/.config/lint.json
|
||||
echo "TEST" > backend.go
|
||||
git add -A
|
||||
- uses: ./
|
||||
id: filter
|
||||
with:
|
||||
base: HEAD
|
||||
list-files: shell
|
||||
predicate-quantifier: 'some-with-excludes'
|
||||
filters: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
- '!mobile/**/*.md'
|
||||
- '!mobile/.config/**'
|
||||
excludesOnly:
|
||||
- '!**/*.md'
|
||||
- name: Print 'mobile_files'
|
||||
run: echo ${{steps.filter.outputs.mobile_files}}
|
||||
- name: filter-test
|
||||
if: |
|
||||
steps.filter.outputs.mobile != 'true'
|
||||
|| steps.filter.outputs.mobile_count != 1
|
||||
|| steps.filter.outputs.mobile_files != 'mobile/main.kt'
|
||||
|| steps.filter.outputs.excludesOnly != 'false'
|
||||
run: exit 1
|
||||
|
||||
test-change-type:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -81,6 +81,7 @@ For more scenarios see [examples](#examples) section.
|
||||
|
||||
## What's New
|
||||
|
||||
- Add `some-with-excludes` value of the `predicate-quantifier` input parameter
|
||||
- Automatic workaround for git `dubious ownership` errors in container jobs
|
||||
- New major release `v4` after update to Node 24 [Breaking change]
|
||||
- Add `ref` input parameter
|
||||
@@ -172,14 +173,17 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
|
||||
token: ''
|
||||
|
||||
# Optional parameter to override the default behavior of file matching algorithm.
|
||||
# By default files that match at least one pattern defined by the filters will be included.
|
||||
# This parameter allows to override the "at least one pattern" behavior to make it so that
|
||||
# all of the patterns have to match or otherwise the file is excluded.
|
||||
# Supported values:
|
||||
# 'some' - File is included if it matches at least one pattern (default).
|
||||
# 'every' - File is included only if it matches all of the patterns.
|
||||
# 'some-with-excludes' - File is included if it matches at least one pattern
|
||||
# and no negated pattern (the ones prefixed with '!').
|
||||
#
|
||||
# An example scenario where this is useful if you would like to match all
|
||||
# .ts files in a sub-directory but not .md files.
|
||||
# The filters below will match markdown files despite the exclusion syntax UNLESS
|
||||
# you specify 'every' as the predicate-quantifier parameter. When you do that,
|
||||
# it will only match the .ts files in the subdirectory as expected.
|
||||
# you specify 'every' or 'some-with-excludes' as the predicate-quantifier parameter.
|
||||
# When you do that, it will only match the .ts files in the subdirectory as expected.
|
||||
#
|
||||
# backend:
|
||||
# - 'pkg/a/b/c/**'
|
||||
@@ -197,6 +201,9 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
|
||||
- With `predicate-quantifier: 'every'`:
|
||||
- `'true'` - if **any** changed file matches **all** of the filter's rules
|
||||
- `'false'` - if **no** changed file matches **all** of the filter's rules
|
||||
- With `predicate-quantifier: 'some-with-excludes'`:
|
||||
- `'true'` - if **any** changed file matches **at least one** of the filter's rules and **none** of its negated rules
|
||||
- `'false'` - if **no** changed file matches **at least one** of the filter's rules and **none** of its negated rules
|
||||
- Each filter sets an output variable with the name `${FILTER_NAME}_count` to the count of matching files.
|
||||
- If enabled, for each filter it sets an output variable with the name `${FILTER_NAME}_files`. It will contain a list of all files matching the filter.
|
||||
- `changes` - JSON array with names of all filters matching any of the changed files.
|
||||
@@ -533,6 +540,32 @@ jobs:
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Detect changes in multiple unrelated paths and exclude some file extensions</summary>
|
||||
|
||||
```yaml
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
# With 'some-with-excludes' a file is matched when it matches at least one pattern
|
||||
# and none of the negated ones. The filter below therefore matches all the files
|
||||
# in the 'mobile' folder and the workflow file, but never a markdown file or
|
||||
# anything in 'mobile/.config'.
|
||||
#
|
||||
# An exclusion is final - a file excluded by one pattern can't be included back
|
||||
# by another one. Consequently, a filter consisting of negated patterns only
|
||||
# never matches anything.
|
||||
predicate-quantifier: 'some-with-excludes'
|
||||
filters: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
- '!mobile/**/*.md'
|
||||
- '!mobile/.config/**'
|
||||
- '.github/workflows/test_mobile.yml'
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Custom processing of changed files
|
||||
|
||||
<details>
|
||||
|
||||
@@ -148,6 +148,99 @@ describe('matching tests', () => {
|
||||
expect(otherPkgJpegMatch.backend).toEqual([])
|
||||
})
|
||||
|
||||
test('ignores exclusions when using the default predicate quantifier', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- 'src/**'
|
||||
- '!**/*.md'
|
||||
`
|
||||
const filter = new Filter(yaml)
|
||||
|
||||
// A negated pattern is just another pattern for the 'some' quantifier - a markdown file
|
||||
// inside 'src' still matches 'src/**' and any other file matches the negated pattern.
|
||||
const files = modified(['src/README.md', 'other/file.txt'])
|
||||
expect(filter.match(files).src).toEqual(files)
|
||||
})
|
||||
|
||||
test('matches files of every pattern when set to PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
|
||||
const yaml = `
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
- '!mobile/**/*.md'
|
||||
- '!mobile/.config/**'
|
||||
- '.github/workflows/test_mobile.yml'
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
const sourceFiles = modified(['mobile/main.kt', 'mobile/src/some/Activity.kt'])
|
||||
const workflowFiles = modified(['.github/workflows/test_mobile.yml'])
|
||||
const docsFiles = modified(['mobile/README.md', 'mobile/docs/some/page.md'])
|
||||
const configFiles = modified(['mobile/.config/lint.json', 'mobile/.config/nested/lint.json'])
|
||||
const otherFiles = modified(['backend/main.go', '.github/workflows/test_backend.yml'])
|
||||
|
||||
expect(filter.match(sourceFiles).mobile).toEqual(sourceFiles)
|
||||
expect(filter.match(workflowFiles).mobile).toEqual(workflowFiles)
|
||||
expect(filter.match(docsFiles).mobile).toEqual([])
|
||||
expect(filter.match(configFiles).mobile).toEqual([])
|
||||
expect(filter.match(otherFiles).mobile).toEqual([])
|
||||
})
|
||||
|
||||
test('excludes file with PredicateQuantifier.SOME_WITH_EXCLUDES regardless of the pattern order', () => {
|
||||
const yaml = `
|
||||
excludeFirst:
|
||||
- '!**/*.md'
|
||||
- 'src/**'
|
||||
excludeLast:
|
||||
- 'src/**'
|
||||
- '!**/*.md'
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
const match = filter.match(modified(['src/index.ts', 'src/README.md']))
|
||||
expect(match.excludeFirst).toEqual(modified(['src/index.ts']))
|
||||
expect(match.excludeLast).toEqual(modified(['src/index.ts']))
|
||||
})
|
||||
|
||||
test('keeps file excluded with PredicateQuantifier.SOME_WITH_EXCLUDES even if a later pattern includes it', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- 'src/**'
|
||||
- '!**/*.md'
|
||||
- 'src/docs/**'
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
const match = filter.match(modified(['src/docs/guide.md', 'src/docs/logo.png']))
|
||||
expect(match.src).toEqual(modified(['src/docs/logo.png']))
|
||||
})
|
||||
|
||||
test('matches nothing with PredicateQuantifier.SOME_WITH_EXCLUDES when there is no include pattern', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- '!**/*.md'
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
const match = filter.match(modified(['src/index.ts', 'src/README.md']))
|
||||
expect(match.src).toEqual([])
|
||||
})
|
||||
|
||||
test('treats negated extglob as an include pattern with PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
|
||||
const yaml = `
|
||||
backend:
|
||||
- '!(**/*.tsx|**/*.less)'
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
expect(filter.match(modified(['src/server.py'])).backend).toEqual(modified(['src/server.py']))
|
||||
expect(filter.match(modified(['src/ui.tsx'])).backend).toEqual([])
|
||||
})
|
||||
|
||||
test('matches path based on rules included using YAML anchor', () => {
|
||||
const yaml = `
|
||||
shared: &shared
|
||||
@@ -197,6 +290,61 @@ describe('matching specific change status', () => {
|
||||
expect(match.addOrModify).toEqual(files)
|
||||
})
|
||||
|
||||
test('respects change status of exclude patterns when set to PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- 'src/**'
|
||||
- deleted: '!src/generated/**'
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
const files = [
|
||||
{status: ChangeStatus.Deleted, filename: 'src/generated/api.ts'},
|
||||
{status: ChangeStatus.Modified, filename: 'src/generated/api.ts'}
|
||||
]
|
||||
const match = filter.match(files)
|
||||
expect(match.src).toEqual([files[1]])
|
||||
})
|
||||
|
||||
test('matches multiple patterns of single change status when set to PredicateQuantifier.SOME_WITH_EXCLUDES', () => {
|
||||
const yaml = `
|
||||
docs: &docs
|
||||
- '!**/*.md'
|
||||
src:
|
||||
- added|modified: 'src/**'
|
||||
- added|modified: *docs
|
||||
`
|
||||
const filterConfig: FilterConfig = {predicateQuantifier: PredicateQuantifier.SOME_WITH_EXCLUDES}
|
||||
const filter = new Filter(yaml, filterConfig)
|
||||
|
||||
const files = [
|
||||
{status: ChangeStatus.Added, filename: 'src/index.ts'},
|
||||
{status: ChangeStatus.Added, filename: 'src/README.md'},
|
||||
{status: ChangeStatus.Deleted, filename: 'src/legacy.ts'}
|
||||
]
|
||||
const match = filter.match(files)
|
||||
expect(match.src).toEqual([files[0]])
|
||||
})
|
||||
|
||||
test('or-es patterns of single change status when using the default predicate quantifier', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- added|modified: ['src/**', '!**/*.md']
|
||||
`
|
||||
const filter = new Filter(yaml)
|
||||
|
||||
// Both patterns are OR-ed into a single rule, therefore a markdown file inside 'src'
|
||||
// matches through 'src/**' and any other file matches through the negated pattern.
|
||||
const files = [
|
||||
{status: ChangeStatus.Added, filename: 'src/README.md'},
|
||||
{status: ChangeStatus.Added, filename: 'other/file.txt'},
|
||||
{status: ChangeStatus.Deleted, filename: 'src/index.ts'}
|
||||
]
|
||||
const match = filter.match(files)
|
||||
expect(match.src).toEqual([files[0], files[1]])
|
||||
})
|
||||
|
||||
test('matches when using an anchor', () => {
|
||||
const yaml = `
|
||||
shared: &shared
|
||||
|
||||
+5
-1
@@ -46,7 +46,11 @@ inputs:
|
||||
default: '100'
|
||||
predicate-quantifier:
|
||||
description: |
|
||||
allows to override the "at least one pattern" behavior to make it so that all of the patterns have to match or otherwise the file is excluded.
|
||||
allows to override the "at least one pattern" behavior:
|
||||
'some' - file is included if it matches at least one pattern (default).
|
||||
'every' - file is included only if it matches all of the patterns.
|
||||
'some-with-excludes' - file is included if it matches at least one pattern
|
||||
and no negated pattern (the ones prefixed with '!').
|
||||
required: false
|
||||
default: 'some'
|
||||
outputs:
|
||||
|
||||
Vendored
+63
-17
@@ -86,6 +86,16 @@ var PredicateQuantifier;
|
||||
* specify anything as a predicate quantifier.
|
||||
*/
|
||||
PredicateQuantifier["SOME"] = "some";
|
||||
/**
|
||||
* When choosing 'some-with-excludes' in the config it means that files will get matched if
|
||||
* at least one of the patterns matches them and none of the negated patterns (the ones
|
||||
* prefixed with '!') matches them. An exclusion is final - a file excluded by one pattern
|
||||
* can't be included back by another one.
|
||||
*
|
||||
* A filter which consists of negated patterns only never matches anything,
|
||||
* because there is no pattern which could include a file in the first place.
|
||||
*/
|
||||
PredicateQuantifier["SOME_WITH_EXCLUDES"] = "some-with-excludes";
|
||||
})(PredicateQuantifier || (exports.PredicateQuantifier = PredicateQuantifier = {}));
|
||||
/**
|
||||
* An array of strings (at runtime) that contains the valid/accepted values for
|
||||
@@ -126,15 +136,35 @@ class Filter {
|
||||
return result;
|
||||
}
|
||||
isMatch(file, patterns) {
|
||||
var _a;
|
||||
const aPredicate = (rule) => {
|
||||
return (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename);
|
||||
var _a, _b, _c;
|
||||
const isStatusMatch = (rule) => {
|
||||
return rule.status === undefined || rule.status.includes(file.status);
|
||||
};
|
||||
if (((_a = this.filterConfig) === null || _a === void 0 ? void 0 : _a.predicateQuantifier) === 'every') {
|
||||
return patterns.every(aPredicate);
|
||||
}
|
||||
else {
|
||||
return patterns.some(aPredicate);
|
||||
const aPredicate = (rule) => {
|
||||
return isStatusMatch(rule) && rule.isMatch(file.filename);
|
||||
};
|
||||
switch ((_a = this.filterConfig) === null || _a === void 0 ? void 0 : _a.predicateQuantifier) {
|
||||
case PredicateQuantifier.EVERY:
|
||||
return patterns.every(aPredicate);
|
||||
case PredicateQuantifier.SOME_WITH_EXCLUDES: {
|
||||
let isIncluded = false;
|
||||
for (const rule of patterns) {
|
||||
if (!isStatusMatch(rule)) {
|
||||
continue;
|
||||
}
|
||||
// Once a file is excluded it stays excluded - no other pattern can include it back.
|
||||
// Therefore all the patterns have to be evaluated even if the file is already included.
|
||||
if ((_b = rule.isExclude) === null || _b === void 0 ? void 0 : _b.call(rule, file.filename)) {
|
||||
return false;
|
||||
}
|
||||
if (!isIncluded && ((_c = rule.isInclude) === null || _c === void 0 ? void 0 : _c.call(rule, file.filename))) {
|
||||
isIncluded = true;
|
||||
}
|
||||
}
|
||||
return isIncluded;
|
||||
}
|
||||
default:
|
||||
return patterns.some(aPredicate);
|
||||
}
|
||||
}
|
||||
parseFilterItemYaml(item) {
|
||||
@@ -142,21 +172,19 @@ class Filter {
|
||||
return flat(item.map(i => this.parseFilterItemYaml(i)));
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return [{ status: undefined, isMatch: (0, picomatch_1.default)(item, MatchOptions) }];
|
||||
return [createRuleItem(item)];
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.entries(item).map(([key, pattern]) => {
|
||||
if (typeof key !== 'string' || (typeof pattern !== 'string' && !Array.isArray(pattern))) {
|
||||
this.throwInvalidFormatError(`Expected [key:string]= pattern:string | string[], but [${key}:${typeof key}]= ${pattern}:${typeof pattern} found`);
|
||||
}
|
||||
return {
|
||||
status: key
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => x.toLowerCase()),
|
||||
isMatch: (0, picomatch_1.default)(pattern, MatchOptions)
|
||||
};
|
||||
const status = key
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => x.toLowerCase());
|
||||
return createRuleItem(pattern, status);
|
||||
});
|
||||
}
|
||||
this.throwInvalidFormatError(`Unexpected element type '${typeof item}'`);
|
||||
@@ -171,6 +199,24 @@ exports.Filter = Filter;
|
||||
function flat(arr) {
|
||||
return arr.reduce((acc, val) => acc.concat(val), []);
|
||||
}
|
||||
// Compiles filename pattern(s) of a single filter rule item into matchers.
|
||||
// Multiple patterns are OR-ed together, which is how picomatch treats an array of globs.
|
||||
// Patterns are also split by their polarity, so PredicateQuantifier.SOME_WITH_EXCLUDES
|
||||
// can tell inclusions from exclusions. Note that only a leading '!' negates the whole
|
||||
// pattern - the '!(...)' extglob is a regular pattern matching everything it doesn't enumerate.
|
||||
function createRuleItem(patterns, status) {
|
||||
const matchers = (Array.isArray(patterns) ? patterns : [patterns]).map(pattern => (0, picomatch_1.default)(pattern, MatchOptions, true));
|
||||
// picomatch inverts the result of a matcher created from a negated pattern.
|
||||
// Inverting it back gives a matcher of the filenames such pattern excludes.
|
||||
const includes = matchers.filter(matcher => !matcher.state.negated);
|
||||
const excludes = matchers.filter(matcher => matcher.state.negated);
|
||||
return {
|
||||
status,
|
||||
isMatch: str => matchers.some(matcher => matcher(str)),
|
||||
isInclude: includes.length > 0 ? str => includes.some(matcher => matcher(str)) : undefined,
|
||||
isExclude: excludes.length > 0 ? str => excludes.some(matcher => !matcher(str)) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
+74
-16
@@ -21,6 +21,12 @@ const MatchOptions = {
|
||||
interface FilterRuleItem {
|
||||
status?: ChangeStatus[] // Required change status of the matched files
|
||||
isMatch: (str: string) => boolean // Matches the filename
|
||||
// Matchers for the individual polarities of the patterns this item was created from.
|
||||
// They are used only by the PredicateQuantifier.SOME_WITH_EXCLUDES quantifier,
|
||||
// which has to tell inclusions and exclusions apart. The other quantifiers keep
|
||||
// evaluating the item as a whole via 'isMatch'.
|
||||
isInclude?: (str: string) => boolean // Matches any of the patterns which are not negated
|
||||
isExclude?: (str: string) => boolean // Matches any of the patterns which are negated (e.g. '!**/*.md')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +53,17 @@ export enum PredicateQuantifier {
|
||||
* at least one pattern that matches them. This is the default behavior if you don't
|
||||
* specify anything as a predicate quantifier.
|
||||
*/
|
||||
SOME = 'some'
|
||||
SOME = 'some',
|
||||
/**
|
||||
* When choosing 'some-with-excludes' in the config it means that files will get matched if
|
||||
* at least one of the patterns matches them and none of the negated patterns (the ones
|
||||
* prefixed with '!') matches them. An exclusion is final - a file excluded by one pattern
|
||||
* can't be included back by another one.
|
||||
*
|
||||
* A filter which consists of negated patterns only never matches anything,
|
||||
* because there is no pattern which could include a file in the first place.
|
||||
*/
|
||||
SOME_WITH_EXCLUDES = 'some-with-excludes'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,13 +120,35 @@ export class Filter {
|
||||
}
|
||||
|
||||
private isMatch(file: File, patterns: FilterRuleItem[]): boolean {
|
||||
const aPredicate = (rule: Readonly<FilterRuleItem>): boolean => {
|
||||
return (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename)
|
||||
const isStatusMatch = (rule: Readonly<FilterRuleItem>): boolean => {
|
||||
return rule.status === undefined || rule.status.includes(file.status)
|
||||
}
|
||||
if (this.filterConfig?.predicateQuantifier === 'every') {
|
||||
return patterns.every(aPredicate)
|
||||
} else {
|
||||
return patterns.some(aPredicate)
|
||||
const aPredicate = (rule: Readonly<FilterRuleItem>): boolean => {
|
||||
return isStatusMatch(rule) && rule.isMatch(file.filename)
|
||||
}
|
||||
|
||||
switch (this.filterConfig?.predicateQuantifier) {
|
||||
case PredicateQuantifier.EVERY:
|
||||
return patterns.every(aPredicate)
|
||||
case PredicateQuantifier.SOME_WITH_EXCLUDES: {
|
||||
let isIncluded = false
|
||||
for (const rule of patterns) {
|
||||
if (!isStatusMatch(rule)) {
|
||||
continue
|
||||
}
|
||||
// Once a file is excluded it stays excluded - no other pattern can include it back.
|
||||
// Therefore all the patterns have to be evaluated even if the file is already included.
|
||||
if (rule.isExclude?.(file.filename)) {
|
||||
return false
|
||||
}
|
||||
if (!isIncluded && rule.isInclude?.(file.filename)) {
|
||||
isIncluded = true
|
||||
}
|
||||
}
|
||||
return isIncluded
|
||||
}
|
||||
default:
|
||||
return patterns.some(aPredicate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +158,7 @@ export class Filter {
|
||||
}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
return [{status: undefined, isMatch: picomatch(item, MatchOptions)}]
|
||||
return [createRuleItem(item)]
|
||||
}
|
||||
|
||||
if (typeof item === 'object') {
|
||||
@@ -130,14 +168,12 @@ export class Filter {
|
||||
`Expected [key:string]= pattern:string | string[], but [${key}:${typeof key}]= ${pattern}:${typeof pattern} found`
|
||||
)
|
||||
}
|
||||
return {
|
||||
status: key
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => x.toLowerCase()) as ChangeStatus[],
|
||||
isMatch: picomatch(pattern, MatchOptions)
|
||||
}
|
||||
const status = key
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => x.toLowerCase()) as ChangeStatus[]
|
||||
return createRuleItem(pattern, status)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -154,3 +190,25 @@ export class Filter {
|
||||
function flat<T>(arr: T[][]): T[] {
|
||||
return arr.reduce((acc, val) => acc.concat(val), [])
|
||||
}
|
||||
|
||||
// Compiles filename pattern(s) of a single filter rule item into matchers.
|
||||
// Multiple patterns are OR-ed together, which is how picomatch treats an array of globs.
|
||||
// Patterns are also split by their polarity, so PredicateQuantifier.SOME_WITH_EXCLUDES
|
||||
// can tell inclusions from exclusions. Note that only a leading '!' negates the whole
|
||||
// pattern - the '!(...)' extglob is a regular pattern matching everything it doesn't enumerate.
|
||||
function createRuleItem(patterns: string | string[], status?: ChangeStatus[]): FilterRuleItem {
|
||||
const matchers = (Array.isArray(patterns) ? patterns : [patterns]).map(pattern =>
|
||||
picomatch(pattern, MatchOptions, true)
|
||||
)
|
||||
// picomatch inverts the result of a matcher created from a negated pattern.
|
||||
// Inverting it back gives a matcher of the filenames such pattern excludes.
|
||||
const includes = matchers.filter(matcher => !matcher.state.negated)
|
||||
const excludes = matchers.filter(matcher => matcher.state.negated)
|
||||
|
||||
return {
|
||||
status,
|
||||
isMatch: str => matchers.some(matcher => matcher(str)),
|
||||
isInclude: includes.length > 0 ? str => includes.some(matcher => matcher(str)) : undefined,
|
||||
isExclude: excludes.length > 0 ? str => excludes.some(matcher => !matcher(str)) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user