[Finder] Ignore paths from .gitignore #26714

This commit is contained in:
Ahmed Abdou 2019-03-05 01:59:42 +01:00 committed by Fabien Potencier
parent d2e9a7051f
commit 9491393dc2
8 changed files with 297 additions and 1 deletions

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
4.3.0
-----
* added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore
4.2.0
-----

View File

@ -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;

View 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;
}
}

View File

@ -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',

View 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'],
],
];
}
}

View File

@ -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',

View File

@ -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',

View File

@ -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'));
}