Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 559639b

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

File tree

4 files changed

+532
-12
lines changed
Filter options

4 files changed

+532
-12
lines changed

‎src/Symfony/Component/Finder/Gitignore.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Finder/Gitignore.php
+12-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Finder/Iterator/VcsIgnoredFilterIterator.php
+48-10Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.