Skip to content

Commit 29f42aa

Browse files
bug #43704 [Finder] Fix .gitignore infinite loop (julienfalque)
This PR was merged into the 5.4 branch. Discussion ---------- [Finder] Fix .gitignore infinite loop | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | #43239 (comment) | License | MIT | Doc PR | - Extracted from #43239. Commits ------- dcdc67f [Finder] Fix .gitignore infinite loop
2 parents 0e24210 + dcdc67f commit 29f42aa

File tree

8 files changed

+71
-18
lines changed

8 files changed

+71
-18
lines changed

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

+50-11
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class VcsIgnoredFilterIterator extends \FilterIterator
2727

2828
public function __construct(\Iterator $iterator, string $baseDir)
2929
{
30-
$this->baseDir = $baseDir;
30+
$this->baseDir = $this->normalizePath($baseDir);
3131

3232
parent::__construct($iterator);
3333
}
@@ -36,35 +36,65 @@ public function accept(): bool
3636
{
3737
$file = $this->current();
3838

39-
$fileRealPath = $file->getRealPath();
40-
if ($file->isDir()) {
39+
$fileRealPath = $this->normalizePath($file->getRealPath());
40+
if ($file->isDir() && !str_ends_with($fileRealPath, '/')) {
4141
$fileRealPath .= '/';
4242
}
4343

44-
$parentDirectory = $fileRealPath;
45-
46-
do {
47-
$parentDirectory = \dirname($parentDirectory);
48-
$relativeFilePath = substr($fileRealPath, \strlen($parentDirectory) + 1);
44+
foreach ($this->parentsDirectoryDownward($fileRealPath) as $parentDirectory) {
45+
$fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1);
4946

5047
$regex = $this->readGitignoreFile("{$parentDirectory}/.gitignore");
5148

52-
if (null !== $regex && preg_match($regex, $relativeFilePath)) {
49+
if (null !== $regex && preg_match($regex, $fileRelativePath)) {
5350
return false;
5451
}
55-
} while ($parentDirectory !== $this->baseDir);
52+
53+
if (0 !== strpos($parentDirectory, $this->baseDir)) {
54+
break;
55+
}
56+
}
5657

5758
return true;
5859
}
5960

61+
/**
62+
* @return list<string>
63+
*/
64+
private function parentsDirectoryDownward(string $fileRealPath): array
65+
{
66+
$parentDirectories = [];
67+
68+
$parentDirectory = $fileRealPath;
69+
70+
while (true) {
71+
$newParentDirectory = \dirname($parentDirectory);
72+
73+
// dirname('/') = '/'
74+
if ($newParentDirectory === $parentDirectory) {
75+
break;
76+
}
77+
78+
$parentDirectory = $newParentDirectory;
79+
80+
if (0 !== strpos($parentDirectory, $this->baseDir)) {
81+
break;
82+
}
83+
84+
$parentDirectories[] = $parentDirectory;
85+
}
86+
87+
return array_reverse($parentDirectories);
88+
}
89+
6090
private function readGitignoreFile(string $path): ?string
6191
{
6292
if (\array_key_exists($path, $this->gitignoreFilesCache)) {
6393
return $this->gitignoreFilesCache[$path];
6494
}
6595

6696
if (!file_exists($path)) {
67-
return null;
97+
return $this->gitignoreFilesCache[$path] = null;
6898
}
6999

70100
if (!is_file($path) || !is_readable($path)) {
@@ -73,4 +103,13 @@ private function readGitignoreFile(string $path): ?string
73103

74104
return $this->gitignoreFilesCache[$path] = Gitignore::toRegex(file_get_contents($path));
75105
}
106+
107+
private function normalizePath(string $path): string
108+
{
109+
if ('\\' === \DIRECTORY_SEPARATOR) {
110+
return str_replace('\\', '/', $path);
111+
}
112+
113+
return $path;
114+
}
76115
}

src/Symfony/Component/Finder/Tests/FinderTest.php

+10-6
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,18 @@ public function testIgnoreVCSIgnored()
433433
->ignoreVCSIgnored(true)
434434
);
435435

436-
copy(__DIR__.'/Fixtures/gitignore/b.txt', __DIR__.'/Fixtures/gitignore/a.txt');
437-
copy(__DIR__.'/Fixtures/gitignore/dir/a.txt', __DIR__.'/Fixtures/gitignore/dir/b.txt');
436+
copy(__DIR__.'/Fixtures/gitignore/search_root/b.txt', __DIR__.'/Fixtures/gitignore/search_root/a.txt');
437+
copy(__DIR__.'/Fixtures/gitignore/search_root/b.txt', __DIR__.'/Fixtures/gitignore/search_root/c.txt');
438+
copy(__DIR__.'/Fixtures/gitignore/search_root/dir/a.txt', __DIR__.'/Fixtures/gitignore/search_root/dir/b.txt');
439+
copy(__DIR__.'/Fixtures/gitignore/search_root/dir/a.txt', __DIR__.'/Fixtures/gitignore/search_root/dir/c.txt');
438440

439441
$this->assertIterator($this->toAbsoluteFixtures([
440-
'gitignore/b.txt',
441-
'gitignore/dir',
442-
'gitignore/dir/a.txt',
443-
]), $finder->in(__DIR__.'/Fixtures/gitignore')->getIterator());
442+
'gitignore/search_root/b.txt',
443+
'gitignore/search_root/c.txt',
444+
'gitignore/search_root/dir',
445+
'gitignore/search_root/dir/a.txt',
446+
'gitignore/search_root/dir/c.txt',
447+
]), $finder->in(__DIR__.'/Fixtures/gitignore/search_root')->getIterator());
444448
}
445449

446450
public function testIgnoreVCSCanBeDisabledAfterFirstIteration()
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/a.txt
1+
c.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/a.txt

src/Symfony/Component/Finder/Tests/Iterator/VcsIgnoredFilterIteratorTest.php

+9
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@ public function getAcceptData(): iterable
202202
];
203203
}
204204

205+
public function testAcceptAtRootDirectory()
206+
{
207+
$inner = new InnerNameIterator([__FILE__]);
208+
209+
$iterator = new VcsIgnoredFilterIterator($inner, '/');
210+
211+
$this->assertIterator([__FILE__], $iterator);
212+
}
213+
205214
private function toAbsolute(array $files): array
206215
{
207216
foreach ($files as &$path) {

0 commit comments

Comments
 (0)