Skip to content

Commit

Permalink
[Finder] Fix GitIgnore parser when dealing with (sub)directories and …
Browse files Browse the repository at this point in the history
…take order of lines into account
  • Loading branch information
Jeroeny authored and fabpot committed Jul 31, 2020
1 parent 2727aa3 commit e5fe073
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 32 deletions.
88 changes: 58 additions & 30 deletions Gitignore.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,56 @@ public static function toRegex(string $gitignoreFileContent): string
{
$gitignoreFileContent = preg_replace('/^[^\\\r\n]*#.*/m', '', $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);
});
$positives = [];
$negatives = [];
foreach ($gitignoreLines as $i => $line) {
$line = trim($line);
if ('' === $line) {
continue;
}

$ignoreLinesNegative = array_map(function (string $line) {
return preg_replace('/^!(.*)/', '${1}', $line);
}, $ignoreLinesNegative);
$ignoreLinesNegative = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesNegative);
if (1 === preg_match('/^!/', $line)) {
$positives[$i] = null;
$negatives[$i] = self::getRegexFromGitignore(preg_replace('/^!(.*)/', '${1}', $line), true);

$ignoreLinesPositive = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesPositive);
if (empty($ignoreLinesPositive)) {
return '/^$/';
continue;
}
$negatives[$i] = null;
$positives[$i] = self::getRegexFromGitignore($line);
}

if (empty($ignoreLinesNegative)) {
return sprintf('/%s/', implode('|', $ignoreLinesPositive));
$index = 0;
$patterns = [];
foreach ($positives as $pattern) {
if (null === $pattern) {
continue;
}

$negativesAfter = array_filter(\array_slice($negatives, ++$index));
if ($negativesAfter !== []) {
$pattern .= sprintf('(?<!%s)', implode('|', $negativesAfter));
}

$patterns[] = $pattern;
}

return sprintf('/(?=^(?:(?!(%s)).)*$)(%s)/', implode('|', $ignoreLinesNegative), implode('|', $ignoreLinesPositive));
return sprintf('/^((%s))$/', implode(')|(', $patterns));
}

private static function getRegexFromGitignore(string $gitignorePattern): string
private static function getRegexFromGitignore(string $gitignorePattern, bool $negative = false): string
{
$regex = '(';
if (0 === strpos($gitignorePattern, '/')) {
$gitignorePattern = substr($gitignorePattern, 1);
$regex = '';
$isRelativePath = false;
// If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to the directory level of the particular .gitignore file itself
$slashPosition = strpos($gitignorePattern, '/');
if (false !== $slashPosition && \strlen($gitignorePattern) - 1 !== $slashPosition) {
if (0 === $slashPosition) {
$gitignorePattern = substr($gitignorePattern, 1);
}

$isRelativePath = true;
$regex .= '^';
} else {
$regex .= '(^|\/)';
}

if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) {
Expand All @@ -71,17 +85,29 @@ private static function getRegexFromGitignore(string $gitignorePattern): string

$iMax = \strlen($gitignorePattern);
for ($i = 0; $i < $iMax; ++$i) {
$tripleChars = substr($gitignorePattern, $i, 3);
if ('**/' === $tripleChars || '/**' === $tripleChars) {
$regex .= '.*';
$i += 2;
continue;
}

$doubleChars = substr($gitignorePattern, $i, 2);
if ('**' === $doubleChars) {
$regex .= '.+';
$regex .= '.*';
++$i;
continue;
}
if ('*/' === $doubleChars) {
$regex .= '[^\/]*\/?[^\/]*';
++$i;
continue;
}

$c = $gitignorePattern[$i];
switch ($c) {
case '*':
$regex .= '[^\/]+';
$regex .= $isRelativePath ? '[^\/]*' : '[^\/]*\/?[^\/]*';
break;
case '/':
case '.':
Expand All @@ -97,9 +123,11 @@ private static function getRegexFromGitignore(string $gitignorePattern): string
}
}

$regex .= '($|\/)';
$regex .= ')';
if ($negative) {
// a lookbehind assertion has to be a fixed width (it can not have nested '|' statements)
return sprintf('%s$|%s\/$', $regex, $regex);
}

return $regex;
return '(?>'.$regex.'($|\/.*))';
}
}
14 changes: 12 additions & 2 deletions Tests/GitignoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function provider(): array
[
'
*
!/bin
!/bin/bash
',
['bin/cat', 'abc/bin/cat'],
Expand Down Expand Up @@ -99,8 +100,8 @@ public function provider(): array
],
[
'app/cache/',
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt', 'a/app/cache/file.txt'],
[],
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt'],
['a/app/cache/file.txt'],
],
[
'
Expand Down Expand Up @@ -133,6 +134,15 @@ public function provider(): array
['app/cache/file.txt', 'app/cache/subdir/ile.txt', '#file.txt', 'another_file.txt'],
['a/app/cache/file.txt', 'IamComment', '#IamComment'],
],
[
'
/app/**
!/app/bin
!/app/bin/test
',
['app/test/file', 'app/bin/file'],
['app/bin/test'],
],
];
}
}

0 comments on commit e5fe073

Please sign in to comment.