Gitignore.php 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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 Ahmed Abdou <mail@ahmd.io>
  15. */
  16. class Gitignore
  17. {
  18. /**
  19. * Returns a regexp which is the equivalent of the gitignore pattern.
  20. *
  21. * @return string The regexp
  22. */
  23. public static function toRegex(string $gitignoreFileContent): string
  24. {
  25. $gitignoreFileContent = preg_replace('/^[^\\\r\n]*#.*/m', '', $gitignoreFileContent);
  26. $gitignoreLines = preg_split('/\r\n|\r|\n/', $gitignoreFileContent);
  27. $positives = [];
  28. $negatives = [];
  29. foreach ($gitignoreLines as $i => $line) {
  30. $line = trim($line);
  31. if ('' === $line) {
  32. continue;
  33. }
  34. if (1 === preg_match('/^!/', $line)) {
  35. $positives[$i] = null;
  36. $negatives[$i] = self::getRegexFromGitignore(preg_replace('/^!(.*)/', '${1}', $line), true);
  37. continue;
  38. }
  39. $negatives[$i] = null;
  40. $positives[$i] = self::getRegexFromGitignore($line);
  41. }
  42. $index = 0;
  43. $patterns = [];
  44. foreach ($positives as $pattern) {
  45. if (null === $pattern) {
  46. continue;
  47. }
  48. $negativesAfter = array_filter(\array_slice($negatives, ++$index));
  49. if ([] !== $negativesAfter) {
  50. $pattern .= sprintf('(?<!%s)', implode('|', $negativesAfter));
  51. }
  52. $patterns[] = $pattern;
  53. }
  54. return sprintf('/^((%s))$/', implode(')|(', $patterns));
  55. }
  56. private static function getRegexFromGitignore(string $gitignorePattern, bool $negative = false): string
  57. {
  58. $regex = '';
  59. $isRelativePath = false;
  60. // 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
  61. $slashPosition = strpos($gitignorePattern, '/');
  62. if (false !== $slashPosition && \strlen($gitignorePattern) - 1 !== $slashPosition) {
  63. if (0 === $slashPosition) {
  64. $gitignorePattern = substr($gitignorePattern, 1);
  65. }
  66. $isRelativePath = true;
  67. $regex .= '^';
  68. }
  69. if ('/' === $gitignorePattern[\strlen($gitignorePattern) - 1]) {
  70. $gitignorePattern = substr($gitignorePattern, 0, -1);
  71. }
  72. $iMax = \strlen($gitignorePattern);
  73. for ($i = 0; $i < $iMax; ++$i) {
  74. $tripleChars = substr($gitignorePattern, $i, 3);
  75. if ('**/' === $tripleChars || '/**' === $tripleChars) {
  76. $regex .= '.*';
  77. $i += 2;
  78. continue;
  79. }
  80. $doubleChars = substr($gitignorePattern, $i, 2);
  81. if ('**' === $doubleChars) {
  82. $regex .= '.*';
  83. ++$i;
  84. continue;
  85. }
  86. if ('*/' === $doubleChars) {
  87. $regex .= '[^\/]*\/?[^\/]*';
  88. ++$i;
  89. continue;
  90. }
  91. $c = $gitignorePattern[$i];
  92. switch ($c) {
  93. case '*':
  94. $regex .= $isRelativePath ? '[^\/]*' : '[^\/]*\/?[^\/]*';
  95. break;
  96. case '/':
  97. case '.':
  98. case ':':
  99. case '(':
  100. case ')':
  101. case '{':
  102. case '}':
  103. $regex .= '\\'.$c;
  104. break;
  105. default:
  106. $regex .= $c;
  107. }
  108. }
  109. if ($negative) {
  110. // a lookbehind assertion has to be a fixed width (it can not have nested '|' statements)
  111. return sprintf('%s$|%s\/$', $regex, $regex);
  112. }
  113. return '(?>'.$regex.'($|\/.*))';
  114. }
  115. }