8000 [Finder] Add .gitignore nested negated patterns support · symfony/symfony@559639b · GitHub
[go: up one dir, main page]

Skip to content

Commit 559639b

Browse files
committed
[Finder] Add .gitignore nested negated patterns support
1 parent 7a8f773 commit 559639b

File tree

4 files changed

+532
-12
lines changed

4 files changed

+532
-12
lines changed

src/Symfony/Component/Finder/Gitignore.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@ class Gitignore
2525
* Format specification: https://git-scm.com/docs/gitignore#_pattern_format
2626
*/
2727
public static function toRegex(string $gitignoreFileContent): string
28+
{
29+
return self::buildRegex($gitignoreFileContent, false);
30+
}
31+
32+
public static function toRegexMatchingNegatedPatterns(string $gitignoreFileContent): string
33+
{
34+
return self::buildRegex($gitignoreFileContent, true);
35+
}
36+
37+
private static function buildRegex(string $gitignoreFileContent, bool $inverted): string
2838
{
2939
$gitignoreFileContent = preg_replace('~(?<!\\\\)#[^\n\r]*~', '', $gitignoreFileContent);
3040
$gitignoreLines = preg_split('~\r\n?|\n~', $gitignoreFileContent);
3141

3242
$res = self::lineToRegex('');
33-
foreach ($gitignoreLines as $i => $line) {
43+
foreach ($gitignoreLines as $line) {
3444
$line = preg_replace('~(?<!\\\\)[ \t]+$~', '', $line);
3545

3646
if ('!' === substr($line, 0, 1)) {
@@ -41,7 +51,7 @@ public static function toRegex(string $gitignoreFileContent): string
4151
}
4252

4353
if ('' !== $line) {
44-
if ($isNegative) {
54+
if ($isNegative xor $inverted) {
4555
$res = '(?!'.self::lineToRegex($line).'$)'.$res;
4656
} else {
4757
$res = '(?:'.$res.'|'.self::lineToRegex($line).')';

src/Symfony/Component/Finder/Iterator/VcsIgnoredFilterIterator.php

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
2121
private $baseDir;
2222

2323
/**
24-
* @var array<string, string|null>
24+
* @var array<string, array{0: string, 1: string}|null>
2525
*/
2626
private $gitignoreFilesCache = [];
2727

28+
/**
29+
* @var array<string, bool>
30+
*/
31+
private $ignoredPathsCache = [];
32+
2833
public function __construct(\Iterator $iterator, string $baseDir)
2934
{
3035
$this->baseDir = $this->normalizePath($baseDir);
@@ -37,25 +42,50 @@ public function accept(): bool
3742
$file = $this->current();
3843

3944
$fileRealPath = $this->normalizePath($file->getRealPath());
40-
if ($file->isDir() && !str_ends_with($fileRealPath, '/')) {
45+
46+
return !$this->isIgnored($fileRealPath);
47+
}
48+
49+
private function isIgnored(string $fileRealPath): bool
50+
{
51+
if (is_dir($fileRealPath) && !str_ends_with($fileRealPath, '/')) {
4152
$fileRealPath .= '/';
4253
}
4354

55+
if (isset($this->ignoredPathsCache[$fileRealPath])) {
56+
return $this->ignoredPathsCache[$fileRealPath];
57+
}
58+
59+
$ignored = false;
60+
4461
foreach ($this->parentsDirectoryDownward($fileRealPath) as $parentDirectory) {
62+
if ($this->isIgnored($parentDirectory)) {
63+
$ignored = true;
64+
65+
// rules in ignored directories are ignored, no need to check further.
66+
break;
67+
}
68+
4569
$fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1);
4670

47-
$regex = $this->readGitignoreFile("{$parentDirectory}/.gitignore");
71+
if (null === $regexps = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) {
72+
continue;
73+
}
74+
75+
[$exclusionRegex, $inclusionRegex] = $regexps;
76+
77+
if (preg_match($exclusionRegex, $fileRelativePath)) {
78+
$ignored = true;
4879

49-
if (null !== $regex && preg_match($regex, $fileRelativePath)) {
50-
return false;
80+
continue;
5181
}
5282

53-
if (0 !== strpos($parentDirectory, $this->baseDir)) {
54-
break;
83+
if (preg_match($inclusionRegex, $fileRelativePath)) {
84+
$ignored = false;
5585
}
5686
}
5787

58-
return true;
88+
return $this->ignoredPathsCache[$fileRealPath] = $ignored;
5989
}
6090

6191
/**
@@ -87,7 +117,10 @@ private function parentsDirectoryDownward(string $fileRealPath): array
87117
return array_reverse($parentDirectories);
88118
}
89119

90-
private function readGitignoreFile(string $path): ?string
120+
/**
121+
* @return array{0: string, 1: string}|null
122+
*/
123+
private function readGitignoreFile(string $path): ?array
91124
{
92125
if (\array_key_exists($path, $this->gitignoreFilesCache)) {
93126
return $this->gitignoreFilesCache[$path];
@@ -101,7 +134,12 @@ private function readGitignoreFile(string $path): ?string
101134
throw new \RuntimeException("The \"ignoreVCSIgnored\" option cannot be used by the Finder as the \"{$path}\" file is not readable.");
102135
}
103136

104-
return $this->gitignoreFilesCache[$path] = Gitignore::toRegex(file_get_contents($path));
137+
$gitignoreFileContent = file_get_contents($path);
138+
139+
return $this->gitignoreFilesCache[$path] = [
140+
Gitignore::toRegex($gitignoreFileContent),
141+
Gitignore::toRegexMatchingNegatedPatterns($gitignoreFileContent),
142+
];
105143
}
106144

107145
private function normalizePath(string $path): string

0 commit comments

Comments
 (0)
0