mirror of
https://git.mirrors.martin98.com/https://github.com/actions/toolkit
synced 2026-05-03 13:58:07 +08:00
multiple glob patterns (#287)
This commit is contained in:
242
packages/glob/src/internal-globber.ts
Normal file
242
packages/glob/src/internal-globber.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as globOptionsHelper from './internal-glob-options-helper'
|
||||
import * as path from 'path'
|
||||
import * as patternHelper from './internal-pattern-helper'
|
||||
import {GlobOptions} from './internal-glob-options'
|
||||
import {MatchKind} from './internal-match-kind'
|
||||
import {Pattern} from './internal-pattern'
|
||||
import {SearchState} from './internal-search-state'
|
||||
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
export {GlobOptions}
|
||||
|
||||
/**
|
||||
* Used to match files and directories
|
||||
*/
|
||||
export interface Globber {
|
||||
/**
|
||||
* Returns the search path preceeding the first glob segment, from each pattern.
|
||||
* Duplicates and descendants of other paths are filtered out.
|
||||
*
|
||||
* Example 1: The patterns `/foo/*` and `/bar/*` returns `/foo` and `/bar`.
|
||||
*
|
||||
* Example 2: The patterns `/foo/*` and `/foo/bar/*` returns `/foo`.
|
||||
*/
|
||||
getSearchPaths(): string[]
|
||||
|
||||
/**
|
||||
* Returns files and directories matching the glob patterns.
|
||||
*
|
||||
* Order of the results is not guaranteed.
|
||||
*/
|
||||
glob(): Promise<string[]>
|
||||
|
||||
/**
|
||||
* Returns files and directories matching the glob patterns.
|
||||
*
|
||||
* Order of the results is not guaranteed.
|
||||
*/
|
||||
globGenerator(): AsyncGenerator<string, void>
|
||||
}
|
||||
|
||||
export class DefaultGlobber implements Globber {
|
||||
private readonly options: GlobOptions
|
||||
private readonly patterns: Pattern[] = []
|
||||
private readonly searchPaths: string[] = []
|
||||
|
||||
private constructor(options?: GlobOptions) {
|
||||
this.options = globOptionsHelper.getOptions(options)
|
||||
}
|
||||
|
||||
getSearchPaths(): string[] {
|
||||
// Return a copy
|
||||
return this.searchPaths.slice()
|
||||
}
|
||||
|
||||
async glob(): Promise<string[]> {
|
||||
const result: string[] = []
|
||||
for await (const itemPath of this.globGenerator()) {
|
||||
result.push(itemPath)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async *globGenerator(): AsyncGenerator<string, void> {
|
||||
// Fill in defaults options
|
||||
const options = globOptionsHelper.getOptions(this.options)
|
||||
|
||||
// Implicit descendants?
|
||||
const patterns: Pattern[] = []
|
||||
for (const pattern of this.patterns) {
|
||||
patterns.push(pattern)
|
||||
if (
|
||||
options.implicitDescendants &&
|
||||
(pattern.trailingSeparator ||
|
||||
pattern.segments[pattern.segments.length - 1] !== '**')
|
||||
) {
|
||||
patterns.push(
|
||||
new Pattern(pattern.negate, pattern.segments.concat('**'))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Push the search paths
|
||||
const stack: SearchState[] = []
|
||||
for (const searchPath of patternHelper.getSearchPaths(patterns)) {
|
||||
core.debug(`Search path '${searchPath}'`)
|
||||
|
||||
// Exists?
|
||||
try {
|
||||
// Intentionally using lstat. Detection for broken symlink
|
||||
// will be performed later (if following symlinks).
|
||||
await fs.promises.lstat(searchPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
continue
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
stack.unshift(new SearchState(searchPath, 1))
|
||||
}
|
||||
|
||||
// Search
|
||||
const traversalChain: string[] = [] // used to detect cycles
|
||||
while (stack.length) {
|
||||
// Pop
|
||||
const item = stack.pop() as SearchState
|
||||
|
||||
// Match?
|
||||
const match = patternHelper.match(patterns, item.path)
|
||||
const partialMatch =
|
||||
!!match || patternHelper.partialMatch(patterns, item.path)
|
||||
if (!match && !partialMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Stat
|
||||
const stats: fs.Stats | undefined = await DefaultGlobber.stat(
|
||||
item,
|
||||
options,
|
||||
traversalChain
|
||||
)
|
||||
|
||||
// Broken symlink, or symlink cycle detected, or no longer exists
|
||||
if (!stats) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Directory
|
||||
if (stats.isDirectory()) {
|
||||
// Matched
|
||||
if (match & MatchKind.Directory) {
|
||||
yield item.path
|
||||
}
|
||||
// Descend?
|
||||
else if (!partialMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Push the child items in reverse
|
||||
const childLevel = item.level + 1
|
||||
const childItems = (await fs.promises.readdir(item.path)).map(
|
||||
x => new SearchState(path.join(item.path, x), childLevel)
|
||||
)
|
||||
stack.push(...childItems.reverse())
|
||||
}
|
||||
// File
|
||||
else if (match & MatchKind.File) {
|
||||
yield item.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a DefaultGlobber
|
||||
*/
|
||||
static async create(
|
||||
patterns: string,
|
||||
options?: GlobOptions
|
||||
): Promise<DefaultGlobber> {
|
||||
const result = new DefaultGlobber(options)
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
patterns = patterns.replace(/\r\n/g, '\n')
|
||||
patterns = patterns.replace(/\r/g, '\n')
|
||||
}
|
||||
|
||||
const lines = patterns.split('\n').map(x => x.trim())
|
||||
for (const line of lines) {
|
||||
// Empty or comment
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue
|
||||
}
|
||||
// Pattern
|
||||
else {
|
||||
result.patterns.push(new Pattern(line))
|
||||
}
|
||||
}
|
||||
|
||||
result.searchPaths.push(...patternHelper.getSearchPaths(result.patterns))
|
||||
return result
|
||||
}
|
||||
|
||||
private static async stat(
|
||||
item: SearchState,
|
||||
options: GlobOptions,
|
||||
traversalChain: string[]
|
||||
): Promise<fs.Stats | undefined> {
|
||||
// Note:
|
||||
// `stat` returns info about the target of a symlink (or symlink chain)
|
||||
// `lstat` returns info about a symlink itself
|
||||
let stats: fs.Stats
|
||||
if (options.followSymbolicLinks) {
|
||||
try {
|
||||
// Use `stat` (following symlinks)
|
||||
stats = await fs.promises.stat(item.path)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (options.omitBrokenSymbolicLinks) {
|
||||
core.debug(`Broken symlink '${item.path}'`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No information found for the path '${item.path}'. This may indicate a broken symbolic link.`
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
} else {
|
||||
// Use `lstat` (not following symlinks)
|
||||
stats = await fs.promises.lstat(item.path)
|
||||
}
|
||||
|
||||
// Note, isDirectory() returns false for the lstat of a symlink
|
||||
if (stats.isDirectory() && options.followSymbolicLinks) {
|
||||
// Get the realpath
|
||||
const realPath: string = await fs.promises.realpath(item.path)
|
||||
|
||||
// Fixup the traversal chain to match the item level
|
||||
while (traversalChain.length >= item.level) {
|
||||
traversalChain.pop()
|
||||
}
|
||||
|
||||
// Test for a cycle
|
||||
if (traversalChain.some((x: string) => x === realPath)) {
|
||||
core.debug(
|
||||
`Symlink cycle detected for path '${item.path}' and realpath '${realPath}'`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Update the traversal chain
|
||||
traversalChain.push(realPath)
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user