mirror of
https://github.com/dorny/paths-filter.git
synced 2025-06-08 00:59:04 +00:00
commit
fa09ebae05
6 changed files with 18310 additions and 836 deletions
41
.github/workflows/pull-request-verification.yml
vendored
41
.github/workflows/pull-request-verification.yml
vendored
|
@ -94,44 +94,3 @@ jobs:
|
|||
- name: count-test
|
||||
if: steps.filter.outputs.local_count != 1
|
||||
run: exit 1
|
||||
|
||||
test-change-type:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: configure GIT user
|
||||
run: git config user.email "john@nowhere.local" && git config user.name "John Doe"
|
||||
- name: modify working tree
|
||||
run: touch add.txt && rm README.md && echo "TEST" > LICENSE
|
||||
- name: commit changes
|
||||
run: git add -A && git commit -a -m 'testing this action'
|
||||
- uses: ./
|
||||
id: filter
|
||||
with:
|
||||
token: ''
|
||||
list-files: shell
|
||||
filters: |
|
||||
added:
|
||||
- added: "add.txt"
|
||||
deleted:
|
||||
- deleted: "README.md"
|
||||
modified:
|
||||
- modified: "LICENSE"
|
||||
any:
|
||||
- added|deleted|modified: "*"
|
||||
- name: Print 'added_files'
|
||||
run: echo ${{steps.filter.outputs.added_files}}
|
||||
- name: Print 'modified_files'
|
||||
run: echo ${{steps.filter.outputs.modified_files}}
|
||||
- name: Print 'deleted_files'
|
||||
run: echo ${{steps.filter.outputs.deleted_files}}
|
||||
- name: filter-test
|
||||
if: |
|
||||
steps.filter.outputs.added != 'true'
|
||||
|| steps.filter.outputs.deleted != 'true'
|
||||
|| steps.filter.outputs.modified != 'true'
|
||||
|| steps.filter.outputs.any != 'true'
|
||||
|| steps.filter.outputs.added_files != 'add.txt'
|
||||
|| steps.filter.outputs.modified_files != 'LICENSE'
|
||||
|| steps.filter.outputs.deleted_files != 'README.md'
|
||||
run: exit 1
|
||||
|
|
|
@ -87,8 +87,6 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
|
|||
# Each filter has a name and a list of rules.
|
||||
# Rule is a glob expression - paths of all changed
|
||||
# files are matched against it.
|
||||
# Rule can optionally specify if the file
|
||||
# should be added, modified, or deleted.
|
||||
# For each filter, there will be a corresponding output variable to
|
||||
# indicate if there's a changed file matching any of the rules.
|
||||
# Optionally, there can be a second output variable
|
||||
|
|
|
@ -117,6 +117,37 @@ describe('matching tests', () => {
|
|||
expect(pyMatch.backend).toEqual(pyFiles)
|
||||
})
|
||||
|
||||
test('matches single rule with negation', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- '!src/**/*.js'
|
||||
`
|
||||
const filter = new Filter(yaml)
|
||||
const files = modified(['src/app/module/file.js'])
|
||||
const match = filter.match(files)
|
||||
expect(match.src).toEqual([])
|
||||
})
|
||||
|
||||
test('matches multiple rules with negation', () => {
|
||||
const yaml = `
|
||||
src:
|
||||
- 'src/**/*.ts'
|
||||
- '!src/**/*.test.ts'
|
||||
`
|
||||
const filter = new Filter(yaml)
|
||||
const jsFiles = modified(['src/app/module/file.js'])
|
||||
const tsFiles = modified(['src/app/module/file.ts'])
|
||||
const tsTestFiles = modified(['src/app/module/file.test.ts'])
|
||||
|
||||
const jsMatch = filter.match(jsFiles)
|
||||
const tsMatch = filter.match(tsFiles)
|
||||
const tsTestMatch = filter.match(tsTestFiles)
|
||||
|
||||
expect(jsMatch.src).toEqual([])
|
||||
expect(tsMatch.src).toEqual(tsFiles)
|
||||
expect(tsTestMatch.src).toEqual([])
|
||||
})
|
||||
|
||||
test('matches path based on rules included using YAML anchor', () => {
|
||||
const yaml = `
|
||||
shared: &shared
|
||||
|
@ -133,54 +164,6 @@ describe('matching tests', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('matching specific change status', () => {
|
||||
test('does not match modified file as added', () => {
|
||||
const yaml = `
|
||||
add:
|
||||
- added: "**/*"
|
||||
`
|
||||
let filter = new Filter(yaml)
|
||||
const match = filter.match(modified(['file.js']))
|
||||
expect(match.add).toEqual([])
|
||||
})
|
||||
|
||||
test('match added file as added', () => {
|
||||
const yaml = `
|
||||
add:
|
||||
- added: "**/*"
|
||||
`
|
||||
let filter = new Filter(yaml)
|
||||
const files = [{status: ChangeStatus.Added, filename: 'file.js'}]
|
||||
const match = filter.match(files)
|
||||
expect(match.add).toEqual(files)
|
||||
})
|
||||
|
||||
test('matches when multiple statuses are configured', () => {
|
||||
const yaml = `
|
||||
addOrModify:
|
||||
- added|modified: "**/*"
|
||||
`
|
||||
let filter = new Filter(yaml)
|
||||
const files = [{status: ChangeStatus.Modified, filename: 'file.js'}]
|
||||
const match = filter.match(files)
|
||||
expect(match.addOrModify).toEqual(files)
|
||||
})
|
||||
|
||||
test('matches when using an anchor', () => {
|
||||
const yaml = `
|
||||
shared: &shared
|
||||
- common/**/*
|
||||
- config/**/*
|
||||
src:
|
||||
- modified: *shared
|
||||
`
|
||||
let filter = new Filter(yaml)
|
||||
const files = modified(['config/file.js', 'common/anotherFile.js'])
|
||||
const match = filter.match(files)
|
||||
expect(match.src).toEqual(files)
|
||||
})
|
||||
})
|
||||
|
||||
function modified(paths: string[]): File[] {
|
||||
return paths.map(filename => {
|
||||
return {filename, status: ChangeStatus.Modified}
|
||||
|
|
18964
package-lock.json
generated
18964
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -6,6 +6,7 @@
|
|||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"tscheck": "tsc --noEmit",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
|
@ -29,14 +30,13 @@
|
|||
"@actions/exec": "^1.0.4",
|
||||
"@actions/github": "^2.2.0",
|
||||
"@octokit/webhooks": "^7.6.2",
|
||||
"picomatch": "^2.2.2"
|
||||
"micromatch": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/minimatch": "^3.0.3",
|
||||
"@types/micromatch": "^4.0.2",
|
||||
"@types/node": "^14.0.5",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
"@typescript-eslint/parser": "^3.3.0",
|
||||
"@zeit/ncc": "^0.22.3",
|
||||
"eslint": "^7.3.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as jsyaml from 'js-yaml'
|
||||
import picomatch from 'picomatch'
|
||||
import micromatch from 'micromatch'
|
||||
import {File, ChangeStatus} from './file'
|
||||
|
||||
// Type definition of object we expect to load from YAML
|
||||
|
@ -8,19 +8,18 @@ interface FilterYaml {
|
|||
}
|
||||
type FilterItemYaml =
|
||||
| string // Filename pattern, e.g. "path/to/*.js"
|
||||
| {[changeTypes: string]: string | string[]} // Change status and filename, e.g. added|modified: "path/to/*.js"
|
||||
| FilterItemYaml[] // Supports referencing another rule via YAML anchor
|
||||
|
||||
// Minimatch options used in all matchers
|
||||
const MatchOptions = {
|
||||
// Micromatch options used in all matchers
|
||||
const MatchOptions: micromatch.Options = {
|
||||
dot: true
|
||||
}
|
||||
|
||||
// Internal representation of one item in named filter rule
|
||||
// Created as simplified form of data in FilterItemYaml
|
||||
interface FilterRuleItem {
|
||||
status?: ChangeStatus[] // Required change status of the matched files
|
||||
isMatch: (str: string) => boolean // Matches the filename
|
||||
/** returns the list of matched files */
|
||||
matcher: (files: string[]) => string[]
|
||||
}
|
||||
|
||||
export interface FilterResults {
|
||||
|
@ -28,7 +27,7 @@ export interface FilterResults {
|
|||
}
|
||||
|
||||
export class Filter {
|
||||
rules: {[key: string]: FilterRuleItem[]} = {}
|
||||
rules: {[key: string]: string[]} = {}
|
||||
|
||||
// Creates instance of Filter and load rules from YAML if it's provided
|
||||
constructor(yaml?: string) {
|
||||
|
@ -49,49 +48,32 @@ export class Filter {
|
|||
}
|
||||
|
||||
for (const [key, item] of Object.entries(doc)) {
|
||||
this.rules[key] = this.parseFilterItemYaml(item)
|
||||
this.rules[key] = this.getPatterns(item)
|
||||
}
|
||||
}
|
||||
|
||||
match(files: File[]): FilterResults {
|
||||
const result: FilterResults = {}
|
||||
const filesMap = files.reduce((result, x) => {
|
||||
result.set(x.filename, x)
|
||||
return result
|
||||
}, new Map<string, File>())
|
||||
|
||||
for (const [key, patterns] of Object.entries(this.rules)) {
|
||||
result[key] = files.filter(file => this.isMatch(file, patterns))
|
||||
const matchingFileNames = micromatch([...filesMap.keys()], patterns, MatchOptions)
|
||||
result[key] = matchingFileNames.map(x => filesMap.get(x)).filter((x): x is File => !!x)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private isMatch(file: File, patterns: FilterRuleItem[]): boolean {
|
||||
return patterns.some(
|
||||
rule => (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename)
|
||||
)
|
||||
}
|
||||
|
||||
private parseFilterItemYaml(item: FilterItemYaml): FilterRuleItem[] {
|
||||
private getPatterns(item: FilterItemYaml): string[] {
|
||||
if (Array.isArray(item)) {
|
||||
return flat(item.map(i => this.parseFilterItemYaml(i)))
|
||||
return flat(item.map(i => this.getPatterns(i)))
|
||||
}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
return [{status: undefined, isMatch: picomatch(item, MatchOptions)}]
|
||||
}
|
||||
|
||||
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()) as ChangeStatus[],
|
||||
isMatch: picomatch(pattern, MatchOptions)
|
||||
}
|
||||
})
|
||||
return [item]
|
||||
}
|
||||
|
||||
this.throwInvalidFormatError(`Unexpected element type '${typeof item}'`)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue