@@ -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}
0 commit comments