Gitignore.php 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Finder;
  11. /**
  12. * Gitignore matches against text.
  13. *
  14. * @author Michael Voříšek <vorismi3@fel.cvut.cz>
  15. * @author Ahmed Abdou <mail@ahmd.io>
  16. */
  17. class Gitignore
  18. {
  19. /**
  20. * Returns a regexp which is the equivalent of the gitignore pattern.
  21. *
  22. * Format specification: https://git-scm.com/docs/gitignore#_pattern_format
  23. */
  24. public static function toRegex(string $gitignoreFileContent): string
  25. {
  26. return self::buildRegex($gitignoreFileContent, false);
  27. }
  28. public static function toRegexMatchingNegatedPatterns(string $gitignoreFileContent): string
  29. {
  30. return self::buildRegex($gitignoreFileContent, true);
  31. }
  32. private static function buildRegex(string $gitignoreFileContent, bool $inverted): string
  33. {
  34. $gitignoreFileContent = preg_replace('~(?<!\\\\)#[^\n\r]*~', '', $gitignoreFileContent);
  35. $gitignoreLines = preg_split('~\r\n?|\n~', $gitignoreFileContent);
  36. $res = self::lineToRegex('');
  37. foreach ($gitignoreLines as $line) {
  38. $line = preg_replace('~(?<!\\\\)[ \t]+$~', '', $line);
  39. if (str_starts_with($line, '!')) {
  40. $line = substr($line, 1);
  41. $isNegative = true;
  42. } else {
  43. $isNegative = false;
  44. }
  45. if ('' !== $line) {
  46. if ($isNegative xor $inverted) {
  47. $res = '(?!'.self::lineToRegex($line).'$)'.$res;
  48. } else {
  49. $res = '(?:'.$res.'|'.self::lineToRegex($line).')';
  50. }
  51. }
  52. }
  53. return '~^(?:'.$res.')~s';
  54. }
  55. private static function lineToRegex(string $gitignoreLine): string
  56. {
  57. if ('' === $gitignoreLine) {
  58. return '$f'; // always false
  59. }
  60. $slashPos = strpos($gitignoreLine, '/');
  61. if (false !== $slashPos && \strlen($gitignoreLine) - 1 !== $slashPos) {
  62. if (0 === $slashPos) {
  63. $gitignoreLine = substr($gitignoreLine, 1);
  64. }
  65. $isAbsolute = true;
  66. } else {
  67. $isAbsolute = false;
  68. }
  69. $regex = preg_quote(str_replace('\\', '', $gitignoreLine), '~');
  70. $regex = preg_replace_callback('~\\\\\[((?:\\\\!)?)([^\[\]]*)\\\\\]~', fn (array $matches): string => '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', $regex);
  71. $regex = preg_replace('~(?:(?:\\\\\*){2,}(/?))+~', '(?:(?:(?!//).(?<!//))+$1)?', $regex);
  72. $regex = preg_replace('~\\\\\*~', '[^/]*', $regex);
  73. $regex = preg_replace('~\\\\\?~', '[^/]', $regex);
  74. return ($isAbsolute ? '' : '(?:[^/]+/)*')
  75. .$regex
  76. .(!str_ends_with($gitignoreLine, '/') ? '(?:$|/)' : '');
  77. }
  78. }