VersionGuesser.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Package\Version;
  12. use Composer\Config;
  13. use Composer\Repository\Vcs\HgDriver;
  14. use Composer\IO\NullIO;
  15. use Composer\Semver\VersionParser as SemverVersionParser;
  16. use Composer\Util\Git as GitUtil;
  17. use Composer\Util\ProcessExecutor;
  18. use Composer\Util\Svn as SvnUtil;
  19. /**
  20. * Try to guess the current version number based on different VCS configuration.
  21. *
  22. * @author Jordi Boggiano <j.boggiano@seld.be>
  23. * @author Samuel Roze <samuel.roze@gmail.com>
  24. */
  25. class VersionGuesser
  26. {
  27. /**
  28. * @var Config
  29. */
  30. private $config;
  31. /**
  32. * @var ProcessExecutor
  33. */
  34. private $process;
  35. /**
  36. * @var SemverVersionParser
  37. */
  38. private $versionParser;
  39. /**
  40. * @param Config $config
  41. * @param ProcessExecutor $process
  42. * @param VersionParser $versionParser
  43. */
  44. public function __construct(Config $config, ProcessExecutor $process, SemverVersionParser $versionParser)
  45. {
  46. $this->config = $config;
  47. $this->process = $process;
  48. $this->versionParser = $versionParser;
  49. }
  50. /**
  51. * @param array $packageConfig
  52. * @param string $path Path to guess into
  53. *
  54. * @return array versionData, 'version' and 'commit' keys
  55. */
  56. public function guessVersion(array $packageConfig, $path)
  57. {
  58. if (function_exists('proc_open')) {
  59. $versionData = $this->guessGitVersion($packageConfig, $path);
  60. if (null !== $versionData) {
  61. return $versionData;
  62. }
  63. $versionData = $this->guessHgVersion($packageConfig, $path);
  64. if (null !== $versionData) {
  65. return $versionData;
  66. }
  67. return $this->guessSvnVersion($packageConfig, $path);
  68. }
  69. }
  70. private function guessGitVersion(array $packageConfig, $path)
  71. {
  72. GitUtil::cleanEnv();
  73. $version = null;
  74. $commit = null;
  75. $version = $this->versionFromGitTags($path);
  76. // try to fetch current version from git branch
  77. if (0 === $this->process->execute('git branch --no-color --no-abbrev -v', $output, $path)) {
  78. $branches = array();
  79. $isFeatureBranch = false;
  80. // find current branch and collect all branch names
  81. foreach ($this->process->splitLines($output) as $branch) {
  82. if ($branch && preg_match('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
  83. if (!$version) {
  84. if ($match[1] === '(no branch)' || substr($match[1], 0, 10) === '(detached ') {
  85. $version = 'dev-' . $match[2];
  86. $isFeatureBranch = true;
  87. } else {
  88. $version = $this->versionParser->normalizeBranch($match[1]);
  89. $isFeatureBranch = 0 === strpos($version, 'dev-');
  90. if ('9999999-dev' === $version) {
  91. $version = 'dev-' . $match[1];
  92. }
  93. }
  94. }
  95. if ($match[2]) {
  96. $commit = $match[2];
  97. }
  98. }
  99. if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
  100. if (preg_match('{^(?:\* )? *(\S+) *([a-f0-9]+) .*$}', $branch, $match)) {
  101. $branches[] = $match[1];
  102. }
  103. }
  104. }
  105. if ($isFeatureBranch) {
  106. // try to find the best (nearest) version branch to assume this feature's version
  107. $version = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path);
  108. }
  109. }
  110. return array('version' => $version, 'commit' => $commit);
  111. }
  112. private function versionFromGitTags($path)
  113. {
  114. // try to fetch current version from git tags
  115. if (0 === $this->process->execute('git describe --exact-match --tags', $output, $path)) {
  116. try {
  117. return $this->versionParser->normalize(trim($output));
  118. } catch (\Exception $e) {
  119. }
  120. }
  121. return null;
  122. }
  123. private function guessHgVersion(array $packageConfig, $path)
  124. {
  125. // try to fetch current version from hg branch
  126. if (0 === $this->process->execute('hg branch', $output, $path)) {
  127. $branch = trim($output);
  128. $version = $this->versionParser->normalizeBranch($branch);
  129. $isFeatureBranch = 0 === strpos($version, 'dev-');
  130. if ('9999999-dev' === $version) {
  131. $version = 'dev-' . $branch;
  132. }
  133. if (!$isFeatureBranch) {
  134. return $version;
  135. }
  136. // re-use the HgDriver to fetch branches (this properly includes bookmarks)
  137. $driver = new HgDriver(array('url' => $path), new NullIO(), $this->config, $this->process);
  138. $branches = array_keys($driver->getBranches());
  139. // try to find the best (nearest) version branch to assume this feature's version
  140. $version = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path);
  141. return array('version' => $version, 'commit' => '');
  142. }
  143. }
  144. private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path)
  145. {
  146. // ignore feature branches if they have no branch-alias or self.version is used
  147. // and find the branch they came from to use as a version instead
  148. if ((isset($packageConfig['extra']['branch-alias']) && !isset($packageConfig['extra']['branch-alias'][$version]))
  149. || strpos(json_encode($packageConfig), '"self.version"')
  150. ) {
  151. $branch = preg_replace('{^dev-}', '', $version);
  152. $length = PHP_INT_MAX;
  153. $nonFeatureBranches = '';
  154. if (!empty($packageConfig['non-feature-branches'])) {
  155. $nonFeatureBranches = implode('|', $packageConfig['non-feature-branches']);
  156. }
  157. foreach ($branches as $candidate) {
  158. // return directly, if branch is configured to be non-feature branch
  159. if ($candidate === $branch && preg_match('{^(' . $nonFeatureBranches . ')$}', $candidate)) {
  160. return $version;
  161. }
  162. // do not compare against other feature branches
  163. if ($candidate === $branch || !preg_match('{^(master|trunk|default|develop|\d+\..+)$}', $candidate, $match)) {
  164. continue;
  165. }
  166. $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline);
  167. if (0 !== $this->process->execute($cmdLine, $output, $path)) {
  168. continue;
  169. }
  170. if (strlen($output) < $length) {
  171. $length = strlen($output);
  172. $version = $this->versionParser->normalizeBranch($candidate);
  173. if ('9999999-dev' === $version) {
  174. $version = 'dev-' . $match[1];
  175. }
  176. }
  177. }
  178. }
  179. return $version;
  180. }
  181. private function guessSvnVersion(array $packageConfig, $path)
  182. {
  183. SvnUtil::cleanEnv();
  184. // try to fetch current version from svn
  185. if (0 === $this->process->execute('svn info --xml', $output, $path)) {
  186. $trunkPath = isset($packageConfig['trunk-path']) ? preg_quote($packageConfig['trunk-path'], '#') : 'trunk';
  187. $branchesPath = isset($packageConfig['branches-path']) ? preg_quote($packageConfig['branches-path'], '#') : 'branches';
  188. $tagsPath = isset($packageConfig['tags-path']) ? preg_quote($packageConfig['tags-path'], '#') : 'tags';
  189. $urlPattern = '#<url>.*/(' . $trunkPath . '|(' . $branchesPath . '|' . $tagsPath . ')/(.*))</url>#';
  190. if (preg_match($urlPattern, $output, $matches)) {
  191. if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) {
  192. // we are in a branches path
  193. $version = $this->versionParser->normalizeBranch($matches[3]);
  194. if ('9999999-dev' === $version) {
  195. $version = 'dev-' . $matches[3];
  196. }
  197. return array('version' => $version, 'commit' => '');
  198. }
  199. $version = $this->versionParser->normalize(trim($matches[1]));
  200. return array('version' => $version, 'commit' => '');
  201. }
  202. }
  203. }
  204. }