[Finder] Ignore paths from .gitignore #26714
This commit is contained in:
parent
d2e9a7051f
commit
9491393dc2
@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
|
@ -39,6 +39,7 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
{
|
||||
const IGNORE_VCS_FILES = 1;
|
||||
const IGNORE_DOT_FILES = 2;
|
||||
const IGNORE_VCS_IGNORED_FILES = 4;
|
||||
|
||||
private $mode = 0;
|
||||
private $names = [];
|
||||
@ -373,6 +374,24 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces Finder to obey .gitignore and ignore files based on rules listed there.
|
||||
*
|
||||
* This option is disabled by default.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function ignoreVCSIgnored(bool $ignoreVCSIgnored)
|
||||
{
|
||||
if ($ignoreVCSIgnored) {
|
||||
$this->ignore |= static::IGNORE_VCS_IGNORED_FILES;
|
||||
} else {
|
||||
$this->ignore &= ~static::IGNORE_VCS_IGNORED_FILES;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds VCS patterns.
|
||||
*
|
||||
@ -685,6 +704,14 @@ class Finder implements \IteratorAggregate, \Countable
|
||||
$notPaths[] = '#(^|/)\..+(/|$)#';
|
||||
}
|
||||
|
||||
if (static::IGNORE_VCS_IGNORED_FILES === (static::IGNORE_VCS_IGNORED_FILES & $this->ignore)) {
|
||||
$gitignoreFilePath = sprintf('%s/.gitignore', $dir);
|
||||
if (!is_readable($gitignoreFilePath)) {
|
||||
throw new \RuntimeException(sprintf('The "ignoreVCSIgnored" option cannot be used by the Finder as the "%s" file is not readable.', $gitignoreFilePath));
|
||||
}
|
||||
$notPaths = array_merge($notPaths, [Gitignore::toRegex(file_get_contents($gitignoreFilePath))]);
|
||||
}
|
||||
|
||||
$minDepth = 0;
|
||||
$maxDepth = PHP_INT_MAX;
|
||||
|
||||
|
107
src/Symfony/Component/Finder/Gitignore.php
Normal file
107
src/Symfony/Component/Finder/Gitignore.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Finder;
|
||||
|
||||
/**
|
||||
* Gitignore matches against text.
|
||||
*
|
||||
* @author Ahmed Abdou <mail@ahmd.io>
|
||||
*/
|
||||
class Gitignore
|
||||
{
|
||||
/**
|
||||
* Returns a regexp which is the equivalent of the gitignore pattern.
|
||||
*
|
||||
* @param string $gitignoreFileContent
|
||||
*
|
||||
* @return string The regexp
|
||||
*/
|
||||
public static function toRegex(string $gitignoreFileContent): string
|
||||
{
|
||||
$gitignoreFileContent = preg_replace('/^[^\\\\]*#.*/', '', $gitignoreFileContent);
|
||||
$gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent);
|
||||
$gitignoreLines = array_map('trim', $gitignoreLines);
|
||||
$gitignoreLines = array_filter($gitignoreLines);
|
||||
|
||||
$ignoreLinesPositive = array_filter($gitignoreLines, function (string $line) {
|
||||
return !preg_match('/^!/', $line);
|
||||
});
|
||||
|
||||
$ignoreLinesNegative = array_filter($gitignoreLines, function (string $line) {
|
||||
return preg_match('/^!/', $line);
|
||||
});
|
||||
|
||||
$ignoreLinesNegative = array_map(function (string $line) {
|
||||
return preg_replace('/^!(.*)/', '${1}', $line);
|
||||
}, $ignoreLinesNegative);
|
||||
$ignoreLinesNegative = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesNegative);
|
||||
|
||||
$ignoreLinesPositive = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesPositive);
|
||||
if (empty($ignoreLinesPositive)) {
|
||||
return '/^$/';
|
||||
}
|
||||
|
||||
if (empty($ignoreLinesNegative)) {
|
||||
return sprintf('/%s/', implode('|', $ignoreLinesPositive));
|
||||
}
|
||||
|
||||
return sprintf('/(?=^(?:(?!(%s)).)*$)(%s)/', implode('|', $ignoreLinesNegative), implode('|', $ignoreLinesPositive));
|
||||
}
|
||||
|
||||
private static function getRegexFromGitignore(string $gitignorePattern): string
|
||||
{
|
||||
$regex = '(';
|
||||
if (0 === strpos($gitignorePattern, '/')) {
|
||||
$gitignorePattern = substr($gitignorePattern, 1);
|
||||
$regex .= '^';
|
||||
} else {
|
||||
$regex .= '(^|\/)';
|
||||
}
|
||||
|
||||
if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) {
|
||||
$gitignorePattern = substr($gitignorePattern, 0, -1);
|
||||
}
|
||||
|
||||
$iMax = \strlen($gitignorePattern);
|
||||
for ($i = 0; $i < $iMax; ++$i) {
|
||||
$doubleChars = substr($gitignorePattern, $i, 2);
|
||||
if ('**' === $doubleChars) {
|
||||
$regex .= '.+';
|
||||
++$i;
|
||||
continue;
|
||||
}
|
||||
|
||||
$c = $gitignorePattern[$i];
|
||||
switch ($c) {
|
||||
case '*':
|
||||
$regex .= '[^\/]+';
|
||||
break;
|
||||
case '/':
|
||||
case '.':
|
||||
case ':':
|
||||
case '(':
|
||||
case ')':
|
||||
case '{':
|
||||
case '}':
|
||||
$regex .= '\\'.$c;
|
||||
break;
|
||||
default:
|
||||
$regex .= $c;
|
||||
}
|
||||
}
|
||||
|
||||
$regex .= '($|\/)';
|
||||
$regex .= ')';
|
||||
|
||||
return $regex;
|
||||
}
|
||||
}
|
@ -347,6 +347,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
$finder = $this->buildFinder();
|
||||
$this->assertSame($finder, $finder->ignoreVCS(false)->ignoreDotFiles(false));
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
@ -373,6 +374,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
$finder = $this->buildFinder();
|
||||
$finder->ignoreVCS(false)->ignoreVCS(false)->ignoreDotFiles(false);
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
@ -399,6 +401,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
$finder = $this->buildFinder();
|
||||
$this->assertSame($finder, $finder->ignoreVCS(true)->ignoreDotFiles(false));
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
'test.php',
|
||||
@ -421,6 +424,28 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
]), $finder->in(self::$tmpDir)->getIterator());
|
||||
}
|
||||
|
||||
public function testIgnoreVCSIgnored()
|
||||
{
|
||||
$finder = $this->buildFinder();
|
||||
$this->assertSame(
|
||||
$finder,
|
||||
$finder
|
||||
->ignoreVCS(true)
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCSIgnored(true)
|
||||
);
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
'test.py',
|
||||
'toto',
|
||||
'foo bar',
|
||||
'qux',
|
||||
'qux/baz_100_1.py',
|
||||
'qux/baz_1_2.py',
|
||||
]), $finder->in(self::$tmpDir)->getIterator());
|
||||
}
|
||||
|
||||
public function testIgnoreVCSCanBeDisabledAfterFirstIteration()
|
||||
{
|
||||
$finder = $this->buildFinder();
|
||||
@ -428,6 +453,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
$finder->ignoreDotFiles(false);
|
||||
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
'qux',
|
||||
@ -450,7 +476,9 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
]), $finder->getIterator());
|
||||
|
||||
$finder->ignoreVCS(false);
|
||||
$this->assertIterator($this->toAbsolute(['.git',
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
'qux',
|
||||
@ -479,6 +507,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
$finder = $this->buildFinder();
|
||||
$this->assertSame($finder, $finder->ignoreDotFiles(false)->ignoreVCS(false));
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'.bar',
|
||||
'.foo',
|
||||
@ -505,6 +534,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
$finder = $this->buildFinder();
|
||||
$finder->ignoreDotFiles(false)->ignoreDotFiles(false)->ignoreVCS(false);
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'.bar',
|
||||
'.foo',
|
||||
@ -574,6 +604,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
|
||||
$finder->ignoreDotFiles(false);
|
||||
$this->assertIterator($this->toAbsolute([
|
||||
'.gitignore',
|
||||
'foo',
|
||||
'foo/bar.tmp',
|
||||
'qux',
|
||||
@ -842,6 +873,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
|
||||
|
||||
$expected = [
|
||||
self::$tmpDir.\DIRECTORY_SEPARATOR.'test.php',
|
||||
__DIR__.\DIRECTORY_SEPARATOR.'GitignoreTest.php',
|
||||
__DIR__.\DIRECTORY_SEPARATOR.'FinderTest.php',
|
||||
__DIR__.\DIRECTORY_SEPARATOR.'GlobTest.php',
|
||||
self::$tmpDir.\DIRECTORY_SEPARATOR.'qux_0_1.php',
|
||||
|
118
src/Symfony/Component/Finder/Tests/GitignoreTest.php
Normal file
118
src/Symfony/Component/Finder/Tests/GitignoreTest.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Finder\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Gitignore;
|
||||
|
||||
class GitignoreTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provider
|
||||
*
|
||||
* @param string $patterns
|
||||
* @param array $matchingCases
|
||||
* @param array $nonMatchingCases
|
||||
*/
|
||||
public function testCases(string $patterns, array $matchingCases, array $nonMatchingCases)
|
||||
{
|
||||
$regex = Gitignore::toRegex($patterns);
|
||||
|
||||
foreach ($matchingCases as $matchingCase) {
|
||||
$this->assertRegExp($regex, $matchingCase, sprintf('Failed asserting path [%s] matches gitignore patterns [%s] using regex [%s]', $matchingCase, $patterns, $regex));
|
||||
}
|
||||
|
||||
foreach ($nonMatchingCases as $nonMatchingCase) {
|
||||
$this->assertNotRegExp($regex, $nonMatchingCase, sprintf('Failed asserting path [%s] not matching gitignore patterns [%s] using regex [%s]', $nonMatchingCase, $patterns, $regex));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array return is array of
|
||||
* [
|
||||
* [
|
||||
* '', // Git-ignore Pattern
|
||||
* [], // array of file paths matching
|
||||
* [], // array of file paths not matching
|
||||
* ],
|
||||
* ]
|
||||
*/
|
||||
public function provider()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'
|
||||
*
|
||||
!/bin/bash
|
||||
',
|
||||
['bin/cat', 'abc/bin/cat'],
|
||||
['bin/bash'],
|
||||
],
|
||||
[
|
||||
'fi#le.txt',
|
||||
[],
|
||||
['#file.txt'],
|
||||
],
|
||||
[
|
||||
'
|
||||
/bin/
|
||||
/usr/local/
|
||||
!/bin/bash
|
||||
!/usr/local/bin/bash
|
||||
',
|
||||
['bin/cat'],
|
||||
['bin/bash'],
|
||||
],
|
||||
[
|
||||
'*.py[co]',
|
||||
['file.pyc', 'file.pyc'],
|
||||
['filexpyc', 'file.pycx', 'file.py'],
|
||||
],
|
||||
[
|
||||
'dir1/**/dir2/',
|
||||
['dir1/dirA/dir2/', 'dir1/dirA/dirB/dir2/'],
|
||||
[],
|
||||
],
|
||||
[
|
||||
'dir1/*/dir2/',
|
||||
['dir1/dirA/dir2/'],
|
||||
['dir1/dirA/dirB/dir2/'],
|
||||
],
|
||||
[
|
||||
'/*.php',
|
||||
['file.php'],
|
||||
['app/file.php'],
|
||||
],
|
||||
[
|
||||
'\#file.txt',
|
||||
['#file.txt'],
|
||||
[],
|
||||
],
|
||||
[
|
||||
'*.php',
|
||||
['app/file.php', 'file.php'],
|
||||
['file.phps', 'file.phps', 'filephps'],
|
||||
],
|
||||
[
|
||||
'app/cache/',
|
||||
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt', 'a/app/cache/file.txt'],
|
||||
[],
|
||||
],
|
||||
[
|
||||
'
|
||||
#IamComment
|
||||
/app/cache/',
|
||||
['app/cache/file.txt', 'app/cache/subdir/ile.txt'],
|
||||
['a/app/cache/file.txt'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ class DepthRangeFilterIteratorTest extends RealIteratorTestCase
|
||||
public function getAcceptData()
|
||||
{
|
||||
$lessThan1 = [
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'test.py',
|
||||
'foo',
|
||||
@ -51,6 +52,7 @@ class DepthRangeFilterIteratorTest extends RealIteratorTestCase
|
||||
];
|
||||
|
||||
$lessThanOrEqualTo1 = [
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'test.py',
|
||||
'foo',
|
||||
|
@ -31,6 +31,7 @@ class ExcludeDirectoryFilterIteratorTest extends RealIteratorTestCase
|
||||
public function getAcceptData()
|
||||
{
|
||||
$foo = [
|
||||
'.gitignore',
|
||||
'.bar',
|
||||
'.foo',
|
||||
'.foo/.bar',
|
||||
@ -53,6 +54,7 @@ class ExcludeDirectoryFilterIteratorTest extends RealIteratorTestCase
|
||||
];
|
||||
|
||||
$fo = [
|
||||
'.gitignore',
|
||||
'.bar',
|
||||
'.foo',
|
||||
'.foo/.bar',
|
||||
@ -77,6 +79,7 @@ class ExcludeDirectoryFilterIteratorTest extends RealIteratorTestCase
|
||||
];
|
||||
|
||||
$toto = [
|
||||
'.gitignore',
|
||||
'.bar',
|
||||
'.foo',
|
||||
'.foo/.bar',
|
||||
|
@ -63,6 +63,8 @@ abstract class RealIteratorTestCase extends IteratorTestCase
|
||||
file_put_contents(self::toAbsolute('test.php'), str_repeat(' ', 800));
|
||||
file_put_contents(self::toAbsolute('test.py'), str_repeat(' ', 2000));
|
||||
|
||||
file_put_contents(self::toAbsolute('.gitignore'), '*.php');
|
||||
|
||||
touch(self::toAbsolute('foo/bar.tmp'), strtotime('2005-10-15'));
|
||||
touch(self::toAbsolute('test.php'), strtotime('2005-10-15'));
|
||||
}
|
||||
|
Reference in New Issue
Block a user