8000 [Finder] Fix GitIgnore parser when dealing with (sub)directories and … · symfony/symfony@609dcf6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 609dcf6

Browse files
Jeroenyfabpot
authored andcommitted
[Finder] Fix GitIgnore parser when dealing with (sub)directories and take order of lines into account
1 parent 73cb33a commit 609dcf6

File tree

2 files changed

+70
-32
lines changed

2 files changed

+70
-32
lines changed

src/Symfony/Component/Finder/Gitignore.php

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,42 +27,56 @@ public static function toRegex(string $gitignoreFileContent): string
2727
{
2828
$gitignoreFileContent = preg_replace('/^[^\\\r\n]*#.*/m', '', $gitignoreFileContent);
2929
$gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent);
30-
$gitignoreLines = array_map('trim', $gitignoreLines);
31-
$gitignoreLines = array_filter($gitignoreLines);
3230

33-
$ignoreLinesPositive = array_filter($gitignoreLines, function (string $line) {
34-
return !preg_match('/^!/', $line);
35-
});
36-
37-
$ignoreLinesNegative = array_filter($gitignoreLines, function (string $line) {
38-
return preg_match('/^!/', $line);
39-
});
31+
$positives = [];
32+
$negatives = [];
33+
foreach ($gitignoreLines as $i => $line) {
34+
$line = trim($line);
35+
if ('' === $line) {
36+
continue;
37+
}
4038

41-
$ignoreLinesNegative = array_map(function (string $line) {
42-
return preg_replace('/^!(.*)/', '${1}', $line);
43-
}, $ignoreLinesNegative);
44-
$ignoreLinesNegative = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesNegative);
39+
if (1 === preg_match('/^!/', $line)) {
40+
$positives[$i] = null;
41+
$negatives[$i] = self::getRegexFromGitignore(preg_replace('/^!(.*)/', '${1}', $line), true);
4542

46-
$ignoreLinesPositive = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesPositive);
47-
if (empty($ignoreLinesPositive)) {
48-
return '/^$/';
43+
continue;
44+
}
45+
$negatives[$i] = null;
46+
$positives[$i] = self::getRegexFromGitignore($line);
4947
}
5048

51-
if (empty($ignoreLinesNegative)) {
52-
return sprintf('/%s/', implode('|', $ignoreLinesPositive));
49+
$index = 0;
50+
$patterns = [];
51+
foreach ($positives as $pattern) {
52+
if (null === $pattern) {
53+
continue;
54+
}
55+
56+
$negativesAfter = array_filter(\array_slice($negatives, ++$index));
57+
if ($negativesAfter !== []) {
58+
$pattern .= sprintf('(?<!%s)', implode('|', $negativesAfter));
59+
}
60+
61+
$patterns[] = $pattern;
5362
}
5463

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

58-
private static function getRegexFromGitignore(string $gitignorePattern): string
67+
private static function getRegexFromGitignore(string $gitignorePattern, bool $negative = false): string
5968
{
60-
$regex = '(';
61-
if (0 === strpos($gitignorePattern, '/')) {
62-
$gitignorePattern = substr($gitignorePattern, 1);
69+
$regex = '';
70+
$isRelativePath = false;
71+
// 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
72+
$slashPosition = strpos($gitignorePattern, '/');
73+
if (false !== $slashPosition && \strlen($gitignorePattern) - 1 !== $slashPosition) {
74+
if (0 === $slashPosition) {
75+
$gitignorePattern = substr($gitignorePattern, 1);
76+
}
77+
78+
$isRelativePath = true;
6379
$regex .= '^';
64-
} else {
65-
$regex .= '(^|\/)';
6680
}
6781

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

7286
$iMax = \strlen($gitignorePattern);
7387
for ($i = 0; $i < $iMax; ++$i) {
88+
$tripleChars = substr($gitignorePattern, $i, 3);
89+
if ('**/' === $tripleChars || '/**' === $tripleChars) {
90+
$regex .= '.*';
91+
$i += 2;
92+
continue;
93+
}
94+
7495
$doubleChars = substr($gitignorePattern, $i, 2);
7596
if ('**' === $doubleChars) {
76-
$regex .= '.+';
97+
$regex .= '.*';
98+
++$i;
99+
continue;
100+
}
101+
if ('*/' === $doubleChars) {
102+
$regex .= '[^\/]*\/?[^\/]*';
77103
++$i;
78104
continue;
79105
}
80106

81107
$c = $gitignorePattern[$i];
82108
switch ($c) {
83109
case '*':
84-
$regex .= '[^\/]+';
110+
$regex .= $isRelativePath ? '[^\/]*' : '[^\/]*\/?[^\/]*';
85111
break;
86112
case '/':
87113
case '.':
@@ -97,9 +123,11 @@ private static function getRegexFromGitignore(string $gitignorePattern): string
97123
}
98124
}
99125

100-
$regex .= '($|\/)';
101-
$regex .= ')';
126+
if ($negative) {
127+
// a lookbehind assertion has to be a fixed width (it can not have nested '|' statements)
128+
return sprintf('%s$|%s\/$', $regex, $regex);
129+
}
102130

103-
return $regex;
131+
return '(?>'.$regex.'($|\/.*))';
104132
}
105133
}
< 10000 /td>

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function provider(): array
4747
[
4848
'
4949
*
50+
!/bin
5051
!/bin/bash
5152
',
5253
['bin/cat', 'abc/bin/cat'],
@@ -99,8 +100,8 @@ public function provider(): array
99100
],
100101
[
101102
'app/cache/',
102-
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt', 'a/app/cache/file.txt'],
103-
[],
103+
['app/cache/file.txt', 'app/cache/dir1/dir2/file.txt'],
104+
['a/app/cache/file.txt'],
104105
],
105106
[
106107
'
@@ -133,6 +134,15 @@ public function provider(): array
133134
['app/cache/file.txt', 'app/cache/subdir/ile.txt', '#file.txt', 'another_file.txt'],
134135
['a/app/cache/file.txt', 'IamComment', '#IamComment'],
135136
],
137+
[
138+
'
139+
/app/**
140+
!/app/bin
141+
!/app/bin/test
142+
',
143+
['app/test/file', 'app/bin/file'],
144+
['app/bin/test'],
145+
],
136146
];
137147
}
138148
}

0 commit comments

Comments
 (0)
0