1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Finder;
13
14/**
15 * Gitignore matches against text.
16 *
17 * @author Ahmed Abdou <mail@ahmd.io>
18 */
19class Gitignore
20{
21    /**
22     * Returns a regexp which is the equivalent of the gitignore pattern.
23     *
24     * @return string The regexp
25     */
26    public static function toRegex(string $gitignoreFileContent): string
27    {
28        $gitignoreFileContent = preg_replace('/^[^\\\r\n]*#.*/m', '', $gitignoreFileContent);
29        $gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent);
30        $gitignoreLines = array_map('trim', $gitignoreLines);
31        $gitignoreLines = array_filter($gitignoreLines);
32
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        });
40
41        $ignoreLinesNegative = array_map(function (string $line) {
42            return preg_replace('/^!(.*)/', '${1}', $line);
43        }, $ignoreLinesNegative);
44        $ignoreLinesNegative = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesNegative);
45
46        $ignoreLinesPositive = array_map([__CLASS__, 'getRegexFromGitignore'], $ignoreLinesPositive);
47        if (empty($ignoreLinesPositive)) {
48            return '/^$/';
49        }
50
51        if (empty($ignoreLinesNegative)) {
52            return sprintf('/%s/', implode('|', $ignoreLinesPositive));
53        }
54
55        return sprintf('/(?=^(?:(?!(%s)).)*$)(%s)/', implode('|', $ignoreLinesNegative), implode('|', $ignoreLinesPositive));
56    }
57
58    private static function getRegexFromGitignore(string $gitignorePattern): string
59    {
60        $regex = '(';
61        if (0 === strpos($gitignorePattern, '/')) {
62            $gitignorePattern = substr($gitignorePattern, 1);
63            $regex .= '^';
64        } else {
65            $regex .= '(^|\/)';
66        }
67
68        if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) {
69            $gitignorePattern = substr($gitignorePattern, 0, -1);
70        }
71
72        $iMax = \strlen($gitignorePattern);
73        for ($i = 0; $i < $iMax; ++$i) {
74            $doubleChars = substr($gitignorePattern, $i, 2);
75            if ('**' === $doubleChars) {
76                $regex .= '.+';
77                ++$i;
78                continue;
79            }
80
81            $c = $gitignorePattern[$i];
82            switch ($c) {
83                case '*':
84                    $regex .= '[^\/]+';
85                    break;
86                case '/':
87                case '.':
88                case ':':
89                case '(':
90                case ')':
91                case '{':
92                case '}':
93                    $regex .= '\\'.$c;
94                    break;
95                default:
96                    $regex .= $c;
97            }
98        }
99
100        $regex .= '($|\/)';
101        $regex .= ')';
102
103        return $regex;
104    }
105}
106