Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9491393

Browse files
Ahmed Abdoufabpot
Ahmed Abdou
authored andcommitted
[Finder] Ignore paths from .gitignore #26714
1 parent d2e9a70 commit 9491393

8 files changed

+297
-1
lines changed

src/Symfony/Component/Finder/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
4.3.0
5+
-----
6+
7+
* added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore
8+
49
4.2.0
510
-----
611

src/Symfony/Component/Finder/Finder.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Finder implements \IteratorAggregate, \Countable
3939
{
4040
const IGNORE_VCS_FILES = 1;
4141
const IGNORE_DOT_FILES = 2;
42+
const IGNORE_VCS_IGNORED_FILES = 4;
4243

4344
private $mode = 0;
4445
private $names = [];
@@ -373,6 +374,24 @@ public function ignoreVCS($ignoreVCS)
373374
return $this;
374375
}
375376

377+
/**
378+
* Forces Finder to obey .gitignore and ignore files based on rules listed there.
379+
*
380+
* This option is disabled by default.
381+
*
382+
* @return $this
383+
*/
384+
public function ignoreVCSIgnored(bool $ignoreVCSIgnored)
385+
{
386+
if ($ignoreVCSIgnored) {
387+
$this->ignore |= static::IGNORE_VCS_IGNORED_FILES;
388+
} else {
389+
$this->ignore &= ~static::IGNORE_VCS_IGNORED_FILES;
390+
}
391+
392+
return $this;
393+
}
394+
376395
/**
377396
* Adds VCS patterns.
378397
*
@@ -685,6 +704,14 @@ private function searchInDirectory(string $dir): \Iterator
685704
$notPaths[] = '#(^|/)\..+(/|$)#';
686705
}
687706

707+
if (static::IGNORE_VCS_IGNORED_FILES === (static::IGNORE_VCS_IGNORED_FILES & $this->ignore)) {
708+
$gitignoreFilePath = sprintf('%s/.gitignore', $dir);
709+
if (!is_readable($gitignoreFilePath)) {
710+
throw new \RuntimeException(sprintf('The "ignoreVCSIgnored" option cannot be used by the Finder as the "%s" file is not readable.', $gitignoreFilePath));
711+
}
712+
$notPaths = array_merge($notPaths, [Gitignore::toRegex(file_get_contents($gitignoreFilePath))]);
713+
}
714+
688715
$minDepth = 0;
689716
$maxDepth = PHP_INT_MAX;
690717

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Finder;
13+
14+
/**
15+
* Gitignore matches against text.
16+
*
17+
* @author Ahmed Abdou <[email protected]>
18+
*/
19+
class Gitignore
20+
{
21+
/**
22+
* Returns a regexp which is the equivalent of the gitignore pattern.
23+
*
24+
* @param string $gitignoreFileContent
25+
*
26+
* @return string The regexp
27+
*/
28+
public static function toRegex(string $gitignoreFileContent): string
29+
{
30+
$gitignoreFileContent = preg_replace('/^[^\\\\]*#.*/', '', $gitignoreFileContent);
31+
$gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent);
32+
$gitignoreLines = array_map('trim', $gitignoreLines);
33+
$gitignoreLines = array_filter($gitignoreLines);
34+
35+
$ignoreLinesPositive = array_filter($gitignoreLines, function (string $line) {
36+
return !preg_match('/^!/', $line);
37+
});
38+
39+
$ignoreLinesNegative = array_filter($gitignoreLines, function (string $line) {
40+
return preg_match('/^!/', $line);
41+
});
42+
43+
$ignoreLinesNegative = array_map(function (string $line) {
44+
return preg_replace('/^!(.*)/', '${1}', $line);
45+
}, $ignoreLinesNegative);
46+
$ignoreLinesNegative = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesNegative);
47+
48+
$ignoreLinesPositive = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesPositive);
49+
if (empty($ignoreLinesPositive)) {
50+
return '/^$/';
51+
}
52+
53+
if (empty($ignoreLinesNegative)) {
54+
return sprintf('/%s/', implode('|', $ignoreLinesPositive));
55+
}
56+
57+
return sprintf('/(?=^(?:(?!(%s)).)*$)(%s)/', implode('|', $ignoreLinesNegative), implode('|', $ignoreLinesPositive));
58+
}
59+
60+
private static function getRegexFromGitignore(string $gitignorePattern): string
61+
{
62+
$regex = '(';
63+
if (0 === strpos($gitignorePattern, '/')) {
64+
$gitignorePattern = substr($gitignorePattern, 1);
65+
$regex .= '^';
66+
} else {
67+
$regex .= '(^|\/)';
68+
}
69+
70+
if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) {
71+
$gitignorePattern = substr($gitignorePattern, 0, -1);
72+
}
73+
74+
$iMax = \strlen($gitignorePattern);
75+
for ($i = 0; $i < $iMax; ++$i) {
76+
$doubleChars = substr($gitignorePattern, $i, 2);
77+
if ('**' === $doubleChars) {
78+
$regex .= '.+';
79+
++$i;
80+
continue;
81+
}
82+
83+
$c = $gitignorePattern[$i];
84+
switch ($c) {
85+
case '*':
86+
$regex .= '[^\/]+';
87+
break;
88+
case '/':
89+
case '.':
90+
case ':':
91+
case '(':
92+
case ')':
93+
case '{':
94+
case '}':
95+
$regex .= '\\'.$c;
96+
break;
97+
default:
98+
$regex .= $c;
99+
}
100+
}
101+
102+
$regex .= '($|\/)';
103+
$regex .= ')';
104+
105+
return $regex;
106+
}
107+
}

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public function testIgnoreVCS()
347347
$finder = $this->buildFinder();
348348
$this->assertSame($finder, $finder->ignoreVCS(false)->ignoreDotFiles(false));
349349
$this->assertIterator($this->toAbsolute([
350+
'.gitignore',
350351
'.git',
351352
'foo',
352353
'foo/bar.tmp',
@@ -373,6 +374,7 @@ public function testIgnoreVCS()
373374
$finder = $this->buildFinder();
374375
$finder->ignoreVCS(false)->ignoreVCS(false)->ignoreDotFiles(false);
375376
$this->assertIterator($this->toAbsolute([
377+
'.gitignore',
376378
'.git',
377379
'foo',
378380
'foo/bar.tmp',
@@ -399,6 +401,7 @@ public function testIgnoreVCS()
399401
$finder = $this->buildFinder();
400402
$this->assertSame($finder, $finder->ignoreVCS(true)->ignoreDotFiles(false));
401403
$this->assertIterator($this->toAbsolute([
404+
'.gitignore',
402405
'foo',
403406
'foo/bar.tmp',
404407
'test.php',
@@ -421,13 +424,36 @@ public function testIgnoreVCS()
421424
]), $finder->in(self::$tmpDir)->getIterator());
422425
}
423426

427+
public function testIgnoreVCSIgnored()
428+
{
429+
$finder = $this->buildFinder();
430+
$this->assertSame(
431+
$finder,
432+
$finder
433+
->ignoreVCS(true)
434+
->ignoreDotFiles(true)
435+
->ignoreVCSIgnored(true)
436+
);
437+
$this->assertIterator($this->toAbsolute([
438+
'foo',
439+
'foo/bar.tmp',
440+
'test.py',
441+
'toto',
442+
'foo bar',
443+
'qux',
444+
'qux/baz_100_1.py',
445+
'qux/baz_1_2.py',
446+
]), $finder->in(self::$tmpDir)->getIterator());
447+
}
448+
424449
public function testIgnoreVCSCanBeDisabledAfterFirstIteration()
425450
{
426451
$finder = $this->buildFinder();
427452
$finder->in(self::$tmpDir);
428453
$finder->ignoreDotFiles(false);
429454

430455
$this->assertIterator($this->toAbsolute([
456+
'.gitignore',
431457
'foo',
432458
'foo/bar.tmp',
433459
'qux',
@@ -450,7 +476,9 @@ public function testIgnoreVCSCanBeDisabledAfterFirstIteration()
450476
]), $finder->getIterator());
451477

452478
$finder->ignoreVCS(false);
453-
$this->assertIterator($this->toAbsolute(['.git',
479+
$this->assertIterator($this->toAbsolute([
480+
'.gitignore',
481+
'.git',
454482
'foo',
455483
'foo/bar.tmp',
456484
'qux',
@@ -479,6 +507,7 @@ public function testIgnoreDotFiles()
479507
$finder = $this->buildFinder();
480508
$this->assertSame($finder, $finder->ignoreDotFiles(false)->ignoreVCS(false));
481509
$this->assertIterator($this->toAbsolute([
510+
'.gitignore',
482511
'.git',
483512
'.bar',
484513
'.foo',
@@ -505,6 +534,7 @@ public function testIgnoreDotFiles()
505534
$finder = $this->buildFinder();
506535
$finder->ignoreDotFiles(false)->ignoreDotFiles(false)->ignoreVCS(false);
507536
$this->assertIterator($this->toAbsolute([
537+
'.gitignore',
508538
'.git',
509539
'.bar',
510540
'.foo',
@@ -574,6 +604,7 @@ public function testIgnoreDotFilesCanBeDisabledAfterFirstIteration()
574604

575605
$finder->ignoreDotFiles(false);
576606
$this->assertIterator($this->toAbsolute([
607+
'.gitignore',
577608
'foo',
578609
'foo/bar.tmp',
579610
'qux',
@@ -842,6 +873,7 @@ public function testIn()
842873

843874
$expected = [
844875
self::$tmpDir.\DIRECTORY_SEPARATOR.'test.php',
876+
__DIR__.\DIRECTORY_SEPARATOR.'GitignoreTest.php',
845877
__DIR__.\DIRECTORY_SEPARATOR.'FinderTest.php',
846878
__DIR__.\DIRECTORY_SEPARATOR.'GlobTest.php',
847879
self::$tmpDir.\DIRECTORY_SEPARATOR.'qux_0_1.php',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
/*
3+
* This file is part of the Symfony package.
4+
*
5+
* (c) Fabien Potencier <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace Symfony\Component\Finder\Tests;
12+
13+
use PHPUnit\Framework\TestCase;
14+
use Symfony\Component\Finder\Gitignore;
15+
16+
class GitignoreTest extends TestCase
17+
{
18+
/**
19+
* @dataProvider provider
20+
*
21+
* @param string $patterns
22+
* @param array $matchingCases
23+
* @param array $nonMatchingCases
24+
*/
25+
public function testCases(string $patterns, array $matchingCases, array $nonMatchingCases)
26+
{
27+
$regex = Gitignore::toRegex($patterns);
28+
29+
foreach ($matchingCases as $matchingCase) {
30+
$this->assertRegExp($regex, $matchingCase, sprintf('Failed asserting path [%s] matches gitignore patterns [%s] using regex [%s]', $matchingCase, $patterns, $regex));
31+
}
32+
33+
foreach ($nonMatchingCases as $nonMatchingCase) {
34+
$this->assertNotRegExp($regex, $nonMatchingCase, sprintf('Failed asserting path [%s] not matching gitignore patterns [%s] using regex [%s]', $nonMatchingCase, $patterns, $regex));
35+
}
36+
}
37+
38+
/**
39+
* @return array return is array of
40+
* [
41+
* [
42+
* '', // Git-ignore Pattern
43+
* [], // array of file paths matching
44+
* [], // array of file paths not matching
45+
* ],
46+
* ]
47+
*/
48+
public function provider()
49+
{
50+
return [
51+
[
52+
'
53+
*
54+
!/bin/bash
55+
',
56+
['bin/cat', 'abc/bin/cat'],
57+
['bin/bash'],
58+
],
59+
[
60+
'fi#le.txt',
61+
[],
62+
['#file.txt'],
63+
],
64+
[
65+
'
66+
/bin/
67+
/usr/local/
68+
!/bin/bash
69+
!/usr/local/bin/bash
70+
',
71+
['bin/cat'],
72+
['bin/bash'],
73+
],
74+
[
75+
'*.py[co]',
76+
['file.pyc', 'file.pyc'],
77+
['filexpyc', 'file.pycx', 'file.py'],
78+
],
79+
[
80+
'dir1/**/dir2/',
81+
['dir1/dirA/dir2/', 'dir1/dirA/dirB/dir2/'],
82+
[],
83+
],
84+
[
85+
'dir1/*/dir2/',
86+
['dir1/dirA/dir2/'],
87+
['dir1/dirA/dirB/dir2/'],
88+
],
89+
[
90+
'/*.php',
91+
['file.php'],
92+
['app/file.php'],
93+
],
94+
[
95+
'\#file.txt',
96+
['#file.txt'],
97+
[],
98+
],
99+
[
100+
'*.php',
101+
['app/file.php', 'file.php'],
102+
['file.phps', 'file.phps', 'filephps'],
103+
],
104+
[
105+
'app/cache/',
106+
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt', 'a/app/cache/file.txt'],
107+
[],
108+
],
109+
[
110+
'
111+
#IamComment
112+
/app/cache/',
113+
['app/cache/file.txt', 'app/cache/subdir/ile.txt'],
114+
['a/app/cache/file.txt'],
115+
],
116+
];
117+
}
118+
}

0 commit comments

Comments
 (0)